配置文件详解
- 四项基础配置
- 1. _base_/datasets
- 2. _base_/models
- 3. _base_/schedules
- 4. _base_/default_runtime.py
- 基于基础配置的完整配置
- 1. 继承
- 2. 继承并修改
- 3. 忽略基础配置的某些字段
- 其它(关于源码)
- 1. mask标注信息的读取
- 2. 数据类别的加载逻辑
本文不涉及mmdetection的环境安装,主要记录一下自己在学习mmdetection时的心得体会。如有错误,欢迎大家在评论区指正。
四项基础配置
config/_base_
文件夹下有 4 个基本组件类型,分别是:数据集(dataset),模型(model),训练策略(schedule)和运行时的默认设置(default runtime),官网称它们为原始配置(primitive)。
1. base/datasets
mmdetection支持 COCO、PASCAL VOC等常用的数据集,以 coco_instance.py为例:
dataset_type = 'CocoDataset' # 数据集类型
data_root = 'data/coco/' # 数据集的根路径
img_norm_cfg = dict( # 图像归一化的配置
mean=[123.675, 116.28, 103.53], # 归一化后数据的均值
std=[58.395, 57.12, 57.375], to_rgb=True) # 归一化后数据集的标准差
train_pipeline = [ # 训练集图像的处理流程
dict(type='LoadImageFromFile'), # 1. 从文件路径中加载数据
dict(
type='LoadAnnotations', # 2. 加载注解
with_bbox=True, # 是否加载标注框(bounding box), 目标检测需要设置为 True。
with_mask=True, # 是否加载mask信息,实例分割需要设置为 True。
),
dict(
type='Resize', # 3. 缩放图像
img_scale=(1333, 800), # 图像的最大尺寸
keep_ratio=True # 是否保持图像的长宽比。
),
dict(
type='RandomFlip', # 4. 随机翻转图像和注解
flip_ratio=0.5 # 翻转概率
),
dict(
type='Normalize', # 5. 归一化图像
**img_norm_cfg # 上述的配置
),
dict(
type='Pad', # 6. 填充当前图像到指定大小
size_divisor=32 # 要求填充后图像可以被32整除
),
dict(type='DefaultFormatBundle'), # 7. 最后转换成的数据格式,具体描述见 ./mmdet/pipelines/formatting.py
dict(
type='Collect', # 8. 决定数据中哪些键应该传递给检测器
keys=['img', 'gt_bboxes', 'gt_labels', 'gt_masks'] # 把原图、边界框、边界框的标签、语义信息传递给检测器
),
]
test_pipeline = [ # 测试集图像的处理流程
dict(type='LoadImageFromFile'), # 1. 从文件中加载数据
dict(
type='MultiScaleFlipAug', # 2. 数据增广:多尺度翻转增强
img_scale=(1333, 800), # 测试时可图像的最大规模
flip=False, # 是否翻转图像
transforms=[ # 增强方式
dict(
type='Resize', # 缩放
keep_ratio=True # 是否保持宽高比
),
dict(type='RandomFlip'), # 随机翻转, flip=False 时不反转
dict(type='Normalize', **img_norm_cfg), # 归一化配置项
dict(type='Pad', size_divisor=32), # 图像尺寸padding到32的倍数
dict(type='ImageToTensor', keys=['img']), # 将图像转为张量
dict(type='Collect', keys=['img']), # 将原图传递给检测器
])
]
data = dict(
samples_per_gpu=2, # 单个 GPU 的 Batch size
workers_per_gpu=2, # 单个 GPU 分配的数据加载线程数,(为加快数据集的加载速度,一般会使用多线程加载)
train=dict( # 训练数据集配置
type=dataset_type, # 数据集类别, 这里是COCO
ann_file=data_root + 'annotations/instances_train2017.json', # 训练集的注解路径
img_prefix=data_root + 'train2017/', # 训练集的图片路径
pipeline=train_pipeline), # 训练时的数据增强流程
val=dict(
type=dataset_type, # 数据集类别, 这里是COCO
ann_file=data_root + 'annotations/instances_val2017.json', # 验证集的注解路径
img_prefix=data_root + 'val2017/', # 验证集的图片路径
pipeline=test_pipeline), # 验证时的数据增强流程
test=dict(
type=dataset_type, # 数据集类别, 这里是COCO
ann_file=data_root + 'annotations/instances_val2017.json', # 测试集的注解路径
img_prefix=data_root + 'val2017/', # 测试集的图片路径
pipeline=test_pipeline)) # 测试时的数据增强流程
evaluation = dict( # 评估设置
interval=1, # 验证的间隔
metric=['bbox', 'segm'] # 验证时使用的评价指标
)
-
dataset_type
:官方提供了多种格式的数据集加载器,除了Coco、VOC外,同时还提供了类平衡数据加载等。 - 支持用户编写自定义的数据集加载器,继承
CustomDataset
,并重写load_annotations(self, ann_file)
以及get_ann_info(self, idx)
这两个方法。 - 数据集加载器中已经声明了数据类别,如果训练自己的数据集,有两种方案。
- 方式一
可以直接在mmdet/datasets
下修改类别,如mmdet/datasets/coco.py
@DATASETS.register_module()
class CocoDataset(CustomDataset):
CLASSES = ('person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
'train', 'truck', 'boat', 'traffic light', 'fire hydrant',
'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog',
'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe',
'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat',
'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl',
'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot',
'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop',
'mouse', 'remote', 'keyboard', 'cell phone', 'microwave',
'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock',
'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush')
- 方式二
在配置文件中设置类别,
classes = ('person', 'bicycle', 'car')
data = dict(
train=dict(classes=classes),
val=dict(classes=classes),
test=dict(classes=classes))
- 也可以根据文件读取类别。
classes = 'path/to/classes.txt'
data = dict(
train=dict(classes=classes),
val=dict(classes=classes),
test=dict(classes=classes))
classes.txt
的格式如下:
person
bicycle
car
2. base/models
该路径下提供了常用的目标检测模型,如Fast R-CNN、 Faster R-CNN、RetinaNet、SSD300等,也有实例分割模型Mask R-CNN,这里以Mask R-CNN的配置为例进行介绍
Mask R-CNN的大致流程为:
- image输入网络后,主干网络进行特征提取
- neck部分使用特征金字塔(FPN)进行增强特征提取
- RPN网络根据FPN的输出生成一定数量的推荐框
- RoI Align根据推荐框映射生成尺寸为7×7的特征图
- 根据RoI Align的输出(7×7的特征图)进行分类和回归计算,得到目标框
- RoI Align根据目标框映射生成尺寸为14×14的特征图
- 14×14的特征图经过全卷积网络输出28×28的语义分割结果
- 语义分割结果缩放到与目标框相同的大小,得到最终的实例分割结果
# model settings
model = dict(
type='MaskRCNN', # wang'l
backbone=dict( # 主干网络设置
type='ResNet', # 主干网络名称
depth=50, # 层数,resnet有res50、res101、res152
num_stages=4, # 输出四个阶段的特征图
out_indices=(0, 1, 2, 3), # 四个阶段的index
frozen_stages=1, # 冻结第一个阶段
norm_cfg=dict(
type='BN', # 归一化层(norm layer)的配置项
requires_grad=True # 是否训练归一化里的 gamma 和 beta。
), #
norm_eval=True, # 是否冻结 BN 里的统计项。
style='pytorch', # 主干网络的风格,有pytorch和caffe两种
init_cfg=dict( # 初始化设置
type='Pretrained', # 适用于预训练模型
checkpoint='torchvision://resnet50' #
)), #
neck=dict( # 检测器的颈部
type='FPN', # 使用特征金字塔
in_channels=[256, 512, 1024, 2048], # neck的输入,对应主干网络输出的特征图的通道数
out_channels=256, # 特征金字塔的输出的通道数
num_outs=5 # 输出的通道数
),
rpn_head=dict( # RPN网络
type='RPNHead', # 网络类型
in_channels=256, # 每个输入特征图的输入通道,这与 neck 的输出通道一致。
feat_channels=256, # head 卷积层的特征通道
anchor_generator=dict( # 锚框生成相关的配置
type='AnchorGenerator', # 大多是方法使用 AnchorGenerator 作为锚点生成器, SSD 检测器使用 `SSDAnchorGenerator`
scales=[8], # 锚点的基本比例,特征图某一位置的锚点面积为 scale * base_sizes
ratios=[0.5, 1.0, 2.0], # 锚框的宽高比
strides=[4, 8, 16, 32, 64]), # 锚生成器的步幅。这与 FPN 特征步幅一致
bbox_coder=dict( # RPN需要对建议框进行编码和解码。
type='DeltaXYWHBBoxCoder', # 框编码器的类别
target_means=[.0, .0, .0, .0], # 用于编码和解码框的目标均值
target_stds=[1.0, 1.0, 1.0, 1.0]), # 用于编码和解码框的标准差
loss_cls=dict( # RPN分类分支的损失函数
type='CrossEntropyLoss', # 使用交叉熵损失
use_sigmoid=True, # RPN通常进行二分类,所以通常使用sigmoid函数
loss_weight=1.0), # 分类分支的损失权重
loss_bbox=dict( # 回归分支的损失函数配置
type='L1Loss', # 使用L1损失
loss_weight=1.0 # 回归分支的损失权重
)
),
roi_head=dict( # RoI,包括了特征映射以及Mask R-CNN需要的分类分支、回归分支、mask分支
type='StandardRoIHead', # 使用标准的RoI head 的类型
bbox_roi_extractor=dict( # 用于目标检测预测的 RoI 特征提取器
type='SingleRoIExtractor', # RoI 特征提取器的类型
roi_layer=dict(
type='RoIAlign', # RoI 层的类别
output_size=7, # RoI输出的特征图大小
sampling_ratio=0), # 提取 RoI 特征时的采样率。0 表示自适应比率
out_channels=256, # 输出的通道数
featmap_strides=[4, 8, 16, 32]), # 多尺度特征图的步幅,应该与主干的架构保持一致
bbox_head=dict( # RoIHead 中 box head 的配置
type='Shared2FCBBoxHead', # 两个权值共享的全连接层用于预测框回归
in_channels=256, # 输入通道数,对应bbox_roi_extractor的输出通道数
fc_out_channels=1024, # 全连接层的输出通道
roi_feat_size=7, # RoI输出的特征图大小
num_classes=80, # 预测的类别数
bbox_coder=dict( # 预测框编码
type='DeltaXYWHBBoxCoder',
target_means=[0., 0., 0., 0.],
target_stds=[0.1, 0.1, 0.2, 0.2]),
reg_class_agnostic=False, # 回归是否与类别无关
loss_cls=dict( # # 分类分支的损失函数配置
type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0),
loss_bbox=dict(type='L1Loss', loss_weight=1.0)),
mask_roi_extractor=dict( # 用于mask分支的 RoI 特征提取器
type='SingleRoIExtractor',
roi_layer=dict(type='RoIAlign', output_size=14, sampling_ratio=0),
out_channels=256,
featmap_strides=[4, 8, 16, 32]),
mask_head=dict( # mask分支
type='FCNMaskHead', # 使用全卷积网络进行预测
num_convs=4,
in_channels=256,
conv_out_channels=256,
num_classes=80,
loss_mask=dict( # mask分支的损失函数
type='CrossEntropyLoss', use_mask=True, loss_weight=1.0))),
# model training and testing settings
train_cfg=dict( # 训练是的超参数配置
rpn=dict( # RPN网络的超参数
assigner=dict( # 分配器,即区分正负样本
type='MaxIoUAssigner',
pos_iou_thr=0.7, # IoU >= 0.7(阈值) 被视为正样本。
neg_iou_thr=0.3, # IoU < 0.3(阈值) 被视为负样本
min_pos_iou=0.3, # 框作为正样本的最小 IoU 阈值
match_low_quality=True,
ignore_iof_thr=-1),
sampler=dict( # 正/负采样器(sampler)的配置
type='RandomSampler',
num=256,
pos_fraction=0.5, # 正样本占总样本的比例。
neg_pos_ub=-1, # 基于正样本数量的负样本上限
add_gt_as_proposals=False), # 采样后是否添加 GT 作为 proposal
allowed_border=-1,
pos_weight=-1, # 训练期间正样本的权重
debug=False),
rpn_proposal=dict( # 在训练期间生成 proposals 的配置
nms_pre=2000, # NMS 前的 box 数
max_per_img=1000, # NMS 要保留的 box 的数量
nms=dict( # NMS 的配置
type='nms',
iou_threshold=0.7),
min_bbox_size=0 # # 允许的最小 box 尺寸
),
rcnn=dict(
assigner=dict(
type='MaxIoUAssigner',
pos_iou_thr=0.5,
neg_iou_thr=0.5,
min_pos_iou=0.5,
match_low_quality=True,
ignore_iof_thr=-1),
sampler=dict(
type='RandomSampler',
num=512,
pos_fraction=0.25,
neg_pos_ub=-1,
add_gt_as_proposals=True),
mask_size=28, # mask 的大小
pos_weight=-1,
debug=False)),
test_cfg=dict(
rpn=dict(
nms_pre=1000,
max_per_img=1000,
nms=dict(type='nms', iou_threshold=0.7),
min_bbox_size=0),
rcnn=dict(
score_thr=0.05,
nms=dict(type='nms', iou_threshold=0.5),
max_per_img=100, # 每张图像的最大检测次数, 对应nms输出的最大个数
mask_thr_binary=0.5)))
- 一般的目标检测的模型可以分为backbonde、neck、head三个部分
- Mask R-CNN的配置项比较多,主要有
- 主干提取(backbone)的配置
- 增强特征提取(neck)的配置
- RPN_head:RPN网络负责生成建议框,因此包含锚框生成、建议框分类分支、建议框的回归分支
- RoI_head:负责对建议框进行修正,包括感兴趣区域对齐(RoI Align),分类分支、回归分支、mask分支。
3. base/schedules
该路径下配置训练计划,如训练使用的优化器、学习率的调整策略等
以schedule_1x.py为例
# optimizer
optimizer = dict(
type='SGD', # 优化器种类
lr=0.02, # 优化器的学习率
momentum=0.9, # 动量(Momentum)
weight_decay=0.0001 # SGD 的权重衰减
)
optimizer_config = dict(grad_clip=None) # # 大多数方法不使用梯度限制(grad_clip)
# learning policy
lr_config = dict( # 学习率调整配置
policy='step', # 调度流程(scheduler)的策略
warmup='linear', # 预热(warmup)策略
warmup_iters=500, # 预热的迭代次数
warmup_ratio=0.001, # 用于预热的起始学习率的比率
step=[8, 11]) # 衰减学习率的起止回合数
runner = dict(
type='EpochBasedRunner', # runner 的类别,有 EpochBasedRunner 和 IterBasedRunner 两种
max_epochs=12) # 训练的迭代数
- 优化器可以自行选择,有SGD、Adam、AdamW等
- 好像是使用的pytorch里的优化器,可以看mmcv里的代码 mmcv.runner.base_runner
- 所谓的1x,2x,3x是指epoch的大小,1x = 12个epoch。这里有一些官网的解释:COCO数据集上1x模式下为什么不采用多尺度训练
4. base/default_runtime.py
该文件配置一些程序运行时的参数
checkpoint_config = dict(
interval=1, # 训练过程中保存权重的间隔
by_epoch=True, # interval的计数单位默认是epoch,设置为False则计数单位为iter
)
# yapf:disable
log_config = dict( # 日志配置
interval=50, # 打印日志的间隔
hooks=[ #
dict(type='TextLoggerHook'),
# dict(type='TensorboardLoggerHook')
])
# yapf:enable
custom_hooks = [dict(type='NumClassCheckHook')]
dist_params = dict(backend='nccl')
log_level = 'INFO' # 日志的级别。
load_from = None # 从一个给定路径里加载模型作为预训练模型
resume_from = None # 从给定路径里恢复检查点(checkpoints),训练模式将从检查点保存的轮次开始恢复训练
workflow = [('train', 1)] # runner 的工作流程,[('train', 1)] 表示只有一个工作流且工作流仅执行一次。根据 total_epochs 工作流训练 12个回合。
# disable opencv multithreading to avoid system being overloaded
opencv_num_threads = 0
# set multi-process start method as `fork` to speed up the training
mp_start_method = 'fork'
基于基础配置的完整配置
1. 继承
一个完整的配置文件包括上述的四部分配置,因此可以直接继承上述配置文件
mask_rcnn_r50_fpn_1x_coco.py
_base_ = [
'../_base_/models/mask_rcnn_r50_fpn.py',
'../_base_/datasets/coco_instance.py',
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]
2. 继承并修改
也可基于配置文件进行一定的修改,如将主干网络从res50替换为res101
mask_rcnn_r101_fpn_1x_coco.py
_base_ = [
'../_base_/models/mask_rcnn_r50_fpn.py',
'../_base_/datasets/coco_instance.py',
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]
model = dict(
backbone=dict(
depth=101,
init_cfg=dict(type='Pretrained',
checkpoint='torchvision://resnet101')))
3. 忽略基础配置的某些字段
如果需要完全修改某一个配置项,如优化器,可以先删除原始配置,再添加新的配置,如使用新的优化器,此时需要使用 _delete_=True
-
_delete_=True
表示删除原有的配置。如果不删除原有的配置项,SGD
的momentum=0.9
配置项会作为参数传递给优化器的建造器,但是AdamW
不需要该参数,此时会发生报错。 mask_rcnn_r50_fpn_1x_coco.py
使用AdamW
优化器
_base_ = [
'../_base_/models/mask_rcnn_r50_fpn.py',
'../_base_/datasets/coco_instance.py',
'../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]
optimizer = dict(
_delete_=True,
type='AdamW',
lr=0.0001,
betas=(0.9, 0.999),
weight_decay=0.05,
paramwise_cfg=dict(
custom_keys={
'absolute_pos_embed': dict(decay_mult=0.),
'relative_position_bias_table': dict(decay_mult=0.),
'norm': dict(decay_mult=0.)
}))
其它(关于源码)
该部分是一些关于源码的理解
1. mask标注信息的读取
mmdet/datasets/voc.py
没有读取mask信息,但是 mmdet/datasets/coco.py
中读取了语义标注。所以如果使用voc格式需要跑实例分割的话,要么转换成COCO格式,要么重写一个voc的数据加载器。
voc.py
没有重写mmdet/datasets/get_ann_info(self, idx):
,它继承自mmdet/datasets/xml_style.py
,所以直接看xml_style.py
的代码。mmdet/datasets/xml_style.py
@DATASETS.register_module()
class XMLDataset(CustomDataset):
...
def get_ann_info(self, idx):
...
# 此处没有读取mask信息
ann = dict(
bboxes=bboxes.astype(np.float32),
labels=labels.astype(np.int64),
bboxes_ignore=bboxes_ignore.astype(np.float32),
labels_ignore=labels_ignore.astype(np.int64))
return ann
mmdet/datasets/coco.py
@DATASETS.register_module()
class CocoDataset(CustomDataset):
def get_ann_info(self, idx):
img_id = self.data_infos[idx]['id']
ann_ids = self.coco.get_ann_ids(img_ids=[img_id])
ann_info = self.coco.load_anns(ann_ids)
return self._parse_ann_info(self.data_infos[idx], ann_info)
def _parse_ann_info(self, img_info, ann_info):
gt_masks_ann = []
for i, ann in enumerate(ann_info):
if ann.get('ignore', False):
continue
x1, y1, w, h = ann['bbox']
inter_w = max(0, min(x1 + w, img_info['width']) - max(x1, 0))
inter_h = max(0, min(y1 + h, img_info['height']) - max(y1, 0))
if inter_w * inter_h == 0:
continue
if ann['area'] <= 0 or w < 1 or h < 1:
continue
if ann['category_id'] not in self.cat_ids:
continue
bbox = [x1, y1, x1 + w, y1 + h]
if ann.get('iscrowd', False):
gt_bboxes_ignore.append(bbox)
else:
gt_bboxes.append(bbox)
gt_labels.append(self.cat2label[ann['category_id']])
gt_masks_ann.append(ann.get('segmentation', None))
...
# 读取了mask信息
ann = dict(
bboxes=gt_bboxes,
labels=gt_labels,
bboxes_ignore=gt_bboxes_ignore,
masks=gt_masks_ann,
seg_map=seg_map)
return ann
2. 数据类别的加载逻辑
CustomDataset
是CocoDataset与VOCDataset的父类,它的源码在 mmdet/datasets/custom.py
里
mmdet/datasets/custom.py
@DATASETS.register_module()
class CustomDataset(Dataset):
def __init__(self,
ann_file,
pipeline,
classes=None,
data_root=None,
img_prefix='',
seg_prefix=None,
proposal_file=None,
test_mode=False,
filter_empty_gt=True,
file_client_args=dict(backend='disk')):
...
self.CLASSES = self.get_classes(classes)
...
@classmethod
def get_classes(cls, classes=None):
# 如果配置文件中没有classes,则使用类内默认的CLASSES
if classes is None:
return cls.CLASSES
# 如果classes是字符串,则从文件中读取类别
if isinstance(classes, str):
# take it as a file path
class_names = mmcv.list_from_file(classes)
# 如果classes是元组或列表,则直接赋值给classes
elif isinstance(classes, (tuple, list)):
class_names = classes
else:
raise ValueError(f'Unsupported type {type(classes)} of classes.')
return class_names