MMDetection 半监督目标检测实战指南:SoftTeacher 与 SemiBaseDetector 的配置与训练全解析
【免费下载链接】mmdetectionOpenMMLab Detection Toolbox and Benchmark项目地址: https://gitcode.com/gh_mirrors/mm/mmdetection
半监督目标检测(Semi-Supervised Object Detection, SSOD)在仅有少量标注框、大量无标注图片的条件下训练检测器,是降低标注成本、提升检测精度的有效技术路线。本指南以 MMDetection 仓库内基于伪标签的教师-学生联合训练框架为核心,系统讲解从数据集划分、多分支数据流程配置、半监督模型构建到 MeanTeacherHook 与 TeacherStudentValLoop 的完整落地流程,读完即可按图索骥配置并启动自己的半监督检测训练。本文内容对应 docs/zh_cn/user_guides/semi_det.md,并结合仓库源码与真实配置文件进行深度扩充。
半监督目标检测的基本思想
半监督目标检测同时利用标签数据和无标签数据进行训练。与纯监督训练相比,它有两个直接收益:
- 减少模型对大量检测框标注的依赖,在标注预算有限时依然可以获得可用的检测器;
- 通过挖掘海量未标注数据中的结构信息,进一步提升模型精度。
半监督学习有两条主流技术路线:一致性正则化(Consistency Regularization)与伪标签(Pseudo-Label)。一致性正则化要求模型对同一输入的不同扰动版本输出一致,往往需要精细的正则化设计;伪标签方法则相对简单,直接让教师模型为无标签数据生成伪标注、再监督学生模型训练,更易于迁移到检测等下游任务。
MMDetection 采用基于伪标签 + 教师-学生联合训练的框架,其核心结构在 mmdet/models/detectors/semi_base.py 中定义:SemiBaseDetector内部维护结构完全相同的两个检测器——student与teacher。学生模型通过梯度下降更新参数,教师模型则通过学生参数的指数滑动平均(EMA)缓慢更新,因此教师模型在训练过程中更平滑、积累的知识更稳定。此外,mmdet/models/detectors/soft_teacher.py 中的SoftTeacher是这一框架在 Faster R-CNN 上的具体实现,引入了针对回归分支的不确定性度量来筛选高质量伪框。
准备和拆分数据集
下载并解压 COCO 数据集
仓库提供了数据集下载脚本,默认下载 COCO2017 数据集并自动解压:
python tools/misc/download_dataset.py解压后的数据集目录结构如下:
mmdetection ├── data │ ├── coco │ │ ├── annotations │ │ │ ├── image_info_unlabeled2017.json │ │ │ ├── instances_train2017.json │ │ │ ├── instances_val2017.json │ │ ├── test2017 │ │ ├── train2017 │ │ ├── unlabeled2017 │ │ ├── val2017实验设置一:按百分比划分 train2017(五折交叉验证)
在 COCO 上进行半监督检测有两种通用实验设置。第一种是将train2017按固定百分比(1%、2%、5%、10%)划分出一部分作为标签数据集,其余部分作为无标签数据集。由于不同的划分方式对训练结果影响较大,官方采用五折交叉验证评估算法性能,并提供了划分脚本:
python tools/misc/split_coco.py该脚本 tools/misc/split_coco.py 的实现逻辑是:以 fold 序号作为随机种子,按指定百分比随机抽取图片作为标签集,剩余图片连同其标注整体归入无标签集。默认参数为:--data-root ./data/coco/、--out-dir ./data/coco/semi_anns/、--labeled-percent 1 2 5 10、--fold 5。生成文件命名规则如下:
- 标签数据集标注:
instances_train2017.{fold}@{percent}.json - 无标签数据集标注:
instances_train2017.{fold}@{percent}-unlabeled.json
其中fold用于交叉验证编号,percent表示标签数据占 train2017 的比例。划分后的目录结构为:
mmdetection ├── data │ ├── coco │ │ ├── annotations │ │ │ ├── image_info_unlabeled2017.json │ │ │ ├── instances_train2017.json │ │ │ ├── instances_val2017.json │ │ ├── semi_anns │ │ │ ├── instances_train2017.1@1.json │ │ │ ├── instances_train2017.1@1-unlabeled.json │ │ │ ├── instances_train2017.1@2.json │ │ │ ├── instances_train2017.1@2-unlabeled.json │ │ │ ├── instances_train2017.1@5.json │ │ │ ├── instances_train2017.1@5-unlabeled.json │ │ │ ├── instances_train2017.1@10.json │ │ │ ├── instances_train2017.1@10-unlabeled.json │ │ │ ├── instances_train2017.2@1.json │ │ │ ├── instances_train2017.2@1-unlabeled.json │ │ ├── test2017 │ │ ├── train2017 │ │ ├── unlabeled2017 │ │ ├── val2017实验设置二:train2017 + unlabeled2017
第二种设置将train2017作为标签数据集、官方unlabeled2017作为无标签数据集。由于image_info_unlabeled2017.json缺少categories字段,无法直接初始化CocoDataset,因此需要将instances_train2017.json中的categories写入image_info_unlabeled2017.json,另存为instances_unlabeled2017.json:
from mmengine.fileio import load, dump anns_train = load('instances_train2017.json') anns_unlabeled = load('image_info_unlabeled2017.json') anns_unlabeled['categories'] = anns_train['categories'] dump(anns_unlabeled, 'instances_unlabeled2017.json')处理后的目录结构:
mmdetection ├── data │ ├── coco │ │ ├── annotations │ │ │ ├── image_info_unlabeled2017.json │ │ │ ├── instances_train2017.json │ │ │ ├── instances_unlabeled2017.json │ │ │ ├── instances_val2017.json │ │ ├── test2017 │ │ ├── train2017 │ │ ├── unlabeled2017 │ │ ├── val2017该设置在 configs/base/datasets/semi_coco_detection.py 中作为默认配置使用(labeled_dataset指向instances_train2017.json,unlabeled_dataset指向instances_unlabeled2017.json),而按百分比划分的设置则通过子配置文件重写ann_file字段实现,详见下文 SoftTeacher 完整配置示例。
配置多分支数据流程
教师-学生联合训练要求同一批数据以不同视图喂给不同角色:标签数据走强监督分支(送学生模型),无标签数据则拆分为弱增强视图(送教师模型产出伪标注)与强增强视图(送学生模型做无监督训练)。为此需要配置三条 pipeline。
标签数据流程 sup_pipeline
标签数据经过常规检测增强后送入学生模型进行有监督训练:
# pipeline used to augment labeled data, # which will be sent to student model for supervised training. sup_pipeline = [ dict(type='LoadImageFromFile', backend_args=backend_args), dict(type='LoadAnnotations', with_bbox=True), dict(type='RandomResize', scale=scale, keep_ratio=True), dict(type='RandomFlip', prob=0.5), dict(type='RandAugment', aug_space=color_space, aug_num=1), dict(type='FilterAnnotations', min_gt_bbox_wh=(1e-2, 1e-2)), dict(type='MultiBranch', sup=dict(type='PackDetInputs')) ]关键点:RandAugment从color_space(ColorTransform、AutoContrast、Equalize、Sharpness、Posterize、Solarize、Color、Contrast、Brightness 等色彩增强集合)随机选择 1 个变换;FilterAnnotations过滤掉宽或高小于1e-2的过小标注;最终用MultiBranch将数据打包进sup分支。scale与color_space、geometric等共享定义位于 configs/base/datasets/semi_coco_detection.py。
无标签数据流程 weak_pipeline 与 strong_pipeline
无标签数据必须弱增强和强增强双视图并存:弱增强视图送教师模型预测伪实例,强增强视图送学生模型做无监督训练,两者通过随机几何变换的保持一致性建立监督信号。
# pipeline used to augment unlabeled data weakly, # which will be sent to teacher model for predicting pseudo instances. weak_pipeline = [ dict(type='RandomResize', scale=scale, keep_ratio=True), dict(type='RandomFlip', prob=0.5), dict( type='PackDetInputs', meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape', 'scale_factor', 'flip', 'flip_direction', 'homography_matrix')), ] # pipeline used to augment unlabeled data strongly, # which will be sent to student model for unsupervised training. strong_pipeline = [ dict(type='RandomResize', scale=scale, keep_ratio=True), dict(type='RandomFlip', prob=0.5), dict( type='RandomOrder', transforms=[ dict(type='RandAugment', aug_space=color_space, aug_num=1), dict(type='RandAugment', aug_space=geometric, aug_num=1), ]), dict(type='RandomErasing', n_patches=(1, 5), ratio=(0, 0.2)), dict(type='FilterAnnotations', min_gt_bbox_wh=(1e-2, 1e-2)), dict( type='PackDetInputs', meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape', 'scale_factor', 'flip', 'flip_direction', 'homography_matrix')), ]两条 pipeline 的PackDetInputs都额外保留了homography_matrix元信息,这是伪标注在不同视图之间做**坐标投影(bbox_project)**的前提——教师模型在弱增强视图上生成的伪框,必须通过该矩阵映射回原始尺寸,再投影到强增强视图,才能与学生模型的输出对齐,相关投影逻辑见 mmdet/models/detectors/semi_base.py。
组合成 unsup_pipeline
最后用MultiBranch将弱、强两条 pipeline 组装成无标签分支:
# pipeline used to augment unlabeled data into different views unsup_pipeline = [ dict(type='LoadImageFromFile', backend_args=backend_args), dict(type='LoadEmptyAnnotations'), dict( type='MultiBranch', unsup_teacher=weak_pipeline, unsup_student=strong_pipeline, ) ]LoadEmptyAnnotations为无标签图片初始化空标注容器;branch_field = ['sup', 'unsup_teacher', 'unsup_student']在 configs/base/datasets/semi_coco_detection.py 中定义了全部分支名。经过MultiBranch之后,数据被组织为{sup, unsup_teacher, unsup_student}三个分支的 dict,这正是后续MultiBranchDataPreprocessor与半监督模型loss()期望的输入格式。
配置半监督数据加载
用 ConcatDataset 拼接标签与无标签数据集
半监督训练需要一个同时包含标签样本和无标签样本的 Dataloader,官方用ConcatDataset将两者拼接,并分别配置各自的 pipeline 与过滤规则:
labeled_dataset = dict( type=dataset_type, data_root=data_root, ann_file='annotations/instances_train2017.json', data_prefix=dict(img='train2017/'), filter_cfg=dict(filter_empty_gt=True, min_size=32), pipeline=sup_pipeline) unlabeled_dataset = dict( type=dataset_type, data_root=data_root, ann_file='annotations/instances_unlabeled2017.json', data_prefix=dict(img='unlabeled2017/'), filter_cfg=dict(filter_empty_gt=False), pipeline=unsup_pipeline) train_dataloader = dict( batch_size=batch_size, num_workers=num_workers, persistent_workers=True, sampler=dict( type='GroupMultiSourceSampler', batch_size=batch_size, source_ratio=[1, 4]), dataset=dict( type='ConcatDataset', datasets=[labeled_dataset, unlabeled_dataset]))注意标签数据集设置了filter_empty_gt=True(丢弃无 GT 的图片)并过滤min_size=32以下的小图,而无标签数据集必须设置filter_empty_gt=False,否则会被全部过滤掉。
多源采样器 GroupMultiSourceSampler
GroupMultiSourceSampler按source_ratio控制每个 batch 中标签数据与无标签数据的比例(如[1, 4]表示每 5 张中 1 张来自标签集、4 张来自无标签集),同时保证同一 batch 内图片的长宽比相近,避免 padding 浪费。其采样示意可归纳为:
sup=1000:标签数据集规模 1000;sup_h=200其中长宽比 ≥ 1 的图片规模 200;sup_w=800长宽比 < 1 的图片规模 800;unsup=9000:无标签数据集规模 9000;unsup_h=1800其中长宽比 ≥ 1 的规模 1800;unsup_w=7200长宽比 < 1 的规模 7200;- 采样时按两个数据集的总体长宽比分布随机选取一组图片,再按
source_ratio从两个数据集中抽样组成 batch。
因此标签集与无标签集在一个 epoch 内的重复采样次数不同,无标签数据被更频繁地利用。如果不需要保证 batch 内长宽比一致,可以直接使用MultiSourceSampler。
配置半监督模型
方案一:以 SoftTeacher 训练 Faster R-CNN
官方默认选择Faster R-CNN作为detector进行半监督训练,以SoftTeacher算法为例。模型配置继承_base_/models/faster-rcnn_r50_fpn.py,并把骨干网络替换为 caffe 风格(BN 冻结、bgr_to_rgb=False、使用 detectron2 的 ResNet-50 预训练权重)。与监督训练的关键差异是:detector 是model的一个属性,而不是model本身;同时data_preprocessor必须替换为MultiBranchDataPreprocessor,以分别处理三个分支图片的填充与归一化;最后通过semi_train_cfg和semi_test_cfg配置半监督训练/测试参数:
_base_ = [ '../_base_/models/faster-rcnn_r50_fpn.py', '../_base_/default_runtime.py', '../_base_/datasets/semi_coco_detection.py' ] detector = _base_.model detector.data_preprocessor = dict( type='DetDataPreprocessor', mean=[103.530, 116.280, 123.675], std=[1.0, 1.0, 1.0], bgr_to_rgb=False, pad_size_divisor=32) detector.backbone = dict( type='ResNet', depth=50, num_stages=4, out_indices=(0, 1, 2, 3), frozen_stages=1, norm_cfg=dict(type='BN', requires_grad=False), norm_eval=True, style='caffe', init_cfg=dict( type='Pretrained', checkpoint='open-mmlab://detectron2/resnet50_caffe')) model = dict( _delete_=True, type='SoftTeacher', detector=detector, data_preprocessor=dict( type='MultiBranchDataPreprocessor', data_preprocessor=detector.data_preprocessor), semi_train_cfg=dict( freeze_teacher=True, sup_weight=1.0, unsup_weight=4.0, pseudo_label_initial_score_thr=0.5, rpn_pseudo_thr=0.9, cls_pseudo_thr=0.9, reg_pseudo_thr=0.02, jitter_times=10, jitter_scale=0.06, min_pseudo_bbox_wh=(1e-2, 1e-2)), semi_test_cfg=dict(predict_on='teacher'))各关键参数的作用,结合 mmdet/models/detectors/soft_teacher.py 源码说明如下:
freeze_teacher=True:冻结教师模型参数(semi_base.py 中的freeze()会将其置为 eval 模式并关闭所有参数梯度);sup_weight=1.0/unsup_weight=4.0:有监督分支与无监督分支损失的加权系数,体现无监督数据的重要性更高;pseudo_label_initial_score_thr=0.5:教师模型伪框的初始分数阈值,低于该分数的伪实例先被过滤(get_pseudo_instances中调用filter_gt_instances);rpn_pseudo_thr=0.9:用于 RPN 无监督损失的伪框分数阈值(soft_teacher.py);cls_pseudo_thr=0.9:用于分类分支无监督损失的伪框分数阈值(soft_teacher.py);reg_pseudo_thr=0.02:回归分支的不确定性阈值,仅保留reg_uncs < 0.02的伪框参与回归损失(soft_teacher.py);jitter_times=10/jitter_scale=0.06:SoftTeacher 的核心创新——对每个伪框做 10 次高斯抖动(幅度为框尺寸的 0.06 倍),让教师模型对抖动后的框分别预测,取预测框的标准差作为回归不确定性(compute_uncertainty_with_aug与aug_box,见 soft_teacher.py);min_pseudo_bbox_wh=(1e-2, 1e-2):过滤过小伪框;semi_test_cfg=dict(predict_on='teacher'):推理时默认使用教师模型(semi_base.py)。
MultiBranchDataPreprocessor的实现细节位于 mmdet/models/data_preprocessors/data_preprocessor.py:训练时它按分支(sup、unsup_teacher、unsup_student)对输入分组,过滤掉 batch 中对应位置为None的样本,再复用内部的DetDataPreprocessor对各分支数据逐一做归一化与填充,最终重新组装成按分支索引的inputs与data_sample。
方案二:以 SemiBaseDetector 训练任意检测器
SoftTeacher的抖动不确定性机制依赖 Faster R-CNN 的 RPN/RoI 结构。若要使用RetinaNet、Cascade R-CNN等其他检测器,应改用通用基类SemiBaseDetector(其无监督损失通过直接过滤伪框后调用学生模型的loss()计算,见 semi_base.py):
_base_ = [ '../_base_/models/retinanet_r50_fpn.py', '../_base_/default_runtime.py', '../_base_/datasets/semi_coco_detection.py' ] detector = _base_.model model = dict( _delete_=True, type='SemiBaseDetector', detector=detector, data_preprocessor=dict( type='MultiBranchDataPreprocessor', data_preprocessor=detector.data_preprocessor), semi_train_cfg=dict( freeze_teacher=True, sup_weight=1.0, unsup_weight=1.0, cls_pseudo_thr=0.9, min_pseudo_bbox_wh=(1e-2, 1e-2)), semi_test_cfg=dict(predict_on='teacher'))沿用 SoftTeacher 的配置、将batch_size改为 2、source_ratio改为[1, 1]后,各检测器在 10% COCO 训练集上的监督/半监督对比结果如下(数据来自仓库文档):
| Model | Detector | BackBone | Style | sup-0.1-coco mAP | semi-0.1-coco mAP |
|---|---|---|---|---|---|
| SemiBaseDetector | RetinaNet | R-50-FPN | caffe | 23.5 | 27.7 |
| SemiBaseDetector | Faster R-CNN | R-50-FPN | caffe | 26.7 | 28.4 |
| SemiBaseDetector | Cascade R-CNN | R-50-FPN | caffe | 28.0 | 29.7 |
| SoftTeacher | Faster R-CNN | R-50-FPN | caffe | 26.7 | 31.1 |
可以看到,半监督训练在 10% 标注下普遍比纯监督提升 1.7~4.4 个 mAP 点,且 SoftTeacher 借助不确定性过滤获得了最大增益。
完整可运行示例
仓库 configs/soft_teacher/ 下提供了 1%、2%、5%、10% 四种标注比例的完整配置,以 soft-teacher_faster-rcnn_r50-caffe_fpn_180k_semi-0.1-coco.py 为例,它在上述模型配置之外还包含:
# 10% coco train2017 is set as labeled dataset labeled_dataset = _base_.labeled_dataset unlabeled_dataset = _base_.unlabeled_dataset labeled_dataset.ann_file = 'semi_anns/instances_train2017.1@10.json' unlabeled_dataset.ann_file = 'semi_anns/' \ 'instances_train2017.1@10-unlabeled.json' unlabeled_dataset.data_prefix = dict(img='train2017/') train_dataloader = dict( dataset=dict(datasets=[labeled_dataset, unlabeled_dataset])) # training schedule for 180k train_cfg = dict( type='IterBasedTrainLoop', max_iters=180000, val_interval=5000) val_cfg = dict(type='TeacherStudentValLoop') test_cfg = dict(type='TestLoop') # learning rate policy param_scheduler = [ dict( type='LinearLR', start_factor=0.001, by_epoch=False, begin=0, end=500), dict( type='MultiStepLR', begin=0, end=180000, by_epoch=False, milestones=[120000, 160000], gamma=0.1) ] # optimizer optim_wrapper = dict( type='OptimWrapper', optimizer=dict(type='SGD', lr=0.01, momentum=0.9, weight_decay=0.0001)) default_hooks = dict( checkpoint=dict(by_epoch=False, interval=10000, max_keep_ckpts=2)) log_processor = dict(by_epoch=False) custom_hooks = [dict(type='MeanTeacherHook')]该配置是基于迭代(Iter)的训练调度:总迭代 18 万次、每 5000 次迭代验证一次,前 500 次迭代用 LinearLR 线性预热(起始系数 0.001),再在 120000、160000 次迭代处按 0.1 倍衰减学习率;SGD 优化器学习率 0.01、权重衰减 0.0001。启动训练的命令与普通训练一致:
python tools/train.py configs/soft_teacher/soft-teacher_faster-rcnn_r50-caffe_fpn_180k_semi-0.1-coco.py配置 MeanTeacherHook
教师模型通常通过**指数滑动平均(EMA)**跟随学生模型更新:每轮迭代把学生参数按动量momentum融合进教师参数,即teacher = (1 - momentum) * teacher + momentum * student。在 MMDetection 中通过custom_hooks一键启用:
custom_hooks = [dict(type='MeanTeacherHook')]mmdet/engine/hooks/mean_teacher_hook.py 的实现要点:
- 默认
momentum=0.001、interval=1(每个迭代都更新)、skip_buffers=True(只融合可训练参数,跳过 BN 的 running_mean / running_var 等 buffer); before_train中在首个迭代(iter == 0)执行一次momentum_update(model, 1),即用学生参数硬拷贝初始化教师模型;after_train_iter中按interval间隔执行 EMA 更新。
配置 TeacherStudentValLoop
教师-学生框架中存在两个模型,普通ValLoop只能验证其中一个。用TeacherStudentValLoop替换ValLoop,可在训练过程中同时验证教师与学生两个模型的精度:
val_cfg = dict(type='TeacherStudentValLoop')从 mmdet/engine/runner/loops.py 的实现可以看到:该 Loop 遍历predict_on的['teacher', 'student']两个取值,依次切换model.semi_test_cfg['predict_on']完成两轮验证,并把指标合并为teacher/xxx与student/xxx的形式输出,结束后恢复原始配置。这样既可以用学生模型持续评估当前训练状态,也可以用更平滑的教师模型考察最终部署精度。
总结
MMDetection 半监督目标检测的落地路径可以概括为五步:划分数据集 → 配置多分支数据流程 → 用 ConcatDataset 与 GroupMultiSourceSampler 组织半监督加载 → 用 SoftTeacher 或 SemiBaseDetector 组装教师-学生模型 → 挂载 MeanTeacherHook 与 TeacherStudentValLoop。其核心工程机制(多分支数据组织、伪框跨视图投影、EMA 教师更新)分别由 configs/base/datasets/semi_coco_detection.py、mmdet/models/data_preprocessors/data_preprocessor.py、mmdet/models/detectors/semi_base.py 与 mmdet/models/detectors/soft_teacher.py 实现。若需在自建数据集上复现,只需替换data_root、标注文件与dataset_type,并参考 configs/soft_teacher/ 调整标签占比与训练迭代数即可。
【免费下载链接】mmdetectionOpenMMLab Detection Toolbox and Benchmark项目地址: https://gitcode.com/gh_mirrors/mm/mmdetection
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考