☰
U-Net图像分割实战:从数据标注到ONNX部署全流程
2026/10/19 2:39:16 网站建设 项目流程

U-Net系列写到这里,前面几篇把网络结构、变体改进和损失函数都梳理得差不多了,但后台留言里问得最多的其实不是某个模块怎么理解,而是同一个问题:这些内容到底怎么落到自己的数据集上?所以这篇不再谈理论,直接走一个完整的图像分割实战项目,从原始标注数据到最终对外提供推理服务的部署形态,把所有关键步骤一次性串起来。

我以一个“目标区域分割”的模拟项目X为例来展开,数据用的是内部标注的一批细胞类影像,但整个流程换成工业表面缺陷、遥感地物、文档版面分析都完全适用。任务定义很纯粹:输入一张图,模型输出同尺寸的掩膜,背景为0、目标为1。技术栈选的是PyTorch训练加ONNX Runtime部署,这套组合在CPU、GPU和边缘设备上都能跑,也是目前工程落地比较稳的一条路线。适合准备做第一个分割项目的读者,也适合手里已经有标注数据、但不知道如何变成可用服务的开发者。

下面按照一个项目真正会经历的节奏来拆解:先做方案选型,再处理数据,然后训练调参,最后导出部署,每部分都会附上实操代码和踩坑记录。

1. 项目整体思路拆解

1.1 任务定义与方案选型

开始动手之前,我第一件事不是选模型,而是把项目的基础参数盘清楚。对于分割任务,需要先确认几个事实:数据总量大概多少、标注完整度如何、图像分辨率多大、有哪几种类别、推理速度的上限是多少。这些问题直接决定后面所有技术选型。举个例子,如果数据量只有两三百张,那么任何大型分割模型都注定过拟合;如果推理平台是普通CPU,那么高性能解码器反而会成为短板。

我把模拟项目X的初始盘参数列成了下面这张表:

项目参数实际值选型影响
数据总量约3000张中等规模,U-Net完全能够驾驭
标注形式多边形JSON与PNG掩膜混合需要统一预处理流程
图像分辨率512×512左右保留细节同时显存可控
类别数单类前景分割输出通道数为1,用Sigmoid
推理场景CPU服务器,单张推理要求小于200ms优先考虑标准U-Net并导出ONNX

为什么在这个场景下选U-Net而不是近年来更花哨的Transformer分割模型?两个原因。第一,数据规模不够大,Transformer这类模型对数据和训练时长的要求普遍更高,小规模数据下很容易陷入过拟合或收敛不稳定的局面。第二,部署环境的约束摆在那里,U-Net这种对称编解码结构简单直接,不会在推理阶段造成额外开销。数据不多、目标相对规则、对速度有要求,这三个条件叠加起来,U-Net变体就是性价比最优解。

尤其强调一点:我在原有U-Net基础上加了少量残差连接,让信息在编解码路径中流通得更顺畅,训练收敛更稳。这个改动成本极低,带来的收益却很明显,后续模型构建部分会给出具体代码。

1.2 工程目录结构与技术栈

分割项目看起来只是“训练一个模型”,但实际工程里要处理的东西远不止模型本身。我建议从第一天起就按照功能模块划分目录,而不是把清洗脚本、训练代码和部署代码混在一个文件里。下面是模拟项目X使用的目录结构:

unet_seg_demo/ ├── data/ │ ├── raw/ # 原始图像与标注 │ ├── processed/ # 清洗后生成的掩膜 │ └── splits/ # 数据集划分索引 ├── src/ │ ├── dataset.py # 数据读取与增强 │ ├── model.py # U-Net模型定义 │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估指标计算 │ └── export_onnx.py # 模型导出 ├── deploy/ │ └── server.py # 部署服务 └── outputs/ ├── checkpoints/ # 模型权重 └── logs/ # 训练日志

这个目录结构最核心的思想是隔离不同阶段的工作内容。数据清洗、训练、部署各自独立,出问题的时候能快速定位,不会因为改一个预处理函数反而影响了模型加载逻辑。部署目录单独抽出来还有一个好处:部署环境和训练环境往往依赖不同,分开之后不会互相污染。

技术栈方面,我选择的是PyTorch做模型训练和导出,albumentations做数据增强,ONNX Runtime做CPU端推理服务。选这套组合的核心理由是所有环节都是拆开的,升级或者替换其中一环不会导致全盘重构。整个流程下来,数据读取和增强在dataset.py,模型结构在model.py,训练逻辑放在train.py,每一步都有明确归属。

2. 数据准备与增强实战

2.1 标注格式解析与掩膜生成

数据准备是整个分割项目里最容易被低估的环节。很多初学者拿到数据后会直接扔给模型训练,结果验证集指标忽高忽低,最后发现是掩膜生成环节出了错。模拟项目X的原始标注有一部分是标注工具导出的JSON多边形坐标,另一部分已经是PNG掩膜。为了统一格式,我先编写了一个标注转换模块,把JSON坐标解析出来后重新画成和目标图像尺寸完全一致的掩膜。

这段转换逻辑大致如下:

import json import numpy as np import cv2 def json_to_mask(json_path, img_shape): with open(json_path, encoding="utf-8") as f: annotation = json.load(f) # 根据标注工具导出的格式不同,需要适配坐标系 mask = np.zeros(img_shape[:2], dtype=np.uint8) for shape in annotation["shapes"]: points = np.array(shape["points"], dtype=np.int32) # 单类目标填入1;多类则可以给不同label编号 cv2.fillPoly(mask, [points], color=1) return mask

这段代码的关键点有两个。第一,掩膜必须是单通道,背景为0,前景为1,不要为了可视化方便而存成三通道RGB,否则后续计算损失时会因为通道维度对不上而报错。第二,不管原始JSON的坐标系里有什么缩放信息,最后都要保证掩膜尺寸和原图完全一致。如果原图是512×512,掩膜就一定也输出512×512,否则训练时无法对齐。

每次转换完标注,我都有一个固定习惯:把原图、掩膜、以及原图与掩膜的叠加图并排保存到一张可视化图里,直接用肉眼检查。这一步看起来简单,却能提前发现很多诡异问题,比如标注坐标偏移、掩膜反色、目标区域被填充到错误位置等。以下代码就是检查可视化结果用的:

import matplotlib.pyplot as plt def visualize_sample(image, mask): overlay = image.copy() overlay[mask == 1] = (255, 0, 0) # 掩膜区域染成红色 fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(image) axes[1].imshow(mask, cmap="gray") axes[2].imshow(overlay) plt.savefig("check_visualization.png")

2.2 数据集划分与增强策略

掩膜生成之后,下一步是数据集划分。很多项目随手就把数据按7:3拆成训练集和测试集,这种做法在分割任务里其实有隐患。更稳妥的方式是在划分之前按图像来源或采集批次分组,确保同一个来源的数据不会同时出现在训练集和验证集里,否则会因为数据泄漏导致验证结果虚高。模拟项目X的数据来自多个采集批次,我先按批次编号排列,再以8:1:1比例拆成训练集、验证集和测试集。

增强策略方面,我的原则是训练阶段做强度合理的增强,验证和测试阶段只做最基本的尺寸调整和归一化,绝不把随机翻转、随机光照这类操作带到验证集上去。具体增强操作如下:

import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.Resize(height=512, width=512), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.1), A.RandomRotate90(p=0.5), A.RandomBrightnessContrast(p=0.3), A.ElasticTransform(p=0.2, alpha=120, sigma=120 * 0.05), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ]) valid_transform = A.Compose([ A.Resize(height=512, width=512), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ])

有一个细节值得单独说明:这里使用的均值和标准差是ImageNet预训练模型通用的数值,即便你不是直接用预训练权重,这套归一化数值也能用。如果你用的是自己的backbone并且打算从零开始训练,那么归一化数值影响不大;但如果你加载了预训练编码器,归一化方式和预训练时的设置必须保持一致,否则模型的预测表现会有明显退化。

增强力度并不是越大越好。我见过有项目把旋转和弹性变形开到很高,结果模型学到的目标区域形状都变形了,验证集上Dice反而下降。模拟项目X的阶段我用的是比较克制的参数,保证目标结构不被增强破坏得太离谱。对于稀疏的小目标分割任务,还可以考虑在增强阶段加入随机裁剪,在裁剪过程中自动过滤掉没有目标的区域,这样能缓解前景背景严重不平衡的问题。

3. 模型构建与训练

3.1 基于U-Net的模型构建

模型部分我并没有直接照搬原始的U-Net结构,而是在基础骨架上做了一点微调。标准的U-Net分为编码器、解码器和跳跃连接三部分,编码器逐级下采样获取语义特征,解码器逐步恢复空间分辨率,跳跃连接把编码器各层细节信息带到解码器对应层。模拟项目X在每一个编码器块内部加入了一个残差连接,这样梯度在反向传播时可以绕过一个卷积块直接流向前层,整体收敛速度更快,深层网络也不容易退化。

下面是一段简化但可运行的核心模型定义,方便对照理解:

import torch import torch.nn as nn class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1) self.bn1 = nn.BatchNorm2d(out_ch) self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1) self.bn2 = nn.BatchNorm2d(out_ch) self.relu = nn.ReLU(inplace=True) def forward(self, x): identity = x out = self.relu(self.bn1(self.conv1(x))) out = self.relu(self.bn2(self.conv2(out))) # 当输入输出通道一致时使用残差连接 if identity.shape[1] != out.shape[1]: identity = nn.Conv2d(identity.shape[1], out.shape[1], 1)(identity) return self.relu(out + identity) # 编码器和解码器之间通过跳跃连接传递特征 class UNetWithResidual(nn.Module): def __init__(self, in_channels=3, out_channels=1, base=32): super().__init__() self.enc1 = ConvBlock(in_channels, base) self.pool1 = nn.MaxPool2d(2) self.enc2 = ConvBlock(base, base * 2) self.pool2 = nn.MaxPool2d(2) self.enc3 = ConvBlock(base * 2, base * 4) self.pool3 = nn.MaxPool2d(2) self.enc4 = ConvBlock(base * 4, base * 8) self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2) self.dec3 = ConvBlock(base * 8, base * 4) self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2) self.dec2 = ConvBlock(base * 4, base * 2) self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2) self.dec1 = ConvBlock(base * 2, base) self.out = nn.Conv2d(base, out_channels, 1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool1(e1)) e3 = self.enc3(self.pool2(e2)) e4 = self.enc4(self.pool3(e3)) d3 = self.up3(e4) d3 = self.dec3(torch.cat([d3, e3], dim=1)) d2 = self.up2(d3) d2 = self.dec2(torch.cat([d2, e2], dim=1)) d1 = self.up1(d2) d1 = self.dec1(torch.cat([d1, e1], dim=1)) return self.out(d1)

这里有一个实践中常踩的坑:跳跃连接做特征拼接时,编码器特征图的尺寸和解码器上采样后的特征图尺寸必须完全一致,否则拼接时就会报错。所以网络的下采样次数一般要和输入尺寸相匹配,保证每次上采样都能恢复到对应尺寸。这也是为什么要求输入图像的长宽能被2的幂次整除。很多项目训练时报错说拼接张量尺寸不匹配,原因就是输入尺寸没控制好,比如用了513×513这样的奇数分辨率。

基础通道数base这里设置成32,是为了在CPU推理时减少计算量。如果你有GPU并且对精度要求更高,可以把base提高到64或者直接改用带预训练权重的编码器。总体思路很明确:先用小配置把整个流程跑通验证,再根据验证指标逐步放大模型。

3.2 损失函数与评价指标

分割任务最常用的损失函数是交叉熵和Dice损失,但单独使用任何一个都有偏科的问题。交叉熵在类别不平衡的时候会让模型偏向预测背景,Dice损失对类别不平衡更鲁棒,但在目标极小时梯度变化剧烈。因此我采用BCE和Dice的组合损失:

class BCEDiceLoss(nn.Module): def __init__(self, weight_bce=0.5, weight_dice=0.5): super().__init__() self.weight_bce = weight_bce self.weight_dice = weight_dice def forward(self, pred, target): bce = nn.functional.binary_cross_entropy_with_logits( pred, target.float() ) pred_probs = torch.sigmoid(pred) smooth = 1e-6 intersection = (pred_probs * target).sum() dice = 1 - (2.0 * intersection + smooth) / ( pred_probs.sum() + target.sum() + smooth ) return self.weight_bce * bce + self.weight_dice * dice

关于组合损失的权重,模拟项目X里我设置为0.5和0.5。如果你发现验证集的Dice始终上不去,可以考虑把Dice损失的权重调大一些,比如0.7,这样会让模型更关注正样本区域的拟合精度。需要注意的是,直接对logits计算BCE,而不是对Sigmoid之后的结果计算,这也是实际项目中容易遗漏的细节。

评估指标我选择了IoU和Dice两个指标同时记录。代码实现时不要把每个batch单独算完再平均,正确的做法是维护一个累积混淆矩阵,也就是把整个验证集所有图片的预测结果和真实掩膜累加到一起,最后再统一计算IoU和Dice。这样做避免了小batch造成的指标抖动,尤其是当验证集里的某张图恰好没有目标区域时,单独计算会出现除零或者指标为0的假象。

def compute_metrics(pred_mask, gt_mask): pred_flat = (pred_mask > 0.5).reshape(-1).astype(np.uint8) gt_flat = gt_mask.reshape(-1).astype(np.uint8) tp = np.sum((pred_flat == 1) & (gt_flat == 1)) fp = np.sum((pred_flat == 1) & (gt_flat == 0)) fn = np.sum((pred_flat == 0) & (gt_flat == 1)) iou = tp / (tp + fp + fn + 1e-6) dice = 2 * tp / (2 * tp + fp + fn + 1e-6) return iou, dice

3.3 训练流程与调参细节

训练部分没有什么神秘魔法,核心思路是用相对保守的基础参数把流程跑通,然后根据验证指标做微调。模拟项目X使用的基本配置如下:

优化器:AdamW,初始学习率 1e-3 批次大小:8 最大训练轮数:60 学习率调整:每连续5轮验证集Dice不上升,学习率乘以0.5 早停策略:连续10轮验证集Dice不上升,停止训练 混合精度:使用自动混合精度

这里有几个经验分享。第一,不要一上来就用很大的学习率。我见过很多项目在分割任务上把初始学习率设置成1e-2,结果损失直接震荡成锯齿状,完全没有收敛趋势。除非你心中有数,否则1e-3这个起点会更安全。第二,批次大小通常受显存限制,如果显示卡吃不下8张512×512的图,可以把批次降到4,同时启用梯度累积,等效扩大批次大小。第三,在训练过程里一定要记录每个epoch验证集上的Dice和IoU,而不是只看训练集损失下降。以我的经验,分割任务的过拟合信号往往出现在训练集损失还在下降但验证集Dice已经连续多轮不涨的时候。

混合精度训练也是模拟项目X里一个重要环节。PyTorch的自动混合精度使用起来非常简洁:

scaler = torch.cuda.amp.GradScaler() with torch.autocast(device_type="cuda", dtype=torch.float16): loss = criterion(logits, mask) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

开启混合精度后,训练速度提升了大约40%,而对最终Dice的影响几乎可以忽略。关键在于所有涉及归一化或数值稳定的张量操作仍然会以fp32执行,模型核心计算在fp16完成,显卡利用率明显提高。如果你拿到的部署设备也是CPU而非GPU,在训练阶段就可以有意识地把模型规模控制得保守一点。

训练结束后保存模型遵循一条原则:永远保存验证集Dice最高的那一次权重,而不是最后一轮的训练权重。后者往往已经处于轻微过拟合状态。我在train.py里会在每个epoch结束后比较当前验证Dice和历史最优值,只有提升时才覆盖保存模型文件。

4. 模型导出与部署落地

4.1 PyTorch模型转ONNX

模型训练到验证集Dice稳定在0.85以上后,下一步就是把PyTorch模型导出为ONNX格式。ONNX的核心价值在于格式中立,PyTorch训练好的模型导出后,无论在Windows、Linux还是嵌入式Linux上,只要能找到对应的ONNX Runtime,就能跑起来,不需要额外安装庞大的Python深度学习框架。

导出代码需要特别注意两个地方:输入张量的尺寸和输出层形态。模拟项目X的输入尺寸是512×512×3,导出时为固定尺寸:

import torch model = UNetWithResidual(in_channels=3, out_channels=1) checkpoint = torch.load("outputs/checkpoints/best_model.pth", map_location="cpu") model.load_state_dict(checkpoint["model_state_dict"]) model.eval() dummy_input = torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy_input, "outputs/checkpoints/model.onnx", export_params=True, opset_version=12, input_names=["input"], output_names=["output"], dynamic_axes=None, )

这里有两个选择项需要讲清楚。如果设置dynamic_axes为None,导出的模型只能接受512×512的输入,部署端不能随意传其他尺寸的图。模拟项目X的部署端是固定图像尺寸输入的,所以固定尺寸最简单可靠。但如果你希望部署时能自适应不同输入尺寸,则可以把宽高设为动态轴,在部署端再配合预处理代码完成尺寸归一化。动态尺寸虽然灵活,却会带来一个隐患:如果输入尺寸没有做对齐处理,模型中特征的拼接和上采样可能因为尺寸不匹配直接报错。

另一个容易被忽略的问题:模型内如果包含BatchNorm层,导出前必须调用model.eval()。否则BatchNorm仍然使用当前batch的统计量,导出后的模型在推理时就会出现输出漂移,看起来是模型被“驯服”了,实际原因就是BN层模式错误。

4.2 使用ONNX Runtime进行推理

导出完成后,就可以在部署环境里直接加载ONNX模型。模拟项目X的部署推理代码非常简单:

import numpy as np import onnxruntime as ort session = ort.InferenceSession("outputs/checkpoints/model.onnx", providers=["CPUExecutionProvider"]) def predict(image): # 假设image已经完成resize和归一化,形状为(1,3,512,512) input_name = session.get_inputs()[0].name logits = session.run(None, {input_name: image})[0] prob = 1.0 / (1.0 + np.exp(-logits)) mask = (prob > 0.5).astype(np.uint8) return mask

这段代码里最关键的是做了Sigmoid操作。很多模型导出时直接输出logits,没有经过激活函数,部署端如果不做Sigmoid就把结果拿去二值化,会导致整个掩膜区域判定错误。我建议在部署代码里显式完成概率转换,而不是依赖训练时对损失函数内部的处理。

部署端的预处理必须和训练时保持严格一致:先resize到512×512,再使用相同的均值和标准差归一化,最后把通道顺序调整为CHW并增加batch维度。很多部署问题看起来像是模型导出坏了,实际是预处理参数跟训练时不一致,导致模型输入分布完全变了。

4.3 从脚本到服务:部署形态选择

如果你的项目只是内部离线分析,那么上面的predict函数已经够用了。但大多数时候,部署的目标是给其他业务系统调用,需要提供HTTP接口。我在部署目录下写了一个轻量服务,整体代码不长,核心就是把predict函数包进一个Web接口里。

from fastapi import FastAPI, UploadFile, File import cv2 import numpy as np app = FastAPI() @app.post("/predict") async def predict_endpoint(file: UploadFile = File(...)): # 读取上传图像并转换为numpy数组 img_bytes = await file.read() img_np = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) img_np = cv2.cvtColor(img_np, cv2.COLOR_BGR2RGB) # 预处理:resize、归一化、转CHW img_resized = cv2.resize(img_np, (512, 512)) img_normalized = img_resized.astype(np.float32) / 255.0 img_normalized = (img_normalized - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] input_tensor = np.transpose(img_normalized, (2, 0, 1)).astype(np.float32) input_tensor = np.expand_dims(input_tensor, axis=0) mask = predict(input_tensor) # 输出掩膜可用PNG格式编码返回 mask_resized = cv2.resize((mask * 255).astype(np.uint8), (img_np.shape[1], img_np.shape[0])) success, encoded_img = cv2.imencode(".png", mask_resized) return {"mask_base64": encoded_img.tobytes().decode("latin1")}

模拟项目X当前阶段使用这个方案完全足够:内部工具、单并发场景、CPU推理延迟大约150毫秒。如果以后并发量上来,再考虑批量预处理、模型量化(比如转INT8进一步压缩体积和加速)或者推到更高性能的推理引擎上。但架构从一开始不用做得很重,先让业务跑起来比什么都重要。

5. 常见问题与排查技巧实录

5.1 训练阶段的典型问题

这部分内容是我在这类分割项目里反复遇到的高频问题,也算是一个排查速查表。

第一个高频问题是损失不下降。如果训练刚开始几轮损失就卡在某个值不动,或者loss直接变成NaN,大概率不是模型结构问题,而是数据问题。常见原因包括:掩膜编号出现了超出预期范围的值、输入图像包含全黑或全白的异常样本、学习率设置过大导致梯度爆炸。我的排查顺序是先检查数据可视化,再检查归一化范围,最后才动学习率。

第二个高频问题是验证Dice很高但可视化结果边界很粗糙。这种情况通常不是因为模型没有学到位,而是因为评估代码或后处理太粗糙。训练时预测直接对整张512×512输出,如果原图非常大,在缩放过程中小目标的结构信息会丢失,要么考虑滑窗预测,要么提高输入分辨率。还有一个常见原因是输出层阈值固定为0.5,但模型实际输出概率分布偏低,可以针对验证集稍微做一次阈值搜索。

第三个高频问题是显存溢出。很多人一想到显存不够就换更小的模型,其实可以先尝试缩小批次大小,再把输入尺寸从512降到384,或者开启梯度累积。分割任务对显存的消耗大头在编码器特征图保存,降低输入尺寸的收益立竿见影。

第四个问题是验证集指标波动非常大。这几乎都是因为训练集和验证集划分没有做好分布隔离,导致两张同样内容的图片分别落在训练集和验证集里,指标虚高且不稳定。重新按来源批次划分数据后,这个问题通常立刻缓解。

5.2 部署阶段容易踩的隐藏坑

部署阶段的坑往往比训练阶段更隐蔽,因为程序不会报错,但输出结果就是不对。

最常见的是预处理不一致。训练时图像用RGB顺序读取并除以255,部署端用OpenCV读取却是BGR顺序,导致输入通道全部错乱,模型输出一团糟。这种情况不会崩溃,但输出掩膜完全不可用,排查起来非常让人头疼。对策是封装一个共享的预处理函数,训练和部署都调用同一个实现。

第二个隐藏坑是尺寸对齐。模型导出固定为512×512,部署端实际收到的是1920×1080的图,resize之后再预测,最后输出掩膜需要映射回原图尺寸。如果映射时坐标没有认真处理,掩膜和原图之间会出现偏移或模糊。解决办法是在resize时明确记录缩放比例,后处理阶段用同一比例还原,尽可能避免多次resize造成信息损失。

我把上述遇到的问题整理成了速查表,方便后续项目直接对照:

现象可能原因解决思路
损失变成NaN学习率过大、数据含异常标签降低学习率,检查数据范围
验证Dice高但边缘粗糙输入分辨率不足、后处理阈值不合理提高输入尺寸,做阈值搜索
显存溢出批次过大或输入尺寸过高减小batch,开启梯度累积
部署输出全黑或全白预处理不一致、遗漏激活函数统一预处理函数,显式做Sigmoid
掩膜偏移resize映射不当记录缩放比例并统一还原
验证指标波动大数据划分泄漏按批次/来源分组划分

总的来说,分割项目的bug往往不是出在网络结构上,而是出在数据流转和前后处理上。我自己现在做这类项目时,习惯在每个关键节点都输出一份可视化结果看一眼:数据预处理后看一遍,验证集预测后又看一遍,部署完成后再把同样的图跑一遍对比。这三处检查能拦住绝大多数隐藏问题。

最后再分享一个我个人的习惯:把高效的事情先做扎实。所谓高效,不是急着调参或换backbone,而是先花半天时间把数据可视化、掩膜生成、预处理流程全部验证正确,再把最小模型完整跑通训练和部署。很多新手一上来就追求模型精度,结果因为最底层的数据没有处理好,在后面不断返工。这个Demo完成后,如果后续要扩展,方向也很明确:多类分割只需要把输出通道数改为类别数,并配合Softmax取argmax;若要追求更高速度,可以压缩编码器通道数或用预训练轻量backbone替换编码器。原理都一样,只是工程细节不同。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询