
简介一份基于Pytorch的语义分割与场景理解项目实战包面向计算机视觉初学者及进阶开发者。资源围绕MIT-ADE20K大规模数据集提供从数据预处理、模型构建到训练评估的完整流程帮助读者掌握像素级分类与场景结构分析的关键方法。压缩包共101个文件其中68个Python脚本涵盖核心网络与训练逻辑14个YAML配置文件用于实验参数管理另含标注文件、CSV类别信息、可视化演示及说明文档整体大小仅2.93MB轻量且便于快速部署。目前已有781人学习浏览项目内含SegNet等经典分割网络实现并附Notebook交互式演示可直接观察分割效果。通过阅读源码并运行实验读者能深入理解数据增强、损失函数和评估指标的实际运用也可基于现有代码调整超参数或尝试新结构进一步拓展场景理解方向的研究与实践。1. 从像素分类到场景理解为什么 ADE20K 才是绕不过去的考场做语义分割的人常有这样一种错觉在 Cityscapes 或 VOC 上把 mIoU 刷到 70% 以上就觉得自己已经把“分割”这件事搞明白了。但一旦把模型丢到 MIT-ADE20K 上很多人会得到一个非常难看的数字——不是模型代码写错了而是 ADE20K 这个数据集的难度曲线和前面那些赛道完全不在一个量级。它包含了 150 个语义类别从家具、交通工具到墙面的材质、天空的纹理很多类别在单张图里只占几十个像素长尾分布极其严重。也就是说语义分割在这个数据集上的目标不只是“把物体轮廓描出来”而是让模型具备真正的场景理解能力知道一张图里“哪里是什么、彼此是什么关系、哪些小目标容易被漏掉”。这篇内容打算顺着“Pytorch MIT-ADE20K”这条线把一个可落地、可复现的语义分割与场景理解工程拆开讲清楚从数据集的加载与评估协议到基于 Pytorch 的模型搭建与训练参数设置再到多尺度推理、逐类指标诊断和模型导出。整个方案不依赖某个特定仓库的魔改代码而是用 Pytorch 基础框架能力一步步把工程里最常见的坑和最优做法过一遍。适合正在做算法落地、研究生课题或者自己动手复现论文的实验者阅读看完可以直接在自己的显卡上跑通。2. ADE20K 数据集与场景理解任务先搞清楚你要预测什么2.1 150 类的长尾分布决定了损失函数和评估方式ADE20K 的官方训练集包含大约 2 万张图像验证集约 2000 张每张图像都对应一个像素级标注的 mask覆盖面非常广。和 VOC 的 21 类、Cityscapes 的 19 类不同ADE20K 的 150 个类别里既有“人”“车”“树”这种常见大类也有“灯”“开关”“插座”这种极难识别的小物体。官方还额外提供场景分类标签因此它也常被用来做场景理解和语义分割的联合研究。做这个数据集的第一步是理解它的评估协议。官方推荐使用 mIoUmean Intersection over Union作为主指标计算时对 150 个类别的 IoU 取平均。由于类别极度不均衡那些出现频率低的类别对最终分数的拖累非常大。指标计算方式对长尾的敏感度mIoU各类别 IoU 的算术平均高mAcc各类别准确率的平均中aAcc全像素准确率低Frequency Weighted IoU按像素频率加权低我一般会同时观察 mIoU 和 mAcc前者反映边界质量与类别召回后者能暴露模型是否把某些类彻底忽略了。如果 mAcc 比 mIoU 低很多说明某些类预测出来但位置偏了反之则说明小类被漏检。2.1.1 自制 Dataset 的加载细节torchvision 没有直接封装 ADE20K 的官方下载所以需要自己写 Dataset。常见做法是使用官方发布的annotations目录里的training和validation文件夹图像与 mask 文件名一一对应。下面这段代码给出了一个可跑的骨架import os import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class ADE20KDataset(Dataset): def __init__(self, image_dir, mask_dir, transformNone, mask_transformNone): self.image_dir image_dir self.mask_dir mask_dir self.transform transform self.mask_transform mask_transform self.images sorted(os.listdir(image_dir)) def __len__(self): return len(self.images) def __getitem__(self, idx): img_name self.images[idx] img_path os.path.join(self.image_dir, img_name) mask_path os.path.join(self.mask_dir, img_name.replace(.jpg, .png)) image Image.open(img_path).convert(RGB) mask Image.open(mask_path) if self.transform is not None: image self.transform(image) if self.mask_transform is not None: mask self.mask_transform(mask) # mask 转为 LongTensor类别索引从 0 到 149 mask torch.as_tensor(np.array(mask), dtypetorch.long) return image, mask这里有几个容易被忽略的细节。第一ADE20K 的 mask 图像里背景类和其他物体的索引是连续排列的训练时不需要像 VOC 那样做 -1 偏移但需要确认类别数是否为 150。第二mask 在读取时不能做归一化因为它是类别索引而不是概率图。第三如果使用了随机裁剪、翻转等数据增强必须保证 image 和 mask 使用同一组随机参数最简单的做法是把二者拼在一起做变换或者使用 torchvision 的Compose分别传入同一个随机种子。2.2 数据增强与类别权重解决长尾问题的第一步长尾分布最直接的危害是模型倾向把稀缺类别预测成常见类别。例如“插座”和“白色墙壁”在像素层面高度相似如果训练数据里插座出现得少模型很容易把插座区域直接当成墙壁。应对策略有两个层面数据层面做增强和重采样损失函数层面做类别加权。训练时我会保留以下增强组合随机缩放0.5 到 2.0然后随机裁剪到固定尺寸例如 512x512左右翻转概率 0.5颜色抖动包括亮度、对比度、饱和度调整幅度不要太大否则会影响材质类别的判断随机遮挡CutOut 的一种变体帮助模型减少对局部纹理的依赖由于 ADE20K 图像尺寸差异大直接 resize 会把小物体的比例压变形多尺度训练有利于小物体分割。常见做法是把训练分辨率设置在 384 到 640 之间随机选择验证时固定为 512。类别权重的计算方式一般采用中位数频率平衡median frequency balancing或者简单的 inverse frequency。计算代码如下import numpy as np def compute_class_weights(mask_paths, num_classes150): freq np.zeros(num_classes, dtypenp.float64) for mask_path in mask_paths: mask np.array(Image.open(mask_path)) for c in range(num_classes): freq[c] (mask c).sum() # 中位数频率平衡 median_freq np.median(freq[freq 0]) weights median_freq / (freq 1e-6) return torch.from_numpy(weights.astype(np.float32))这个权重表可以直接传给 CrossEntropyLoss 的weight参数。但要注意权重设置过大容易导致训练初期 loss 剧烈震荡建议对权重做一次截断比如限制在 [0.1, 10] 范围内或者先不使用权重等模型训练到中段再加入这种渐进式加权策略在许多实验中比全程加权更稳。3. 基于 Pytorch 的语义分割模型搭建从 DeepLabV3 到 backbone 替换3.1 为什么 DeepLabV3 是 ADE20K 上最稳妥的起点语义分割的模型选型通常会面临一个矛盾U-Net 类模型结构简单、易训练但感受野有限难以捕捉全局场景信息而 Transformer 类模型性能强但对数据量和训练技巧要求高。DeepLabV3 正好处于一个平衡点它通过 ASPP空洞空间金字塔池化模块用不同膨胀率的空洞卷积并行采样能够在不大幅增加计算量的前提下扩大感受野非常适合 ADE20K 这种同时存在大物体墙壁、天空和小物体开关、插座的复杂场景。Pytorch 官方torchvision库直接提供了 DeepLabV3 的实现使用deeplabv3_resnet101或deeplabv3_resnet50作为主干。以下是把它迁移到 ADE20K 150 类的最小代码import torchvision.models.segmentation as segmentation model segmentation.deeplabv3_resnet50( weightssegmentation.DeepLabV3_ResNet50_Weights.COCO_WITH_VOC_LABELS_V1, num_classes150 )需要注意num_classes参数必须和预训练权重维度不一致。如果直接加载 COCO 预训练权重然后修改输出层Pytorch 会报 shape mismatch。常见做法是先把model.classifier[4]最后的 1x1 卷积替换成输出 150 维的新卷积层然后只加载 backbone 部分的权重或者放弃预训练权重从零开始训练。从零训练 DeepLabV3 在 ADE20K 上需要较长时间收敛因此更推荐冻结 backbone 前几层、只解冻后半部分和 ASPP 头的迁移学习方案。3.2 用 timm 替换 backbone当 ResNet 不够用时的灵活方案标准 DeepLabV3 的可扩展性略差因为 ASPP 模块的输入通道数是固定的替换主干网络时需要同步调整空洞卷积的输入维度。社区里更工程化的做法是使用timm库加载各种 backbone然后自己组装一个分割头。下面展示一个用timm.create_model替换主干的最小实现import timm import torch.nn as nn import torch class SimpleDeepLabV3(nn.Module): def __init__(self, backbone_nameresnet50, num_classes150): super().__init__() self.backbone timm.create_model( backbone_name, pretrainedTrue, features_onlyTrue, out_indices[4], output_stride16 ) # 获取 backbone 输出通道数 self.aspp nn.Sequential( nn.Conv2d(self.backbone.feature_info.channels()[-1], 256, 1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), ) self.classifier nn.Conv2d(256, num_classes, kernel_size1) def forward(self, x): features self.backbone(x)[0] # 取最后一个 feature map x self.aspp(features) x self.classifier(x) # 上采样到原图尺寸 x nn.functional.interpolate( x, sizex.shape[-2:], modebilinear, align_cornersFalse ) return x真正做场景理解任务时只取最后一个特征图仍会丢失小目标的细节。更合理的做法是保留两个层级的特征图用 FPN 结构将其融合在 ADE20K 长尾类别检测上效果提升相当明显。out_indices参数可以返回多个尺度特征例如out_indices[2, 3, 4]然后分别做 1x1 卷积降维、上采样相加再用一个简单分割头输出。这个方法的价值在于换 backbone 从改模型结构变成改一行字符串想测试convnext、efficientnet、mobilenet或者带 Transformer 结构的maxvit不需要改其他任何逻辑。3.3 辅助损失与深监督让梯度在长尾任务里流动得更充分单纯依赖最后的分割输出计算 loss对深层的分类层有效但对浅层细节边缘的监督不足。受 PSPNet 和 U-Net 系列启发我在中间特征层上额外挂一个辅助分割头用一个较小的权重如 0.4把辅助损失叠加到总损失上。这个技巧在 ADE20K 上的收益比在 Cityscapes 上更明显原因是小类别物体往往依赖中低层语义信息。def auxiliary_loss(model, outputs, targets, aux_weight0.4): # 假设 model 的 forward 返回 (main_out, aux_out) main_out, aux_out outputs main_loss nn.functional.cross_entropy(main_out, targets) aux_loss nn.functional.cross_entropy(aux_out, targets) return main_loss aux_weight * aux_loss注意辅助分支的输出特征图尺寸必须和 targets 对齐一般做法是直接把 aux 分支的特征图上采样到 targets 的分辨率再算损失。推理阶段不用修改模型结构因为辅助分支只在训练时参与。4. 训练策略与排错让模型在 ADE20K 上真正收敛4.1 超参数配置表照着跑就能复现一份不错的结果模型结构确定后训练策略直接决定最终指标。下面是默认推荐的超参数涵盖不同显存容量下 batch size 的对应设置。超参数推荐取值说明学习率0.01poly 策略batch size 16 时SGD 搭配 momentum0.9, weight_decay1e-4初始分辨率512x512显存不够时先用 384后期再调大Batch Size按显存调建议至少 8 张过小会导致 BatchNorm 统计不稳定训练轮数60 到 80 epochADE20K 数据量大少于 40 epoch 难收敛学习率调度polypower0.9比 cosine 更适合分割任务的长尾收敛混合精度开启 AMP可减少 40% 显存占用训练速度提升明显poly 学习率策略的核心公式是lr base_lr * (1 - iter/total_iter)^power它让学习率后期缓慢下降能够更精细地收敛长尾类别的决策边界。Pytorch 实现时可以用torch.optim.lr_scheduler.LambdaLR配合如下代码def poly_lr(epoch, max_epoch, power0.9): return (1 - epoch / max_epoch) ** power scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambdalambda epoch: poly_lr(epoch, max_epoch80) )4.2 一个完整的训练循环骨架完整的训练流程需要关注几个细节混合精度缩放、梯度裁剪、mask 上采样对齐、loss 统计。以下代码段给出核心逻辑scaler torch.cuda.amp.GradScaler() criterion nn.CrossEntropyLoss(ignore_index255) for images, masks in train_loader: images images.cuda() masks masks.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images)[out] # torchvision 模型 outputs nn.functional.interpolate( outputs, sizemasks.shape[-2:], modebilinear, align_cornersFalse ) loss criterion(outputs, masks) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) scaler.step(optimizer) scaler.update() scheduler.step()这段代码里最容易出错的地方是interpolate。DeepLabV3 输出的是输入图像的 1/8 或 1/16 分辨率特征图如果直接与原始尺寸 mask 算 lossPytorch 会报维度错误。使用ignore_index255可以跳过无效区域在图像边界裁剪时避免影响 loss。梯度裁剪对分割任务不是必需的但在混合精度环境下能避免梯度爆炸引起的 NaN loss。4.3 训练失败时的三个排错视角loss 不下降和 mIoU 始终在低位徘徊是最常见的两个问题排查顺序要按数据、模型、环境三层递进。数据层面先确认 mask 可视化是否与图像对齐。很多人在做“语义分割数据集如何制作”的自制数据时最容易犯的错是 mask 和原图 dimensions 不匹配。先用matplotlib把原图和 mask 叠加打印出来看确认物体轮廓是否重合再检查类别索引是否有空洞。ADE20K 的索引是连续分配的但部分第三方转换脚本可能会把背景类设为 255这时必须重映射为 0。模型层面观察验证集逐类准确率。训练 10 个 epoch 后如果大多数类别的 IoU 都是零说明学习率过大导致模型饱和如果常见类别 IoU 正常但小众类别全零则检查类别权重是否生效以及是否启用了 OHEM 在线困难样本挖掘。OHEM 在长尾任务中比较有效但实现稍复杂可以用 top-k 像素 loss 近似替代。环境层面混合精度下 loss 为 NaN优先确认 CUDA 和 cuDNN 版本是否匹配。Pytorch 2.x 搭配 CUDA 12.x 时某些旧版本 GPU 驱动会在 AMP 反向传播阶段出现数值溢出常见解决办法是关闭 AMP 或者在GradScaler中设置init_scale2**10减少初始缩放。5. 推理、可视化与逐类诊断把分割结果变成可用的场景理解5.1 多尺度加翻转 TTA不增加训练成本就能提升指标验证时如果直接把单个尺度的模型输出上采样再计算 mIoU通常会比训练时的验证指标低 1 到 3 个点。多尺度推理Multi-Scale Inference是分割任务的标准提分手段常见做法是对输入图像取 0.75、1.0、1.5 三个尺度同时加入水平翻转最后将各结果插值回原尺寸并取平均。import torch.nn.functional as F def tta_inference(model, image, scales[0.75, 1.0, 1.5], flipTrue): model.eval() _, h, w image.shape logits_sum None with torch.no_grad(): for scale in scales: new_h, new_w int(h * scale), int(w * scale) img_resized F.interpolate( image.unsqueeze(0), size(new_h, new_w), modebilinear, align_cornersFalse ) logits model(img_resized.cuda())[out] logits F.interpolate(logits, size(h, w), modebilinear, align_cornersFalse) if flip: logits_flip model(torch.flip(img_resized.cuda(), dims[3]))[out] logits_flip F.interpolate(logits_flip, size(h, w), modebilinear, align_cornersFalse) logits torch.flip(logits_flip, dims[3]) if logits_sum is None: logits_sum logits else: logits_sum logits logits_sum / len(scales) * (2 if flip else 1) return logits_sum.argmax(dim1).squeeze(0)这段代码的逻辑是每个尺度的 logits 都先上采样回原始尺寸再累加避免在小尺度图像上直接 argmax 导致物体边缘锯齿。翻转推理时要把 logits 翻转回原方向再累加不能翻转预测结果。多尺度推理会让推理时间翻倍如果是视频或在线推理场景建议只在精度评测或离线任务里使用。5.2 按类计算 IoU定位模型短板的唯一正确方法平均 mIoU 会掩盖很多问题必须把 150 个类别的 IoU 输出成表格按升序排列观察尾部类别。下面是一个完整的逐类评估代码def compute_per_class_iou(preds, targets, num_classes150): ious [] for c in range(num_classes): pred_c (preds c) target_c (targets c) intersection (pred_c target_c).sum().item() union (pred_c | target_c).sum().item() if union 0: ious.append(float(nan)) else: ious.append(intersection / union) return ious把结果写入 CSV 后你会看到模型在“天花板”“地板”“墙壁”这类大面积背景类上表现很好而在“玻璃杯”“台灯”“画框”这类小物体上 IoU 可能只有个位数。这就是场景理解能力不足的真实体现——模型没有学会利用上下文关系来推断这些物体的位置。这时候再针对性地补充这些类别在训练集中的数量或者调整 loss 权重才有意义。5.3 分割结果可视化与颜色映射的工程实现把预测 mask 叠加到原图上是算法工程师和 reviewer 沟通最直观的方式。每次运行时使用的颜色映射必须固定否则不同时刻跑出的可视化结果颜色不一致无法做对比分析。下面用固定 colormap 实现可视化import matplotlib.pyplot as plt import numpy as np def decode_segmap(mask, num_classes150): # 固定生成一组 RGB 颜色 cmap np.random.RandomState(42).rand(num_classes 1, 3) rgb cmap[mask] return (rgb * 255).astype(np.uint8) mask pred.cpu().numpy() colored decode_segmap(mask) plt.imshow(colored)颜色映射在工程部署时往往被忽略但在做 PPT 汇报、写技术文档或对比实验时如果两次运行的颜色不一致很难说服别人相信两个结果是同一个模型产出的。把 colormap 序列化保存到 json 文件里每次运行时加载要比每次重新生成稳妥得多。如果你想继续深挖可以尝试训练一个辅助场景分类头把图像级场景标签和像素级分割结果做联合推理——例如模型预测出“沙发”和“茶几”那么图像场景大概率是“客厅”。这种场景理解层面的推理要比单纯堆 mIoU 更能体现 ADE20K 的设计初衷也是这个数据集区别于其他分割 benchmark 的最大价值所在。本文还有配套的精品资源点击获取