半监督目标检测¶
半监督目标检测使用标记数据和未标记数据来训练模型。它不仅减少了训练高性能目标检测器的标注负担,而且通过使用大量的未标记数据进一步提高了目标检测器的性能。
训练半监督目标检测器的一个典型流程如下
准备和分割数据集¶
我们提供了一个数据集下载脚本,它默认下载 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.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
配置多分支管道¶
半监督学习主要有两种方法,一致性正则化 和 伪标签。一致性正则化通常需要一些仔细的设计,而伪标签的形式更简单,更容易扩展到下游任务。我们采用基于伪标签的师生联合训练半监督目标检测框架,因此标记数据和未标记数据需要配置不同的数据管道
(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) 使用多源数据集采样器。使用 GroupMultiSourceSampler
从 labeled_dataset
和 labeled_dataset
中的批次进行数据采样,source_ratio
控制批次中标记数据和未标记数据的比例。 GroupMultiSourceSampler
还可以确保同一个批次中的图像具有相似的纵横比。如果您不需要保证批次中图像的纵横比,可以使用 MultiSourceSampler
。 GroupMultiSourceSampler
的采样图如下

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
作为 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'))
此外,我们还支持其他检测模型的半监督训练,例如 RetinaNet
和 Cascade 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]
,RetinaNet
、Faster R-CNN
、Cascade R-CNN
和 SoftTeacher
在 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')