
简介这份资源是一个完整的UNet图像分割项目面向深度学习初学者与计算机视觉开发者帮助理解并落地基于卷积神经网络的像素级分割任务。压缩包共253个文件大小32.34MB主要包含Python脚本、Jupyter Notebook、tif/png图像及Markdown说明。ipynb文件覆盖数据准备与模型训练两个核心环节py脚本用于模型结构定义、损失计算与训练配置大量png文件则直观展示了分割结果和中间输出。已有4335人学习下载。通过该项目读者能掌握UNet收缩路径与扩张路径的设计、跳跃连接的作用以及数据预处理到模型评估的完整流程相关代码可迁移至医学影像、遥感分割等场景是一份兼顾原理与实战的入门资料。1. 拿到 unet网络实现图像分割 这个压缩包先搞清楚它要解决什么拿到 unet网络实现图像分割 这个压缩包先别急着解压跑代码搞清楚任务本身更重要图像分割不是给整张图分类而是为每个像素打标签。UNet 是这类任务里最经典、最容易跑通的网络U 形结构左侧下采样提语义、右侧上采样还原细节跳跃连接再把丢掉的边缘拼回来。对新手它是第一个能出效果的分割模型对熟手它是验证数据标注质量最快的基线。我调试分割类项目的经验里一半的踩坑不在网络而在数据喂法和参数习惯。下面按压缩包里最常见的五块内容展开网络结构怎么搭、数据怎么喂、训练参数怎么配、哪些坑最常翻车、以及怎么用 mIoU 量化效果。新手照着能跑通熟手可以直接核对参数和边界。2. UNet 编码器与解码器跳跃连接为什么是分割的灵魂2.1 编码器路径双卷积块与通道翻倍的节奏UNet 的编码器由 4 个 stage 组成每个 stage 做两件事先过一组双卷积块再用 2x2 最大池化把特征图尺寸减半。双卷积块的标准配置是连续两个 3x3 卷积每个卷积后面接 ReLU 激活。工程实现里很多人会在卷积和激活之间加一个 BatchNorm原版论文没有但现成的代码包几乎都加了——加了之后梯度更稳训练前期 loss 下降明显更快对新手特别友好。通道数的节奏是固定翻倍输入是 RGB 三通道第一个 stage 把通道从 3 变到 64每下采样一次翻倍依次变成 128、256、512底部还有一个 bottleneck 双卷积把通道从 512 扩到 1024。这样设计的目的很直白下采样越来越狠空间分辨率越来越低但每个像素能看到的上下文越来越大所以通道要加宽去容纳更抽象的语义。这个分辨率减半、通道翻倍的节奏是整个 UNet 的参数骨架解码器的所有计算都围绕它展开。选 3x3 而不是 5x5 或 7x7是经典 VGG 风格的思路两个 3x3 堆叠等价于 5x5 感受野参数量只有后者的 18/25还多一层非线性。分割任务里卷积核大小直接影响边缘感受野的把控3x3 是性价比极高的选择。记住一个关键数字无论代码怎么写编码器最后输出的特征图一定是输入尺寸的 1/164 次池化后面很多报错都跟它有关。2.2 解码器路径上采样方式与跳跃连接的通道拼接解码器的工作是把 1/16 分辨率的特征图逐级放大回原图大小。放大手段有两类转置卷积ConvTranspose2d和双线性插值 3x3 卷积。转置卷积是原版 UNet 的做法核大小 2、步长 2正好把宽高翻倍参数可学习理论上能学出更精细的上采样权重但调不好容易产生棋盘格伪影。双线性插值 卷积更稳上采样本身没有可学习参数棋盘格问题基本消失代价是插值信息固定遇到极小目标时可能不如转置卷积灵活。我在分割类图像处理 Demo 里两种都用过常见做法是先转置卷积跑通边缘出现规律性伪影再切到双线性。跳跃连接才是 UNet 的灵魂。解码器每次上采样后要先把编码器同尺度的特征图拼过来再做双卷积。拼接用 torch.cat 而不是相加因为相加是混合拼接是保留全部信息——编码器的低层特征包含大量边缘、纹理和位置细节这些正是分割边缘最需要的东西。拼接前的通道计算要格外留神bottleneck 输出 1024 通道上采样变回 512拼上编码器第 4 层同尺度的 512 通道 skip 特征变成 1024 通道输入双卷积输出 512。每个解码器 stage 的输入通道都是输出的 2 倍写代码时通道没对齐报错最常发生在这一步。2.3 最小可运行的 UNetPyTorch 实现与通道参数说明下面这份代码是 UNet 最常见的工程写法双卷积块单独抽出来编码、bottleneck、解码按 4 个 stage 展开。它不追求和某个压缩包逐行一致但结构等价几乎所有 UNet 实现都能映射到这套框架上。import torch import torch.nn as nn class DoubleConv(nn.Module): 双卷积块两个 3x3 卷积 BatchNorm ReLU def __init__(self, in_channels, out_channels): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size3, padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), nn.Conv2d(out_channels, out_channels, kernel_size3, padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, out_channels1, features(64, 128, 256, 512)): super().__init__() # 编码器四个 stage self.enc1 DoubleConv(in_channels, features[0]) self.enc2 DoubleConv(features[0], features[1]) self.enc3 DoubleConv(features[1], features[2]) self.enc4 DoubleConv(features[2], features[3]) self.pool nn.MaxPool2d(kernel_size2, stride2) # bottleneck self.bottleneck DoubleConv(features[3], features[3] * 2) # 解码器四个 stage self.upconv4 nn.ConvTranspose2d(features[3] * 2, features[3], kernel_size2, stride2) self.dec4 DoubleConv(features[3] * 2, features[3]) self.upconv3 nn.ConvTranspose2d(features[3], features[2], kernel_size2, stride2) self.dec3 DoubleConv(features[2] * 2, features[2]) self.upconv2 nn.ConvTranspose2d(features[2], features[1], kernel_size2, stride2) self.dec2 DoubleConv(features[1] * 2, features[1]) self.upconv1 nn.ConvTranspose2d(features[1], features[0], kernel_size2, stride2) self.dec1 DoubleConv(features[0] * 2, features[0]) # 输出层1x1 卷积把通道映射到类别数 self.out_conv nn.Conv2d(features[0], out_channels, kernel_size1) def forward(self, x): # 编码保留每层输出给跳跃连接 e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) # 解码先上采样再拼接再双卷积 d self.upconv4(b) d self.dec4(torch.cat([d, e4], dim1)) d self.upconv3(d) d self.dec3(torch.cat([d, e3], dim1)) d self.upconv2(d) d self.dec2(torch.cat([d, e2], dim1)) d self.upconv1(d) d self.dec1(torch.cat([d, e1], dim1)) return self.out_conv(d)逻辑说明forward 里图像依次过 4 个编码器 stage每次池化前把特征图存成 e1 到 e4这是跳跃连接的记忆体。bottleneck 之后进入解码器每一步先转置卷积放大再和对应编码器特征在通道维拼接最后过双卷积。输出层 1x1 卷积out_channels1 时得到每个像素的前景得分网络本身不套 sigmoid交给损失函数处理数值稳定性更好。参数推荐值说明in_channels3RGB 输入灰度图改 1out_channels1二分类多类改成类别总数features(64, 128, 256, 512)显存紧张改 (32, 64, 128, 256)输入尺寸256 或 512长宽必须是 16 的倍数参数说明features 元组控制网络宽度显存小的机器改成 (32, 64, 128, 256) 训练快很多mIoU 通常只会损失不到 1 个点。out_channels 在二分类时设 1多类分割设类别总数并配 softmax 交叉熵。全尺寸 UNet 参数量在 3100 万左右8GB 显存跑 256x256、batch 16 已经是极限附近后面避坑章节会讲怎么省。注意这个 UNet 没有任何预训练权重冷启动的前 10 个 epoch 效果会很粗糙这是正常的别急着调参换结构。3. 数据集准备mask 的读取方式决定训练成败3.1 目录结构与标注文件规范先检查 mask 像素值压缩包里如果带了数据十有八九是这种目录结构dataset/ ├── images/ # 原始图像jpg/png 均可 │ ├── 0001.jpg │ └── 0002.jpg └── masks/ # 标注掩码推荐 png 单通道 ├── 0001.png └── 0002.png图像和 mask 一一对应文件名一致。mask 的格式是整个训练里最容易踩坑的环节它必须是单通道灰度图背景像素值是 0前景是 255或 1。很多标注工具导出的是 RGB 三通道彩色掩码甚至带调色板的 P 模式 PNG直接用 convert(RGB) 读出来再当单通道用模型会学到一堆中间色。我踩过的坑是某次拿到一份 mask肉眼看着是黑白图实际像素值是 0 和 128阈值设错整个数据集就废了。如果你用的是现成数据集第一件事是写个脚本统计 mask 的像素值分布from PIL import Image import numpy as np img Image.open(mask.png).convert(L) vals np.unique(np.array(img)) print(unique pixel values:, vals)如果输出里有 0 和 255阈值设 127 就行有 0 和 1 直接当标签混着 128、200 这类值说明是多个类别编码的调色板图要先做类别映射否则二值化会把所有非 0 值一刀切。另一个必查项是 mask 和图像有没有对齐把两者半透明叠加看边缘是否吻合。这一步只花几分钟能省掉后面所有调试时间。3.2 数据加载Dataset 类的完整实现与参数说明PyTorch 里写分割数据加载核心是getitem同时返回图像张量和 mask 张量并且两者经过完全相同的几何变换。下面是二分类场景的标准写法import os import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class SegmentationDataset(Dataset): def __init__(self, image_dir, mask_dir, image_size(256, 256)): self.image_dir image_dir self.mask_dir mask_dir self.image_size image_size self.file_names sorted(os.listdir(image_dir)) def __len__(self): return len(self.file_names) def __getitem__(self, index): name self.file_names[index] # 图像按 RGB 读mask 按灰度单通道读 image Image.open(os.path.join(self.image_dir, name)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, name)).convert(L) image image.resize(self.image_size) mask mask.resize(self.image_size, resampleImage.NEAREST) # 图像归一化到 [0,1]mask 转成 0/1 标签 image np.array(image).astype(np.float32) / 255.0 mask np.array(mask).astype(np.int64) mask (mask 0).astype(np.int64) # HWC - CHWmask 加一维作为单通道 image_tensor torch.from_numpy(image).permute(2, 0, 1) mask_tensor torch.from_numpy(mask).unsqueeze(0).float() return image_tensor, mask_tensor逻辑说明图像读成 RGBmask 读成灰度 L 模式。resize 时图像用默认双线性插值mask 必须指定 resampleImage.NEAREST否则双线性会在 0 和 255 之间插出 127 这类中间值等于给标签人工加噪声。图像像素压到 0-1mask 阈值化成 0/1最后 mask 加一维变成 (1, H, W)和网络输出 out_channels1 对齐。参数说明image_size 统一取 256 或 512。256 显存友好、训练快适合先验证流程512 边缘细节更好适合小目标多的场景。长宽必须是 16 的倍数原因前面讲过。batch size 一般取 8 到 16。如果 mask 前景占比特别小建议顺便打印前景像素比例心里有数再进训练。3.3 数据增强哪些变换可以共用哪些必须分开分割的数据增强比分类要谨慎几何变换翻转、旋转、缩放、裁剪必须图像和 mask 一起做像素变换亮度、对比度、颜色抖动只能作用在图像上不能动 mask。最常见的组合是随机水平翻转加随机旋转import random def train_transform(image, mask): # 水平翻转图像和 mask 用同一个随机数控制 if random.random() 0.5: image image.transpose(Image.FLIP_LEFT_RIGHT) mask mask.transpose(Image.FLIP_LEFT_RIGHT) # 随机旋转 [-10, 10] 度 angle random.uniform(-10, 10) image image.rotate(angle, resampleImage.BILINEAR, expandFalse) mask mask.rotate(angle, resampleImage.NEAREST, expandFalse) return image, mask逻辑说明翻转和旋转的随机参数要复用同一个随机数保证图像和 mask 的变换完全同步。旋转区间设 ±10 度比较保守太大的角度会让 mask 边缘产生空洞尤其是细长目标。expandFalse 表示保持原尺寸旋转后的边角空白被裁掉适合目标居中分布的数据如果目标位置飘忽建议改成随机裁剪再缩放。参数说明翻转概率 0.5 是通用默认值。旋转角度看数据特性车牌、道路、遥感建筑这类方向感强的场景不建议旋转细胞、农田这类方向无关的场景可以放宽到 ±30 度。归一化还有一条路是 ImageNet 均值方差归一化mean[0.485,0.456,0.406]std[0.229,0.224,0.225]对 ImageNet 预训练权重是标配对随机初始化的 UNet 影响不大。我一般先做简单 0-1 归一化跑通再考虑上预训练。4. 训练配置损失函数、优化器与训练循环的推荐参数4.1 损失函数单用 BCE 不够单用 Dice 太飘二分类分割最常用 BCEWithLogitsLoss它把 sigmoid 和二元交叉熵合在一起数值比分开算稳定。但只靠 BCE 有个突出问题前景目标占比很小时比如 256x256 图里目标只有几百像素BCE 会倾向全预测成背景因为全部判负的 loss 也不大于是出现预测全黑。Dice Loss 专门针对这种不平衡它衡量预测和真实的重叠程度目标占比越小对漏检惩罚越狠。但 Dice 在目标极小时梯度不平滑单独用前期容易震荡。工程上最省心的做法是两者叠加import torch def dice_loss(pred_logits, target, smooth1.0): pred_logits 是未过 sigmoid 的输出target 是 0/1 标签 pred torch.sigmoid(pred_logits) pred pred.view(pred.size(0), -1) # 展平每个样本 target target.view(target.size(0), -1) intersection (pred * target).sum(dim1) dice (2.0 * intersection smooth) / (pred.sum(dim1) target.sum(dim1) smooth) return 1.0 - dice.mean() # 训练时: loss bce(logits, masks) 0.5 * dice_loss(logits, masks)逻辑说明sigmoid 把 logits 压到 0-1 当作概率intersection 是预测与真实逐元素相乘求和即重叠像素数dice 分母是预测前景加真实前景的总像素数加 smooth 防止分母为 0。返回的是 1 减 dice越小越好。参数说明smooth 取 1.0 是通用值兼顾梯度平滑和除零兜底。组合系数 0.5 表示 BCE 是主导、Dice 是修正。前景占比小于 5% 时把系数提到 1.0前景占一半以上的均衡数据单用 BCE 就够Dice 反而可能放大噪声。4.2 优化器与学习率Adam 的默认参数够不够用对从零训练的 UNetAdam 是当前最常见的默认选择学习率 1e-3、weight_decay 1e-5 就能稳定起步。如果用 ImageNet 预训练编码器学习率要降到 1e-4预训练权重已在一个良好解附近步子太大会破坏。SGD 加动量也能逼近 Adam但对超参数更敏感新手不建议从 SGD 起步。学习率调度器推荐 CosineAnnealingLR不依赖人工设置衰减节点后期平滑下降对收敛和精度都有帮助。完整训练循环骨架如下epochs 60 model UNet(in_channels3, out_channels1).cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) bce torch.nn.BCEWithLogitsLoss() scaler torch.cuda.amp.GradScaler() # 混合精度可选 for epoch in range(epochs): model.train() total_loss 0.0 for images, masks in train_loader: images, masks images.cuda(), masks.cuda() with torch.cuda.amp.autocast(): logits model(images) loss bce(logits, masks) 0.5 * dice_loss(logits, masks) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad() total_loss loss.item() scheduler.step() avg_loss total_loss / len(train_loader) print(fEpoch {epoch 1}/{epochs}, loss: {avg_loss:.4f}, flr: {scheduler.get_last_lr()[0]:.2e})逻辑说明autocast 和 GradScaler 是 PyTorch 原生混合精度前向用 FP16、梯度更新用 FP32显存能省接近一半8GB 卡跑 256x256、batch 16 就靠它。scaler.step 替代 optimizer.steploss 必须先 scale 再 backward。不用 AMP 的环境删掉这三行即可其余逻辑不受影响。参数说明epochs 取 60 是几百到几千张图数据集的经验值数据更少要配更强的增强防过拟合。loss 如果前 5 个 epoch 纹丝不动优先怀疑学习率。学习率这个参数在分割里最接近玄学出现震荡就先降一个数量级。4.3 训练循环细节验证、模型保存与早停最小可跑的版本之外要出效果还得加两个环节。第一是验证每个 epoch 结束后在验证集算一次 loss 和 IoU只盯训练 loss 会被过拟合欺骗。第二是保存只存验证指标最好的权重而不是最后 epoch 的权重。best_iou 0.0 for epoch in range(epochs): # 训练循环和上面一致省略 model.eval() val_loss 0.0 with torch.no_grad(): for images, masks in val_loader: images, masks images.cuda(), masks.cuda() logits model(images) val_loss (bce(logits, masks) 0.5 * dice_loss(logits, masks)).item() current_iou evaluate_iou(model, val_loader) # 定义见第 6 章 if current_iou best_iou: best_iou current_iou torch.save(model.state_dict(), unet_best.pth) print(fEpoch {epoch1}, val_loss: {val_loss / len(val_loader):.4f}, IoU: {current_iou:.4f})逻辑说明验证必须包在 torch.no_grad() 里否则验证也建计算图白占显存和算力。evaluate_iou 内部把 logits 过 sigmoid 再阈值化和真实 mask 算交集并集比。保存用 state_dict 而不是整个 model文件小、迁移方便。参数说明早停可以做得更严格——验证 IoU 连续 10 个 epoch 不涨就中断训练并回退到最佳权重。如果最佳权重正好出现在最后一个 epoch说明还没收敛加大 epochs 或调小学习率重跑。5. 避坑指南UNet 训练和推理最常翻车的 5 个问题这一章把我在 UNet 训练和推理里真正踩过的坑按出现频率排序每条按现象、原因、解决的顺序写方便直接对照排查。5.1 预测结果全黑或全白99% 是 mask 读取方式错了现象训练 loss 正常下降验证指标看着也行但把单张图送进模型推理输出要么全黑只剩零星白点要么全白一团。原因最常见的是 Dataset 里用 convert(RGB) 读 mask单通道 0/255 被展开成三通道模型被迫同时猜三个通道的组合学出来的决策边界是歪的。另一种是 mask 本身带调色板convert(L) 之后映射到 128 这类中间灰度二值化阈值一设错前后景直接颠倒。解决读 mask 固定用 convert(L)resize 固定用 NEAREST进训练前跑一次像素值统计脚本确认只有两种取值验证阶段把预测结果打印成图片而不是只看数字。这个坑我完整踩过一遍修复只改读取一行但排查花了一晚上属于典型的没看数据就开训。5.2 训练 loss 居高不下或剧烈震荡先查数据再查学习率现象前 5 个 epoch loss 降得很快随后卡在 0.7 到 0.8 不动或者每个 batch 的 loss 忽高忽低曲线像锯齿。原因loss 卡住优先查数据。图像没归一化像素值 0-255 直接喂网络梯度量级被放大几个数量级mask 标注错误率偏高网络在学矛盾的标签前景占比极端失衡且没配 Dice。以上都没问题再查超参——SGD 配 0.01 起步、或 Adam 的 lr 手滑设成 1e-2都会让 loss 后期震荡。解决按数据 → 归一化 → 超参顺序排查。具体动作打印一批图像张量的 min/max确认在 0-1 区间人眼抽查 10 张 mask 是否对齐把 lr 降到 1e-4 重跑 5 个 epoch 看趋势。多数情况下归一化没做是头号嫌疑。5.3 显存溢出batch size、输入尺寸和混合精度三板斧现象训练到第二个 epoch 报 CUDA out of memory或者 DataLoader 的 worker 把显存吃满后进程被杀。原因全尺寸 UNet 约 3100 万参数256x256 输入、batch 16 在 8GB 卡上已经很紧张batch 32 基本必炸。很多人还同时开着多个实验进程显存峰值更高。解决按优先级来——batch 先降到 8输入尺寸从 512 缩到 256开启 AMP 混合精度。三板斧之后还炸把 features 改成 (32, 64, 128, 256)参数量降到约 780 万显存腰斩mIoU 代价通常小于 1 个点。改 features 时注意所有通道都要从元组推导不要写死 64、128 这些数字。这块的教训是先定显存预算再定模型宽度不要反过来。5.4 分割边缘锯齿严重上采样方式与训练-推理尺度不一致现象大目标整体分割对了但边缘像锯齿参差不齐细长结构断成一节一节或者同一张图训练集表现好、测试集边缘明显变差。原因两个典型原因。第一推理输入尺寸和训练不一致比如训练 256、推理直接喂原图 1024模型在小尺度学到的边缘语义被放大后失真。第二解码器用转置卷积产生棋盘格伪影边缘区域呈现规律性异常高分辨率输出时尤其明显。解决先保证推理预处理和训练完全一致resize 到同一个尺寸再送模型。其次把上采样从 ConvTranspose2d 换成双线性插值 3x3 卷积棋盘格基本消失。想再提升边缘质量用 TTA翻转后平均概率比任何后处理都稳。CRF 这类传统后处理我只在最终调优时试用提升有限且多一组参数要调。5.5 跳跃连接拼接报错输入尺寸不是 16 的倍数现象前向传播报 RuntimeError提示在 torch.cat([d, e4], dim1) 处尺寸不匹配比如某个维度一个是 16 一个是 17。原因UNet 有 4 次 stride2 下采样输入空间尺寸必须能被 2^416 整除。奇数尺寸或 513 这类数在第 4 次下采样后错位解码器上采样回来的尺寸和 skip 特征对不上。解决最省事的是在 Dataset 的 resize 步骤固定尺寸为 256 或 512天然满足倍数。如果输入源的宽高比不能等比缩放就先 pad 到 16 的倍数再推理输出后裁剪回来def pad_to_multiple(image, multiple16): h, w image.shape[:2] pad_h (multiple - h % multiple) % multiple pad_w (multiple - w % multiple) % multiple return cv2.copyMakeBorder(image, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value0)逻辑说明函数计算宽高各差多少像素到 16 的倍数用常数 0 补齐边推理完成后裁掉对应像素即可。这个函数对图像和 mask 都适用滑动窗口推理也常用到。6. 用 mIoU 验证分割效果计算方式、可视化检查与进阶技巧6.1 mIoU 计算的正确姿势mIoU 是分割任务里最通用的量化指标它计算预测和真实标注重叠的像素占两者总并集的比例对类别不平衡比像素精度诚实得多。二分类时只需算前景类的 IoU背景类永远接近 1算进去没有意义。def compute_iou(pred_mask, true_mask, eps1e-6): pred (pred_mask 0.5).astype(np.int64) true (true_mask 0.5).astype(np.int64) intersection np.logical_and(pred, true).sum() union np.logical_or(pred, true).sum() return intersection / (union eps) miou np.mean([compute_iou(p, t) for p, t in zip(preds, trues)])逻辑说明阈值 0.5 把 sigmoid 概率压成 0/1 标签是没有类别先验时的合理默认。每个样本独立算 IoU 再平均避免大图上若干小目标被整体均值稀释。多分类时把二值化换成 argmax逐类统计后再平均。6.2 可视化检查与两个进阶技巧指标数字会骗人分割可视化才是最后的照妖镜。模型内部是黑匣子但输出图对不对一目了然。我的习惯是每 20 个 epoch 从验证集挑 5 张图把原图、真实 mask、预测 mask 拼成一行保存重点看小目标有没有漏检、相邻目标有没有粘连、预测有没有整体偏移这三个问题在 mIoU 数字里很难暴露。进阶技巧有两个。多类分割把 out_channels 改成类别数损失换 CrossEntropyLoss输出后接 softmax 取 argmax部署用 torch.onnx.export 导出 ONNX量化成 float16 体积减半。性价比最高的是推理时做测试时增强输入水平翻转后再推一次两次概率平均再阈值化通常能换来 0.5 到 1 个点的 mIoU代价只是推理时间翻倍。我现在拿到新数据集的第一件事仍然是先打印 5 张图-标注对和 mask 像素分布数据干净了才开训。这套流程跑顺之后UNet 在多数二值分割场景都能给出够用的基线再往后换复杂结构也有据可依。希望帮到你。本文还有配套的精品资源点击获取