☰
PyTorch原生多类别Unet语义分割项目:从数据加载到实时推理全链路实现
2026/11/6 3:55:00 网站建设 项目流程

简介:本资源是面向深度学习初学者与计算机视觉实践者的PyTorch语义分割实战项目,聚焦多类别图像分割任务,适用于医学影像、遥感分析等需精细像素级分类的场景。资源完整实现U-Net网络结构,涵盖数据加载、模型定义(含编码器-解码器与跳跃连接)、多类别输出适配、交叉熵损失计算、Adam优化及评估指标(IoU、混淆矩阵)可视化等核心环节。压缩包共46个文件,以19个Python源码(如unet.py、train.py、datasets/模块)为主体,辅以24个编译缓存文件、2个配置说明txt及1个info.json元信息文件,结构清晰,模块职责分明,便于理解整体训练流程与代码组织逻辑。资源包仅69KB,轻量易部署,已有15289人学习下载。读者可直接复现端到端训练流程,获取含数据增强、学习率调度、权重计算、日志记录与结果可视化的完整工程框架,显著降低从理论到落地的实践门槛。

1. 一个能跑通的多类别Unet语义分割项目:从标注图到预测掩码,全程PyTorch原生实现,不碰任何黑匣子封装库

上周帮某高校实验室调试一个农业病害图像分割任务,他们手头有327张带像素级标注的叶片图(健康、早期褐斑、晚期腐烂、虫蛀四类),但用现成的Segmentation Models库死活调不出IoU>0.65的结果。最后发现根本不是模型问题——是数据加载时把四通道标注图当RGB读了,label值全被clip到0~255,类别ID直接错乱。这件事让我重新翻出这个纯PyTorch实现的Unet项目:它不依赖torchvision的transforms做自动归一化,不封装Dataset抽象层,所有预处理逻辑明明白白写在dataset.py里;训练循环不用Lightning的trainer,每个loss.backward()和optimizer.step()都裸写在train.py中。如果你正卡在“数据对不上”“loss不降”“预测全是灰块”这些玄学问题上,或者想把Unet嵌进自己的工业检测流水线里(比如接OpenCV实时推理),这个项目就是你该下载的第一个可运行基线——它用最直白的PyTorch语法,把多类别语义分割从数据准备、模型构建、训练监控到结果可视化,拆成你能逐行debug的代码块。

2. 项目结构与技术栈:为什么选PyTorch原生而非HuggingFace或Albumentations

这个项目刻意规避了当前主流的高阶封装库,核心动机很实际:当你的标注图是16位TIFF格式(显微镜图像常见)、类别数超过10类、且需要自定义损失函数(比如给罕见病灶加focal loss权重)时,Albumentations的transform pipeline会 silently drop 非uint8数据,而HuggingFace的Trainer默认把label当作分类任务处理,无法正确映射像素级类别索引。本项目采用极简技术栈组合:PyTorch 1.13+(兼容CUDA 11.7)、NumPy 1.23、OpenCV-Python 4.8,所有增强操作用原生numpy数组完成,确保每一步数据变换都能打印shape和unique值验证。这种“返璞归真”的设计让调试成本大幅降低——你不需要去读Albumentations源码找某个transform的边界条件,直接在dataset.py第47行加print(f"Before normalize: {img.max()}, {mask.max()}")就能定位问题。

2.1 目录结构解析:五个文件撑起完整流程

项目解压后只有5个核心文件,无任何隐藏配置或动态生成模块:

unet_multiclass/ ├── dataset.py # 数据加载核心:支持PNG/TIFF/JP2K,自动识别单通道灰度标注图 ├── model.py # Unet实现:编码器用ResNet34 backbone(可替换),解码器含4级上采样+skip connection ├── train.py # 训练主逻辑:含混合精度训练(AMP)、学习率warmup、early stopping ├── predict.py # 推理脚本:支持单图/批量预测,输出彩色mask+叠加原图+CSV统计表 └── config.yaml # 全局配置:类别名、颜色映射、路径、超参(非硬编码!)

提示:config.yaml是唯一需要你手动修改的配置文件。其他文件全部保持原样即可运行,避免“改一处崩三处”的血泪经验。

2.2 dataset.py:多类别标注图加载的关键细节

多类别语义分割最大的坑在于标注图的存储格式。很多新手用Photoshop保存标注图,结果导出为RGB三通道图,每个类别用不同颜色(如红色=类别1,绿色=类别2),但模型需要的是单通道整数矩阵,其中每个像素值等于类别ID(0,1,2,3...)。本项目的dataset.py通过以下逻辑解决该问题:

# dataset.py 第32-45行 def _load_mask(self, mask_path): mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) # 强制读取原始位深 if len(mask.shape) == 3: # RGB格式标注图 # 将RGB转为单通道类别ID:查表法,避免浮点误差 rgb_to_id = { (0, 0, 0): 0, # 背景黑色 (255, 0, 0): 1, # 红色→类别1 (0, 255, 0): 2, # 绿色→类别2 (0, 0, 255): 3, # 蓝色→类别3 } h, w, _ = mask.shape id_mask = np.zeros((h, w), dtype=np.uint8) for rgb, cid in rgb_to_id.items(): id_mask[(mask == rgb).all(axis=2)] = cid return id_mask else: # 单通道灰度图,直接返回 return mask.astype(np.uint8)

这段代码的关键在于:它不假设标注图一定是单通道,而是主动检测并转换RGB标注图。实践中我们遇到过某医疗数据集用(255,128,0)表示肿瘤区域,只需在rgb_to_id字典里新增一行即可适配,无需重绘标注图。

2.3 model.py:Unet编码器-解码器的PyTorch原生实现

模型文件model.py完全基于torch.nn.Module构建,未使用任何第三方backbone封装。编码器部分采用ResNet34(可通过修改encoder_depth参数切换为ResNet18/50),其输出特征图尺寸严格遵循2的幂次衰减规律(H/2, H/4, H/8, H/16, H/32),这保证了skip connection时张量尺寸能精确对齐:

# model.py 第89-95行:解码器上采样模块 class DecoderBlock(nn.Module): def __init__(self, in_channels, out_channels, skip_channels=0): super().__init__() self.conv1 = nn.Conv2d(in_channels + skip_channels, out_channels, 3, padding=1) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1) self.bn2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x, skip=None): x = F.interpolate(x, scale_factor=2, mode="bilinear", align_corners=False) # 双线性插值上采样 if skip is not None: x = torch.cat([x, skip], dim=1) # 拼接skip connection x = self.relu(self.bn1(self.conv1(x))) x = self.relu(self.bn2(self.conv2(x))) return x

注意F.interpolate的align_corners=False参数——这是PyTorch 1.2+的默认行为,但很多老教程仍写True,会导致上采样后边缘像素偏移,最终mask出现1像素错位。本项目强制设为False,与OpenCV的resize行为一致,避免部署时前后端结果不一致。

3. 运行全流程:从准备数据到生成预测报告

要让这个项目真正跑起来,你不需要从零开始造轮子。整个流程分为四个明确阶段:数据组织 → 配置修改 → 训练启动 → 结果分析。每个阶段都有可验证的中间产物,杜绝“运行完不知道成功没”的焦虑。

3.1 数据准备:按规范组织文件夹结构

项目要求数据必须按以下结构存放(路径可自定义,但相对关系不能变):

data/ ├── images/ # 原图,支持.jpg/.png/.tiff,建议统一为.png │ ├── 001.png │ ├── 002.png │ └── ... ├── masks/ # 标注图,必须与images同名,支持.png/.tiff │ ├── 001.png # 单通道灰度图 或 RGB伪彩色图 │ ├── 002.png │ └── ... └── splits/ # 划分文件(可选,若不提供则自动按8:1:1划分) ├── train.txt # 每行一个文件名(不含扩展名) ├── val.txt └── test.txt

注意:masks/下的标注图必须与images/下同名,且尺寸完全一致。若尺寸不匹配,dataset.py会在__getitem__中抛出ValueError并提示具体哪张图尺寸异常,这是比静默跳过更友好的设计。

3.2 修改config.yaml:三处必改项说明

打开config.yaml,重点关注以下三个字段(其他参数可保持默认):

# config.yaml data: root_dir: "./data" # 数据根目录,绝对路径或相对路径均可 num_classes: 4 # 类别总数(含背景类!) class_names: ["background", "leaf", "spot", "rot"] # 类别名列表,顺序必须与ID一致 class_colors: ["#000000", "#FF0000", "#00FF00", "#0000FF"] # 十六进制颜色,用于可视化 train: batch_size: 8 # 根据GPU显存调整:RTX3090可设16,GTX1060建议4 epochs: 100 lr: 0.001 weight_decay: 1e-4 model: encoder_name: "resnet34" # 可选:"resnet18", "resnet34", "resnet50" pretrained: true # 是否加载ImageNet预训练权重

关键细节:num_classes必须包含背景类(background),所以如果你有4个目标类别(如叶片、病斑、腐烂、虫蛀),这里填4,class_names列表长度也必须是4。class_colors的颜色顺序必须与class_names严格对应,否则可视化时颜色和类别会错位。

3.3 启动训练:监控关键指标的正确姿势

在项目根目录执行训练命令:

python train.py --config config.yaml --device cuda:0

训练过程中,控制台会实时输出以下关键信息(每10个batch刷新一次):

Epoch 1/100 | Batch 10/125 | Loss: 1.8243 | IoU: 0.321 | Acc: 0.789 | LR: 0.0010

其中:

  • Loss: 加权交叉熵损失(对每个类别按频率反比加权)
  • IoU: 所有类别IoU的宏平均(macro-average),即先算每个类别的IoU,再求均值
  • Acc: 像素级总体准确率(不具参考价值,仅作辅助)

提示:前5个epoch的IoU通常低于0.2,这是正常现象。重点观察第10~20epoch的IoU是否稳定上升。若连续10个epoch IoU无提升,检查config.yaml中的weight_decay是否过大(导致权重衰减过猛)。

训练完成后,模型权重保存在checkpoints/best_model.pth,同时生成logs/train_metrics.csv记录每轮指标,可用Excel或pandas直接分析。

4. 避坑指南:五个真实踩过的坑及解决方案

在多个实际项目中复现此代码时,我们反复遇到以下问题。这些问题看似琐碎,却能让训练停滞数小时甚至数天。以下是经过验证的解决方案:

4.1 现象:训练loss为nan,且从第一个batch就开始

原因:标注图中存在超出num_classes范围的像素值(如类别ID=5,但num_classes=4),导致CrossEntropyLoss计算log(0)产生-inf,后续梯度爆炸。
解决:在dataset.py的_load_mask方法末尾添加校验:

# dataset.py 第52行追加 assert mask.max() < self.num_classes, f"Mask {mask_path} contains invalid class ID {mask.max()}, max allowed is {self.num_classes-1}"

运行python train.py前先执行此校验,报错时立即修正标注图。

4.2 现象:验证集IoU远高于训练集IoU(如train:0.45 vs val:0.72)

原因:train.py中数据增强(如RandomHorizontalFlip)仅应用于训练集,但val_loader的dataset未设置augmentation=None,导致验证时也应用了随机翻转,破坏了评估一致性。
解决:检查train.py第112行,确保验证集Dataset初始化时传入augmentation=None:

val_dataset = SegmentationDataset( img_dir=os.path.join(cfg.data.root_dir, "images"), mask_dir=os.path.join(cfg.data.root_dir, "masks"), split_file=os.path.join(cfg.data.root_dir, "splits", "val.txt"), augmentation=None, # 必须显式设为None! ... )

4.3 现象:预测结果全是单一颜色(如全为红色)

原因:predict.py中apply_color_map函数将预测的logits直接argmax后映射颜色,但未对logits做softmax归一化,导致argmax结果受数值尺度影响不稳定。
解决:修改predict.py第68行,增加softmax:

# predict.py 第68行 pred_probs = torch.softmax(pred_logits, dim=1) # 先归一化 pred_mask = torch.argmax(pred_probs, dim=1).cpu().numpy() # 再argmax

4.4 现象:训练速度极慢(单batch>5秒),GPU利用率<10%

原因:dataset.py中cv2.imread读取TIFF时默认使用多线程解码,与PyTorch DataLoader的多进程冲突,引发I/O锁死。
解决:在dataset.py顶部添加环境变量设置:

# dataset.py 第1行 import os os.environ["OPENCV_IO_ENABLE_JASPER"] = "0" # 禁用Jasper TIFF解码器 os.environ["OPENCV_IO_MAX_IMAGE_PIXELS"] = str(1024*1024*1024) # 允许大图

4.5 现象:predict.py报错RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same

原因:模型权重用CPU保存,但预测时指定--device cuda:0,导致模型在GPU而输入在CPU(或反之)。
解决:统一设备加载,在predict.py第42行修改模型加载逻辑:

# predict.py 第42行 model = load_model(cfg.model, cfg.data.num_classes) model.load_state_dict(torch.load(cfg.checkpoint_path, map_location=device)) # 显式指定map_location model = model.to(device) # 再移动到目标设备

5. 多类别分割的二次开发:从单图预测到工业级流水线集成

当你已跑通基础训练并获得满意IoU后,真正的价值在于改造它以适配你的业务场景。下面以某工业质检项目为例,展示三个关键改造点:如何接入实时摄像头流、如何导出ONNX供C++部署、以及如何为长尾类别定制损失函数。

5.1 改造predict.py支持OpenCV实时推理

原predict.py只支持静态图片,但产线需要USB摄像头实时检测。我们只需在predict.py中新增--source参数,并重写推理循环:

# predict.py 新增第156行 if args.source == "webcam": cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # 预处理:BGR->RGB->归一化->tensor img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img_tensor = torch.from_numpy(img_rgb).float().permute(2,0,1) / 255.0 img_tensor = img_tensor.unsqueeze(0).to(device) # 添加batch维度 with torch.no_grad(): pred_logits = model(img_tensor) pred_mask = torch.argmax(torch.softmax(pred_logits, dim=1), dim=1).cpu().numpy()[0] # 可视化:将mask叠加到原图 color_mask = apply_color_map(pred_mask, cfg.data.class_colors) overlay = cv2.addWeighted(frame, 0.6, color_mask, 0.4, 0) cv2.imshow("Real-time Segmentation", overlay) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

此改造的关键在于:预处理必须与训练时完全一致。训练时dataset.py用cv2.imread读图后做/255.0归一化,此处也必须用相同方式,否则模型输入分布偏移导致漏检。

5.2 导出ONNX模型供C++部署

PyTorch模型需转为ONNX才能被OpenCV DNN模块或TensorRT加载。在model.py中确保模型forward方法无动态控制流(如if/for),然后执行导出:

python -c " import torch from model import UNet model = UNet(encoder_name='resnet34', num_classes=4) model.load_state_dict(torch.load('checkpoints/best_model.pth')) model.eval() dummy_input = torch.randn(1, 3, 512, 512) # 输入尺寸需与训练一致 torch.onnx.export(model, dummy_input, 'unet_multiclass.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch', 2: 'height', 3: 'width'}, 'output': {0: 'batch', 2: 'height', 3: 'width'}}) "

导出后用Netron工具打开unet_multiclass.onnx,确认输入输出shape正确。特别注意dynamic_axes参数——它允许C++端传入任意尺寸图像(只要能被32整除),避免固定尺寸带来的裁剪失真。

5.3 为长尾类别定制Focal Loss

在某缺陷检测任务中,背景类占92%,划痕类仅占3%,标准CE Loss导致模型忽略划痕。我们在train.py中替换损失函数:

# train.py 第203行,原loss_fn = nn.CrossEntropyLoss() class FocalLoss(nn.Module): def __init__(self, alpha=1, gamma=2, reduction='mean'): super().__init__() self.alpha = alpha self.gamma = gamma self.reduction = reduction def forward(self, inputs, targets): ce_loss = F.cross_entropy(inputs, targets, reduction='none') pt = torch.exp(-ce_loss) focal_weight = (1 - pt) ** self.gamma loss = self.alpha * focal_weight * ce_loss if self.reduction == 'mean': return loss.mean() return loss # 在train_loop中使用 loss_fn = FocalLoss(alpha=2.0, gamma=2.0) # alpha加大难样本权重

alpha值需根据类别频率反比设置:若划痕类占比3%,则alpha ≈ 1/0.03 ≈ 33,但实践中从2.0开始试,逐步增大至loss不再下降为止。

从那以后我每次接到新分割任务,第一件事就是用这个项目搭baseline:先跑通train.py看loss曲线是否健康,再用predict.py --source webcam验证实时性,最后根据业务需求在model.py或train.py中插入定制逻辑。它不追求SOTA指标,但保证每一步都透明可控——当客户指着屏幕问“为什么这个区域没标出来”,我能立刻定位到是数据加载时的mask读取逻辑、还是损失函数的类别权重设置,而不是在封装库的层层抽象中迷失。希望帮到你。

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

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

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

立即咨询