最近大概每隔几天就会收到一类消息:街景语义分割结果能不能帮忙跑一下,点名要用Mapillary Vistas数据集。对方往往不是真想研究算法,而是需要一张能交代的效果图。我可以直说,这个任务没有想象中那么难,但也绝不是装个环境点个鼠标就能出结果。从数据集解析到模型训练,再到评测指标,每一步都有坑。这篇就围绕街景语义分割和Mapillary Vistas数据集,把我自己跑通的完整流程、选型思路和踩坑记录从头到尾盘一遍。
很多人一上来就想着“找个开源模型,跑一下,完事”,但Mapillary Vistas和Cityscapes那种稍作预处理就能跑的benchmark不太一样。它的标注类别更细、场景光照差异大、文件量大、类别极不平衡,很多在demo里无比丝滑的代码,换到这个数据集上立刻被打回原形。如果你正好在纠结怎么跑出可信的街景语义分割结果,或者想弄明白这套数据集里到底藏了什么坑,这篇应该能帮你省下不少试错时间。
1. 先搞明白Mapillary Vistas是一份什么样的数据
1.1 一份“不是让你轻松过baseline”的街景数据集
Mapillary Vistas是面向街景理解的公开数据集,官方发布了数万张高分辨率街景图像,覆盖道路、建筑、人行道、车辆、行人、交通标志、杆件、天空等大量类别。它和普通语义分割数据集最大的区别在于“真实”:照片来自不同地区的众包采集,拍摄设备、角度、季节、天气都不一样,所以模型很难在某个城市风格上学得过分舒适。
常见评测版本里包含80个语义类别,听着好像比Cityscapes的19类多几十个而已,但实际跑到训练里完全是另一个量级。很多类别在图像中只占很小面积,比如远处的摩托车、路边的消防栓、电线杆上的小标志牌,这几类像素量加起来可能都不到一张图的万分之一,如果不做任何处理,网络很容易把它们当成背景扔掉。
另外,这份数据集的图像尺寸很大,长边经常超过4000像素。如果你直接把原图塞进GPU,哪怕是一张RTX 4090也装不下几个batch。所以训练时大家普遍采用随机裁剪或缩放策略,先切成1024×1024左右的patch再进网络。这个预处理思路本身不难,但会直接影响模型的感受野和目标尺度感知,后面会细说。
1.2 它和Cityscapes等常见数据集的本质差异
很多新手会把Mapillary Vistas理解为“另一个Cityscapes”,这其实低估了它的难度。两者最大的差异可以从数据来源和粒度来判断,我习惯用一张表说明:
| 对比维度 | Mapillary Vistas | Cityscapes |
|---|---|---|
| 图像规模 | 数万张高分辨率街景 | 几千张德国城市街景 |
| 类别粒度 | 常用80类,细到路缘、杆件、交通标志 | 19类,面向车载驾驶场景 |
| 拍摄方式 | 众包采集,视角和距离不统一 | 车载相机,视角相对固定 |
| 场景多样性 | 不同城市、天气、季节差异大 | 主要是城市道路和不同天气 |
| 标注形式 | 语义、实例、全景标注都有 | 语义和实例标注为主 |
这个差异直接决定了实验结果:在Cityscapes上能跑到80% mIoU的模型,迁移到Mapillary Vistas上经常会掉到60%甚至更低。不是因为模型不行,而是类别更多、长尾更严重、场景分布更散。很多人拿预训练权重直接跑,mIoU一出来就以为代码有问题,其实这是数据分布本身在说话。
所以,选择这个数据集做街景语义分割,本质上是在和“真实世界的长尾分布”对抗。理解了这一点,后面的所有模型选型和训练策略都会变得很自然。
2. 动手前的选型:模型、显存、运行时间要一起权衡
2.1 不要一上来就追最新模型,先想清楚你的目标是什么
街景语义分割的模型选择非常多,从FCN到DeepLab系列,再到HRNet、SegFormer、Mask2Former,每年都有新论文。但实际做项目时,我的建议是先反问自己一个问题:你是想快速拿到一个稳定可复现的结果,还是想冲击这个数据集的SOTA?这两个目标对应的模型选择完全不同。
我见过太多人一上来就上最大的Transformer模型,结果显存爆了、训练时间翻几倍,最后产出的mIoU还不如一个调参得当的DeepLabV3+。这里不是说大模型没用,而是Mapillary Vistas本身数据量大、图像分辨率高,训练开销会成倍放大,模型越重,每个实验迭代的代价越高。我常用的选型对比是这样的:
| 模型 | 核心思路 | 对Mapillary Vistas的适配性 | 显存/训练成本 |
|---|---|---|---|
| DeepLabV3+ | 空洞卷积+Encoder-Decoder | 最稳,适合第一版baseline,各类别均衡 | 中等 |
| HRNet | 高分辨率多分支并行 | 边界定位好,小目标保留更充分 | 偏高 |
| SegFormer | 分层Transformer | 长尾类别受益,但数据预处理要求高 | 速度波动较大 |
| Mask2Former | 掩码+Query统一分割 | 适合同时做全景分割,任务太重 | 很高 |
我在这个数据集上通常先用DeepLabV3+把流程全部跑通,确认数据加载、评估脚本、损失函数都没有问题,再换SegFormer或HRNet去刷指标。这样定位问题最快,也省显存和精力。
2.2 环境配置和数据准备的完整清单
环境这块没有太多花活,PyTorch是主流选择。如果不想自己管理训练循环,可以直接用现成框架,比如MMSegmentation或PaddleSeg,它们内置了Mapillary Vistas的配置和评估脚本,能帮你规避许多低级错误。我的环境大致是这样:
conda create -n seg python=3.9 -y conda activate seg pip install torch torchvision pytorch-cuda=12.1 -c pytorch -c nvidia pip install mmsegmentation opencv-python pillow tqdm数据准备阶段有几个地方需要特别小心。Mapillary Vistas的原始标注不是简单的单通道整数图,有些版本提供的是RGB可视化图,有些是带palette的单通道索引图。训练代码能直接吃的是连续整数标签,所以你需要先写一个映射脚本,把原始像素值转换成从0开始的训练ID。
另一个容易忽略的坑是图像缩放。很多框架在加载图像时会对训练图做短边对齐,但对标签图必须使用最近邻插值,不能使用双线性插值。双线性插值会在类别边界上产生混叠,导致标注边缘出现不属于任何类别的灰色值。这个错误很难察觉,落在IoU评测里可能只是零点几个点,但会在可视化结果上留下明显的伪影。
3. 训练阶段真正决定结果的东西:损失、权重、增强策略
3.1 类别不平衡比你想的严重得多
如果你直接用CrossEntropyLoss去训练Mapillary Vistas,大概率会得到一个mIoU很低、且可视化时大面积都是“道路”的模型。原因很简单:道路和建筑在街景种占的像素比例太高,网络只需要把这些类学明白,loss就已经降得很好看了。
解决这个问题有三个常用路线,我按推荐顺序列出来:
| 策略 | 适用情况 | 注意事项 |
|---|---|---|
| 类别加权CrossEntropy | 长尾类别明显,需要稳定收敛 | 权重别设太大,容易震荡 |
| OHEM(在线困难样本挖掘) | 想要把注意力集中在难例上 | 需要自己实现,或使用现成框架 |
| Focal Loss | 类别极度不平衡且难易样本差距大 | 训练收敛更慢,需要调系数 |
我在Mapillary Vistas上比较推荐“类别加权CrossEntropy + OHEM”的组合。先用类别像素频率的倒数计算权重,再在训练过程中对样本做困难样本挖掘,相当于先给长尾类一个“保底”,再让网络把精力放到容易错的地方。这样下来,自行车、摩托车、交通标志这些类别的IoU会明显改善。
3.2 我的训练策略和关键参数
下面这组参数不是唯一解,但它是我在多个实验中验证过比较稳的一套,适合DeepLabV3+或SegFormer这类模型:
- 输入尺寸:随机裁剪1024×1024,推理时使用全图resize至1024或1280;
- Batch Size:单卡尽量保证8以上,多卡用16;
- 优化器:SGD,momentum=0.9,weight_decay=1e-4;
- 初始学习率:0.01,使用poly策略衰减,power=0.9;
- 训练轮次:60~80个epoch,或者按迭代数控制;
- 数据增强:随机水平翻转、随机缩放、颜色抖动、随机裁剪。
训练代码核心逻辑可以大概长这样:
criterion = WeightedCrossEntropyLoss(weights=class_weights, ignore_index=255) optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4) def adjust_lr(step, total_steps): return 0.01 * (1 - step / total_steps) ** 0.9 for img, label in train_loader: img, label = img.cuda(), label.cuda() out = model(img) loss = criterion(out, label) optimizer.zero_grad() loss.backward() optimizer.step()这套配置跑下来,单卡A100大概需要一天多到两天,mIoU可以到比较稳定的水平。如果显存只有24G,就减小crop尺寸或batch,但尽量别低于1024,因为街景里的小目标很依赖分辨率。
3.3 怎么判断训练是否正常
很多新手只看loss曲线,loss降了就觉得万事大吉,其实不够。我在训练时会同时盯三个信号:
第一,训练loss在下降,但验证mIoU连续5个epoch不动,这通常意味着过拟合或数据增强不够,需要检查crop尺寸是否太小,或者类别权重是否压得过头。第二,可视化验证集的预测结果,不要只看mIoU数值。如果很多输出图中的建筑边缘出现锯齿状、交通标志被吞掉,说明模型对细粒度结构学习不足,可能是分辨率不够或backbone太浅。第三,看负样本行为,比如所有道路都被预测成建筑,那大概率是标签映射出了问题,而不是模型能力问题。
养成定期保存训练曲线的习惯,最好把每次实验的配置文件和mIoU结果一起记录。这个数据集不是跑一次就结束的,后续换模型、调增强、调损失,都需要和之前的基线对比才能判断改动是否有益。
4. 跑通后别急着发图:结果分析才是验收重点
4.1 常见预测错误:边界、小目标、语义混淆
我第一次跑通Mapillary Vistas时,从验证集里挑了几张可视化图,整体看着挺像样,道路、建筑、天空都分得出来。但放大看细节,问题立刻暴露了。最常见的是三类错误。
第一是边缘粗糙。车辆和行人的轮廓经常和背景粘在一起,尤其是深色车辆和阴影下的道路。根本原因是下采样让边界像素的位置信息损失了,再加上街景中大量遮挡关系。第二是小目标消失。远处的摩托车、行人、交通标志如果占不到二三十个像素,网络很容易直接忽略。第三是语义混淆,卡车和公交车、植被和树木、人行道和路缘这类视觉特征很接近的类别,预测时经常相互串。
下面是一张我在实验里统计过的逐类IoU片段,数值不代表固定结果,但分布很有代表性:
| 类别 | IoU(示意) |
|---|---|
| 道路 | 0.95 |
| 建筑 | 0.90 |
| 天空 | 0.97 |
| 人行道 | 0.84 |
| 车辆 | 0.78 |
| 行人 | 0.63 |
| 自行车 | 0.47 |
| 交通标志 | 0.41 |
整体mIoU可能很可观,比如看起来有80%上下,但交通标志和自行车只有40%多。这说明网络对长尾小目标的建模还远远不够,部署到实际系统中依然很危险。
4.2 单类IoU和整体mIoU之间的真相
很多人汇报结果时只报一个“mIoU 75%”,这其实掩盖了很多信息。语义分割这个任务里,整体mIoU是大类撑起来的,道路、建筑、天空只要不出错,分数就低不到哪里去。问题在于真正消耗人力的往往是小类、难类和边缘不清晰的类。
所以,拿到结果之后我强烈建议你按类别输出完整IoU表格,像上面那样。然后逐类观察哪几个类低于平均线,去可视化里看它们的误分类模式。比如自行车的IoU低,是预测成行人还是背景?交通标志的IoU低,是因为漏检还是边界不准?这种归因分析比单纯调模型参数更有价值。
如果你发现多个小类都低,而且模型基线已经很深了,可以考虑四个方向:增加裁剪分辨率、加入更多细粒度增强、提高小类别的损失权重、或者用边界感知的辅助损失。不要一上来就换一个更大的Backbone,那样训练成本翻倍,问题可能依旧。
5. 从踩坑记录里挑几条最值得说的
5.1 标签值不是连续编号,踩了转码的坑
这个算是Mapillary Vistas新手最常栽的坑。原始数据集的标签经常是一张RGB图,每个类别对应一个固定的RGB三元组,不经过映射就直接喂给模型,输出永远是乱七八糟的色块。正确的做法是写一个从RGB到连续训练ID的映射字典,同时把未标注区域设置为一个固定的ignore_index(通常是255)。
比如在数据处理脚本里,你可以这样做:
# 示意:把RGB值映射到连续训练ID palette2train = { (128, 64, 128): 0, # 道路 (244, 35, 232): 1, # 建筑 # 按官方类别映射继续添加... } train_mask = np.zeros((h, w), dtype=np.uint8) for rgb, label_id in palette2train.items(): mask = np.all(img == np.array(rgb).reshape(1, 1, 3), axis=-1) train_mask[mask] = label_id这里有个细节容易被忽略:映射完成后,最好统计一下训练集中每个类别的像素占比,并检查是否有类别的像素数几乎为0。有些长尾类在测试集里出现,但在某个训练切分里可能非常稀疏,如果不看分布就盲目设置权重,后面会出问题。
5.2 多GPU训练时BatchNorm的问题
Mapillary Vistas图像分辨率太高,单张图切到1024×1024后,8GB显存也很紧张。为了加大batch,很多同学会开多卡训练,这时BatchNorm就容易翻车。默认情况下每个GPU上的BN统计量是独立的,如果每个GPU的有效batch只有2,BN的均值和方差会很不稳定。
代码上的修法有几个:第一,用PyTorch的SyncBatchNorm把多卡BN同步起来,但这一步会显著增加通信开销,最好梯度累积后再做一步同步。第二,如果模型本身是Transformer结构,可以考虑使用LayerNorm,这类模型对batch size不如BN敏感。第三,尽量避免单卡batch为1或2的配置,宁可先在中等分辨率下把batch拉大,再在后期用高分辨率微调。
5.3 评测脚本的细节差异导致分数虚高
还有一次我拿到一个“很好看的mIoU”,一查评测脚本才发现问题:它没有屏蔽ignore_index区域,把未标注区域也当成一个类别算进去了。由于这些区域通常占比不小,IoU会被明显抬高两三个点甚至更多。Mapillary Vistas的评测必须把标注为ignore的像素排除,且要按官方类别顺序计算混淆矩阵,不能自己随意重排。
用同一个小改动分别评估,分数差距能到5个点以上。所以和其他baseline对比时,一定要确认你们的评估方式是否一致,最好统一使用同一个评测脚本或同一个深度学习分割框架的评估接口。否则,结果根本不可比。
6. 给“找代做”的朋友一句实话
6.1 为什么我不建议你一上来就找人代做
回到标题里那两个字“接代做”。我能理解有人想省时间,但如果只是为了拿一张结果图,代做交付给你的东西通常只有一张PNG和一个模型权重,别人不会告诉你训练集怎么切分、增强怎么设置、类别权重怎么配。你只拿到一个“结果”,没有任何过程中可能需要的分析能力。
更现实的问题是,一旦需要调整场景、增删类别,或者把模型集成到自己的系统里,代做版本往往很难扩展。做出来的模型黑盒又脆弱,出了问题你甚至不知道是代码问题还是数据问题。如果是学习,我强烈不建议跳过训练过程。因为Mapillary Vistas这个数据集真正锻炼人的地方不是“跑通”,而是理解长尾分布、类别不平衡和评测一致性,这些只有自己动手踩过坑才能掌握。
6.2 如果真的必须短时间拿结果,优先走这些路
如果时间确实紧张,我也不赞成完全从零开始。比较正规的做法是:直接用现成开源仓库里提供好的Mapillary Vistas预训练权重做推理,先出一版可视化结果,再针对你的兴趣区域做小规模微调。这本质上还是你自己在跑,但对时间预算非常友好。
另一种方式是先做一个很小的子实验集,比如随机抽2000张训练图,用轻量模型把整个流程跑通,确认数据加载、损失计算、评估脚本都正确后,再扩展到全量数据。这个方法能在半天内暴露所有流程问题,比一上来就全量训练节约太多时间。
总而言之,这个数据集的功夫都在“看不出来”的地方。模型结构、损失函数、数据预处理、评测口径,每一项都会直接改变你的结果。把基础打扎实,跑出来的才叫真正的街景语义分割结果,而不是一张没法解释的渲染图。