19年7月,Kai Chen等人写了一篇文章MMDetection(https://arxiv.org/pdf/1906.07155.pdf
),介绍了他们在mmdetection(https://github.com/open-mmlab/mmdetection
)上的一些工作。包括mmdetection的设计逻辑,已实现的算法等。猜:KaiChen在不知道经历了一些什么之后,觉得对各种实现迥异的检测算法抽象一些公共的组件出来也许是一件不错的事。这里尝试对代码做一些简单的解析,见下。
组件设计:
代码结构:
configs: 网络组件结构等配置信息
tools: 训练和测试的最终包装和一些实用脚本
mmdet:
Figure1: Framework
Figure2: Trainning pipline
从tools/train.py中能看到整体可分如下4个步骤:
头上装饰@X.register_module,并同时在
对象所在包的初始化文件中调用
,即可将
保存到registry.module_dict中,完成注册。
后续说说配置文件,注册机制和训练逻辑。
2.0相比1.x,就代码组织上,在模块化这方面有了更好的贯彻。能拆分的就拆分,比如配置文件,数据的信息整合、变换、采样迭代, 以前版本命名不严格的一律改掉, 比如anchor_heads。
1.x版本是将所有配置信息放到一个x.py配置文件中,2.0增加了配置文件的模块化和继承能力,这样在实验中能提高组合不同部分的效率。执行python tools/print_config.py /PATH/TO/CONFIG 能看到配置信息。--options xxx.yyy=zzz可看到更新信息。
基础配置文件在config/_base_中,有dataset, model, schedule,default_runtime四个部分,对应1.x版本单个配置文件的 不同部分。
将coco数据训练的模型作为CitySpace等数据预训练模型,需要做以下五处改动
基础模型继承自mask_rcnn_r50_fpn,数据继承自cityscapes风格,训练schedules继承自默认的default_runtime,在 配置文件顶部增加如下代码:
_base_ = [
'../_base_/models/mask_rcnn_r50_fpn.py',
'../_base_/datasets/cityscapes_instance.py', '../_base_/default_runtime.py'
]
如果新旧模型的类别不同,则需要改一下类别数目。
model = dict(
pretrained=None,
roi_head=dict(
bbox_head=dict(
type='Shared2FCBBoxHead',
in_channels=256,
fc_out_channels=1024,
roi_feat_size=7,
num_classes=8, # new num_classes
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='SmoothL1Loss', beta=1.0, loss_weight=1.0)),
mask_head=dict(
type='FCNMaskHead',
num_convs=4,
in_channels=256,
conv_out_channels=256,
num_classes=8,
loss_mask=dict(
type='CrossEntropyLoss', use_mask=True, loss_weight=1.0)))
预训练的模型权重除最后的预测层不会加载预用模型权值,其他均会被加载。
仿照VOC, WIDER FACE, COCO and Cityscapes 数据类重写自己的数据整合方式。改一下数据类的名字即可。具体改写细节见下小节。
优化器,训练超参数等的修改。
# optimizer
# lr is set for a batch size of 8
optimizer = dict(type='SGD', lr=0.01, momentum=0.9, weight_decay=0.0001)
optimizer_config = dict(grad_clip=None)
# learning policy
lr_config = dict(
policy='step',
warmup='linear',
warmup_iters=500,
warmup_ratio=0.001,
# [7] yields higher performance than [6]
step=[7])
total_epochs = 8 # actual epoch = 8 * 8 = 64
log_config = dict(interval=100)
load_from = 'https://s3.ap-northeast-2.amazonaws.com/open-mmlab/mmdetection/models/mask_rcnn_r50_fpn_2x_20181010-41d35c05.pth'
最简单的方式就是将自己的数据脚本转换成coco或者voc格式。然后更改配置文件中的数据信息。比如coco格式, 在configs/my_custom_config. py中有:
...
# dataset settings
dataset_type = 'CocoDataset'
classes = ('a', 'b', 'c', 'd', 'e') # 自己的五类名字
...
data = dict(
samples_per_gpu=2,
workers_per_gpu=2,
train=dict(
type=dataset_type,
classes=classes,
ann_file='path/to/your/train/data',
...),
val=dict(
type=dataset_type,
classes=classes,
ann_file='path/to/your/val/data',
...),
test=dict(
type=dataset_type,
classes=classes,
ann_file='path/to/your/test/data',
...))
...
mmdet提供了和coco,voc等兼容的中间格式:
[
{
'filename': 'a.jpg',
'width': 1280,
'height': 720,
'ann': {
'bboxes': <np.ndarray, float32> (n, 4),
'labels': <np.ndarray, int64> (n, ),
'bboxes_ignore': <np.ndarray, float32> (k, 4),
'labels_ignore': <np.ndarray, int64> (k, ) (optional field)
}
},
...
]
使用方式:
一个例子, 假设标注文件格式如下
#
000001.jpg
1280 720
2
10 20 40 60 1
20 40 50 60 2
#
000002.jpg
1280 720
3
50 20 40 60 2
20 40 30 45 2
30 40 50 60 3
我们可以写一个继承自CustomDataset的新类如下:
import mmcv
import numpy as np
from .builder import DATASETS
from .custom import CustomDataset
@DATASETS.register_module()
class MyDataset(CustomDataset):
CLASSES = ('person', 'bicycle', 'car', 'motorcycle')
def load_annotations(self, ann_file):
ann_list = mmcv.list_from_file(ann_file)
data_infos = []
for i, ann_line in enumerate(ann_list):
if ann_line != '#':
continue
img_shape = ann_list[i + 2].split(' ')
width = int(img_shape[0])
height = int(img_shape[1])
bbox_number = int(ann_list[i + 3])
anns = ann_line.split(' ')
bboxes = []
labels = []
for anns in ann_list[i + 4:i + 4 + bbox_number]:
bboxes.append([float(ann) for ann in anns[:4]])
labels.append(int(anns[4]))
data_infos.append(
dict(
filename=ann_list[i + 1],
width=width,
height=height,
ann=dict(
bboxes=np.array(bboxes).astype(np.float32),
labels=np.array(labels).astype(np.int64))
))
return data_infos
def get_ann_info(self, idx):
return self.data_infos[idx]['ann']
# 配置文件做如下更改:
dataset_A_train = dict(
type='MyDataset',
ann_file = 'image_list.txt',
pipeline=train_pipeline
)
数据合并,Repeat或Concatemate,顾名思义,将同一种数据重复多次,或不同数据concate成一个更大的数据。
如果你只想训练某数据的指定类别,只需要做如下改动:
classes = ('person', 'bicycle', 'car')
# classes = 'path/to/classes.txt' # 或者类别从文件中读取
data = dict(
train=dict(classes=classes),
val=dict(classes=classes),
test=dict(classes=classes))
经典的数据管道如下:
Figure 3: data pipeline
蓝色块为管道算子,一个算子为一个数据增强算法,从左到右,依次字典进,字典出。关于数据结构,可参考第二节数据处理。这里绿色为算子作用后新增的keys,橙色为算子作用与已有keys的values后的更新标记。
# 1.实现新增强函数 my\_pipeline.py
# 和pytorch原始transforms中的增强方式一样,实现__call__方法的类即可
from mmdet.datasets import PIPELINES
@PIPELINES.register_module()
class MyTransform:
def __call__(self, results): # 输入的是mmdet设定的字典格式
results['dummy'] = True
return results
# 2. 导入新类.
from .my_pipeline import MyTransform
# 3. 配置文件调用
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'),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='Resize', img_scale=(1333, 800), keep_ratio=True),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='MyTransform'),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']),
]
每一个组件都如上小节所给的三部曲,实现,导入,修改配置文件。每一步,mmdet中都有一些范例,分别看一下,就能实现改写。
一些模型可能需要对某部分参数做特殊优化处理,比如批归一化层的权重衰减。我们可以通过自定义优化器构造函数来进行细粒度参数调优。
from mmcv.utils import build_from_cfg
from mmdet.core.optimizer import OPTIMIZER_BUILDERS, OPTIMIZERS
from mmdet.utils import get_root_logger
from .cocktail_optimizer import CocktailOptimizer
@OPTIMIZER_BUILDERS.register_module
class CocktailOptimizerConstructor(object):
def __init__(self, optimizer_cfg, paramwise_cfg=None):
def __call__(self, model):
return my_optimizer
关于head相关组件,核心的点我认为在于数据的流向。基础变换层由forward函数得到变换的结果,怎么放到Loss中去,其中所涉及到的数据细节操作,是比较关键的。mmdet中会在head模块中实现对应的loss函数,最终被汇集到检测模型的loss中去。分别调用各自的loss.step,进行权重更新。
这里贴一个新增loss样例:
# 1.在 mmdet/models/losses/my_loss.py实现新的box回归函数
import torch
import torch.nn as nn
from ..builder import LOSSES
from .utils import weighted_loss
@weighted_loss # 加权损失函数,可参考损失函数章节
def my_loss(pred, target):
assert pred.size() == target.size() and target.numel() > 0
loss = torch.abs(pred - target)
return loss
@LOSSES.register_module
class MyLoss(nn.Module):
def __init__(self, reduction='mean', loss_weight=1.0):
super(MyLoss, self).__init__()
self.reduction = reduction
self.loss_weight = loss_weight
def forward(self,
pred,
target,
weight=None,
avg_factor=None,
reduction_override=None):
assert reduction_override in (None, 'none', 'mean', 'sum')
reduction = (
reduction_override if reduction_override else self.reduction)
loss_bbox = self.loss_weight * my_loss(
pred, target, weight, reduction=reduction, avg_factor=avg_factor)
# 2.然后在mmdet/models/losses/__init__.py.中注册
from .my_loss import MyLoss, my_loss
# 3. 配置文件使用
loss_bbox=dict(type='MyLoss', loss_weight=1.0))
执行脚本 tools/upgrade_model_version.py。可能有小于 1%的绝对AP减小,具体可参见configs/legacy。
主要有四点不同:坐标系,基础代码约定,训练超参数,模块设计。
新坐标系与detectron2(https://github.com/facebookresearch/detectron2/
)一致. treats the center of the most left-top pixel as (0, 0) rather than the left-top corner of that pixel. 这句话的意思是将
改为
,这样更加自然和精确(假设长或宽为1,则box退缩为点或线,这是有问题的),同理xyxy2xywh的长宽就不在+1了,生成的anhor的中心偏移也不在是0.5而是0了。与此相关的改动有Box的编解码,与iou计算相关的nms,assinger。另外,现在的坐标为float,1.xx为int,与此相关的有ahchor与特征网格的中心对齐问题,这对anchor-based的方法在性能上有一定影响(变好),ROIAlign也能更好的对齐,mask cropping and pasting更精准, 利用新的RoIAlign 去crop masktargets,会得到更好的结果,因为没有取整等误差了,而且在训练上也有 0.1s/iter的速度提升(少了取整操作)。
为
类对象,2.0中
为背景,
为
类对象。
。
一些训练参数的优化
改为
,这样更平滑,同时也是因为去掉了梯度截断。
2.0相对于1.x版本,从代码上主要有这些改变: