ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SegNet图像分割实战:PyTorch源码详解与调优指南

SegNet图像分割实战:PyTorch源码详解与调优指南 简介基于PyTorch实现SegNet图像分割的Python源码项目曾获导师指导并认可属98分大作业设计。资源面向计算机相关专业正在完成课程设计、期末大作业的学生也适合需要图像分割实战练习的开发者可帮助理解编码器-解码器结构、上采样与跳跃连接等SegNet关键环节。压缩包共119个文件体积27.19MB以py源码与png图像为主另含pyc编译文件、sh运行脚本、pth模型权重、README说明、Dockerfile及日志配置等便于直接复现、训练与调试。已有176人学习下载适合作为课堂项目参考、深度学习入门实践或毕业设计前的基础演练。内容包括完整的项目目录、训练日志与配置模块可据此梳理数据准备、模型搭建、训练评估和推理流程节省从零搭环境与抠代码的时间。1. 一个能直接跑的 SegNet 源码包适合期末大作业也适合快速落地分割任务对于正在赶 pytorch segnet 图像分割任务的人来说最耗时间的往往不是找不到代码而是拿到的工程要么路径写死、一跑就崩要么网络结构和论文对不上训了两天 loss 还在原地打转。这份基于 PyTorch 实现 SegNet 的图像分割任务源码是从一份高分期末大作业里整理出来的完整产物编码器、解码器、数据加载器、训练主循环、指标计算和可视化全部拆开按文件放好目录结构干净改数据集和调参都不用翻遍十几个文件。它适合课程设计、期末大作业也适合需要快速拿一个分割 Demo 验证想法的从业者。下面我按“网络结构 → 数据准备 → 训练推理 → 避坑 → 调优”这条链路带你把它真正跑起来。2. 拆开源码包SegNet 的前向逻辑与工程文件分工2.1 编码器-解码器设计池化索引这个“黑匣子”里存了什么SegNet 是对称的编码器-解码器结构。编码器部分基本沿用经典视觉骨干的前五个卷积段每经过一次 MaxPool特征图尺寸减半、通道数递增到最后阶段通道数来到 512。解码器再逐层把尺寸翻回来通道数对称递减最后一层用卷积把输出压到 num_classes 个通道。整套网络没有任何全连接层所以输入分辨率可以任意变化这一点比固定输入的 CNN 分类模型更适合做分割实验。和常见的 FCN 用反卷积做上采样不同这份源码里的解码器与编码器共用同一组池化索引。PyTorch 的 MaxPool2d 和 MaxUnpool2d 都支持 return_indices 参数编码器在每层池化时把最大值在原特征图上的坐标记录下来解码器用这些坐标把稀疏特征“放回”原位。这一步不引入任何可学习参数因此也叫零参数上采样。它的实际好处是边界恢复得比反卷积更锐利因为物体边缘的位置信息在编码时就固定下来了不需要网络再学一遍。先看编码器某个 block 的实现# encoder.py 片段 class EncoderBlock(nn.Module): 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), ) self.pool nn.MaxPool2d(2, stride2, return_indicesTrue) def forward(self, x): x self.conv(x) x, indices self.pool(x) return x, indices关键在 pool 这一行。return_indicesTrue 让前向输出多一个 indices形状和池化后的特征图一致每个位置记录的是它对应的 2×2 窗口里最大值被取走的下标。这个 indices 要原封不动传给解码器里同深度的 MaxUnpool2d所以前向时一般会用一个列表把它逐层保存下来。解码器侧的实现是这样的# decoder.py 片段 class DecoderBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.unpool nn.MaxUnpool2d(2, stride2) 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, indices): x self.unpool(x, indices) x self.conv(x) return x参数说明MaxUnpool2d 的 kernel_size、stride 必须和编码器里对应的 MaxPool2d 一致否则放回位置错乱训练 loss 会来回收缩、很难压下去。indices 必须来自同深度的编码器层不能跨层复用这是很多人改代码时形状对上了但输出全是噪点的原因。池化索引的代价也藏在名字里每一层 pooling 都要额外保存一份索引数组输入分辨率越大、batch 越大这部分显存占用越明显。训练 512×512 输入时batch size 8 以上很容易触顶显存建议 4 起步这个坑后面专门讲。2.2 文件结构与数据流从 main.py 到 dataloader 的完整调用链打开这份源码包第一层目录结构大概是这样segnet_pytorch/ ├── main.py # 训练入口参数解析、训练循环、权重保存 ├── config.py # 默认超参学习率、batch、epoch、类别数 ├── model/ │ ├── __init__.py │ ├── segnet.py # 组装 encoder decoder输出 logits │ ├── encoder.py # 编码器五个 block带池化索引 │ └── decoder.py # 解码器五个 block 最后输出卷积 ├── dataset/ │ ├── __init__.py │ └── seg_dataset.py # 读图像和标签图的 Dataset ├── utils/ │ ├── metrics.py # 像素准确率、mIoU、逐类 IOU 计算 │ ├── color_map.py # RGB 色表与索引互转 │ └── visualize.py # 预测图着色、原图叠加保存 └── weights/ # 训练后最佳权重存放数据流是这样的main.py 从 config.py 读超参构造 SegDataset 并封装成 train_loader 和 val_loader每个 batch 的图像与标签图都转成 Tensor 送入 model/segnet.py 的 forward得到形状为 (B, C, H, W) 的 logitsC 等于 num_classes每个像素位置取 argmax 得到类别索引loss 由 CrossEntropyLoss 比较 logits 与标签图每轮结束在验证集算一次 mIoU最优权重写回 weights/。这样拆文件对期末大作业有个很实在的好处答辩时能按文件讲清楚每个模块被问到“网络哪一层对应论文的哪部分”直接翻 encoder.py 的第五个 block 就能对上号。想改 dropout、换 backbone、加注意力机制都只碰 model 目录不用在 main.py 里来回找代码。2.3 预训练权重与骨干替换到底要不要加载 VGG16 权重很多课程资料默认 SegNet 的编码器直接随机初始化能跑通但收敛速度确实慢。常见做法是加载经典视觉模型前五个卷积段的预训练权重这部分源码里留了 load_pretrained_vgg 的接口但默认没启用。原因有两层一是预训练权重文件不好找二是编码器的第一层卷积输入通道若是 3 就能直接复用可如果你改了骨干结构或者换成灰度图单通道输入权重形状对不上硬加载会报 strict mismatch。我一般建议课程设计阶段先随机初始化跑通全流程等 mIoU 能稳定在 60% 以上再试着加载预训练权重加速收敛。加载代码比较简单# model/segnet.py 中的权重载入逻辑示意 def load_pretrained(model, pretrained_path): state torch.load(pretrained_path, map_locationcpu) # 只取 encoder 部分的键名decoder 部分保持随机初始化 new_state {k: v for k, v in state.items() if k.startswith(encoder.)} model.load_state_dict(new_state, strictFalse)strictFalse 是关键允许缺失 decoder 相关键只覆盖 encoder 参数。如果直接 load_state_dict 全量加载键名对不上就报错。加载后第一件事是在验证集上跑一次推理确认输出图不是全黑再开始训练。若想换 ResNet 做骨干ResNet 的 stem 和 VGG 前几层结构不同最简单的替代方案是把 encoder.py 里的卷积组整体换成 ResNet 的 layer1 到 layer4同时保留每个 stage 输出的池化索引。但 MaxPool 的 return_indices 和 ResNet 的 stride2 卷积天然冲突改动成本比较高课程作业不建议碰保持 VGG 结构最稳。2.4 依赖与运行条件PyTorch 版本、显存与时间预算源码不挑很新的 PyTorch1.10 到 2.x 都能跑。我常用 2.0.1 配 torchvision 0.15.1依赖上只需要这几个关键库pip install torch2.0.1 torchvision0.15.1 pip install opencv-python pillow numpy matplotlib pyyaml说明opencv-python 用来做图像预处理pillow 在 Dataset 里负责读写 Imagematplotlib 用在 visualize.py 里把原图、标签、预测图并排保存。pyyaml 只在 config.py 用了 yaml 文件时才需要如果配置是 argparse 字典装前三个就够了。运行条件上训练 224×224、batch size 8、50 个 epoch单张 8GB 显存卡足够跑完。真正吃显存的是前向和反向之间的中间特征图不是模型参数本身SegNet 五层编码器特征图没有及时释放输入越大占用越夸张。实在紧张就把输入缩到 160×160 或 192×192。CPU 也能跑但一个 epoch 可能要好几十分钟不推荐用来做完整训练最多跑一次推理验证代码路径没问题。3. 环境与数据准备把标注图清理成可训练的索引图3.1 conda 环境搭建与依赖安装次序第一步是建独立环境避免跟机器上其他实验互相污染。PyTorch 版本要和本地 CUDA driver 匹配不然训练时会悄悄退回 CPU 或直接报错。我的习惯是 CUDA 11.8 PyTorch 2.0.1conda create -n segnet_env python3.9 -y conda activate segnet_env pip install torch2.0.1 torchvision0.15.1 --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow matplotlib numpy如果只想在 CPU 上先把流程跑通第三行去掉 cu118 后缀即可安装包更小。装完后先确认环境python -c import torch; print(torch.cuda.is_available(), torch.__version__)能看到 True 加版本号就说明 GPU 可用。如果打印 False优先检查装的是不是 CPU 版再查 CUDA driver 是否满足最低版本要求。这是最容易被忽视的一步很多人代码写对了但跑得快慢差几十倍根因就在这里。3.2 标签转换从 RGB 彩色标注到 0~N-1 的单通道索引训练时 CrossEntropyLoss 要的 target 是每个像素对应类别索引的整数张量形状和输入图像相同。很多分割数据集的标注图是 RGB 彩色 PNG比如背景纯黑、道路棕色、车辆蓝色直接把它当 target 用loss 会完全错乱。必须先做一次色表映射把每个 RGB 颜色替换成类别 ID。以四分类为例色表定义成 NumPy 数组顺序必须和模型输出通道一一对应# utils/color_map.py import numpy as np from PIL import Image # 类别顺序0背景1道路2建筑3车辆 PALETTE np.array([ [0, 0, 0], # 背景纯黑 [128, 64, 0], # 道路棕色 [128, 128, 128], # 建筑灰色 [192, 0, 0], # 车辆红色 ], dtypenp.int32) def rgb_to_label(rgb_path, out_path): rgb np.array(Image.open(rgb_path).convert(RGB)) h, w, _ rgb.shape label np.zeros((h, w), dtypenp.uint8) for cls_id, color in enumerate(PALETTE): mask (rgb color).all(axis-1) label[mask] cls_id Image.fromarray(label).save(out_path)这段代码的机制是遍历每个类别生成布尔掩码命中就写入对应 ID。实际数据集里色表往往不止四类并且常见“边缘抗锯齿”产生的过渡色这些过渡像素不会命中任何颜色类默认保留为 0看起来无害长期训练会让背景类被过度占优。我的习惯是把未命中的像素做最近邻匹配归到 RGB 距离最近的类别这样边界更干净。3.3 数据加载器Dataset 类写入与 transform 的坑数据加载部分源码里已经写好 SegDataset但很多人拿到后要换自己的数据目录。核心点有两个图像和 mask 的路径对应关系以及 transform 必须同时作用在图片和 mask 上。# dataset/seg_dataset.py import os import glob import numpy as np from PIL import Image from torch.utils.data import Dataset import torchvision.transforms.functional as TF class SegDataset(Dataset): def __init__(self, img_dir, mask_dir): self.img_paths sorted(glob.glob(os.path.join(img_dir, *.jpg))) self.mask_dir mask_dir self.resize (256, 256) def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] img Image.open(img_path).convert(RGB) mask_name os.path.basename(img_path).replace(.jpg, .png) mask_path os.path.join(self.mask_dir, mask_name) mask Image.open(mask_path) img TF.resize(img, self.resize) mask TF.resize(mask, self.resize, interpolationTF.InterpolationMode.NEAREST) img TF.to_tensor(img) # 归一化到 [0,1]形状 C,H,W mask torch.as_tensor(np.array(mask), dtypetorch.long) return img, mask最容易被忽略的是 mask 的 resize 必须用 NEAREST 插值。如果用默认的双线性类别边界会插出 0.5 之类的小数标签图变成浮点后 CrossEntropy 要么报类型错误要么悄悄截断导致边界像素错位。图像本身可以用双线性但 label 图必须保持硬标签这是分割任务数据加载的通用约定不是这段源码独有的问题。另一个隐蔽的坑如果 mask 是 RGB 彩色图但没走 3.2 节的索引映射那么 mask 的 shape 会是 (H, W, 3) 而不是 (H, W)。模型输出是 (B, C, H, W)target 直接变成四维训练时形状对不上立刻报 RuntimeError。所以色彩映射必须在数据进入 Dataset 之前完成Dataset 层只负责读、resize、转 Tensor。3.4 目录组织与命名约定避免写死路径的一劳永逸做法源码里的数据路径建议统一为 train_img、train_mask、val_img、val_mask 四个目录文件名保持一一对应比如 image_001.jpg 对应 image_001.png。这样在 SegDataset 里只需把后缀替换一下不会出现标签错位。# 推荐的数据集目录结构 ./data/semseg/ ├── train_img/ # 训练原图jpg ├── train_mask/ # 训练标签图png单通道索引 ├── val_img/ # 验证原图jpg └── val_mask/ # 验证标签图png单通道索引windows 和 linux 路径分隔符混用的坑也值得一提。config.py 里如果写死了反斜杠路径换到 Linux 跑直接 FileNotFoundError我一般让 main.py 把所有路径都走 os.path.join 拼接而不是手写字符串模板。这样哪怕换机器、换系统只要改 data_root 一处就行。4. 跑通训练与推理main.py 参数、损失设计与可视化回放4.1 训练启动命令行参数与 batch size 的设定逻辑main.py 用 argparse 接管常用超参把 config.py 的默认值当 fallback这样不用改源码也能覆盖单次实验。标准启动命令python main.py --data_root ./data/semseg \ --train_list ./data/semseg/train.txt \ --val_list ./data/semseg/val.txt \ --num_classes 4 \ --batch_size 8 \ --lr 0.01 \ --epochs 60 \ --cuda参数说明data_root 指向数据集根目录train_list 是文本文件每行一个相对路径比直接扫描目录的好处是能手动过滤坏样本。num_classes 绝对是关键值它决定模型最后一层输出通道数也必须和色表里的类别数量一致多一或少一都会在训练中出现 index out of bounds。batch_size 受显存限制8 是一个比较均衡的起点。lr 初期给 0.01配合 poly 学习率调度分割任务普遍比分类更依赖学习率曲线的设计后面专门展开。训练主循环的结构是这样# main.py 训练循环骨架 for epoch in range(args.epochs): model.train() running_loss 0.0 for imgs, masks in train_loader: imgs imgs.to(device) masks masks.to(device) logits model(imgs) # 输出 (B, C, H, W) loss criterion(logits, masks) # CrossEntropyLoss optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() # 每个 epoch 后做验证 val_miou evaluate(model, val_loader, num_classes, device) print(fEpoch {epoch:02d} loss{running_loss/len(train_loader):.4f} miou{val_miou:.4f})这段代码里有几个细节直接决定训练能不能顺利收敛。第一logits 不经过 Softmax 直接给 CrossEntropyLoss因为 PyTorch 的这个损失函数内部已经做了 log_softmax 和 NLL你提前 Softmax 再传进去数值稳定性会被破坏偶尔 loss 跳到 NaN。第二optimizer.zero_grad() 每个 batch 都要调一次否则梯度跨 batch 累积loss 曲线呈现周期性波浪。第三imgs 和 masks 必须同时到同一个 device模型在 GPU 而标签还在 CPUbackward 时报 device mismatch常规做法是两者一起 .to(device)。4.2 损失与优化器类别不平衡时的交叉熵选型源码默认用 nn.CrossEntropyLoss没有手工加类别权重。这在两类情况下会出问题一是背景面积远大于前景二是标签图里存在未标注区域。前者需要给 loss 加 class_weight后者要把未标注像素在 mask 里标记为 ignore_index。# utils/loss.py import torch.nn as nn # 按训练集各类像素占比的倒数做归一化权重示例值仅供参考 class_weights torch.tensor([0.5, 2.0, 3.0, 4.0], devicedevice) criterion nn.CrossEntropyLoss(weightclass_weights, ignore_index255)参数说明ignore_index 设为 255 时mask 中值为 255 的像素完全不参与 loss 计算这对数据集标注不完整的边缘区域很关键。class_weights 建议在训练开始前统计一次像素分布后固定下来不要训到一半反复改。如果你直接保持四个 1.0而数据里背景占 90%模型学出来的 mIoU 看着不低但小物体全被吞掉这也呼应了后面 5.4 节的排查点。优化器方面我建议用 SGD momentum 0.9 weight_decay 0.0001而不是 Adam。分割训练对权重衰减更敏感Adam 学出来的特征倾向于平滑边界不如 SGD 锐利。学习率调度用 poly 策略公式是初始 lr 乘 (1 - iter/total_iter)^0.9比等间隔下降更贴合分割训练的习惯# main.py 里的 poly 学习率调度示意 def adjust_lr(optimizer, iter_now, total_iters, base_lr): lr base_lr * (1.0 - iter_now / total_iters) ** 0.9 for param_group in optimizer.param_groups: param_group[lr] lrpoly 的好处是前 60% 训练阶段学习率下降不快模型还有能力在局部最优附近震荡后半段学习率越来越小收敛更稳。用 StepLR 每 20 个 epoch 乘 0.1 的做法在分割里容易踩坑学习率锐减那一刻模型已经被推到损失曲面的某个陡坡上骤降会让 loss 重新抬起来白白浪费后续 epoch。4.3 推理与可视化把 logits 转回人眼可读的分割图训练结束后源码会在验证集上跑一次完整推理并保存可视化图。推理本身简单但有两个细节经常把人卡住argmax 的维度以及类别索引到颜色的映射。# utils/visualize.py import numpy as np import torch def predict_and_colorize(model, img_batch, palette, device): model.eval() with torch.no_grad(): logits model(img_batch.to(device)) # 对 dim1 取 argmax也就是类别通道维度 pred torch.argmax(logits, dim1).cpu().numpy() # (B, H, W) colored palette[pred] # 生成 (B, H, W, 3) 的彩色图 return pred, coloredpalette 就是 3.2 节定义好的 NumPy 数组shape 为 (num_classes, 3)。pred 是整数索引数组对 palette 做数组索引每个类别 ID 会被自动替换为对应的 RGB 三元组。常见报错是 palette 写成了 Python listlist 无法对多维数组做广播索引解决方式是把 palette 转成 np.ndarray 并确认维度是二维而不是一维。如果需要叠加原图对比按下式做加权融合overlay (0.5 * original 0.5 * colored).astype(np.uint8)original 必须先归一化回 0-255 的无符号整数如果原图还是 0-1 的浮点和 0-255 的 colored 相加会溢出白成一片。保存时尽量同时保存原始预测索引图和彩色叠加图索引图可以用来继续做逐类分析彩色图用来给人看。4.4 权重保存与断点续训best_model.pth 的完整流程源码里每个 epoch 结束后都会比较验证集 mIoU只在比历史最好高时才覆盖 weights/best_model.pth。这个逻辑很朴素但实用# main.py 中的最优权重保存示意 if val_miou best_miou: best_miou val_miou torch.save({ epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_miou: best_miou, }, weights/best_model.pth)把 optimizer_state 一起存进去是关键这样断点续训时能恢复学习率调度器的位置而不是从头再罚一遍。加载权重做推理时注意torch.save 存的是 dict 而不是裸 state_dict加载要按 key 取值checkpoint torch.load(weights/best_model.pth, map_locationdevice) model.load_state_dict(checkpoint[model_state])如果直接把 checkpoint 传给 load_state_dict会报 missing keys因为 dict 外层多了 epoch、optimizer_state 这些键。这类小问题在课程作业现场最容易出现提前在代码里写清楚会省很多麻烦。5. 避坑与排查SegNet 训练路上最常见的 5 个翻车点5.1 现象loss 长时间不降甚至在某个值附近打转原因主要分三类学习率太小模型基本动不了标签图类别 ID 和 num_classes 不匹配导致异常像素梯度BatchNorm 在 batch size 很小的时候统计量抖动太大训练指标震荡明显。解决把初始 lr 从 0.01 提到 0.02~0.05观察前三个 epoch 的 loss 有没有明显下降趋势。打印 masks.unique() 检查标签值域是否在 [0, num_classes) 之内如果出现等于 num_classes 或更大的值说明色表映射漏了类别。batch size 小于等于 2 时要么用梯度累积凑足 8 张图再更新一次权重要么把 BatchNorm 换成 GroupNorm后者对小 batch 更稳。5.2 现象推理出的分割图全是一整块黑色或白色原因通常是最后一层输出通道数设成了 1而不是 num_classes或者 argmax 取错了维度比如对 (B, C, H, W) 在 dim0 取了 argmax把 batch 维度当成了类别维度还有可能是可视化阶段直接保存了 logits 的浮点结果没有做 argmax。解决打印模型最后一层输出 shape确认第二维等于 num_classes。argmax 写成 torch.argmax(logits, dim1)并且把 shape 注释写在旁边格式化成 (B, C, H, W) 再检查。保存时先 pred logits.argmax(dim1)再转彩色不要直接存 float 张量。拿到预测图后用 np.unique 看值分布立刻能判断是索引映射的问题还是维度的问题全程不需要瞎猜。5.3 现象CUDA out of memory训练跑不到几个 batch 就崩原因最大的头在前向和反向之间保存的中间特征图SegNet 编码器五层卷积都保留了完整分辨率的响应输入 512×512 时显存占用指数级增长再加上每层 MaxPool 额外保存索引这部分在高分辨率下尤其费显存。解决先降输入分辨率到 256×256batch size 从 8 降到 4还不够就降到 2 并开启梯度累积。用 torch.cuda.max_memory_allocated() 打印每个阶段的峰值显存判断是 forward 还是 backward 阶段占得更多再决定减分辨率还是减 batch。更极端的做法是调整编码器中间层的 stride让特征图更早降采样但改动成本高课程作业阶段先用前两种方法更划算。5.4 现象验证集 mIoU 数字还行但可视化图里小物体完全没分割出来原因很典型全类别 mIoU 被高占比背景拉高了。背景占 90% 像素时背景分对了前景全输mIoU 也能到 60% 以上但图里目标确实缺胳膊少腿。解决不要再只看一个平均分。用 metrics.py 的逐类 IoU 打印每个类别的 IoU或者算类别平均 F1。训练时给 CrossEntropyLoss 加 class_weights权重按各类像素占比的倒数来设。可视化保存时把每个类别的预测分开 mask逐个检查哪一类错得最多比看一张叠图更直接。5.5 现象换到自己的数据集后训练阶段报 RuntimeErrorshape mismatch 或 target 类型错误原因基本上只有一个方向自己的标签图没先转成 0~N-1 的单通道索引。RGB 彩色 mask 被读成 (H, W, 3)和模型的 (B, C, H, W) 对比形状直接对不上另一种是 mask 里出现了色表之外的像素值target 最大值超过 num_classes-1CrossEntropy 内部检查时会抛 index out of bounds。解决数据准备阶段强制统一走 rgb_to_label 函数之后对每张 mask 执行 np.unique 检查值域。换数据集后先拿单张图完整走一遍推理确认模型输出维度正确、调色板边界完整再启动全量训练。这个习惯能省掉至少半天排查时间因为错误信息早早在第一轮训练就暴露越到后期越难定位。6. 进阶验证mIoU 的计算、学习率曲线与三条快速调优技巧验证 SegNet 跑得好不好不能只看 loss 曲线。最常用的是逐类 IoU 与平均值 mIoU逐类对比比单一数字更能暴露问题# utils/metrics.py def compute_miou(pred, label, num_classes): ious [] for cls in range(num_classes): p (pred cls) t (label cls) inter (p t).sum().item() union (p | t).sum().item() iou inter / union if union 0 else float(nan) ious.append(iou) valid [iou for iou in ious if iou iou] # 去掉 nan return sum(valid) / len(valid) if valid else 0.0这里有个常被误解的点全局像素准确率和 mIoU 是不同的指标。PA 被背景主导看不出小物体的真实表现。我在课程作业里被追问过一次“准确率都好高了为什么目标都缺着”从那以后每轮都强制打印逐类 IoU并单独统计每类像素的占比。后续调参时分得清到底是背景学过头了还是前景类本身数据不足。在快速调优的三板斧里第一板斧是数据增强。分割数据集经常反复训练同一批图加随机水平翻转、随机裁剪和亮度抖动效果比换模型更明显。注意 mask 的增强必须与图像同步图像水平翻转时 mask 也要水平翻转不能只翻一边否则标签和图像错位损失直接崩上涨。第二板斧是给损失函数调权重用频率反比权重多类别场景下比固定权重的收敛速度快不少。第三板斧是学习率调度poly 加前 5 个 epoch 的 warmup从 lr 的十分之一线性升到完整值可以避开 BatchNorm 在小数据集上的早期震荡前十个 epoch 的 mIoU 增长速度肉眼可见地变快。最后分享一个对验证特别有帮助的技巧打印混淆矩阵。把预测与真实类别的对应关系写成矩阵一眼就能看出第 1 类是被分到第 0 类还是第 2 类然后针对性地加权重或补充数据。从那以后我每次训练分割模型都会强制走一遍完整流程读数据确认类别 ID → 逐类 IoU 看分布 → 用混淆矩阵定位误分 → 再回头调 class_weight。这套流程用在 SegNet 源码上几个 epoch 就能判断问题出在数据、模型还是训练策略省下的时间和显卡耗材都很可观。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进