资讯中心

DeepLabv3+ 实战:VOC 与 Cityscapes 训练链路及 mIoU 调优复盘

📅 2026/10/2 5:36:34
DeepLabv3+ 实战:VOC 与 Cityscapes 训练链路及 mIoU 调优复盘
简介本资源面向图像分割初学者与进阶开发者提供基于PyTorch在VOC与Cityscapes数据集上训练DeepLabv3的完整项目源码与流程教程帮助读者掌握语义分割从数据准备到模型部署的全链路实践。压缩包共43个文件约2.13MB以23个Python脚本为核心涵盖网络结构、数据集加载、损失函数、学习率调度与可视化等模块另附17张png可视化结果图、2个txt依赖与说明文件及1个md文档目录按datasets、network、utils等分层组织便于按模块阅读与二次开发。资源重点讲解空洞卷积、ASPP与解码器结构并给出训练、评估、微调及IoU指标监控的完整实现读者可据此复现多类别分割实验理解多尺度信息捕获与像素级分类的关键技巧。目前已有492人学习适合希望快速上手DeepLabv3并积累实战经验的学习者。1. DeepLabv3 在 VOC 与 Cityscapes 上的真实训练链路从环境到 mIoU 的完整复盘很多做图像分割的工程师第一次跑 DeepLabv3 都会遇到同一个尴尬代码能跑通loss 也在降但验证集 mIoU 就是卡在 0.6 上不去或者换到 Cityscapes 上直接崩掉。问题往往不在模型本身而在数据管线、预训练权重加载、学习率策略和类别不平衡处理这几处细节。DeepLabv3 是 Encoder-Decoder 结构加空洞卷积的经典组合Backbone 通常用 ResNet-101 或 Xception在 VOC 2012 上 mIoU 能到 0.87 左右Cityscapes 上 0.80 上下。这套方案适合想从零复现语义分割基线、或者要把分割能力嵌进自己业务里的从业者。下面按环境搭建、数据准备、模型训练、避坑、进阶调优的顺序把整条链路拆开讲清楚。2. 环境搭建与数据管线PyTorch 版本、VOC 与 Cityscapes 的加载差异2.1 PyTorch 环境搭建CUDA、cuDNN 与版本对应关系图像分割训练对显存和算力要求不低环境没搭对后面全是玄学报错。我一般用 conda 建独立环境避免和系统 Python 打架。PyTorch 版本和 CUDA 驱动有对应关系装之前先确认显卡驱动支持的 CUDA 上限。# 查看显卡驱动和 CUDA 版本上限 nvidia-smi # 创建独立环境Python 3.8 是兼容性最稳的版本 conda create -n deeplab python3.8 -y conda activate deeplab # 安装 PyTorch以 CUDA 11.3 为例具体版本按 nvidia-smi 显示的 CUDA Version 选 pip install torch1.12.1cu113 torchvision0.13.1cu113 -f https://download.pytorch.org/whl/torch_stable.html # 验证 GPU 是否可用 python -c import torch; print(torch.cuda.is_available(), torch.version.cuda)这段命令的逻辑是先确认驱动支持的 CUDA 上限再选不超过该上限的 PyTorch 预编译包。torch.cuda.is_available()返回 True 才算装对。如果返回 False八成是 CUDA 版本和驱动不匹配或者装成了 CPU 版。参数上Python 3.8 对大多数分割仓库兼容性最好3.10 以上有些老仓库的 C 扩展会编译失败。提示如果用的是 WSL 环境显卡驱动装在 Windows 侧WSL 内不需要再装驱动直接装 CUDA Toolkit 和 PyTorch 即可。AMD 显卡走 ROCm 路线PyTorch 要装 ROCm 版本命令和 CUDA 版不同。2.2 VOC 与 Cityscapes 数据加载目录结构、标签映射与 transform 差异VOC 2012 和 Cityscapes 的目录结构、标签编码、图像尺寸都不一样不能共用一套 Dataset。VOC 是 21 类含背景Cityscapes 是 19 类训练用。VOC 的标注是 PNG 索引图Cityscapes 也是 PNG 但类别 ID 不连续需要做 label mapping。import os import numpy as np from PIL import Image from torch.utils.data import Dataset import torchvision.transforms as T # VOC 类别到连续 ID 的映射21 类 VOC_CLASSES [background,aeroplane,bicycle,bird,boat,bottle, bus,car,cat,chair,cow,diningtable,dog,horse, motorbike,person,pottedplant,sheep,sofa,train,tv] class VOCSegDataset(Dataset): def __init__(self, root, splittrain, crop_size513): self.root root self.split split self.crop_size crop_size # VOC 的索引文件 self.ids [line.strip() for line in open( os.path.join(root, ImageSets, Segmentation, f{split}.txt))] self.img_dir os.path.join(root, JPEGImages) self.mask_dir os.path.join(root, SegmentationClass) # 训练时随机裁剪 翻转验证时只做 resize if split train: self.transform T.Compose([ T.RandomCrop(crop_size), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225]) ]) else: self.transform T.Compose([ T.Resize((crop_size, crop_size)), T.ToTensor(), T.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225]) ]) def __len__(self): return len(self.ids) def __getitem__(self, idx): name self.ids[idx] img Image.open(os.path.join(self.img_dir, name .jpg)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, name .png)) # 用最近邻插值保持标签的整数性质 mask np.array(mask, dtypenp.uint8) # 255 是 ignore 区域训练时忽略 mask np.where(mask 255, 255, mask).astype(np.uint8) img self.transform(img) mask T.ToTensor()(Image.fromarray(mask)) return img, mask.squeeze(0).long()这段代码的关键点VOC 的 mask 里 255 表示忽略区域训练时 loss 要 ignore_index255transform 里图像用双线性插值标签必须用最近邻否则类别 ID 会被插值成小数。Cityscapes 的 Dataset 结构类似但标签 ID 需要重映射比如原图里 7 是 road训练时映射到 0具体映射表在 Cityscapes 官方脚本里有。注意Cityscapes 原始图像是 2048x1024直接训练显存扛不住通常随机裁剪到 769x769 或 1024x1024。VOC 图像尺寸不一统一 resize 到 513x513 是 DeepLab 系列的常见做法。3. DeepLabv3 模型结构与训练配置Backbone、空洞卷积与损失函数3.1 Backbone 选型ResNet-101 与 Xception 的取舍DeepLabv3 的 Encoder 部分可以用 ResNet-101 或 Xception。ResNet-101 权重好找torchvision 自带预训练改起来方便Xception 在 Cityscapes 上精度略高但参数量大训练慢。我一般先用 ResNet-101 跑通基线确认数据管线没问题再换 Xception 冲精度。import torch import torch.nn as nn import torchvision.models as models class DeepLabv3Plus(nn.Module): def __init__(self, num_classes21, backboneresnet101, output_stride16): super().__init__() # 加载预训练 backbone if backbone resnet101: resnet models.resnet101(pretrainedTrue) # 去掉最后的全连接和 avgpool self.layer0 nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool) self.layer1 resnet.layer1 self.layer2 resnet.layer2 self.layer3 resnet.layer3 self.layer4 resnet.layer4 # output_stride16 时layer4 的 stride 要改成 1用空洞卷积补偿 if output_stride 16: # 修改 layer4 第一个 block 的 stride for n, m in self.layer4.named_modules(): if conv2 in n and isinstance(m, nn.Conv2d): m.stride (1, 1) m.dilation (2, 2) m.padding (2, 2) # ASPP 模块 self.aspp ASPP(2048, 256) # Decoder 部分融合低层特征 self.decoder Decoder(256, 48, num_classes) def forward(self, x): # 低层特征用于 decoder low_level self.layer1(self.layer0(x)) x self.layer2(low_level) x self.layer3(x) x self.layer4(x) x self.aspp(x) x self.decoder(x, low_level) return x这段代码的核心是 output_stride 的控制。DeepLabv3 要求 output_stride16意味着最终特征图是输入的 1/16。ResNet 原始 layer4 的 stride 是 2会让总 stride 变成 32所以要把 layer4 的 stride 改成 1同时用 dilation2 的空洞卷积保持感受野。ASPP 模块用不同 dilation rate 的并行空洞卷积捕捉多尺度信息这是 DeepLab 系列的灵魂。3.2 损失函数与学习率策略CrossEntropy、ignore_index 与 poly 衰减分割任务里类别不平衡很严重背景像素远多于前景。VOC 上还好Cityscapes 里 road 和 sky 占了大半。损失函数一般用 CrossEntropyLoss 加 ignore_index255再配合类别权重。学习率用 poly 衰减初始 lr0.007VOC或 0.01Cityscapespower0.9。import torch.optim as optim from torch.optim.lr_scheduler import LambdaLR # 损失函数忽略 255 区域 criterion nn.CrossEntropyLoss(ignore_index255) # poly 学习率衰减 def poly_lr(base_lr, power0.9, max_iter30000): def lr_lambda(step): return (1 - step / max_iter) ** power return lr_lambda optimizer optim.SGD(model.parameters(), lr0.007, momentum0.9, weight_decay1e-4) scheduler LambdaLR(optimizer, lr_lambdapoly_lr(0.007, 0.9, 30000)) # 训练循环片段 for step, (imgs, masks) in enumerate(train_loader): imgs, masks imgs.cuda(), masks.cuda() outputs model(imgs) loss criterion(outputs, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()poly 衰减的特点是前期 lr 降得快后期平缓适合分割这种需要精细收敛的任务。momentum0.9、weight_decay1e-4 是 DeepLab 官方配置。batch size 受显存限制VOC 上 8 到 16 都行Cityscapes 上 769x769 裁剪通常只能开到 4 到 8可以用梯度累积模拟大 batch。提示如果 loss 震荡厉害先把 lr 降到 0.001 试试。如果 mIoU 一直不涨检查 mask 的 dtype 是不是 longCrossEntropyLoss 要求 target 是 LongTensor。4. 训练避坑从 loss 不降、mIoU 卡住到显存爆炸的排查清单4.1 现象loss 从第一个 epoch 就不降稳定在 ln(num_classes) 附近原因通常是标签没对齐或者 ignore_index 设错。VOC 的 mask 里 255 是忽略区域如果没设 ignore_index模型会去学 255 这个类别但 255 在输出通道里不存在导致 loss 异常。另一个可能是 mask 的 shape 不对比如 (N,1,H,W) 没 squeeze 成 (N,H,W)。解决打印一个 batch 的 mask 唯一值确认范围在 0 到 20 之间加 255。检查 criterion 的 ignore_index 是否为 255。mask 用mask.squeeze(1)去掉多余维度。4.2 现象训练 loss 正常降但验证 mIoU 始终在 0.3 以下原因多半是验证时的预处理和训练不一致。训练用了 RandomCrop 和 Normalize验证如果忘了 Normalize或者 Resize 的插值方式不对分布就对不上。另外 VOC 验证集有 1449 张如果只跑了几十张就统计 mIoU数值波动会很大。解决验证 transform 必须和训练共享 Normalize 参数标签用最近邻。mIoU 统计要跑完整个 val set用混淆矩阵累加再算不要逐 batch 平均。4.3 现象训练到一半显存爆炸报 CUDA out of memory原因可能是验证时没加torch.no_grad()或者 Cityscapes 原图没裁剪直接送进网络。另外 DataLoader 的 num_workers 太多每个 worker 都缓存了一份数据也会吃显存。解决验证循环包在with torch.no_grad():里。Cityscapes 训练裁剪到 769x769验证可以 resize 到 1024x512 再滑窗。num_workers 设成 4 到 8 就够太大反而拖慢。4.4 现象Cityscapes 上训练某些类别 mIoU 一直是 0原因是 Cityscapes 的标签 ID 不连续比如原图里 7 是 road但训练时如果没做 label mapping模型输出的 7 号通道对应的是别的类别。另外 Cityscapes 有些类别在训练集里样本极少比如 motorcycle模型学不到。解决用 Cityscapes 官方的 labels.py 做 ID 重映射确保 0 到 18 连续。对稀有类别可以在 loss 里加类别权重或者用 OHEM 在线难例挖掘。4.5 现象多卡训练时 mIoU 比单卡还低原因是 BatchNorm 在多卡下默认用 sync_bn 才等价于大 batch如果没开 sync_bn每张卡独立算 BN 统计量等效 batch 变小精度就掉。另外多卡下学习率要按卡数线性缩放。解决用nn.SyncBatchNorm.convert_sync_batchnorm(model)转换 BN 层。lr 按base_lr * num_gpus缩放warmup 阶段也要相应调整。5. 进阶调优与验证从 mIoU 0.75 推到 0.85 的几个实操技巧5.1 用 COCO 预训练权重做初始化VOC 和 Cityscapes 的标注量有限直接从 ImageNet 预训练 backbone 开始mIoU 通常卡在 0.75 左右。换成 COCO 上预训练过的 DeepLabv3 权重做初始化mIoU 能涨 3 到 5 个点。COCO 有 80 类和 VOC 的 21 类有重叠迁移效果明显。# 加载 COCO 预训练的 DeepLabv3只取 backbone 和 ASPP 部分 coco_model torch.hub.load(pytorch/vision, deeplabv3_resnet101, pretrainedTrue) # 替换 classifier 为 VOC 的 21 类 coco_model.classifier[4] nn.Conv2d(256, 21, kernel_size1) # 加载时忽略不匹配的 key state_dict coco_model.state_dict() model.load_state_dict(state_dict, strictFalse)这段代码的逻辑是COCO 预训练模型和 VOC 模型结构基本一致只有最后的分类层通道数不同。用strictFalse加载分类层随机初始化其余层用 COCO 权重。注意 COCO 预训练模型的 output_stride 可能和你的配置不同加载后要检查 layer4 的 dilation 设置。5.2 多尺度推理与翻转增强训练完模型后推理阶段用多尺度加翻转能再涨 1 到 2 个点。具体做法是把输入 resize 到 0.5、0.75、1.0、1.25 几个尺度每个尺度再水平翻转所有结果平均后取 argmax。def multi_scale_inference(model, img, scales[0.5, 0.75, 1.0, 1.25]): model.eval() h, w img.shape[-2:] preds [] with torch.no_grad(): for scale in scales: new_h, new_w int(h * scale), int(w * scale) resized F.interpolate(img, size(new_h, new_w), modebilinear, align_cornersFalse) out model(resized) out F.interpolate(out, size(h, w), modebilinear, align_cornersFalse) preds.append(out) # 水平翻转 out_flip model(torch.flip(resized, dims[3])) out_flip torch.flip(out_flip, dims[3]) out_flip F.interpolate(out_flip, size(h, w), modebilinear, align_cornersFalse) preds.append(out_flip) # 平均后取 argmax avg torch.mean(torch.stack(preds), dim0) return avg.argmax(dim1)多尺度推理的代价是推理时间成倍增加实际部署时要权衡。如果延迟敏感可以只保留 1.0 和 1.25 两个尺度加翻转涨点少一些但速度快一倍。5.3 用混淆矩阵算 mIoU 的正确姿势很多人算 mIoU 是逐 batch 算再平均这样类别样本少的 batch 会拉低整体数值。正确做法是维护一个 num_classes x num_classes 的混淆矩阵累加所有像素最后从矩阵算 IoU。def compute_miou(confusion_matrix): # confusion_matrix[i][j] 表示真实类别 i 被预测为 j 的像素数 intersection np.diag(confusion_matrix) union confusion_matrix.sum(axis1) confusion_matrix.sum(axis0) - intersection iou intersection / (union 1e-10) # 忽略背景类或按需选择 return np.nanmean(iou) # 训练循环里累加 confusion_matrix np.zeros((num_classes, num_classes)) for imgs, masks in val_loader: preds model(imgs).argmax(dim1) # 忽略 255 mask_flat masks.flatten() pred_flat preds.flatten() valid mask_flat ! 255 for t, p in zip(mask_flat[valid], pred_flat[valid]): confusion_matrix[t.long(), p.long()] 1混淆矩阵的好处是能看出哪些类别容易混比如 VOC 里 chair 和 diningtable 经常互错Cityscapes 里 truck 和 bus 容易混。针对性地加样本或调权重比盲目调 lr 有效得多。5.4 一个我踩过的坑验证集 mIoU 比测试集高很多有次在 Cityscapes 上训练验证集 mIoU 到了 0.82提交测试集只有 0.71。排查后发现验证集的图像和训练集来自同一批视频序列风格分布太接近模型过拟合了。后来把验证集换成不同城市的序列mIoU 掉到 0.78但测试集涨到 0.76泛化更真实。这个血泪经验告诉我验证集划分一定要按场景或城市分不能随机分。注意如果业务场景和训练数据分布差异大比如训练用白天数据测试有夜间图像mIoU 会断崖式下跌。这种情况要么补夜间数据要么做域适应。5.5 导出 ONNX 做部署验证训练完的 PyTorch 模型要部署通常先转 ONNX 验证算子兼容性。DeepLabv3 里的空洞卷积和插值算子 ONNX 都支持但要注意 output_stride 和输入尺寸的动态轴设置。dummy_input torch.randn(1, 3, 513, 513).cuda() torch.onnx.export( model, dummy_input, deeplabv3plus.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width}}, opset_version11 )导出后可以用 onnxruntime 跑一遍对比 PyTorch 和 ONNX 的输出差异一般 cosine similarity 在 0.999 以上才算正常。如果差异大检查是不是有算子用了 PyTorch 特有实现。这套流程跑下来VOC 上 mIoU 0.85 到 0.87Cityscapes 上 0.78 到 0.80 是能复现的。关键是把数据管线、ignore_index、poly 衰减和多尺度推理这几处做扎实。我自己的习惯是每换一个数据集先跑 500 个 iteration 看 loss 和 mIoU 的趋势趋势对了再开完整训练省得浪费卡时。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案