
简介Unet分割实战项目配套二值图像分割数据集面向深度学习入门到进阶的学习者以及需要复现图像分割任务的开发者。数据集依据DUT-OMRON构建训练集包含4135张图像及对应掩码测试集包含1033对图像与掩码全流程覆盖数据预处理、多尺度训练和自动推理。代码完整注释训练脚本会自动计算灰度均值与方差用于归一化并将样本随机缩放至原尺寸的0.5至1.5倍实现多尺度训练预处理函数在transforms.py中重新实现模型训练50个epoch后mIoU约0.72学习率采用余弦衰减损失与IoU曲线、训练日志及最佳权重均保存在run_results目录中预测脚本可直接推理inference下的全部图片。压缩包共2000个文件以png图片为主体另有py脚本、xml配置、readme说明等整体约223.63MB。已有442人学习下载适合需要一套完整可运行数据集的深度学习者参考训练流程与调参思路。1. DUT-OMRON上的Unet二值分割为什么拿它练手最划算很多入门教程让新手拿VOC或COCO练分割但多类别标注和类别不平衡会第一时间分散注意力。DUT-OMRON图像分割数据集反而是练Unet的好选择5168张512×512图片每张配一张二值掩码任务干净——只分前景和背景。但它不简单大量图片背景纹理繁复、目标边缘精细有些背景块和前景物体高度相似正好把二值图像分割里最真实的踩坑点边缘定位、过拟合、阈值选择一次性摆上台面。跑通一个Unet工程把验证IoU稳定推到0.8以上比单纯跑通demo更能建立对深度学习落地的整体手感。适合刚完成Unet代码复现的人也适合拿分割当毕设基线的同学。2. 看懂DUT-OMRON的数据结构目录、掩码和预处理细节2.1 目录布局图像和掩码的对应关系DUT-OMRON的发布包通常按images和ground_truth两个目录组织文件名一一对应图像是.jpg掩码是.png。第一次拿到压缩包我的习惯是先不急着写网络而是把目录里的图和掩码各打开几张看一遍——确认掩码是纯黑白的二值图还是带抗锯齿灰阶这一点直接决定后面读掩码时要不要做二值化。路径内容格式images/RGB原图.jpg512×512ground_truth/前景白色(255)、背景黑色(0)掩码.png512×512写代码前先做一件事统计两个目录的文件数找到只在某一侧存在的同名文件打印一个清单。这个动作不花时间却能避免训练到一半才发现在某张图上只有图没有掩码DataLoader直接抛异常。用bash一行就能完成ls images | wc -l ls ground_truth | wc -l comm -3 (ls images | sed s/\.jpg// | sort) (ls ground_truth | sed s/\.png// | sort) | head -20逻辑说明wc -l统计文件总数comm -3对比两个排序后的文件名列表输出只出现在其中一侧的文件名head限制最多显示20条。如果com输出的内容为空说明图片和掩码一一对应可以继续往下走。有些从网上二次打包的数据集会有大小写不一致或掩码缺失的情况先用这个命令排除干净后面省得排查。2.2 掩码的读取与二值化三个边界细节读掩码建议用cv2.imread(path, cv2.IMREAD_GRAYSCALE)一步到位得到单通道灰度图。常见的错误是直接cv2.imread默认模式得到三通道BGR然后不对三个通道作区分就喂给网络——浪费内存不说BCE损失会同时对三个通道求梯度等于把掩码信息重复算了三遍收敛看起来快了实际是虚假的梯度放大。第二个边界细节是二值化阈值。DUT-OMRON的掩码绝大多数像素只有0和255两个值但部分图在目标边缘存在压缩残留的灰色过渡像素。如果用mask / 255.0这种写法灰阶像素会变成0.5左右的浮点数交给训练后模型会对边缘灰度区产生模糊响应。用一个固定阈值先二值化mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask (mask 127).astype(np.float32)这样保证标签严格是0或1。127这个阈值没有玄学就是一个最通用折中值——既容忍边缘抗锯齿又把绝大多数残留灰阶清除。如果你的数据集不是DUT-OMRON换牙科X光图像之类的前景标注标注软件导出的PNG可能就是纯0/255但边缘偶尔有一点灰色残留同样用这个处理。第三个边界细节是resize时的插值方式。图像用INTER_LINEAR或INTER_AREA都行掩码必须用INTER_NEAREST。如果用BILINEAR去缩放二值掩码边缘会出现0.5的中间值训练时模型会认为边缘半透明分割结果自然糊。这一点写进Dataset定死后面能省掉一整类疑难杂症。2.3 用Dataset类封装DUT-OMRON一份可直接跑的模板把上面这些规范落到PyTorch里写成一个Dataset类。这个类是我在多个分割项目上改来改去留下来的模板——输入目录、划分比例、训练/验证开关参数单独拎出来方便后面换数据集时复用。import os import random import cv2 import numpy as np import torch from torch.utils.data import Dataset class DUTOMRON(Dataset): def __init__(self, img_dir, mask_dir, size512, trainTrue, seed42, valid_ratio0.2): self.img_dir img_dir self.mask_dir mask_dir self.size size self.train train # 只保留图和掩码两边都存在的文件 all_names sorted(os.listdir(img_dir)) all_names [n for n in all_names if n.lower().endswith(.jpg) and os.path.exists(os.path.join(mask_dir, n.replace(.jpg, .png)))] rng random.Random(seed) rng.shuffle(all_names) split int(len(all_names) * (1 - valid_ratio)) self.names all_names[:split] if train else all_names[split:] def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] # 图像用BGR转RGB便于后面接ImageNet预训练权重 img cv2.imread(os.path.join(self.img_dir, name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (self.size, self.size), interpolationcv2.INTER_LINEAR) # 掩码读灰度图二值化到0/1最近邻resize mask_path os.path.join(self.mask_dir, name.replace(.jpg, .png)) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask (mask 127).astype(np.float32) mask cv2.resize(mask, (self.size, self.size), interpolationcv2.INTER_NEAREST) # 训练时仅做水平翻转降低过拟合的同时不改变目标语义 if self.train: if np.random.rand() 0.5: img img[:, ::-1] mask mask[:, ::-1] # 转Tensor图像归一化到[0,1] img torch.from_numpy(img.copy()).permute(2, 0, 1).float() / 255.0 mask torch.from_numpy(mask.copy()).unsqueeze(0).float() return img, mask逻辑说明Dataset的__init__只做文件列表准备真正的IO放在__getitem__里训练时每个step才真正读图避免一次性把所有图都载入内存。训练时只做水平翻转没有用色彩抖动和随机裁剪原因在避坑章节会展开。注意copy()的用法——cv2读取的数组在转成tensor之后如果又被原地翻转内存中的引用会导致数据被共享修改copy()把它切干净。参数说明size512DUT-OMRON原图就是512×512默认不缩放显存紧张再考虑384或256valid_ratio0.2随机打乱后按8:2划分训练集和验证集seed固定保证每次实验划分一致结果可复现如果数据包中掩码缺失或命名不对列表推导式的过滤条件会直接把这些样本剔除训练前打印一次len(self.names)确认数量比如DUT-OMRON应该在四千一百张左右2.4 图像归一化一个必须先想清楚的岔路口上面Dataset里对图像只做/255.0归一化前提是模型编码器没有用ImageNet预训练权重。如果后面改成用torchvision提供的预训练ResNet做编码器就必须按预训练统计量做标准化mean torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1) std torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1) img (img / 255.0 - mean) / std这个选择要在一开始定下来否则模型换backbone时整个数据管线都要跟着改。二值分割任务里很多开源实现默认不做标准化也能跑那是因为模型从零训练一旦实验到后期想换成预训练编码器之前没加标准化就得回头重新训练浪费一轮时间。3. 搭建Unet编码器选型、跳连结构和损失函数的取舍3.1 编码器从零训练还是接预训练backboneUnet的核心骨架是编码器-解码器。编码器逐层下采样把512×512的RGB图压缩到32×32的低分辨率高语义特征解码器再逐层上采样还原空间分辨率最终输出512×512的1通道概率图。这里第一个决策点是编码器用自建的普通卷积栈还是换成预训练的ResNet或VGG。我刚接触Unet时是从零训练的模型简单、依赖少、哪里都能跑。但DUT-OMRON只有四千多张训练图从零训练base64的Unet在单卡上跑到验证IoU 0.8大约需要一百多个epoch时间成本明显偏高。后来换成torchvision的ResNet34做编码器前50个epoch就能到同样的水平。原因是ImageNet上预训练的卷积核已经具备纹理和边缘的底层特征分割任务需要的第一能力恰好是边缘定位。这个对比说明一个道理在深度学习项目里迁移学习的收益往往比调网络结构大得多尤其是中等规模数据集。如果选用预训练编码器额外记住一个前提输入要按ImageNet统计量做标准化且解码器分支仍然要重新训练。预训练权重只负责编码器部分解码器的卷积核全部随机初始化初始阶段梯度波动大学习率要比从零训练时更保守一般从1e-4往下调到5e-5起步。3.2 跳连接的作用concat而不是sumUnet和普通编码-解码网络最大的区别是跳连。编码器下采样会丢失空间细节而二值分割恰恰需要细节来定位边缘。跳连把每层下采样之前的特征直接送给对应解码层让解码器同时拿到高分辨率细节和低分辨率语义。实现跳连有两种常见接法。原始Unet做法是concat解码器上采样后的特征和跳连特征在通道维度拼接通道数翻倍后续卷积自己学怎么融合。另一种是sum逐元素相加参数少、显存省但等于强制两个特征图直接叠加容易淹没编码器里对比度高的边缘细节。在DUT-OMRON这种背景与前景灰度接近的数据上我一般选concat边缘信息更稳。下面定义基础版Unet采用4次下采样、concat跳连输出1通道logitsimport torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): 两次3x3卷积 BN ReLU的重复单元Unet的基础模块 def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class Down(nn.Module): 下采样最大池化 DoubleConv def __init__(self, in_ch, out_ch): super().__init__() self.mpconv nn.Sequential( nn.MaxPool2d(2), DoubleConv(in_ch, out_ch) ) def forward(self, x): return self.mpconv(x) class Up(nn.Module): 上采样转置卷积 跳连concat DoubleConv def __init__(self, in_ch, skip_ch, out_ch): super().__init__() self.up nn.ConvTranspose2d(in_ch, in_ch // 2, 2, stride2) self.conv DoubleConv(in_ch // 2 skip_ch, out_ch) def forward(self, x, skip): x self.up(x) diff_y skip.size(2) - x.size(2) diff_x skip.size(3) - x.size(3) x F.pad(x, [diff_x // 2, diff_x - diff_x // 2, diff_y // 2, diff_y - diff_y // 2]) x torch.cat([x, skip], dim1) return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch3, out_ch1, base64): super().__init__() self.inc DoubleConv(in_ch, base) self.down1 Down(base, base * 2) self.down2 Down(base * 2, base * 4) self.down3 Down(base * 4, base * 8) self.down4 Down(base * 8, base * 16) self.up1 Up(base * 16, base * 8, base * 8) self.up2 Up(base * 8, base * 4, base * 4) self.up3 Up(base * 4, base * 2, base * 2) self.up4 Up(base * 2, base, base) self.outc nn.Conv2d(base, out_ch, 1) def forward(self, x): x1 self.inc(x) x2 self.down1(x1) x3 self.down2(x2) x4 self.down3(x3) x5 self.down4(x4) x self.up1(x5, x4) x self.up2(x, x3) x self.up3(x, x2) x self.up4(x, x1) return self.outc(x)逻辑说明Down模块最大池化后接DoubleConv特征图尺寸减半、通道翻倍Up模块先用ConvTranspose2d将特征图尺寸翻倍然后与对应层跳连特征在通道维度拼接再经过DoubleConv融合。forward里返回的self.outc(x)是未经过Sigmoid的1通道logits方便后面接BCEWithLogitsLoss。参数说明base64时通道序列为[64,128,256,512,1024]参数量约3100万512输入时中间特征在32×32×1024显存占用主要在浅层512×512×64如果显存吃紧把base从64降到32参数量直接降到约1/4Up里的F.pad做的是尺寸对齐因为转置卷积偶发地会比跳连特征多出一个像素直接cat会报维度错误3.3 损失函数为什么用BCEDice而不是单独BCE二值分割最简单的是BCE。但DUT-OMRON里前景占比波动很大有的图前景只占全图2%左右。这种图单独用BCE几乎不提供有效梯度——模型把整图预测成背景loss照样很低。Dice Loss用预测区域和真实区域的重合度来算损失天然不敏感于类别不平衡。两个损失加权组合是分割任务的标配BCE负责逐像素的精度回归Dice负责整体集合一致。权重常见是五五开但严格来说应该统计一遍数据集的前景占比。如果多数图前景占10%以下把Dice权重加到0.7如果前景占比平均在30%左右BCE权重反而可以更大一点。下面给一个稳定的组合实现class BCEDiceLoss(nn.Module): def __init__(self, weight_bce0.5, weight_dice0.5, smooth1.0): super().__init__() self.bce nn.BCEWithLogitsLoss(reductionmean) self.weight_bce weight_bce self.weight_dice weight_dice self.smooth smooth def forward(self, logits, target): bce self.bce(logits, target) preds torch.sigmoid(logits) inter (preds * target).sum(dim(2, 3)) union preds.sum(dim(2, 3)) target.sum(dim(2, 3)) dice 1 - (2 * inter self.smooth) / (union self.smooth) return self.weight_bce * bce self.weight_dice * dice.mean()逻辑说明前向接收的是logits内部用BCEWithLogitsLoss把Sigmoid和BCE合并数值上更稳定。Dice部分先从logits求sigmoid概率再按batch内每张图分别算Dice系数后取平均。smooth默认1.0避免分母为0同时给梯度一个下限。参数说明weight_bce和weight_dice如果训练前期loss下降太快但边缘效果差往往是dice占比过高它倾向于先优化整体形状不抠细节调成0.3/0.7再试smooth不是越大越好1.0是最常用基准调大相当于给dice加了一个常数项稀释难样本的梯度reductionmean不要改成sum不同batch的loss才能跨训练步对比3.4 超参数初值从一套稳的配置开始再逐个调超参数初始值调整方向输入size512显存不够降到384/256batch size4至少保持4否则BN不稳定优化器AdamW lr1e-4SGDpoly lr2e-2也可最终精度略高但难调学习率调度CosineAnnealing 60epoch平台期换ReduceLROnPlateauweight_decay1e-4UNet里1e-5也常见结合验证集表现输入归一化非预训练除255即可用预训练编码器则按ImageNet统计量表里的配置有两处要强调。第一batch size不要只有2。Unet里BN层的统计量在小batch下抖动大训练loss会来回跳验证IoU也随之不稳。如果显存只能放2张要么把size从512降到384要么用GroupNorm替换BN——后一种改起来不复杂但很多人没用过。第二AdamW和SGD-poly在分割里都能用AdamW好调、收敛快SGDpoly最终精度往往略高但对学习率初始值敏感。DUT-OMRON这种中等规模数据图稳选AdamW第一个项目迭代调好学习率就很少翻车。4. 训练与验证跑通最小循环再用曲线诊断问题4.1 最小训练循环断点续训和梯度裁剪一起上数据集和模型拼起来就是一个最小但完整的训练脚本。下面这个框架不依赖额外可视化本机没装tensorboard也能跑。观察曲线也不一定非要tensorboardDUT-OMRON这种实验里把每个epoch的loss和IoU写进日志文件更直接。from torch.utils.data import DataLoader from torch.optim import AdamW train_ds DUTOMRON(data/DUT-OMRON/images, data/DUT-OMRON/ground_truth, trainTrue) val_ds DUTOMRON(data/DUT-OMRON/images, data/DUT-OMRON/ground_truth, trainFalse) train_loader DataLoader(train_ds, batch_size4, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size4, shuffleFalse, num_workers4, pin_memoryTrue) model UNet().cuda() criterion BCEDiceLoss(weight_bce0.5, weight_dice0.5) optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max60, eta_min1e-6) for epoch in range(1, 61): model.train() total_loss 0.0 for img, mask in train_loader: img img.cuda() mask mask.cuda() logits model(img) loss criterion(logits, mask) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 12.0) # 防梯度爆炸 optimizer.step() total_loss loss.item() scheduler.step() val_iou validate(model, val_loader) print(fepoch {epoch:02d} | loss {total_loss / len(train_loader):.4f} | val_iou {val_iou:.4f}) if (epoch 1) % 5 0: torch.save({ model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), }, fcheckpoints/unet_epoch{epoch1}.pt)逻辑说明每个epoch先train后验证。每5个epoch保存checkpoint里面同时存模型、优化器和调度器状态——因为AdamW有动量和二阶矩只存模型权重的话断点续训会丢失优化器状态恢复后前几个epoch效果明显变差等于白续。clip_grad_norm_的阈值12是常用初始值BCEDice的梯度在遇到难样本时偶尔会到几十的范数超过一定阈值必炸。如果看到loss出现nan第一时间把这个值调小到5再跑。参数说明num_workers4是从文件读图的并行进程数在Windows下要放到ifname main里Linux无此问题pin_memoryTrue在GPU训练时有小幅加速显存不紧张就开着T_max60要和总epoch数一致否则余弦退火会在训练结束前就下探到eta_min尾部学习率变成常量4.2 验证指标IoU怎么算以及准确率为什么骗人二值分割报告里写Accuracy基本没意义。DUT-OMRON大量图前景占比不到10%把整张图预测成背景准确率也有90%以上但分割显然全错。验证集用IoU和F1两种指标一起看。IoU是交集除以并集按单张图算再求平均即mIoU。写验证函数时有个实现细节如果按全图把所有像素累计在一起算IoU大目标前景占比40%的图会主导结果小目标前景占比2%的图涨跌根本看不见。按图平均更符合DUT-OMRON的长尾特性。验证代码def validate(model, loader): model.eval() iou_list [] f1_list [] with torch.no_grad(): for img, mask in loader: logits model(img.cuda()) pred (torch.sigmoid(logits) 0.5).float() inter (pred * mask).sum(dim(2, 3)) union (pred mask).sum(dim(2, 3)) - inter iou (inter 1e-6) / (union 1e-6) f1 2 * inter / (2 * inter (pred mask).sum(dim(2, 3)) - 2 * inter 1e-6) iou_list.append(iou.cpu().numpy()) f1_list.append(f1.cpu().numpy()) return float(np.mean(np.concatenate(iou_list))), float(np.mean(np.concatenate(f1_list)))逻辑说明pred由sigmoid(logits) 0.5得到后面避坑章节会讲0.5不是永远最优。inter与union都是按每张图的空间维度求和得到的是batch内每张图的IoU向量再汇总所有图像求平均。这样每张图权重相等不会被前景占比大的图带偏。4.3 从loss曲线和IoU曲线里读问题第一个epoch的train loss如果直接落在1.0附近BCEDice网络初始输出基本是平均概率0.5正常。之后train loss应稳定下降val IoU在前10-15个epoch爬升明显后面变缓。三种典型异常train loss下降val IoU震荡或停滞过拟合常见于第40个epoch以后。DUT-OMRON训练图只有四千张Unet三千万参数50个epoch后开始记住训练集纹理是常态。对策是增强或提前停止。train loss有下降但很慢val IoU一直低于0.6学习率太小或loss权重失衡。把lr调大3到5倍或把BCE权重调大。train loss突然升高到接近初始值学习率过高或数据里有损坏样本。先把某几个loss特别大的batch彩色打印出来看是不是加载错了图或掩码。5. 避坑记录DUT-OMRON训练Unet的5个典型翻车现场这些坑大多数不是DUT-OMRON独有但在它上面暴露得特别典型——背景复杂、前景占比悬殊、边缘精细每一项都踩中分割模型最容易出问题的点。5.1 现象Loss不降输出全是黑的或者全白的原因最常见是学习率过大。AdamW初始1e-4是分割网络的常用区间如果第一次照搬分类网络的3e-4或更高BCE部分会在前几个step把logits推到极端值之后梯度饱和loss死在一个高位不动。另一个可能原因是输出层的bias初始化为较大的正值导致初始Sigmoid输出接近1网络一上来就认为全是前景。解决lr调回1e-4以下输出层用nn.Conv2d默认初始化即可多数情况下默认负偏置会导致初始全黑输出这是正常现象训练几十个iteration就会纠正。如果用了SGD把lr设为0.02并观察前20个iteration的loss斜率斜率接近0就说明初始配置出了问题。5.2 现象训练loss持续下降验证IoU卡在0.7过拟合原因DUT-OMRON背景纹理重复度高训练后期模型用跳连里的高频特征把背景纹理背下来了验证集上的泛化就停住。此时不要急着上更复杂的模型Unet在DUT-OMRON上的上限远没到0.9先怀疑训练策略。解决把数据增强从只有翻转扩大到小角度旋转±15° 随机缩放0.9到1.1 轻微亮度扰动这能直接打断背景纹理的逐像素记忆。如果加了增强后val IoU反而降说明增强强度过大把角度范围缩小一半再看。增强的强度不是越大越好这一点要记住它是个玄学参数只能靠实验。5.3 现象IoU还行但放大分割图边缘全是棋盘格纹理原因ConvTranspose2d上采样产生棋盘效应。Unet解码器每一层都在用kernel2 stride2的转置卷积边缘处特征图相邻像素的贡献不均匀叠加到最终输出形成规律的格状纹理。DUT-OMRON目标边缘细这个缺陷特别容易被发现。解决把Up模块里的ConvTranspose2d替换成Upsample(modebilinear)加3×3卷积。参数量略降训练会快一点边缘明显干净。替换后看第一个epoch的分割可视化——如果还有棋盘重点确认所有上采样都换了Unet里有几处上采样就要换几处。5.4 现象OOMbatch_size2也爆显存原因512×512输入、base64、编码器最深1024通道最占显存的是第一层512×512×64和第三层128×128×256的激活值。fp32下这些激活值一个batch四张大约占用3到4GB加上反向传播的梯度缓冲8G卡很紧张。解决按顺序试三件事开启AMP混合精度一般能省30%到40%显存把base从64降到32把输入尺寸从512降到384。AMP之后在DUT-OMRON上IoU损失通常在0.5个点以内几乎可以忽略。优先级一定要排对不要一上来就砍输入尺寸——DUT-OMRON很多精细边缘在降采样后直接消失影响是永久性的。5.5 现象验证IoU稳定但可视化目标有空洞前景不连续原因固定用0.5做二值化阈值。BCEDice收敛后模型输出的sigmoid概率在目标内部可能只有0.2到0.4因为Dice优化的是集合相似度它允许内部概率偏低只要边缘包围圈成立。用0.5一刀切就会得到边缘包围但内部有空洞的掩码。解决在验证集上做阈值扫描从0.2到0.8步长0.05画出阈值-IoU曲线取峰值对应阈值作为最终分割阈值。DUT-OMRON上常见最优阈值落在0.3到0.45之间。这也解释了为什么有些项目里概率图看着漂亮、一保存mask就烂——八成就是阈值问题。6. 进阶把验证IoU从0.78推到0.9的三个低成本改动6.1 多层Deep Supervision让浅层解码器也拿监督标准的Unet只在最后输出上计算loss解码器较浅的层只能间接收到梯度在DUT-OMRON背景复杂区域收敛慢。加一个辅助监督分支在up1、up2的输出后各接一个1×1卷积上采样回原尺寸分别算loss并乘以0.3的权重加到总loss上。改动量很小但浅层解码器收敛明显加快。6.2 边界加权BCE边缘像素的loss权重翻倍先用3×3膨胀核对mask做膨胀取膨胀结果与原mask的差作为边缘掩码然后让边缘像素在BCE里的权重乘2。这个改动对目标边缘完整度的提升很直接几行代码实现在DUT-OMRON上边界F1通常能涨1个点左右。6.3 测试时增强水平翻转和垂直翻转取平均验证阶段把每张图分别用原图、水平翻转、垂直翻转、水平垂直同时翻转跑四次四次sigmoid输出取平均再二值化。改动几乎为零却能在DUT-OMRON上稳定带来1到2个点的IoU提升。需要注意batch size要相应缩小显存占用是原来的4倍。我自己的习惯是改进每次只动一个变量把前后IoU记在同一张表里用数据说话。DUT-OMRON上的Unet项目跑通不难但知道每一步改动到底值多少IoU、哪些参数不值得反复调这比复现精度本身更宝贵。希望帮到你。本文还有配套的精品资源点击获取