MMDetection 长尾实例分割实战:Seesaw Loss 原理、LVIS 配置与复现指南
【免费下载链接】mmdetectionOpenMMLab Detection Toolbox and Benchmark项目地址: https://gitcode.com/gh_mirrors/mm/mmdetection
Seesaw Loss(CVPR 2021,《Seesaw Loss for Long-Tailed Instance Segmentation》)是 OpenMMLab MMDetection 中内置的一类专为长尾分布设计的分类损失函数,旨在解决 LVIS 等真实场景数据集中头部类别样本压倒尾部类别、导致尾部目标被误判为背景或头部类别的问题。本文以 MMDetection 仓库中 configs/seesaw_loss 的完整配置集为核心,深入讲解 Seesaw Loss 的 mitigation factor 与 compensation factor 两大机制、源码级实现细节、LVIS 数据集与 RFS 采样策略的搭配方式,并给出 12 个可复现实验的全部参数与结果对照。读完本文,你将掌握在 MMDetection 3.x 中配置并训练长尾实例分割模型(Mask R-CNN / Cascade Mask R-CNN)的完整技术路线。
一、背景:长尾分布下实例分割分类器为何失效
在类均衡的公开基准(如 COCO)上,实例分割模型已经取得了显著进展。但真实世界的物体类别分布天然带有长尾(long tail)特性:少数"头部类别"(head classes,如人、车、狗)占据了数据集的绝大多数样本,而大量"尾部类别"(tail categories)只出现极少的实例。在 LVIS(Large Vocabulary Instance Segmentation)数据集上,这种长尾现象尤为突出——类别数高达 1203 个,且样本数量跨越多个数量级。
长尾数据给分类器训练带来一个核心问题:头部类别的实例在训练中既是尾部类别的正样本来源缺失,又是其最主要的负样本来源。尾部类别在每一轮迭代中接收到的大量负样本梯度(来自被错分类为头部类别或背景的样本)是压倒性的,导致分类器对尾部类别的学习产生严重偏差。最终结果是:尾部类别的物体更容易被误分类为背景或头部类别,模型的长尾识别能力大打折扣。
为解决这一问题,原论文提出Seesaw Loss:为每个类别动态地重新平衡正负样本的梯度贡献,使用两个互补因子——缓解因子(mitigation factor)与补偿因子(compensation factor),并且无需修改网络结构或引入额外的分支,即可端到端地替换标准交叉熵损失。
二、Seesaw Loss 核心原理:缓解因子与补偿因子
Seesaw Loss 的核心思想是"跷跷板"式地动态调节损失权重:当类别间样本数量悬殊时,抑制头部类别作为尾部类别负样本时产生的惩罚(缓解因子);同时对被错误预测为头部类别或背景的样本加大惩罚(补偿因子),避免尾部类别被过度抑制而产生大量误报。
2.1 缓解因子(Mitigation Factor)
缓解因子根据两个类别之间累积训练样本数量的比值来降低对尾部类别的惩罚强度。在 seesaw_ce_loss 的源码实现中:
# mitigation factor if p > 0: sample_ratio_matrix = cum_samples[None, :].clamp( min=1) / cum_samples[:, None].clamp(min=1) index = (sample_ratio_matrix < 1.0).float() sample_weights = sample_ratio_matrix.pow(p) * index + (1 - index) mitigation_factor = sample_weights[labels.long(), :] seesaw_weights = seesaw_weights * mitigation_factor假设类别 j 的累积样本数远大于类别 i,那么当样本的真实类别为 i(尾部类)而预测类别为 j(头部类)时,比值cum_samples[j] / cum_samples[i]远大于 1,缓解因子(ratio)^p会显著放大……准确地说,源码中sample_ratio_matrix[cum_i][cum_j] < 1时(即 i 比 j 更头部),权重为ratio^p(小于 1),从而降低头部类别对尾部类别负样本的惩罚梯度。参数p(默认 0.8)控制缓解的强度,p越大缓解越强。
2.2 补偿因子(Compensation Factor)
补偿因子防止缓解因子带来"矫枉过正":如果一味降低惩罚,模型可能把尾部类别的所有负样本都忽略,导致尾部类别产生大量误报(false positive)。因此,当某个头部类别样本被错误地预测为尾部类别时,补偿因子会依据预测分数的比值加大惩罚:
# compensation factor if q > 0: scores = F.softmax(cls_score.detach(), dim=1) self_scores = scores[torch.arange(0, len(scores)).to(scores.device).long(), labels.long()] score_matrix = scores / self_scores[:, None].clamp(min=eps) index = (score_matrix > 1.0).float() compensation_factor = score_matrix.pow(q) * index + (1 - index) seesaw_weights = seesaw_weights * compensation_factor当某类别的预测分数超过真实类别分数时(score_matrix > 1),以score^q放大该样本的损失贡献;参数q(默认 2.0)控制补偿强度,eps(默认 1e-2)用于平滑除法、保持数值稳定。
最终,将两个因子合成的seesaw_weights以对数形式加到分类 logits 的负标签位置上,再走标准交叉熵计算:
cls_score = cls_score + (seesaw_weights.log() * (1 - onehot_labels)) loss = F.cross_entropy(cls_score, labels, weight=None, reduction='none')2.3 类别累积样本的在线统计
源码在SeesawLoss.forward中通过一个注册缓冲区(buffer)持续累积每个类别的样本数,供缓解因子实时使用:
- 缓冲区定义见 seesaw_loss.py:
self.register_buffer('cum_samples', torch.zeros(self.num_classes + 1, dtype=torch.float)); - 每次前向传播时按标签累加,见 seesaw_loss.py:
unique_labels = labels.unique() for u_l in unique_labels: inds_ = labels == u_l.item() self.cum_samples[u_l] += inds_.sum()由于cum_samples是随训练过程动态累积的全局状态,Seesaw Loss 的训练无法简单地在推理阶段"即插即用",它必须与训练循环绑定;这与纯函数式的 Focal Loss 等静态加权损失有本质区别。
2.4 自定义分类头接口:与 LVIS 双通道预测的适配
LVIS 的分类任务包含"背景 + 1203 个类别 + 前景/背景判定"的复合结构,SeesawLoss通过三个自定义接口适配检测头(对应实现见 seesaw_loss.py):
custom_cls_channels = True与get_cls_channels:分类输出通道数为num_classes + 2(1203 个类别分数 + 2 个 objectness 分数);custom_activation = True与get_activation:分别对类别分数做 softmax、对 objectness 分数做 softmax,再组合得到最终置信度;custom_accuracy = True与get_accuracy:同时统计 objectness 精度与类别精度。
forward返回的损失是一个 dict,包含loss_cls_objectness(前景/背景判定损失,走标准交叉熵)与loss_cls_classes(Seesaw CE 损失,仅在存在正样本时计算,见 seesaw_loss.py),两者共同构成最终的分类损失。
三、配套归一化预测器:NormedLinear 与 NormedConv2d
长尾场景下,分类器权重范数与类别样本量高度相关:头部类别的权重范数通常偏大。为了缓解这一偏差,Seesaw Loss 配置集同时引入了权重归一化的预测层,实现在 normed_predictor.py:
- NormedLinear(定义):在
nn.Linear基础上,对输入与权重分别做 L2 归一化后乘以温度系数tempearture(配置中默认为 20),并对权重做均值为 0、标准差 0.01 的正态初始化。其前向为F.linear(x / ||x|| * temperature, W / ||W||, b); - NormedConv2d(定义):对 Mask Head 的卷积预测层做同样的归一化,支持
norm_over_kernel选项控制是否按卷积核维度归一化。
在配置文件中,二者通过cls_predictor_cfg/predictor_cfg字段注入:
bbox_head=dict( cls_predictor_cfg=dict(type='NormedLinear', tempearture=20), ...) mask_head=dict( predictor_cfg=dict(type='NormedConv2d', tempearture=20), ...)其中"Norm Mask = Y"的实验即表示 Mask Head 使用了NormedConv2d归一化预测器。
四、LVIS 数据集准备与两种数据采样策略
4.1 数据集准备
本文所有实验均基于LVIS v1数据集(1203 类),其配置骨架位于 configs/base/datasets/lvis_v1_instance.py(继承自 lvis_v0.5_instance.py):
- 数据集类型:
LVISV1Dataset,data_root = 'data/lvis_v1/'; - 训练标注:
annotations/lvis_v1_train.json,验证标注:annotations/lvis_v1_val.json; - 评估器:
LVISMetric,指标metric=['bbox', 'segm'],同时输出 box AP 与 mask AP。
详细的 LVIS 下载与目录组织方式请参考 configs/lvis/README.md。请将数据解压到data/lvis_v1/目录下,与配置文件中的data_root保持一致。
4.2 两种采样策略:random 与 RFS
原文档明确指出:
RFS indicates to use oversample strategy here with oversample threshold
1e-3.
在 MMDetection 3.x 中,该策略对应 MMEngine 的ClassBalancedDataset数据集包装器,具体用法见 docs/en/advanced_guides/customize_dataset.md 中 "Customize datasets by dataset wrappers" 一节。其配置形态直接体现在 lvis_v0.5_instance.py:
train_dataloader = dict( batch_size=2, num_workers=2, persistent_workers=True, sampler=dict(type='DefaultSampler', shuffle=True), batch_sampler=dict(type='AspectRatioBatchSampler'), dataset=dict( type='ClassBalancedDataset', oversample_thr=1e-3, # RFS 采样阈值 dataset=dict( type=dataset_type, data_root=data_root, ann_file='annotations/lvis_v0.5_train.json', data_prefix=dict(img='train2017/'), filter_cfg=dict(filter_empty_gt=True, min_size=32), pipeline=train_pipeline, backend_args=backend_args)))oversample_thr=1e-3的含义是:样本占比低于该阈值的类别会被"过采样"(重复抽取)以平衡类别分布,这正是原文档所称的 RFS(Repeat Factor Sampling)策略。本文 12 个实验中:
- Data Sampler = random:直接使用原始 LVIS 训练集,不做过采样;
- Data Sampler = RFS:外层套上
ClassBalancedDataset(oversample_thr=1e-3),对应的随机配置文件名含sample1e-3。
4.3 多尺度训练管线
所有 Seesaw Loss 配置均使用多尺度(ms)训练管线,短边在 640~800 之间随机选择:
train_pipeline = [ dict(type='LoadImageFromFile', backend_args={{_base_.backend_args}}), dict(type='LoadAnnotations', with_bbox=True, with_mask=True), dict( type='RandomChoiceResize', scales=[(1333, 640), (1333, 672), (1333, 704), (1333, 736), (1333, 768), (1333, 800)], keep_ratio=True), dict(type='RandomFlip', prob=0.5), dict(type='PackDetInputs') ]五、配置文件逐项解析
5.1 基线配置:Mask R-CNN R-50-FPN + random 采样
以 mask-rcnn_r50_fpn_seesaw-loss_random-ms-2x_lvis-v1.py 为例,完整配置如下:
_base_ = [ '../_base_/models/mask-rcnn_r50_fpn.py', '../_base_/datasets/coco_instance.py', '../_base_/schedules/schedule_2x.py', '../_base_/default_runtime.py' ] model = dict( roi_head=dict( bbox_head=dict( num_classes=1203, cls_predictor_cfg=dict(type='NormedLinear', tempearture=20), loss_cls=dict( type='SeesawLoss', p=0.8, q=2.0, num_classes=1203, loss_weight=1.0)), mask_head=dict(num_classes=1203)), test_cfg=dict( rcnn=dict( score_thr=0.0001, # LVIS allows up to 300 max_per_img=300)))各关键参数说明如下:
| 参数 | 取值 | 含义与作用 |
|---|---|---|
num_classes | 1203 | LVIS v1 的类别总数,需同时写入bbox_head、mask_head与SeesawLoss |
cls_predictor_cfg.type | NormedLinear | 将 RoI 分类头替换为权重归一化线性层 |
tempearture | 20 | 归一化预测器的温度系数,缩放归一化后的 logits 幅度(注意该参数在源码中的拼写即为tempearture) |
loss_cls.type | SeesawLoss | 使用 Seesaw Loss 替换标准 CrossEntropyLoss |
p | 0.8 | 缓解因子指数,控制对尾部类别负样本惩罚的降低程度 |
q | 2.0 | 补偿因子指数,控制对误分类样本的惩罚放大程度 |
loss_weight | 1.0 | 分类损失在总损失中的权重 |
score_thr | 0.0001 | 推理时的分数阈值,LVIS 类别多、分数普遍偏低,需要放低阈值 |
max_per_img | 300 | 每张图最多保留 300 个检测结果(LVIS 官方评估允许的上限) |
训练周期配置train_cfg = dict(val_interval=24):2x schedule 共 24 个 epoch,且每 24 个 epoch 做一次验证(即训练结束时验证)。训练资源与优化器沿用基础配置(8 块 V100 GPU、SGD + Momentum + Weight Decay,见 metafile.yml 中的 Training Techniques 记录)。
5.2 RFS 变体:替换数据集骨架
mask-rcnn_r50_fpn_seesaw-loss_sample1e-3-ms-2x_lvis-v1.py 与基线唯一的差异是_base_中的数据集骨架从coco_instance.py换成lvis_v1_instance.py,并覆写训练管线:
_base_ = [ '../_base_/models/mask-rcnn_r50_fpn.py', '../_base_/datasets/lvis_v1_instance.py', '../_base_/schedules/schedule_2x.py', '../_base_/default_runtime.py' ] ... train_dataloader = dict(dataset=dict(dataset=dict(pipeline=train_pipeline)))由于lvis_v1_instance.py继承自lvis_v0.5_instance.py,而后者已经用ClassBalancedDataset(oversample_thr=1e-3)包装了训练集,因此这一行即完成了 RFS 采样策略的启用。这也是"random"与"RFS"两组实验在配置上最直观的区别。
5.3 Normed Mask 变体
以 mask-rcnn_r50_fpn_seesaw-loss-normed-mask_sample1e-3-ms-2x_lvis-v1.py 为例,Normed Mask 变体极其精简,只需覆写 mask head 的预测器:
_base_ = './mask-rcnn_r50_fpn_seesaw-loss_sample1e-3-ms-2x_lvis-v1.py' model = dict( roi_head=dict( mask_head=dict( predictor_cfg=dict(type='NormedConv2d', tempearture=20))))即通过局部继承(_base_指向同目录下的基础配置)将 Mask Head 的预测卷积替换为归一化版本。Cascade Mask R-CNN 的 Normed Mask 变体(如 cascade-mask-rcnn_r101_fpn_seesaw-loss-normed-mask_random-ms-2x_lvis-v1.py)采用完全相同的写法,_base_指向同目录下对应的 Cascade 基础配置。
六、实验配置与结果全览(LVIS v1)
以下 12 组实验复现了 CVPR 2021 原论文在 LVIS v1 上的实例分割性能,全部采用 2x schedule(24 epoch)与多尺度训练。训练细节(如 8×V100 GPU)记录在 metafile.yml 中,各模型权重文件的下载地址同样可在该 metafile 的Weights字段中获取。
| Method | Backbone | Style | Lr schd | Data Sampler | Norm Mask | box AP | mask AP | Config |
|---|---|---|---|---|---|---|---|---|
| Mask R-CNN | R-50-FPN | pytorch | 2x | random | N | 25.6 | 25.0 | mask-rcnn_r50_fpn_seesaw-loss_random-ms-2x_lvis-v1.py |
| Mask R-CNN | R-50-FPN | pytorch | 2x | random | Y | 25.6 | 25.4 | mask-rcnn_r50_fpn_seesaw-loss-normed-mask_random-ms-2x_lvis-v1.py |
| Mask R-CNN | R-101-FPN | pytorch | 2x | random | N | 27.4 | 26.7 | mask-rcnn_r101_fpn_seesaw-loss_random-ms-2x_lvis-v1.py |
| Mask R-CNN | R-101-FPN | pytorch | 2x | random | Y | 27.2 | 27.3 | mask-rcnn_r101_fpn_seesaw-loss-normed-mask_random-ms-2x_lvis-v1.py |
| Mask R-CNN | R-50-FPN | pytorch | 2x | RFS | N | 27.6 | 26.4 | mask-rcnn_r50_fpn_seesaw-loss_sample1e-3-ms-2x_lvis-v1.py |
| Mask R-CNN | R-50-FPN | pytorch | 2x | RFS | Y | 27.6 | 26.8 | mask-rcnn_r50_fpn_seesaw-loss-normed-mask_sample1e-3-ms-2x_lvis-v1.py |
| Mask R-CNN | R-101-FPN | pytorch | 2x | RFS | N | 28.9 | 27.6 | mask-rcnn_r101_fpn_seesaw-loss_sample1e-3-ms-2x_lvis-v1.py |
| Mask R-CNN | R-101-FPN | pytorch | 2x | RFS | Y | 28.9 | 28.2 | mask-rcnn_r101_fpn_seesaw-loss-normed-mask_sample1e-3-ms-2x_lvis-v1.py |
| Cascade Mask R-CNN | R-101-FPN | pytorch | 2x | random | N | 33.1 | 29.2 | cascade-mask-rcnn_r101_fpn_seesaw-loss_random-ms-2x_lvis-v1.py |
| Cascade Mask R-CNN | R-101-FPN | pytorch | 2x | random | Y | 33.0 | 30.0 | cascade-mask-rcnn_r101_fpn_seesaw-loss-normed-mask_random-ms-2x_lvis-v1.py |
| Cascade Mask R-CNN | R-101-FPN | pytorch | 2x | RFS | N | 30.0 | 29.3 | cascade-mask-rcnn_r101_fpn_seesaw-loss_sample1e-3-ms-2x_lvis-v1.py |
| Cascade Mask R-CNN | R-101-FPN | pytorch | 2x | RFS | Y | 32.8 | 30.1 | cascade-mask-rcnn_r101_fpn_seesaw-loss-normed-mask_sample1e-3-ms-2x_lvis-v1.py |
从结果可以观察到的规律(均基于仓库内记录的数据):
- RFS 采样对 Mask R-CNN 有明显增益:R-50 的 mask AP 从 25.0 提升到 26.4(Norm Mask Y 时为 26.8),R-101 从 26.7 提升到 27.6;
- Normed Mask 主要提升 mask AP:多个组合下 mask AP 均有 0.4~0.8 的提升(如 Cascade R-101 random 组合从 29.2 升至 30.0);
- Cascade Mask R-CNN 在随机采样下表现最佳(box AP 33.1 / mask AP 29.2),其中 Normed Mask 版本取得了最高的 mask AP(30.0/30.1)。
需要说明的是,以上数值均为仓库官方记录的单次训练结果,不同随机种子与硬件环境下复现时可能出现合理浮动。
七、训练与评估命令
准备工作就绪后,可直接使用仓库的 tools/train.py 与 tools/test.py 进行训练与评估。
单卡训练(以随机采样基线为例):
python tools/train.py configs/seesaw_loss/mask-rcnn_r50_fpn_seesaw-loss_random-ms-2x_lvis-v1.py8 卡分布式训练(与论文设置一致):
bash tools/dist_train.sh configs/seesaw_loss/mask-rcnn_r50_fpn_seesaw-loss_random-ms-2x_lvis-v1.py 8评估(需传入训练得到的 checkpoint 路径,或使用 metafile.yml 中对应模型权重):
python tools/test.py configs/seesaw_loss/mask-rcnn_r50_fpn_seesaw-loss_random-ms-2x_lvis-v1.py /path/to/checkpoint.pth评估输出同时包含 box AP(LVIS 的 bbox 指标)与 mask AP(segm 指标),与配置文件中的val_evaluator = dict(type='LVISMetric', metric=['bbox', 'segm'])对应。
八、使用注意与适用边界
- 仅支持 softmax 输出:
SeesawLoss的构造器中assert not use_sigmoid,即该损失只适配 softmax 分类头,不能直接用于 sigmoid 多标签头(见 seesaw_loss.py); - 必须在线累积样本统计:
cum_samples随训练动态更新,且forward中会修改缓冲区状态,因此该损失不适用于纯推理时替换损失,也不支持在无监督/蒸馏场景下直接套用; - 类别数必须对齐:
SeesawLoss(num_classes=1203)、bbox_head.num_classes、mask_head.num_classes三者必须一致,且get_cls_channels会在类别数不匹配时直接断言失败; - 分数阈值需要放低:LVIS 类别数多、长尾类别分数天然偏低,因此推理配置使用
score_thr=0.0001与max_per_img=300,直接沿用 COCO 默认的阈值(如 0.05)会显著低估长尾类别召回。
九、引用
本文对应的算法论文为Seesaw Loss for Long-Tailed Instance Segmentation(CVPR 2021),仓库内完整 BibTeX 记录如下:
@inproceedings{wang2021seesaw, title={Seesaw Loss for Long-Tailed Instance Segmentation}, author={Jiaqi Wang and Wenwei Zhang and Yuhang Zang and Yuhang Cao and Jiangmiao Pang and Tao Gong and Kai Chen and Ziwei Liu and Chen Change Loy and Dahua Lin}, booktitle={Proceedings of the {IEEE} Conference on Computer Vision and Pattern Recognition}, year={2021} }十、关键文件速查
- 配置与结果总览:configs/seesaw_loss/README.md、configs/seesaw_loss/metafile.yml
- 12 个实验配置:configs/seesaw_loss 目录(
mask-rcnn_*与cascade-mask-rcnn_*系列) - 损失实现:mmdet/models/losses/seesaw_loss.py
- 归一化预测器:mmdet/models/layers/normed_predictor.py
- 数据集骨架:configs/base/datasets/lvis_v1_instance.py、configs/base/datasets/lvis_v0.5_instance.py
- 模型骨架与训练周期:configs/base/models/mask-rcnn_r50_fpn.py、configs/base/schedules/schedule_2x.py
- LVIS 数据准备指南:configs/lvis/README.md
【免费下载链接】mmdetectionOpenMMLab Detection Toolbox and Benchmark项目地址: https://gitcode.com/gh_mirrors/mm/mmdetection
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考