快捷键

半监督目标检测

半监督目标检测使用标记数据和未标记数据来训练模型。它不仅减少了训练高性能目标检测器的标注负担,而且通过使用大量的未标记数据进一步提高了目标检测器的性能。

训练半监督目标检测器的一个典型流程如下

准备和分割数据集

我们提供了一个数据集下载脚本,它默认下载 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

在 coco2017 数据集上进行半监督目标检测,有两种常见的实验设置

(1) 根据固定百分比(1%、2%、5% 和 10%)将 train2017 分割为标记数据集,将 train2017 的其余部分作为未标记数据集。由于作为标记数据集的 train2017 的不同分割会导致半监督检测器精度的显著波动,因此在实践中使用五折交叉验证来评估算法。我们提供数据集分割脚本

python tools/misc/split_coco.py

默认情况下,该脚本将根据标记数据比例 1%、2%、5% 和 10% 分割 train2017,并且每个分割将为交叉验证随机重复 5 次。生成的半监督标注文件名格式如下

  • 标记数据集的名称格式:instances_train2017.{fold}@{percent}.json

  • 未标记数据集的名称格式:instances_train2017.{fold}@{percent}-unlabeled.json

这里,fold 用于交叉验证,percent 表示标记数据的比例。分割后的数据集的目录结构如下

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

(2) 使用 train2017 作为标记数据集,使用 unlabeled2017 作为未标记数据集。由于 image_info_unlabeled2017.json 不包含 categories 信息,因此无法初始化 CocoDataset,所以需要将 instances_train2017.jsoncategories 写入 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

配置多分支管道

半监督学习主要有两种方法,一致性正则化伪标签。一致性正则化通常需要一些仔细的设计,而伪标签的形式更简单,更容易扩展到下游任务。我们采用基于伪标签的师生联合训练半监督目标检测框架,因此标记数据和未标记数据需要配置不同的数据管道

(1) 标记数据的管道

# 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'))
]

(2) 未标记数据的管道

# 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 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,
    )
]

配置半监督数据加载器

(1) 构建半监督数据集。使用 ConcatDataset 将标记数据集和未标记数据集连接起来。

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]))

(2) 使用多源数据集采样器。使用 GroupMultiSourceSamplerlabeled_datasetlabeled_dataset 中的批次进行数据采样,source_ratio 控制批次中标记数据和未标记数据的比例。 GroupMultiSourceSampler 还可以确保同一个批次中的图像具有相似的纵横比。如果您不需要保证批次中图像的纵横比,可以使用 MultiSourceSamplerGroupMultiSourceSampler 的采样图如下

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。 GroupMultiSourceSampler 根据标记数据集和未标记数据集中图像的整体纵横比分布随机选择一个组,然后根据 source_ratio 从两个数据集中采样数据构成批次,因此标记数据集和未标记数据集有不同的重复次数。

配置半监督模型

我们选择 Faster R-CNN 作为半监督训练的 detector。以半监督目标检测算法 SoftTeacher 为例,模型配置可以从 _base_/models/faster-rcnn_r50_fpn.py 继承,将检测器的骨干网络替换为 caffe 风格。注意,与监督训练配置不同, Faster R-CNN 作为 detectormodel 的属性,而不是 model 。此外, data_preprocessor 需要设置为 MultiBranchDataPreprocessor,它用于对来自不同管道的图像进行填充和归一化。最后,半监督训练和测试所需的參數可以通过 semi_train_cfgsemi_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'))

此外,我们还支持其他检测模型的半监督训练,例如 RetinaNetCascade R-CNN。由于 SoftTeacher 仅支持 Faster R-CNN,因此需要将其替换为 SemiBaseDetector,示例如下

_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]RetinaNetFaster R-CNNCascade R-CNNSoftTeacher 在 10% coco train2017 上的监督和半监督训练的实验结果如下

模型 检测器 骨干网络 风格 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

配置 MeanTeacherHook

通常,教师模型通过对学生模型进行指数移动平均 (EMA) 来更新,然后教师模型通过学生模型的优化进行优化,这可以通过配置 custom_hooks 来实现

custom_hooks = [dict(type='MeanTeacherHook')]

配置 TeacherStudentValLoop

由于教师-学生联合训练框架中存在两个模型,因此我们可以用 TeacherStudentValLoop 替换 ValLoop,以在训练过程中测试两个模型的准确性。

val_cfg = dict(type='TeacherStudentValLoop')