☰
U-Net家族四网络实战:从数据集到训练推理的完整语义分割工程
2026/10/28 20:49:30 网站建设 项目流程

简介:面向计算机视觉与医学图像分割方向的学习者与开发者,这套资源整合了Unet、AttentionUnet、R2Unet与R2AUet四种经典分割网络的完整可运行实现,并配套ISIC 2017皮肤病变数据集,可直接用于模型训练、对比实验与论文复现。压缩包共211个文件,以jpg与png图像样本为主体,辅以8个Python脚本承载网络结构与训练流程,另有xml标注、sh运行脚本及md说明文档,整体约25.48MB,目录组织便于按模型与数据模块检索。资源已有2163人学习下载,读者可从中获得四类网络的代码实现、数据预处理与训练配置参考,以及在同一数据集上横向比较各模型分割效果的实验基础,适合作为课程设计、科研入门或分割算法练手的实践素材。

1. 四个分割网络一次跑通:从数据集到训练脚本的完整落地

拿到一份写着「Unet、AttentionUnet、R2Unet、R2AUnet 可运行,包含数据集」的源码包,很多人的第一反应是解压、找train.py、直接python train.py,然后被路径报错、显存溢出、标签维度不匹配轮番教育。这份资源的定位很明确:它不是一个只放模型定义的代码片段合集,而是一套带数据集的语义分割实验工程,四个网络共用同一套数据加载、训练循环和评估流程,改一处配置就能横向对比四种结构的分割效果。适合谁?正在做医学图像、遥感地物、工业缺陷检测这类像素级分类任务,想快速搭一个可复现 baseline 的开发者;也适合刚接触分割、需要一份能跑通的完整工程来理解 U-Net 家族演进脉络的同学。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆开讲。

2. 工程结构与技术选型:四个网络为什么能共用一套训练管线

2.1 目录组织与模块职责

这类带数据集的完整工程,常见做法是把「模型」「数据」「训练」「工具」四块拆开,避免所有逻辑堆在一个文件里。解压后大概率能看到类似下面的结构(不同打包习惯会有出入,但职责划分基本一致):

project/ ├── datasets/ # 数据集根目录 │ ├── train/ │ │ ├── images/ # 训练原图 │ │ └── masks/ # 训练标签(掩码) │ └── val/ │ ├── images/ │ └── masks/ ├── models/ │ ├── unet.py # 基础 U-Net │ ├── attention_unet.py # 带注意力门的 U-Net │ ├── r2unet.py # 循环残差 U-Net │ └── r2attunet.py # 循环残差 + 注意力 ├── utils/ │ ├── dataset.py # Dataset / DataLoader 封装 │ ├── metrics.py # Dice、IoU 等指标 │ └── losses.py # 损失函数 ├── train.py # 统一训练入口 ├── predict.py # 单图/批量推理 └── config.py # 超参与路径配置

这个划分的价值在于:四个网络只负责「特征提取 + 上采样还原」这一件事,输入输出张量形状完全一致(输入[B, C, H, W],输出[B, num_classes, H, W]),所以训练脚本不需要为每个模型写一份。我一般会把模型选择做成配置项,而不是在train.py里写死from models.unet import UNet,这样切换网络只改一行字符串。

2.2 四个网络的结构差异与选型依据

U-Net 的核心是编码器下采样提特征、解码器上采样还原分辨率,再加跳跃连接把浅层细节拼回深层语义。另外三个都是在这个骨架上做加法:

网络关键改动适用场景代价
UNet对称编解码 + 跳跃连接通用分割 baseline参数量最小,边界偏粗
AttentionUNet跳跃连接上加注意力门控目标边界要求高、背景干扰多多一层门控计算,显存略增
R2UNet每个卷积块换成循环残差块(RRC)需要更深感受野、抑制梯度消失单块内多次循环,训练变慢
R2AUNetRRC + 注意力门控小目标、细长结构分割两者代价叠加,显存最吃紧

选型上有个血泪经验:不要一上来就上 R2AUNet 追求「最强」。循环残差块内部会把同一个卷积重复走几次,显存占用和单步耗时明显高于基础 UNet,如果数据集只有几百张图,收益往往抵不过训练成本。稳妥做法是先用 UNet 跑通全流程、拿到一个 Dice 基线,再逐个替换网络对比,这样出问题也容易定位是数据问题还是模型问题。

2.3 数据管线:Dataset 与增强的写法

分割任务的数据加载有两个容易翻车的点:图像和掩码必须做完全相同的几何变换,掩码不能做颜色类增强。常见做法是用 albumentations 的additional_targets把 mask 绑到同一组空间变换上:

import cv2 import numpy as np import albumentations as A from torch.utils.data import Dataset class SegDataset(Dataset): def __init__(self, img_dir, mask_dir, size=(256, 256), train=True): self.img_paths = sorted(Path(img_dir).glob("*.png")) self.mask_dir = Path(mask_dir) self.size = size # 训练集做几何+光度增强,验证集只做 resize if train: self.tf = A.Compose([ A.Resize(*size), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.2), ], additional_targets={"mask": "mask"}) else: self.tf = A.Compose([A.Resize(*size)], additional_targets={"mask": "mask"}) def __getitem__(self, idx): img_path = self.img_paths[idx] img = cv2.imread(str(img_path))[:, :, ::-1] # BGR -> RGB mask = cv2.imread(str(self.mask_dir / img_path.name), 0) # 灰度读掩码 out = self.tf(image=img, mask=mask) img = out["image"].astype(np.float32) / 255.0 mask = (out["mask"] > 127).astype(np.int64) # 二值化 # HWC -> CHW,供 PyTorch 卷积使用 return torch.from_numpy(img).permute(2, 0, 1), torch.from_numpy(mask)

additional_targets={"mask": "mask"}是这段的关键:它保证翻转、缩放对原图和掩码同步生效,否则会出现「图翻了、标签没翻」的错位,训练 loss 看着在降,预测结果却一塌糊涂。掩码用cv2.imread(..., 0)灰度读取再阈值二值化,是为了兼容不同标注工具导出的 0/255 或 0/1 两种取值。permute(2, 0, 1)把通道维提到最前,这是 PyTorch 卷积层的硬性要求,忘了这一步报错信息通常很隐晦。

3. 从零跑起来:环境、训练与推理的完整步骤

3.1 环境准备与依赖安装

这类工程对框架版本不算苛刻,但 PyTorch 和 CUDA 的匹配要自己确认。常见做法是先建独立环境,再按显卡驱动装对应版本的 torch:

conda create -n seg python=3.10 -y conda activate seg # 按本机 CUDA 版本选择,下面以 cu118 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python albumentations tqdm matplotlib

装完先跑一句python -c "import torch; print(torch.cuda.is_available())",输出True再往下走。如果这里是False,后面训练会静默落到 CPU 上,一个 epoch 跑半小时,很容易误以为是代码慢。依赖里albumentations版本差异较大,老版本某些 API 已废弃,遇到AttributeError优先怀疑它。

3.2 配置项与训练入口

训练脚本通常把路径、网络名、超参集中在config.py或argparse里。下面是一个可用的训练主循环骨架,重点看模型切换和指标计算两块:

import torch from torch import nn from torch.utils.data import DataLoader from models.unet import UNet from models.attention_unet import AttentionUNet from models.r2unet import R2UNet from models.r2attunet import R2AUNet from utils.dataset import SegDataset from utils.metrics import dice_coef MODEL_ZOO = { "unet": UNet, "attention_unet": AttentionUNet, "r2unet": R2UNet, "r2attunet": R2AUNet, } def train(cfg): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = MODEL_ZOO[cfg.model_name](in_ch=3, num_classes=1).to(device) train_ds = SegDataset(cfg.train_img, cfg.train_mask, train=True) val_ds = SegDataset(cfg.val_img, cfg.val_mask, train=False) train_loader = DataLoader(train_ds, batch_size=cfg.bs, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=1, shuffle=False) # 二分类用 BCEWithLogits,多类换成 CrossEntropyLoss criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.Adam(model.parameters(), lr=cfg.lr) for epoch in range(cfg.epochs): model.train() for img, mask in train_loader: img, mask = img.to(device), mask.float().unsqueeze(1).to(device) pred = model(img) loss = criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() # 验证阶段算 Dice,作为选模型的依据 model.eval() dices = [] with torch.no_grad(): for img, mask in val_loader: img, mask = img.to(device), mask.float().unsqueeze(1).to(device) pred = torch.sigmoid(model(img)) dices.append(dice_coef(pred, mask).item()) print(f"epoch {epoch} | loss {loss.item():.4f} | dice {sum(dices)/len(dices):.4f}")

几个参数要盯住:num_classes=1对应二分类(前景/背景),多类别分割要改成类别数并把损失换成CrossEntropyLoss,同时去掉squeeze那类操作;mask.float().unsqueeze(1)是把[B, H, W]补成[B, 1, H, W],和模型输出对齐,漏了这步会报维度广播错误;验证集batch_size=1是为了避免不同尺寸图像 padding 带来的指标偏差。dice_coef建议在utils/metrics.py里用平滑项实现,避免全背景图除零。

3.3 推理与结果可视化

训练完拿predict.py做单图推理,核心是把预处理、前向、后处理串起来,并保证和训练时的归一化方式一致:

import cv2, torch, numpy as np def predict_one(model, img_path, size=(256, 256), thr=0.5, device="cuda"): model.eval() img = cv2.imread(img_path)[:, :, ::-1] h, w = img.shape[:2] inp = cv2.resize(img, size).astype(np.float32) / 255.0 inp = torch.from_numpy(inp).permute(2, 0, 1).unsqueeze(0).to(device) with torch.no_grad(): prob = torch.sigmoid(model(inp))[0, 0].cpu().numpy() # 还原到原图尺寸再二值化,避免小图阈值偏差 prob = cv2.resize(prob, (w, h)) return (prob > thr).astype(np.uint8) * 255

阈值thr默认 0.5,但实际项目里前景占比很小时,0.5 往往偏保守,可以拿验证集扫一遍 0.3~0.7 找 Dice 最高的点。cv2.resize(prob, (w, h))放在 sigmoid 之后、二值化之前,是为了让概率图先回到原分辨率再判定,直接对缩略图二值化再放大边缘会有锯齿。

4. 避坑与排查:四个网络跑不通时先看这几处

4.1 显存溢出(CUDA out of memory)

现象:切到 R2UNet 或 R2AUNet 后第一个 batch 就 OOM,UNet 却正常。原因:循环残差块内部对同一特征反复卷积,中间激活值保留更多,显存占用显著上升。解决:先把batch_size减半,再把输入尺寸从 512 降到 256;仍不够就用torch.cuda.amp混合精度训练,通常能省 30%~40% 显存。不要盲目调小学习率,那和显存无关。

4.2 掩码维度或取值不匹配

现象:loss 一开始就是nan,或者 Dice 长期卡在 0 附近。原因:掩码是 0/255 未归一化,或者读成了三通道,和单通道输出做广播后形状对不上。解决:在 Dataset 里打印一次mask.max()和mask.shape,确认是单通道且取值在{0, 1};多类别任务确认标签是0..N-1的整型而不是 one-hot。

4.3 图像与掩码增强不同步

现象:训练集指标很高,验证集惨不忍睹,预测掩码整体偏移。原因:几何增强只作用在原图上,掩码没跟着变换。解决:检查增强库是否配置了additional_targets,或者干脆训练初期关掉所有几何增强,先确认基础流程能收敛,再逐步加增强。

4.4 验证指标虚高

现象:Dice 到 0.95 以上,肉眼看预测却很差。原因:验证集里背景占比极高,全预测背景也能拿到很高的像素准确率,而 Dice 在空掩码上计算时平滑项处理不当会失真。解决:同时看 IoU 和每张图的 Dice 分布,别只看平均值;确认dice_coef对空掩码有正确返回。

4.5 模型切换后忘记改输出通道

现象:换到多类别数据集后,BCEWithLogitsLoss报形状错误。原因:模型num_classes还是 1,损失函数也没换。解决:二分类用num_classes=1 + BCEWithLogitsLoss,多分类用num_classes=N + CrossEntropyLoss,两者不能混搭,这是最常见的配置错位。

5. 二次开发:把这份工程改成你自己的分割任务

5.1 换数据集的最小改动路径

接手一份别人的工程,我习惯先做「最小可跑通改造」:只动数据路径和类别数,其他一律不碰。具体是改config.py里的train_img / train_mask / val_img / val_mask四个路径,确认文件名能一一对应(图像和掩码同名是最省事的约定);然后改模型的in_ch(灰度图改 1,RGB 保持 3)和num_classes;最后同步改损失函数。这三处改完先跑 2 个 epoch,看 loss 是否下降、Dice 是否从 0 往上走,能走通再谈调参。

5.2 加一个新网络的接入方式

工程用MODEL_ZOO字典做模型注册,加网络就是加一个键值对。新模型只要满足「构造函数接受in_ch和num_classes、forward输入输出形状与现有网络一致」这两个契约,就能直接复用训练和推理脚本。我一般会在models/下新建文件,写完先单独跑一次前向:

import torch from models.my_net import MyNet x = torch.randn(2, 3, 256, 256) y = MyNet(in_ch=3, num_classes=1)(x) print(y.shape) # 期望 torch.Size([2, 1, 256, 256])

形状对上了再注册进MODEL_ZOO,能省掉大量在训练循环里排查维度问题的时间。

5.3 对比实验的记录习惯

四个网络横向对比时,最容易犯的错是「改了学习率又换了网络」,最后说不清提升来自哪里。我的做法是固定随机种子、固定数据划分、固定增强策略,只让model_name一个变量变化,每个实验把配置和验证集 Dice 写进一张表:

实验网络输入尺寸batchlr验证 Dice
exp1unet25681e-3待填
exp2attention_unet25681e-3待填
exp3r2unet25641e-3待填
exp4r2attunet25641e-3待填

注意 R2 系列因为显存原因 batch 只能开到 4,这时要么统一把四个网络都降到 4 保证公平,要么在结论里注明 batch 差异,别拿 batch=8 的 UNet 去和 batch=4 的 R2AUNet 直接比。

5.4 一个具体技巧:用验证集扫阈值

二分类分割的最终效果对判定阈值很敏感。训练结束后,把验证集所有概率图存下来,扫一遍阈值找最优:

import numpy as np best_thr, best_dice = 0.5, 0.0 for thr in np.arange(0.3, 0.71, 0.05): dices = [dice_coef((p > thr).float(), m) for p, m in val_probs] d = sum(dices) / len(dices) if d > best_dice: best_thr, best_dice = thr, d print(f"best thr={best_thr:.2f}, dice={best_dice:.4f}")

这个技巧在类别极不均衡的数据上经常能白捡几个点,而且不增加任何训练成本。扫出来的阈值记得写进推理脚本的配置,别训练完就忘了。

这套工程真正的价值不在四个网络本身,而在于它把「数据加载 → 训练 → 评估 → 推理」串成了一条能复现的链路,你换数据集、换网络、换损失都只需要动局部。我踩过最深的坑是拿到新工程先大改结构,结果原始代码能跑、改完跑不通,回头连对照版本都没有。从那以后我每次接手这类源码包,都强制先原样跑通一遍、记下基线指标,再动第一行代码。希望这份拆解能帮你少走几步弯路,把这份带数据集的四网络工程真正用起来。

本文还有配套的精品资源,点击获取

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

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

立即咨询