
简介面向计算机视觉初学者与 PyTorch 开发者这是一套基于 DeepLabv3 的语义分割完整实战资料使用 VOC 与 Cityscapes 数据集覆盖从数据预处理、模型构建、训练评估到结果可视化的全流程可帮助读者快速建立图像分割项目的大局观。包内共 43 个文件主体为 23 个 Python 源码分模块提供 deeplab 网络搭建、ASPP 空洞卷积模块、解码器、损失函数、数据加载与训练脚本另配 17 张分割效果图、2 个说明文档和 1 个 markdown 教程整包约 2.13MB结构清晰便于下载后按目录对照学习。已有 492 人浏览学习适合希望从零跑通语义分割实战的读者。通过阅读源码和文档可掌握空洞卷积、解码器设计以及跨数据集迁移训练的方法同时获得运行测试图片与可视化结果进一步理解 DeepLabv3 在城市场景和多类别目标上的训练细节与调优思路。1. 用 Pytorch 在 VOC 和 Cityscapes 上训练 DeepLabv3大多数人卡在数据这一关图像分割的训练链路比分类任务长得多第一次接触 DeepLabv3 的工程师十有八九不是倒在模型定义上而是倒在数据集上VOC 的标注是调色板索引Cityscapes 的原始标注 id 要转成 trainId255 要当 ignore 处理训练时还要决定是用 512 的 crop 还是保留原始分辨率。这些细节直接决定 loss 能不能降下去、mIoU 能不能算对。这篇内容围绕“基于 Pytorch 在 VOC Cityscapes 上训练 DeepLabv3 图像分割算法”这条完整链路展开从空洞卷积和 ASPP 的原理、Pytorch 环境搭建、数据集解析与增强到训练脚本的关键参数和排错技巧依次讲完。适合已经会用 Pytorch 训练分类模型、想切入语义分割的开发者也适合从分类任务转过来做分割落地的算法工程师。2. DeepLabv3 的取与舍空洞卷积、ASPP 和双数据集训练要解决什么问题2.1 空洞卷积与 ASPPDeepLabv3 怎么在 stride 16 的特征图上做多尺度预测图像分割要求每个像素都有类别输出如果把特征图一路下采样到 32 倍小物体的边缘信息基本丢光了。DeepLabv3 的做法是把 ResNet 最后一个 stage 的卷积 stride 从 2 改成 1并用空洞卷积把膨胀率补上让 encoder 输出的特征图相对原图的 stride 稳定在 16。空洞卷积的作用是在不增加参数量的情况下扩大感受野rate2 的 3×3 卷积等效于 5×5 的视野但计算量不变。多尺度问题是另一个核心矛盾。同一条街景里近处的卡车可能占半个画面远处的红绿灯只有几十个像素单一感受野的卷积很难同时照顾这两类目标。DeepLabv3 在 backbone 之后接了一个 ASPP 模块把特征图并行送入一个 1×1 卷积、三个不同 rate通常是 6、12、18的 3×3 空洞卷积以及一个全局平均池化分支最后 concat 再经过 1×1 卷积压缩通道。这相当于在同一个特征图上做了多尺度观测不同 rate 的分支各自“看到”不同范围的上下文。V3 相比 V3 的关键改动在 Decoder 部分。ASPP 输出先上采样 4 倍再和 backbone 较早层输出的低层特征 concat通过 1×1 卷积统一通道数经过 3×3 卷积融合后上采样回原图尺寸。低层特征保留了更多空间细节可以修正上采样带来的边缘模糊。用 Pytorch 实现时torchvision 提供了 deeplabv3_resnet50 和 deeplabv3_resnet101 的完整结构ResNet101 精度更高但显存占用和训练时间也明显增加我的经验是先用 ResNet50 跑通流程验证 Loss 和 mIoU 都正常后再换 ResNet101比一开始就上大模型高效得多。2.2 损失函数与评估指标Cityscapes 为什么必须设置 ignore_index255语义分割默认的损失函数是逐像素交叉熵。对 VOC 和 Cityscapes 来说直接使用标准交叉熵基本够用因为这两个数据集的背景和前景类别比例没有严重到训练崩溃的程度。但有两个必须处理的细节Cityscapes 的标注里 255 表示未标注区域或 ignore 区域例如车辆窗户、难以界定的边缘VOC 的标注里 255 表示物体边界轮廓这些像素不应该参与 loss 计算否则模型会被无意义的边界像素带偏。Pytorch 的 CrossEntropyLoss 提供了 ignore_index 参数Cityscapes 配置为 255VOC 的边界像素也需要同样处理。如果遇到类别严重不均衡的场景例如从街景中分割广告牌前景像素占比可能只有几个百分点可以考虑加权交叉熵或 Dice Loss。加权交叉熵需要根据验证集统计每个类别的像素频率给低频类别更高权重但权重设置不好容易导致噪声放大。Dice Loss 直接优化区域重叠度对小目标更友好但在训练初期梯度不稳定。医学图像分割里常用的 Dice 与 CE 混合方案也可以迁移过来不同损失函数的选择没有绝对标准要结合验证集的 mIoU 曲线判断。评估指标 mIoU 的实现有几个容易被忽略的偏差。正确做法是维护一个全局混淆矩阵把所有验证图片的预测结果累加进去再对每个类别计算 IoU最后对所有类别取平均。如果对每张图单独算 IoU 再平均会高估小类别性能如果分母用总像素数而不是每个类别的单独像素数算出来的指标完全失真。验证时的输入尺寸、是否使用测试时增强也会影响 mIoU 绝对值对比不同实验时要固定这些条件。2.3 VOC 与 Cityscapes两个数据集的差异决定了模型设计参数选择这两个数据集训练 DeepLabv3是因为它们覆盖了语义分割的两种典型场景VOC 是日常物体的语义分割类别包含人、动物、交通工具、家具等 20 类物体加背景共 21 类图片分辨率不高单张图片里的目标数量多且尺度变化大Cityscapes 是自动驾驶街景分割共 19 个类别分辨率高达 1024×2048类别以道路、车辆、行人、交通标志、广告牌等为主像素标注质量很高但类别分布极度不均衡道路和天空区域占了大量像素。两个数据集的差异直接影响训练配置。VOC 训练集约 1464 张图片通常会采用随机裁剪到 512×512 的训练策略Cityscapes 训练集约 2975 张图片尺寸大常见做法是裁剪到 512×1024 或 768×768 训练Batch Size 要相应缩小。VOC 的标注是调色板 PNG颜色索引直接对应类别号Cityscapes 的原始标注是 id 编码0 到 33 之间有很多类别并不参与最终评估必须先转换成 trainId 再训练背景像素统一用 255 表示。从工程角度看把这两个数据集放到同一个训练框架里意味着数据集加载器必须参数化设计类别数、ignore_index、图片裁剪尺寸、mean/std 归一化值都要通过配置传入而不是在代码里写死。很多开源项目的源码就是围绕这个思路组织的模型定义只关心 num_classes 一个参数数据加载器只关心 ignore_index 和裁剪尺寸训练脚本只关心数据加载器和模型怎么组合。这样才能在切换数据集时改动最少也方便把训练好的 VOC 模型迁移到 Cityscapes 上做领域适配。3. Pytorch 环境与数据集准备环境装错一步后面全是白费3.1 用 Anaconda 配置 Pytorch 训练环境先做一次 CUDA 可用性验证训练 DeepLabv3 的第一步不是写模型而是把 Pytorch 基础框架装到一个干净的虚拟环境里。我的习惯是用 Anaconda 创建独立环境避免把系统 Python 环境弄乱。以 Ubuntu 系统为例下载 Pytorch 教程里最典型的做法是conda create -n deeplab python3.10 -y conda activate deeplab pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121创建环境时指定 Python 3.10是因为当前多数 Pytorch 版本和第三方库对 3.10 的兼容性最稳。pip 安装时通过 index-url 指定 CUDA 12.1 对应的 wheel 源这一步决定了 torch 能否调用 GPU。如果不指定 CUDA 版本pip 默认安装的是 CPU 版本训练速度会慢几十倍。装完先做一次 GPU 可用性验证import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) print(torch.cuda.get_device_capability(0))这段代码会输出 Pytorch 版本、CUDA 是否可用、显卡型号和计算能力。如果 cuda.is_available() 返回 False优先执行 nvidia-smi 确认显卡驱动是否正常再看安装命令里的 cu121 和驱动支持的 CUDA 版本是否匹配。显卡驱动版本决定 CUDA 运行时的上限新驱动兼容旧 CUDA反过来不行。这套验证流程在 Windows 和 Ubuntu 上完全一致Anaconda 配置 Pytorch 环境最常踩的坑就是装完发现 torch 用的是 CPU 版本白白浪费几小时。3.2 下载 VOC 与 Cityscapes统一成同一个数据加载器能认的目录两个数据集的原始目录结构完全不同我一般会在项目里重新组织成统一格式方便加载器处理。项目源码的目录结构约定如下deeplabv3plus-pytorch/ ├── data/ │ ├── voc/ │ │ ├── JPEGImages/ │ │ └── SegmentationClass/ │ └── cityscapes/ │ ├── leftImg8bit/ │ └── gtFine/ ├── src/ │ ├── datasets.py │ ├── model.py │ ├── train.py │ └── evaluate.py └── weights/VOC 的 JPEGImages 放原始图片SegmentationClass 放语义分割标注图片和标注通过文件名一一对应。Cityscapes 则是 standard 的 leftImg8bit 和 gtFine 两套目录。数据集加载器里最关键的代码是解析标注的正确含义from PIL import Image import numpy as np # 读取 VOC 标注P 模式是调色板索引 voc_label np.array(Image.open(data/voc/SegmentationClass/2007_000032.png)) print(VOC label unique:, np.unique(voc_label)) # 输出类似 [0 1 2 ... 20 255]255 是边界 ignore # 读取 Cityscapes 原始标注需要把 id 映射成 trainId city_label np.array(Image.open(data/cityscapes/gtFine/train/aachen/aachen_000000_000019_gtFine_labelIds.png)) city_label[city_label 255] 19 # 示例把 ignore 暂时映射 # 实际必须用官方给出的 id_to_trainId 映射表转换VOC 标注里 0 到 20 直接对应类别号255 是边界Cityscapes 的 labelIds 里类别 id 从 0 到 33其中 19 到 33 大部分不参与评估官方提供了 id_to_trainId 的映射表转换后 0 到 18 对应 19 个类别255 表示 ignore。很多入门项目在 Cityscapes 上 mIoU 怎么都上不去最后发现是标注没转换模型一直在学一个错误的类别映射。建议下载后先编写一个校验脚本统计训练集和验证集里所有标注图片的类别数是否和你预期一致。VOC 应该是 21 类Cityscapes 转换 trainId 后应该是 19 类加 255。这一步能在训练前暴露数据问题而不是等到 loss 跑飞了再排查。3.3 数据增强同步裁剪和翻转才不会让标注错位语义分割的数据增强必须保证图片和标注图做完全相同的几何变换这是和分类任务最大的区别。直接调用 torchvision 的 RandomResizedCrop 处理图片、再用相同参数处理标注是可以的但多张增强组合时容易出错。常见的做法是自己封装一个同步变换类import random from torchvision import transforms class SegCompose: def __init__(self, crop_size512): self.crop_size crop_size def __call__(self, image, mask): # 随机水平翻转 if random.random() 0.5: image image.transpose(Image.FLIP_LEFT_RIGHT) mask mask.transpose(Image.FLIP_LEFT_RIGHT) # 随机裁剪到固定尺寸 w, h image.size if w self.crop_size and h self.crop_size: x1 random.randint(0, w - self.crop_size) y1 random.randint(0, h - self.crop_size) image image.crop((x1, y1, x1 self.crop_size, y1 self.crop_size)) mask mask.crop((x1, y1, x1 self.crop_size, y1 self.crop_size)) # 训练时用 ImageNet 统计值归一化 image transforms.ToTensor()(image) image transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])(image) mask torch.as_tensor(np.array(mask), dtypetorch.long) return image, mask翻转和裁剪都用一个随机变量控制保证 image 和 mask 的空间位置严格对齐。随机裁剪比直接 Resize 更常用因为 Resize 会改变目标物体的尺寸分布而语义分割模型通常希望尽量保留原始尺度信息。Cityscapes 原图是 1024×2048随机裁剪 512×1024 时要注意沿着长边裁否则会丢失太多上下文。ColorJitter 这类颜色增强可以加但对 mask 无影响单独处理即可。4. 用 Pytorch 训练 DeepLabv3从预训练模型到完整训练脚本4.1 借助 Pytorch 模型导入加载 torchvision 预训练模型并改造分类头从头训练一个 DeepLabv3 的 backbone 是不现实的加载在 COCO 上预训练的权重是标准做法。torchvision 提供了 deeplabv3_resnet50 和 deeplabv3_resnet101 两个内置模型可以直接加载预训练权重import torch.nn as nn from torchvision.models.segmentation import deeplabv3_resnet50, DeepLabV3_ResNet50_Weights # 加载 COCO 预训练权重 model deeplabv3_resnet50(weightsDeepLabV3_ResNet50_Weights.COCO_WITH_VOC_LABELS_V1) # 替换主分类头适配自己的类别数 num_classes 19 # Cityscapes 是 19 类 model.classifier[4] nn.Conv2d(256, num_classes, kernel_size1) # 替换辅助分类头训练时用 aux loss 帮助收敛 model.aux_classifier[4] nn.Conv2d(256, num_classes, kernel_size1)这里的关键点是 classifier[4] 是 DeepLabHead 最后一层 1×1 卷积aux_classifier 是 DeepLabV3 结构的辅助分类头。torchvision 的预训练模型输出的是 21 类搬到 Cityscapes 必须替换这两处。如果只替换主分类头而忘记 aux 头前向时 loss 计算会维度不匹配或直接报错训练时也建议保留 aux loss它对早期收敛有明显的稳定作用。得益于 Pytorch 模型导入的便利backbone 选择变得非常灵活。VOC 数据集相对简单ResNet50 足够跑出不错的效果Cityscapes 场景复杂度高ResNet101 是更稳妥的选择。切换模型时只需要把 deeplabv3_resnet50 换成 deeplabv3_resnet101其余代码完全不用改。如果显存不够MobileNetV3 版本的轻量 backbone 也可以作为备选mIoU 会下降几个点但推理速度快很多。4.2 训练脚本骨架SGD 优化器、poly 学习率与 mIoU 计算训练 DeepLabv3 的完整脚本不长核心是数据加载、模型前向、loss 计算和验证循环。以下是一个可运行的训练骨架import torch from torch import nn from torch.utils.data import DataLoader # 数据集和模型定义省略见前面章节 train_loader DataLoader(voc_dataset, batch_size16, shuffleTrue, num_workers8, drop_lastTrue) model model.cuda() # DeepLab 系列最常用的优化器配置 optimizer torch.optim.SGD(model.parameters(), lr0.007, momentum0.9, weight_decay0.0001) criterion nn.CrossEntropyLoss(ignore_index255) def poly_lr(epoch, max_epoch, power0.9): return (1 - epoch / max_epoch) ** power for epoch in range(50): model.train() for images, masks in train_loader: images, masks images.cuda(), masks.cuda() optimizer.zero_grad() output model(images) # output 是 OrderedDict包含 out 和 aux loss criterion(output[out], masks) loss 0.4 * criterion(output[aux], masks) loss.backward() optimizer.step() # 每个 epoch 结束更新学习率 for g in optimizer.param_groups: g[lr] 0.007 * poly_lr(epoch, 50) # 验证集计算 mIoU miou evaluate(model, val_loader) print(fepoch {epoch}, loss {loss.item():.4f}, mIoU {miou:.4f})优化器选择 SGD 而不是 Adam是 DeepLab 系列论文的原始配置在语义分割任务上收敛更稳。学习率 0.007 是针对 batch size 16 设置的如果显存不够把 batch size 降到 8学习率建议同步减半到 0.003 左右这是线性缩放规则。weight_decay 取 0.0001 比较常见也有用 0.00004 的两者对最终 mIoU 影响不大但对训练稳定性有影响。poly 学习率策略让学习率随训练进度逐渐衰减到接近 0比固定学习率或 cosine 衰减更适合分割任务。mIoU 验证时需要在 GPU 上完成前向把预测结果和 mask 的类别索引都转到 CPU然后维护一个全局混淆矩阵。验证时务必关闭数据增强特别是随机翻转和裁剪否则 mIoU 指标会不稳定。训练 50 个 epoch 只是一个参考值VOC 上通常 30 到 40 个 epoch 就能看到 mIoU 超过 60%Cityscapes 由于图片数量少、类别多50 到 80 个 epoch 比较常见。常见超参数取值范围可以参考下表这些参数在 VOC 和 Cityscapes 上的最优值略有差异参数VOC 推荐值Cityscapes 推荐值说明batch size168~16受显存限制线性缩放学习率初始学习率0.0070.007batch 减半则 lr 减半poly 幂次0.90.9DeepLab 默认配置裁剪尺寸512×512512×1024Cityscapes 按短边裁剪weight decay0.00010.0001过大影响收敛4.3 换到 mmsegmentation 跑 Cityscapes 的更省事路径自己写的训练脚本灵活但要做多卡训练、分布式评估或对比多种模型时直接用 mmsegmentation 这类分割工具箱会更省事。mmsegmentation 训练 Cityscapes 有很成熟的配置DeepLabv3 的 ResNet101 变体在开源社区里验证充分甚至可以直接用官方仓库提供的 config 启动训练。使用方式也很简单pip install mmsegmentation mim train mmsegmentation configs/deeplabv3plus/deeplabv3plus_r101-d8_4xb4-80k_cityscapes-512x1024.py --device cuda训练前的必要修改是确认 num_classes 是否为 19、ignore_index 是否为 255以及指定 pretrained 权重目录。mmsegmentation 的配置体系把模型、数据集、优化器、训练策略拆成独立模块改一个参数不需要动代码。它的数据流水线里也内置了 Cityscapes trainId 转换逻辑能省掉自己写映射表的功夫。但 mmsegmentation 对 Pytorch 版本有较严格要求配置环境时容易出现版本不匹配如果只是单卡训练一个模型自己写训练脚本反而更可控。5. DeepLabv3 训练后 mIoU 上不去的五个坑与一次可视化定位训练脚本跑通只是第一步mIoU 上不去才是真正消耗时间的地方。以下五个问题我基本每次都遇到现象排查方向解决方案loss 输出 nan学习率过大或标注里有异常像素调低 lr检查标注是否存在超范围类别值验证 mIoU 不涨BN 在 batch size 太小时不稳定增大 batch size 或使用 SyncBNloss 下降但预测全是背景ignore_index 设置错误检查 loss 输入里是否混入 255 像素Cityscapes 上指标虚高没有正确转换 trainId打印验证集预测结果的类别分布验证不同 epoch 间 mIoU 波动大验证时数据增强未关闭关闭随机翻转和裁剪后再评估如果以上都排查完了mIoU 还是异常最直观的定位方法是把预测结果可视化。光看平均 mIoU 看不出模型具体在哪个类别上崩了我一般会保存一张“原图 预测 标注”的对比图并输出每个类别的 IoU 明细import matplotlib.pyplot as plt class_names [road, sidewalk, building, wall, fence, pole, traffic light, traffic sign, vegetation, terrain] plt.figure(figsize(10, 3)) for i, img in enumerate([image, pred_mask, gt_mask]): plt.subplot(1, 3, i 1) plt.imshow(img, cmapgray if i 0 else None) plt.axis(off) plt.savefig(prediction_compare.png, dpi150, bbox_inchestight)可视化时要同步打印预测结果中每个类别的像素占比和标注中的真实占比做对比。比如 Cityscapes 里道路类别占 30% 像素如果预测里道路只有 5%说明模型把道路错分成了其他类别需要检查是不是数据集加载时类别索引错位了。预测图和标注图叠加显示往往更能定位细节问题可以把预测图以半透明方式叠加在原图上观察边缘是否存在锯齿或者大面积区域缺失。每个 epoch 结束保存一组可视化结果对比训练前后的变化趋势比盯着一堆日志输出直观得多。本文还有配套的精品资源点击获取