写在前面:本人正在学习MMDetection3D的过程中,可能有理解错误,欢迎指正。

参考视频链接:4小时入门深度学习+实操MMDetection 第二课

官方中文文档:MMDetection 文档

一、模型推断

在官方github上下载所需模型(预训练模型参数文件/pth文件)及其配置文件(后面会讲配置文件的内容/py文件)。

然后使用下列代码进行单张图片的推断:

from mmdet import init_detector, inference_detector, show_result_pyplot

config_file = "xxxx.py"                              # 刚才下载的配置文件路径
checkpoint_file = "xxxx.pth"                         # 刚才下载的模型文件路径
model = init_detector(config_file, checkpoint_file)  # 初始化模型并从checkpoint加载模型
result = inference_detector(model, "demo.jpg")       # 得到检测结果
show_result_pyplot(model, "demo.jpg", result)        # 可视化检测结果

二、配置文件

官方教程:教程 1: 学习配置文件 — MMDetection 2.25.1 文档

配置文件定义了完整的训练过程,通常包含多个字段,重要的有

  • model字段:定义模型(包含损失函数、训练和测试时的设置等)

  • data字段:定义数据(包含预处理)

  • optimizer、lr_config等字段:定义训练策略

  • load_from字段:定义预训练模型的参数文件

下面以yolov3_mobilenetv2_mstrain-416_300e_coco.py为例进行简单介绍(暂不深究,目前需要注意的地方见注释):

_base_ = '../_base_/default_runtime.py'
                                      # 表明该配置文件继承自配置文件default_runtime.py(后面会讲到继承)
                                      # 暂时可以理解为等价于将default_runtime.py中的内容复制到这里
# model settings
model = dict(                         # model字段
    type='YOLOV3',
    backbone=dict(                        # 主干网络
        type='MobileNetV2',
        out_indices=(2, 4, 6),
        act_cfg=dict(type='LeakyReLU', negative_slope=0.1),
        init_cfg=dict(
            type='Pretrained', checkpoint='open-mmlab://mmdet/mobilenet_v2')),
    neck=dict(                            # 颈部网络
        type='YOLOV3Neck',
        num_scales=3,
        in_channels=[320, 96, 32],
        out_channels=[96, 96, 96]),
    bbox_head=dict(                       # 检测头
        type='YOLOV3Head',
        num_classes=80,                       # 分类的类别数
        in_channels=[96, 96, 96],
        out_channels=[96, 96, 96],
        anchor_generator=dict(
            type='YOLOAnchorGenerator',
            base_sizes=[[(116, 90), (156, 198), (373, 326)],
                        [(30, 61), (62, 45), (59, 119)],
                        [(10, 13), (16, 30), (33, 23)]],
            strides=[32, 16, 8]),
        bbox_coder=dict(type='YOLOBBoxCoder'),
        featmap_strides=[32, 16, 8],
        loss_cls=dict(                        # 分类损失
            type='CrossEntropyLoss',              # 损失类型:交叉熵
            use_sigmoid=True,
            loss_weight=1.0,
            reduction='sum'),
        loss_conf=dict(                       # 置信度损失(YOLO特有的损失)
            type='CrossEntropyLoss',              # 损失类型:交叉熵
            use_sigmoid=True,
            loss_weight=1.0,
            reduction='sum'),
        loss_xy=dict(                         # 位置分类损失
            type='CrossEntropyLoss',              # 损失类型:交叉熵
            use_sigmoid=True,
            loss_weight=2.0,
            reduction='sum'),
        loss_wh=dict(                         # 长宽回归损失
            type='MSELoss',                       # 损失类型:MSE
            loss_weight=2.0, 
            reduction='sum')),
    # training and testing settings
    train_cfg=dict(                           # 训练配置
        assigner=dict(
            type='GridAssigner',
            pos_iou_thr=0.5,                      # 正锚框的IoU阈值设置
            neg_iou_thr=0.5,                      # 负锚框的IoU阈值设置
            min_pos_iou=0)),
    test_cfg=dict(                            # 测试配置
        nms_pre=1000,
        min_bbox_size=0,
        score_thr=0.05,
        conf_thr=0.005,
        nms=dict(                                 # NMS设置
            type='nms', 
            iou_threshold=0.45),                  # NMS中的IoU阈值设置
        max_per_img=100))
# dataset settings
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'),               # 读取图像
    dict(type='LoadAnnotations', with_bbox=True), # 读取标注
    dict(                                         # 下面都是图像预处理及数据增广等操作
        type='Expand',                       
        mean=img_norm_cfg['mean'],
        to_rgb=img_norm_cfg['to_rgb'],
        ratio_range=(1, 2)),
    dict(
        type='MinIoURandomCrop',
        min_ious=(0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
        min_crop_size=0.3),
    dict(
        type='Resize',
        img_scale=[(320, 320), (416, 416)],
        multiscale_mode='range',
        keep_ratio=True),
    dict(type='RandomFlip', flip_ratio=0.5),
    dict(type='PhotoMetricDistortion'),
    dict(type='Normalize', **img_norm_cfg),
    dict(type='Pad', size_divisor=32),
    dict(type='DefaultFormatBundle'),
    dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]
test_pipeline = [                            # 测试数据读取和预处理
    dict(type='LoadImageFromFile'),
    dict(
        type='MultiScaleFlipAug',
        img_scale=(416, 416),
        flip=False,
        transforms=[
            dict(type='Resize', keep_ratio=True),
            dict(type='RandomFlip'),
            dict(type='Normalize', **img_norm_cfg),
            dict(type='Pad', size_divisor=32),
            dict(type='DefaultFormatBundle'),
            dict(type='Collect', keys=['img'])
        ])
] 
data = dict(                             # data字段    
    samples_per_gpu=24,                      # batch size
    workers_per_gpu=4,
    train=dict(                              # 训练集
        type='RepeatDataset',  # use RepeatDataset to speed up training
        times=10,
        dataset=dict( 
            type=dataset_type,
            ann_file=data_root + 'annotations/instances_train2017.json',
                                                 # 训练集标注文件路径
            img_prefix=data_root + 'train2017/', # 训练集图像所在文件夹路径
            pipeline=train_pipeline)),
    val=dict(                                # 验证集
        type=dataset_type,
        ann_file=data_root + 'annotations/instances_val2017.json',
        img_prefix=data_root + 'val2017/',
        pipeline=test_pipeline),
    test=dict(                               # 测试集
        type=dataset_type,
        ann_file=data_root + 'annotations/instances_val2017.json',
        img_prefix=data_root + 'val2017/',
        pipeline=test_pipeline))
# optimizer 
optimizer = dict(                            # 优化器设置
    type='SGD',                              # 优化器类型
    lr=0.003,                                # 学习率
    momentum=0.9, 
    weight_decay=0.0005)
optimizer_config = dict(grad_clip=dict(max_norm=35, norm_type=2))
# learning policy
lr_config = dict(                        # 学习率变化策略设置
    policy='step',            
    warmup='linear',
    warmup_iters=4000,
    warmup_ratio=0.0001,
    step=[24, 28])
# runtime settings
runner = dict(                           
    type='EpochBasedRunner', 
    max_epochs=30)                       # 训练的总epoch数
evaluation = dict(
    interval=1, 
    metric=['bbox'])
find_unused_parameters = True

# NOTE: `auto_scale_lr` is for automatically scaling LR,
# USER SHOULD NOT CHANGE ITS VALUES.
# base_batch_size = (8 GPUs) x (24 samples per GPU)
auto_scale_lr = dict(base_batch_size=192)

三、自定义数据集进行模型训练

官方教程:教程 2: 自定义数据集 — MMDetection 2.25.1 文档

以目标检测任务为例,若要使用自己的数据集,在预训练模型上进行微调,则步骤如下:

1.下载基础模型的参数和配置文件

2.将自己的数据集整理为MMDetection支持的格式
一个方法是使自己的数据集与已支持的数据集有相同的组织方式(如标注结构与含义等)。
例如,要将自己的图像数据集组织为Coco数据集的形式,首先观察Coco数据集标注文件(json文件)格式:

{ 
    "info" : info,                   # 数据集相关,可忽略
    "images" : [image],              # 图像列表
    "annotations" : [annotation],    # 标注列表
    "license" : [license],           # 数据集相关,可忽略
    "categories" : [categories],     # 类别名称列表
}

其中

image {
    "id" : int,
    "width" : int,
    "height" : int,
    "file_name" : str,
    "license" : int,
    "flickr_url" : str,
    "coco_url" : str,
    "date_captured" : datetime,
}
annotation {
    "id" : int,
    "image_id" : int,
    "category_id" : int,
    "segmentation" : RLE or [polygon],
    "area" : float,
    "bbox" : [x,y,width,height],       # x,y为边界框左上角到图像左上角的距离
    "iscrowd" : 0 or 1,
}
categories [{
    "id" : int,
    "name" : str,
    "supercategory" : str,
}]

按照上述格式建立自己数据集的标注文件即可。

将自定义数据集转换为预训练时所用数据集的形式后,还需要在配置文件的data字段中修改相应的图像文件路径和标注文件路径(见下一步)。
3.修改配置文件(数据路径、分类头、预训练模型加载、优化器配置等)
配置文件的修改可通过继承的方式(当然,也可直接复制预训练模型的配置文件后修改)。
例如新建微调模型的配置文件(如new_model_cfg.py),想在预训练模型的配置文件上修改时,可先写上如下语句表明该配置文件是在原配置文件上进行修改的:

_base_ = ['yolov3_mobilenetv2_mstrain-416_300e_coco.py']    # 预训练模型的配置文件路径

然后修改数据路径只需要找到对应项进行修改(下列代码中未出现的项不变):

data = dict(
    train = dict(
        dataset = dict(
            ann_file = 'xxx',       # 标注文件路径
            img = 'xxx',            # 图像路径
            classes = ("xx", ...))  # 类别名称(新增项)
    ),
    val = dict(...),            # 类似修改验证集和测试集
    test = dict(...)
)

分类头的修改也类似,只需修改类别数:

model = dict(bbox_head(num_classes = xx))

训练配置的修改同理:

runner = dict(max_epochs=xx)
optimizer = dict(lr=xx)
lr_config = None

最后加上load_from字段以使模型从预训练模型开始进行微调:

load_from = "yolov3_mobilenetv2_mstrain-416_300e_coco_20210718_010823-f68a07b3.pth"
                                                    # 预训练模型参数文件路径

输出信息的频率设置如下(原始模型的配置文件中该部分继承自default_runtime.py):

log_config = dict(interval=xx)  # 每xx迭代次数输出一次信息