做了几年视觉分割方向的开发,我最大的体会是:看论文架构图时总觉得一切尽在掌握,真到自己搭模型,才发现每一步都是细节。Backbone选什么、普通ResNet和带空洞卷积的ResNet到底差在哪、要不要加Neck、数据集标签怎么转成训练格式、各种类别的IoU怎么算——这些环节单独看不难,串起来就是一道坎。
后来我大量使用MMSegmentation这个基于PyTorch的开源分割工具箱,才慢慢把“模型”这两个字拆成了“Backbone、Neck、DecodeHead”这样的组件,也终于搞懂了为什么一套代码能跑出U-Net、PSPNet、DeepLabV3+、SegFormer这些风格完全不同的模型。这篇文章就以我实际使用和翻源码的视角,把所有核心Backbone、Neck、数据集组织方式、主流算法族完整拆一遍。适合刚入门的开发者建立全局认知,也适合已经跑通但想搞懂内部逻辑、想做自定义数据集和模型替换的读者。
1. 三段式架构与config机制:为什么分割模型能像积木一样组装
1.1 从“整模型”到“组件化”的思路转变
语义分割模型看起来五花八门,但绝大多数可以抽象成一个固定流程:输入图像经过Backbone提取特征,Neck对特征做多尺度融合或增强,最后通过DecodeHead产出逐像素分类的预测图。有的模型不显式设置Neck,直接拿Backbone最后一层特征送进Head,也有的模型把解码器做得很重,比如U-Net那样逐层上采样拼接——但本质上仍然是“特征提取 + 特征处理 + 预测头”的架构。
MMSegmentation正是按照这个逻辑把整个框架拆成了若干独立组件。Backbone负责前端特征抽取,Neck处理主干输出和中间层特征,DecodeHead负责最终的分割预测和损失计算。每个组件都通过配置文件声明,而不是在代码里写死。所以我只要改一行配置,就能把ResNet换成Swin Transformer,或者把PSPNet的Head换成DeepLabV3+的ASPP模块,训练、验证、推理、可视化的整套流程都不用动。
我最早接触这类工具箱时有个误区,以为替换Backbone需要该模型的完整实现,后来才发现只要组件接口对齐,替换就像换汽车引擎一样简单。前提是你得理解MMSegmentation约定好的数据流:图像进入Backbone后输出多个层级的特征图,通常记为C2、C3、C4、C5;Neck对这些特征图做融合,得到更合适的多尺度特征;DecodeHead再从这些特征中计算最终的分割结果。整条链路全部由config文件控制。
1.2 config文件是所有选择的核心
先看一个最小可用的配置骨架,比空讲概念直接得多:
_base_ = [ '../_base_/models/pspnet_r50.py', '../_base_/datasets/cityscapes_832x832.py', '../_base_/default_runtime.py', ] model = dict( type='EncoderDecoder', backbone=dict( type='ResNetV1c', depth=50, num_stages=4, out_indices=(0, 1, 2, 3), dilations=(1, 1, 1, 2), strides=(1, 2, 1, 1), norm_cfg=dict(type='SyncBN', requires_grad=True), norm_eval=False, style='pytorch'), neck=dict( type='FPN', in_channels=[256, 512, 1024, 2048], out_channels=256, num_outs=4), decode_head=dict( type='PSPHead', in_channels=2048, in_index=3, channels=512, pool_scales=(1, 2, 3, 6), num_classes=19, loss_decode=dict(type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0)), auxiliary_head=dict( type='FCNHead', in_channels=1024, in_index=2, channels=256, num_convs=1, concat_input=False, num_classes=19, loss_decode=dict(type='CrossEntropyLoss', use_sigmoid=False, loss_weight=0.4)), train_cfg=dict(), test_cfg=dict(mode='whole'))这个配置文件里有几个关键细节:
out_indices=(0, 1, 2, 3)表示Backbone要输出全部四个stage的特征,这是FPN等Neck能工作的前提。如果只用最后一层,完全可以只保留out_indices=(3,)。dilations和strides控制空洞卷积的引入位置。这里把后两个阶段的stride改为1,同时设置dilation为1和2,目的是让最终特征图保持相对高的分辨率。neck并不是每个模型都必需。PSPNet原论文其实没有Neck,但很多成熟配置会在中间加FPN,让backbone的中间层特征也被利用起来。auxiliary_head是辅助损失分支,通常挂在backbone的第三层输出上,训练时帮助梯度传播。
我把这种文件驱动的设计当成“配置即代码”。好处非常明显:实验管理变得极轻量,每换一种Backbone或Neck组合,只需要新建一个很小的配置,而不用改动任何模型源码。对于新手,先读一个配置再对照模型结构图,往往比直接啃源码高效得多。
1.3 数据流在组件间怎么传递
理解MMSegmentation的训练流程,我建议把一次迭代拆成四步看:
- 数据流水线从磁盘读取图像和标签,做随机裁剪、翻转、颜色抖动等增强,然后把图像归一化到指定均值方差。
- Backbone接收经过预处理的图像,输出若干层特征。比如ResNet50在
out_indices=(0,1,2,3)时,输出C2(原图H/4)、C3(H/8)、C4(H/16)、C5(H/32)。 - Neck接收这些特征,进行融合。以FPN为例,它会把C2到C5统一通道数,自顶向下逐层上采样相加,得到P2到P5,每一个都包含多尺度语义信息。
- DecodeHead从Neck输出或Backbone输出中收集所需特征,做金字塔池化、ASPP、逐层上采样等操作,最终输出形状为
(N, num_classes, H, W)的预测图,再与真实标签计算交叉熵损失。
整个过程中最容易出错的地方是in_index。它表示当前模块从Backbone输出列表的哪个位置取特征,而不是特征图的实际尺寸。很多新手自定义模型时把这个数字配错,导致通道数对不上或分辨率异常,报错信息还往往很不直观。检查这类问题时,我一般先打印各阶段特征的shape,再回头看in_index。
三言两语讲完数据流,下面进入真正重头的部分:Backbone的选择与原理。
2. 核心Backbone全拆解:ResNet、HRNet与Transformer三大家族的取舍
2.1 卷积基线的双雄:ResNet与ResNetV1c
MMSegmentation里最常见的Backbone是ResNet和ResNetV1c。它们本质上都沿用残差结构:输入经过一个stride为2的7x7卷积完成初始下采样,再通过四个stage逐级降低分辨率并增加通道数。四个stage的深度不同,ResNet50是[3, 4, 6, 3],ResNet101是[3, 4, 23, 3]。
在分割任务里直接沿用ImageNet预训练的ResNet会有个问题:C5的特征图已经缩小到输入的1/32,语义信息强,但空间细节几乎丢失,这对逐像素分类非常不利。于是MMSegmentation在配置里提供了两种通行做法:
- 把最后一个stage的stride从2改成1,同时引入空洞卷积。这样C5分辨率变成输入的1/16甚至1/8,而感受野不缩小。
- 使用ResNetV1c这种变体,把stem部分原来的7x7卷积替换成三个连续的3x3卷积,预处理能力更强,也更容易对齐MMSegmentation预训练权重。
空洞卷积是这里的核心概念,我多用一句话解释:普通3x3卷积感受野是3x3,dilation=2时卷积核仍只有9个参数,但每两个采样点之间空一格,实际感受野变成5x5。所以它能在大分辨率特征图上保持大感受野,这也是DeepLab系列能持续提高分割精度的基础。
我的经验是:如果显存充足、追求精度,优先ResNet101或更大深度的ResNet;如果做快速验证或轻量部署,ResNet18就能跑通流程,但mIoU会低不少。在Cityscapes这类街景数据上,ResNet50与ResNet101的差距通常在1到3个点左右,具体取决于数据集复杂程度。
2.2 HRNet:靠并行分支保住高分辨率信息
HRNet和ResNet完全不是同一条路线。ResNet是串联式下采样,特征图一路变窄;HRNet则是从第一层开始,始终保持一个1/4分辨率的高分辨率分支,同时逐步增加1/8、1/16、1/32的并行分支,多个分支之间通过反复的特征交换模块互相融合。
这样做的好处很直接:高分辨率分支全程保留细节,低分辨率分支提供强语义,最后把多个分支的特征concat再送入Head,边缘和小目标分割效果通常比普通ResNet好。代价也明显:显存占用和计算量都更大,训练速度会慢。
我在实际项目中用到HRNet的场景,大多是工业质检或者卫星遥感切图,这类任务里小缺陷和细长目标很多,高分辨率信息价值极高。但需要提前评估显存,比如输入裁剪尺寸比较大的时候,HRNet-W48比ResNet50要多占不少显存,预算有限时常常被迫减小BatchSize或裁剪尺寸。
2.3 Transformer阵营:ViT、Swin与分层Transformer
ViT类Backbone直接把图像切成16x16的patch,每个patch通过线性投影变成token,再经过多层全局自注意力建模长距离依赖。分割任务里用ViT做Backbone并不新鲜,SETR等模型就是典型代表。但纯ViT有两个问题:一是全局注意力复杂度高,高分辨率输入友好度低;二是patch较大,直接丢失细节,需要专门的分割Head去恢复空间信息。
相比之下,Swin Transformer是日常实践里最常用的Backbone之一。它的核心改进是分层结构和窗口注意力:图像先切成4x4的patch,经过若干stage后通过Patch Merging把2x2的patch合并成一个,实现和ResNet类似的分层下采样;自注意力只在一个固定窗口内计算,复杂度不再是全局平方级,还能通过移动窗口实现跨窗口信息交互。
从工程角度说,Swin的优势是它能输出多尺度特征,正好对应MMSegmentation里FPN、U-Net等Neck和Head的输入需求。替换Backbone时,只需要配置type='SwinTransformer'、embed_dim、depths、num_heads等参数。我在Imagenet预训练权重加载顺利时,Swin-L的精度往往能明显超过ResNet101,但推理速度也明显下降,需要根据任务取舍。
更轻量的选项是SegFormer里用的Mix Transformer(MIT),它去掉了位置编码,允许不同输入分辨率直接使用预训练权重,还有B0到B5多个复杂度档位。想要快速试用Transformer分割模型时,我会优先选它而不是Swin,因为配置简单,显存占用相对友好。
2.4 Backbone对比和选择建议
为了直观对比,我按自己的使用经验整理了一张表:
| Backbone | 结构特点 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|---|
| ResNet18/34 | 轻量残差 | 训练快、显存小 | 语义表达能力一般 | 跑通流程、快速基线 |
| ResNet50/101 | 深层残差+空洞卷积可配 | 精度稳、预训练权重丰富 | 高分辨率特征细节一般 | 通用分割基准 |
| HRNet | 多分支并行高分辨率 | 边缘和小目标细节好 | 显存高、训练慢 | 工业质检、遥感、医疗 |
| Swin Transformer | 分层窗口注意力 | 精度高、多尺度特征自然 | 速度慢、权重体积大 | 追求精度、资源充足 |
| MIT/SegFormer系列 | 无需位置编码的分层Transformer | 轻量、分辨率适应性强 | 超大模型仍需堆算力 | 快速迭代、部署 |
我不会一上来就说“必须用哪个”,而是建议:第一次跑通项目先用ResNet50,把数据、Head、训练策略都调明白,再根据瓶颈换Backbone。如果换上去精度没提升,问题往往不在Backbone,而在Neck或数据侧。做完Backbone替换后,下一步就是Neck。
3. Neck不是可有可无:FPN与特征融合在分割里到底解决了什么
3.1 分割里的多尺度矛盾
很多分割模型直接拿Backbone最后一个stage的特征图做预测,这样做的问题是:深层特征语义强,但分辨率低,小目标和物体边界很容易糊在一起。浅层特征分辨率高,但语义不够抽象,容易把同类物体的不同实例和背景混淆。检测任务早就用FPN解决了多尺度问题,分割任务同样需要。
Neck就是要解决“不同层级的特征如何各取所长”。如果只在最后一层特征上接Head,等于放弃C2、C3这些高分辨率信息;如果简单把多层特征相加,又会带来严重的语义不对齐。于是FPN等结构成为标准选择。
3.2 FPN的工作机制与实际收益
FPN的完整流程是:Backbone输出C2到C5,各自通过1x1卷积统一到相同通道数(比如256);然后从最深层开始,每次把上一层特征上采样2倍,与下一层特征逐元素相加,得到P2到P5。这样P2到P5每层都融合了深层语义和浅层细节。
在MMSegmentation里配FPN非常简单:
neck = dict( type='FPN', in_channels=[256, 512, 1024, 2048], out_channels=256, num_outs=4)之后DecodeHead可以从in_index为0到3的4个特征里做融合。比如SegFormer的Head就是这么干的:把4层特征都上采样到输入分辨率,concat后过MLP分类。
我做过一组简单对比:同样用ResNet50,在不改Head的情况下加一个FPN,整体mIoU大概能提升1到2个点,而显存增加非常有限。对小目标和多尺度场景,提升会更明显。但FPN也不是越多越好。有些简单任务用一层特征就够了,强行加Neck反而让训练变慢、调参变量更多。
3.3 语义FPN和Neck选型的经验
与检测里的FPN不同,分割领域还有一个常见的“语义FPN”,典型实现是把多层特征分别上采样到相同分辨率并拼接,再交给一个简单的分类层。它不做逐层自顶向下融合,而更像“把蛋糕切块再摊平”的思路。Panoptic Segmentation和某些轻量分割模型经常用这种设计。
我在自主搭建分割流程时,Neck的选型准则是:数据集目标尺度差异大、小目标多,就上FPN;医学图像等目标结构固定,则直接使用U-Net的编码器-解码器跳跃连接,不太需要单独强调Neck;如果用Transformer类Backbone,它们本身已经输出多层级特征,Neck用轻量FPN或MLP就够了。
一句话总结:Neck不是每个模型都必须,但它决定了Backbone提取出的多尺度特征能否被有效使用。忽略Neck,等于只拿Backbone最粗的一层特征去做精细分割。
4. 数据集类型盘点:从Cityscapes到自定义数据的格式设计与评估指标
4.1 内置数据集类型与各自特点
MMSegmentation内置了大量数据集类型,我常接触的有四个:Cityscapes、ADE20K、PASCAL VOC和COCO Stuff。
Cityscapes是街景驾驶数据集,31个原始类别中通常只用19个进行评测,图像分辨率高(1024x2048),但训练集只有2975张精细标注图像,这导致模型很容易过拟合,需要比较强的数据增强。ADE20K是场景解析数据集,包含150个物体类别,图像规模约2万多张,类别极不均衡,很多类在训练集中出现次数很少。PASCAL VOC是最经典的精标注数据集,21类含背景,虽然规模不如前面两个,但接口简单,我经常拿它做算法快速验证。COCO Stuff是COCO数据集的Stuff扩展,增加了大量背景区域类别,适合做全景分割或带有强大环境语义的任务。
这些数据集的差异不只是类别数量,标签文件格式也不同。Cityscapes的标注图是彩色PNG,需要转成trainId才能作为训练标签;PASCAL VOC则是单通道调色板PNG;ADE20K的label索引从0到149直接使用。刚开始自定义数据处理时,最容易在这些格式上翻车。
4.2 自定义数据集的标准流程与常见踩坑
在实际项目中,几乎没有人会直接用内置数据集跑业务,都得自己做数据集。MMSegmentation支持两类常见格式:VOC格式和COCO格式。我一般优先VOC格式,目录结构如下:
data/custom/ ├── JPEGImages/ │ ├── img_001.jpg │ └── img_002.jpg ├── SegmentationClass/ │ ├── img_001.png │ └── img_002.png └── ImageSets/ └── Segmentation/ ├── train.txt └── val.txtJPEGImages放原图,SegmentationClass放灰度标签图,train.txt和val.txt里每行是图片名(不带扩展名)。注意标签图中的类别索引必须从0开始,0通常代表背景,255表示ignore区域,训练时会跳过。如果漏掉这条,模型会把背景当成第0类,甚至误把255也算进loss里,导致训练无法收敛。
数据集配置要修改的字段主要包括data_root、img_dir、ann_dir、classes和palette。尤其是palette,它决定可视化时每个类别的颜色,顺序必须和classes一一对应,否则会画出颜色错乱但mask索引正确的图,肉眼难以发现,却在定性评估时造成困扰。
我强烈建议每次新数据集训练前,先跑一个类别像素占比分析脚本。比如某些类只占全部像素的0.1%,在交叉熵loss里几乎不产生梯度。遇到这种情况,要么用class_weight放大稀有类的权重,要么用OHEM损失,要么干脆采集更多该类样本。只盯着总体accuracy很容易被大背景类骗过去。
4.3 评估指标:mIoU与mPA到底怎么算
语义分割最常用的指标是mIoU和mPA。IoU是每个类别预测区域与真实区域的交集除以并集,mIoU是所有类别IoU的平均。mPA则是每个类别预测正确的像素比例的平均。两者都不考虑类别像素占比,所以对稀有类更敏感。
举个小例子:某类在图像中只有10个像素,模型一个没预测对,因为总像素中其他类占99%,整体像素准确率可能依然很高,但mIoU会被这类拉下来不少。这也是为什么论文和比赛几乎都说mIoU,而不是单纯像素准确率。
在实际训练时,我会在验证集上逐类打印IoU,优先看稀有类的表现。如果大多数类都有50%以上的IoU,只有某个类接近0,通常是标注问题、类别数过少、或者特征区分度不够,需要具体分析,而不是盲目调学习率。
数据集这块是整个分割流程的地基,地基不平,模型再先进也没用。下面进入重头戏,把几个有代表性的算法族逐个拆开看。
5. 主流模型算法族逐类剖析:U-Net、PSPNet、DeepLab与SegFormer
5.1 U-Net家族:编码器-解码器与跳跃连接的价值
U-Net来自医学图像分割,结构上就是一个对称的编码器-解码器:编码器逐步下采样提取语义,解码器通过上采样恢复分辨率,每一层解码器都会和对应的编码器特征做拼接,形成跳跃连接。这种设计让细节信息可以直接传到解码端,对小结构、边界不规则的物体非常友好。
MMSegmentation里的U-Net实现可以通过backbone=dict(type='UNet', ...)加载,配置灵活,支持不同stage数、通道数和是否使用预训练权重。我在医学图像和工业小目标分割中多次使用U-Net,它的最大特点是数据需求量相对小、训练稳定。
但如果目标尺度极端多样,或者背景非常复杂,U-Net的拼接式解码器会显得笨重,因为它所有中间特征都同等对待,没有实现自动的重点特征选择。此时后续模型系列会更合适。
5.2 PSPNet:金字塔池化模块如何捕获全局上下文
PSPNet的核心是Pyramid Pooling Module,简称PPM。在Backbone输出的最后一层特征上,PPM分别以1x1、2x2、3x3、6x6等不同尺度做全局平均池化,池化结果经过1x1卷积压缩通道后,全部上采样回原尺寸并和原始特征拼接,最终得到一个融合了全局与局部上下文信息的特征。
这种做法解决了一个朴素问题:只看局部卷积特征时,模型很难知道这是一个“广阔的停车场”还是一个“狭小的路口”。PPM用多尺度池化把不同范围的全局信息都塞进特征,能显著提升场景理解能力。PSPNet还经常配合一个辅助损失分支,在backbone中间层加FCNHead,帮助中低层学到更多语义。
我在比较PSPNet、DeepLabV3和SegFormer时,会特别关注PPM对上下文信息的需求。若数据集中物体尺度差异大且场景语义依赖强,比如无人驾驶和遥感,PSPNet是性价比很高的选择。但它的感受野固定,对极细小的物体边界仍有些吃力。
5.3 DeepLab系列:空洞卷积、ASPP与Encoder-Decoder的推进
DeepLab系列经历了V1到V3+的演进,核心贡献是把空洞卷积用到了极致。DeepLabV3的主力模块是ASPP,它对Backbone最后一层特征并行施加多个不同dilation rate的空洞卷积,再并联全局平均池化分支,最后把多分支结果concat。这样一片特征图中能同时看到不同感受野的信息。
DeepLabV3+在V3基础上加入了解码器:把ASPP输出的低分辨率特征上采样后,与Backbone早期的高分辨率特征拼接,再通过卷积融合。这个改动让边界细节大幅改善,因为浅层特征拥有更多空间位置和边缘信息。实践里DeepLabV3+长期是语义分割的强基线,我在很多公开数据上跑,效果稳定且不容易炸。
使用DeepLabV3+时有一个关键点:空洞卷积率的设置需要配合Backbone输出分辨率。如果原本Config里output_stride=8,直接把dilation rate调得过大,感受野会覆盖太多无关区域,甚至带来棋盘格式的特征空洞;反之率太小,则多尺度建模能力下降。一般默认的ASPP rate为6、12、18,配合ResNet输出的下采样率已经比较稳,除非有特别需求,否则我不会随意改。
5.4 SegFormer:无位置编码的Transformer分割新思路
SegFormer在结构上给出了更简洁的答案:用Mix Transformer做分层Backbone,不用位置编码;解码器只做轻量的MLP融合,把所有层级的特征上采样到同一尺寸后concat,再过MLP直接预测。由于没有位置编码,测试时即使输入分辨率和训练时不同,也不需要调整位置编码插值,模型显得非常“自在”。
SegFormer的MLP解码器看起来简单,但它能充分受益于TransformerBackbone提供的多尺度特征。整体上SegFormer在精度和速度之间的平衡很好,B0到B5可以覆盖移动端到高性能服务器的需求。我试过用SegFormer-B3替换DeepLabV3+的ResNet101,mIoU大致相当甚至略高,但推理速度和显存占用更友好。
需要注意的是,SegFormer在较大输入尺寸下依然需要足够计算资源,B4/B5配合高分辨率小作物时GPU内存占用会飙升。如果资源有限,建议从B2开始试,再根据效果往上顶。
5.5 模型选型对比与建议
下面这组对比来自于我跑过的多个项目,具体数值会随数据集变化,但规律比较稳定:
| 模型 | 核心模块 | 典型优势 | 典型不足 | 适用场景 |
|---|---|---|---|---|
| U-Net | 编码器-解码器+跳跃连接 | 小数据友好、边缘恢复好 | 大尺度场景特征融合弱 | 医疗、工业小目标 |
| PSPNet | PPM多尺度池化 | 全局上下文强 | 边界细节一般 | 街景、场景解析 |
| DeepLabV3 | ASPP空洞卷积 | 多尺度鲁棒、稳定 | 解码器弱、边缘偏粗 | 通用分割基线 |
| DeepLabV3+ | ASPP+解码器 | 多尺度+边缘细节兼顾 | 计算量较大 | 通用精度优先 |
| SegFormer | MIT+MLP解码器 | 轻量高精度、分辨率适应好 | 超大模型推理慢 | 综合权衡、部署 |
选型的真正逻辑是看“任务短板”。小目标多,先考虑HRNet或更深的解码器;上下文严重依赖场景,PSPNet和ASPP效果好;需要快速迭代和部署,SegFormer轻量版本最舒服。没有万能模型,只有合适与否。
6. 训练配置与我最常踩的坑:参数、标签、显存和可视化细节
6.1 训练参数与学习率策略怎么定
MMSegmentation默认采用的训练策略效果很稳:优化器用SGD或AdamW,初始学习率通常设为0.01(配合SGD)或6e-5(配合AdamW),并结合Poly学习率衰减,即学习率按(1 - iter/max_iters)^power逐步下降,power默认0.9。整个训练过程跟随迭代次数而不是epoch轮数,max_iters一般设成160k或320k。
对于常见分割任务,我习惯用SGD+momentum0.9,配合权重衰减0.0005,效果最稳定。Transformer类Backbone则更适合AdamW,学习率更低。此外,配置里还要添加warmup,前1000次迭代逐步把学习率从很小的值升到预设值,避免训练初期剧烈震荡。
6.2 我踩过四次的高频坑
第一次坑是标签类别数不匹配。自定义数据集标注时把0作为背景,但配置了num_classes=20,背景又被算作有效类,导致背景类IoU虚高、实际目标类别学不好。后来统一规则:背景0有效但显式加入class列表,或者用255做ignore区域,并让num_classes=19不包含背景,二者选一,绝不混用。
第二个坑是同步批归一化在多卡训练时的依赖问题。SyncBN能在多卡训练时同步统计均值和方差,对小BatchSize非常有用,但某些PyTorch版本和GPU卡配置下会报错或收敛异常。做多卡训练前,先确认环境和配置里的norm_cfg来源一致,不要出现混合使用BatchNorm和SyncBN的情况。
第三个坑是显存不足时乱调BatchSize导致模型退化。我一度把BatchSize从8降到2,结果精度大跳水,后来才意识到小Batch下BN统计不稳定。正确做法是优先缩小输入裁剪尺寸,其次减少GPU上的评估批次,而不是硬调训练BatchSize。如果一定要小Batch,就换成GroupNorm或者启用SyncBN。
第四个坑是数据增强里的类别不平衡放大。许多分割数据集默认配置里包含随机裁剪、翻转和颜色抖动,但如果我的小类别本来就少,随机裁剪恰好容易把稀有类裁掉,长期训练后稀有类完全消失。我现在的做法是统计类别覆盖面积,给稀有类设定专门的最小保留比例,或用Resize配合固定采样来降低这种随机风险。
6.3 推理加速和TTA取舍
推理阶段,TTA(Test Time Augmentation)可以带来明显提升。MMSegmentation支持多尺度推理和水平翻转推理,通常把test_cfg里配置flip=True并设置多组scale,就能多拿1到3个mIoU点。代价是推理时间成倍增加。生产环境中我会关闭TTA,只保留一个固定分辨率,优先保证延迟稳定。
加载模型进行推理时,也容易被“加载完整模型dict”和“加载backbone预训练”搞混。如果是从头训练后再推理,用load_from指定完整权重;如果只是拿backbone的ImageNet预训练权重,需要配置pretrained字段。两者混用会导致权重覆盖出问题,训练效果反而不如随机初始化。
6.4 可视化与日志检查的技巧
训练过程中不要只盯loss曲线。我会同时开启TensorBoard的mIoU、准确率和每一类的IoU曲线,因为loss下降不代表每个类别都均衡地变好。发现某个类曲线长期平走,通常要先看是不是标签质量有问题:比如图像边界标注不准、某些类别被其他类遮挡过度,都会直接体现在单类IoU上。
分割可视化的一个重要细节是palette对齐。MMSegmentation内置常用数据集的palette,但自定义数据集必须自己给每个类别指定颜色。我习惯把颜色选成视觉差异大的RGB组合,这样一眼就能看出模型是不是发生了混淆,而不是把所有前景都预测成一个类别。
最后说一个我沿用很久的工作习惯:拿到任何一个新分割模型,我都不直接把它塞进项目,而是先用它默认配置里的Backbone和Head跑一次标准数据集,得到baseline,再逐项替换组件。替换Backbone时只改Backbone,替换Neck时只改Neck,绝不一次性变化太多变量。MMSegmentation这种“积木式”组装方式,让我把精力重新放回“模型差异”和“数据本身”,而不是每次换模型都要重写一遍训练测试流程。如果你正准备开始做语义分割,我建议就从这套组件逻辑入手,先把Backbone、Neck、Head各自的角色摸清楚,再往里面填自己的数据和模型,你会发现这条路比想象中顺很多。