作者:Activewaste(CSDN)
原文链接:CSDN-专业IT技术社区-登录
在上篇文章中,讲了train.py训练文件,主要是读取命令行函数和主函数main。main主要先做了一些config,work_dir以及log等操作(这些操作都是从命令行获得的,或者从命令行带有的文件里得到的参数等。)。最主要的三个步骤就是调用build_detector()来创建模型,然后同样调用build_dataset()对数据集创建模型,然后在训练检测器train_detector()。注:build_dataset()和build_detector()不在同一个builder.py中实现,所以以下的builder.py实现的是build_detector(),是在mmdet/models/下的py文件。
具体详情看:mmdetection源码笔记(一):
初识CV:mmdetection源码笔记(一):train.py解读zhuanlan.zhihu.com本篇文章主要就是讲一下,搭建模型的思路,以及registry.py和builder.py中各个函数块的作用。
注:builder.py是在mmdet/models文件夹下,是用来创建BACKBONES、NECKS、ROI_EXTRACTORS、SHARED_HEADS、HEADS、LOSSES、DETECTORS的模型的。而关于build_dataset()(在mmdet/datasets/builder.py中),在后面讲到数据集的时候再来讲它。
在mmdet/utils文件夹下的registry.py为主要的实现过程,后面详细讲解。
先来看在mmdet/models文件夹下的registry.py,较简单,代码如下:
# -*- coding: utf-8 -*-
from mmdet.utils import Registry
BACKBONES = Registry('backbone')
NECKS = Registry('neck')
ROI_EXTRACTORS = Registry('roi_extractor')
SHARED_HEADS = Registry('shared_head')
HEADS = Registry('head')
LOSSES = Registry('loss')
DETECTORS = Registry('detector')
#类的实例化,Registry是一个类,传入的是一个字符串。该字符串为Registry类的name属性值
举个例子:DETECTORS为注册表Registry的实例化对象,DETECTORS.name = 'detector',Registry类的定义在mmdet/utils/文件中。
所以,根据上面代码,我们就应该知道了,不止一个名为DETECTORS的注册表Registry,后面还会有名为NECKS、ROI_EXTRACTORS 、SHARED_HEADS 、HEADS 、LOSSES 的注册表,这些注册表下的_module_dict属性,则是用来存对应的相同类对象的,举个例子:比如DETECTORS的_module_dict下就有可能有:Faster R-CNN、Cascade R-CNN、FPN、HTC等常见的检测器,到这或许你就明白了注册表的作用咯。
而在mmdet/utils/Registry.py中,有一个类Registry的定义和一个方法:build_from_cfg()的实现。
build_from_cfg()方法的作用是从 congfig/py配置文件中获取字典数据,创建module(其实也就是一个class类),然后将这个module添加到之前创建的注册表Registry的属性_module_dict中(这是一个字典,key为类名,value为具体的类),返回值是一个实例化后的类对象。
所以,可以这样理解,从config/py配置文件中,将字典提取出来,然后为其映射成一个类,放进Registry对象的_module_dict属性中。(具体看下面的代码)
以下代码分三部分
Part one:
inspect模块是针对模块,类,方法,功能等对象提供些有用的方法。例如可以帮助我们检查类的内容,检查方法的代码,提取和格式化方法的参数等。
# -*- coding: utf-8 -*-
import inspect
import mmcv
Part two:
通过前面第一段的代码段,我们知道DETECTORS = Registry('detector')
detector是干什么的 ???
其实,DETECTORS = Registry('detector') 只是注册了一个对象名为DETECTORS ,属性name为detector的对象。然后用属性_module_dict 来保存config配置文件中的对应的字典数据所对应的class类(看第三部分代码)。请看如下类Registry的定义代码:
class Registry(object):
def __init__(self, name): #此处的self,是个对象(Object),是当前类的实例,name即为传进来的'detector'值
self._name = name
self._module_dict = dict() #定义的属性,是一个字典
def __repr__(self):
#返回一个可以用来表示对象的可打印字符串,可以理解为java中的toString()。
format_str = self.__class__.__name__ + '(name={}, items={})'.format(
self._name, list(self._module_dict.keys()))
return format_str
@property #把方法变成属性,通过self.name 就能获得name的值。
def name(self):
return self._name
#因为没有定义它的setter方法,所以是个只读属性,不能通过 self.name = newname进行修改。
@property
def module_dict(self):
#同上,通过self.module_dict可以获取属性_module_dict,也是只读的
return self._module_dict
def get(self, key):
#普通方法,获取字典中指定key的value,_module_dict是一个字典,然后就可以通过self.get(key),获取value值
return self._module_dict.get(key, None)
def _register_module(self, module_class):
#关键的一个方法,作用就是Register a module.
#在model文件夹下的py文件中,里面的class定义上面都会出现 @DETECTORS.register_module,意思就是将类当做形参,
#将类送入了方法register_module()中执行。@的具体用法看后面解释。
"""Register a module.
Args:
module (:obj:`nn.Module`): Module to be registered.
"""
if not inspect.isclass(module_class): #判断是否为类,是类的话,就为True,跳过判断
raise TypeError('module must be a class, but got {}'.format(
type(module_class)))
module_name = module_class.__name__ #获取类名
if module_name in self._module_dict: #看该类是否已经登记在属性_module_dict中
raise KeyError('{} is already registered in {}'.format(
module_name, self.name))
self._module_dict[module_name] = module_class #在module中dict新增key和value。key为类名,value为类对象
def register_module(self, cls): #对上面的方法,修改了名字,添加了返回值,即返回类本身
self._register_module(cls)
return cls
@的含义:
Python当解释器读到@的这样的修饰符之后,会先解析@后的内容,直接就把@下一行的函数或者类作为@后边的函数的参数,然后将返回值赋值给下一行修饰的函数对象。
在网上看到一个这样的例子:
def a(x):
if x==2:
return 4
return 6
def b(x):
if x==1:
return 2
return 3
@a
@b
def c():
return 1
python会按照自下而上的顺序把各自的函数结果作为下一个函数(上面的函数)的形参输入,也就是a(b(c()))。
以下我们通过配置文件cascade_rcnn_r50_fpn_1x.py
进行讲解 build 模型的过程。
在train中,最先执行Registry的是DETECTORS,传入的参数是配置文件中的model字典。
#在 train.py中
model = build_detector(
cfg.model, train_cfg=cfg.train_cfg, test_cfg=cfg.test_cfg)
#在builder.py中
def build_detector(cfg, train_cfg=None, test_cfg=None):
return build(cfg, DETECTORS, dict(train_cfg=train_cfg, test_cfg=test_cfg))
所以,后面出现的参数cfg,指的就是配置文件中的model字典。下面是model字典的部分代码:
# model settings
model = dict(
type='CascadeRCNN',
num_stages=3,
pretrained='torchvision://resnet50',
backbone=dict(
type='ResNet',
depth=50,
num_stages=4,
out_indices=(0, 1, 2, 3),
frozen_stages=1,
style='pytorch'),
neck=dict(
type='FPN',
in_channels=[256, 512, 1024, 2048],
out_channels=256,
num_outs=5),
rpn_head=dict(
type='RPNHead',
in_channels=256,
feat_channels=256,
anchor_scales=[8],
anchor_ratios=[0.5, 1.0, 2.0],
anchor_strides=[4, 8, 16, 32, 64],
target_means=[.0, .0, .0, .0],
target_stds=[1.0, 1.0, 1.0, 1.0],
loss_cls=dict(
type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0),
loss_bbox=dict(type='SmoothL1Loss', beta=1.0 / 9.0, loss_weight=1.0)),
我们继续往下看
先看build_from_cfg()方法的参数:
Args:
def build_from_cfg(cfg, registry, default_args=None):
"""Build a module from config dict.
Args:
cfg (dict): Config dict. It should at least contain the key "type".
registry (:obj:`Registry`): The registry to search the type from.
default_args (dict, optional): Default initialization arguments.
Returns:
obj: The constructed object.
"""
assert isinstance(cfg, dict) and 'type' in cfg
assert isinstance(default_args, dict) or default_args is None #两个是断言,相当于判断,否的话抛出异常。
args = cfg.copy() #args相当于temp中间变量,是个字典。
obj_type = args.pop('type') #字典的pop作用:移除序列中key为‘type’的元素,并且返回该元素的值
if mmcv.is_str(obj_type):
obj_type = registry.get(obj_type) #获取obj_type的value。
#如果obj_type已经注册到注册表registry中,即在属性_module_dict中,则obj_type 不为None
if obj_type is None:
raise KeyError('{} is not in the {} registry'.format(
obj_type, registry.name))
elif not inspect.isclass(obj_type):
raise TypeError('type must be a str or valid type, but got {}'.format(
type(obj_type)))
if default_args is not None:
for name, value in default_args.items():#items()返回字典的键值对用于遍历
args.setdefault(name, value)
#将default_args的键值对加入到args中,将模型和训练配置进行整合,然后送入类中返回
return obj_type(**args)
obj_type(**args),* *args是将字典unpack得到各个元素,分别与形参匹配送入函数中;看上面model的截图,所以这边,其实就是将除了’type’的所有字段,当做形参,送入了名为CascadeRCNN()的类中(type =' CascadeRCNN')。所以字典里的key就是类中的属性?继续看下面。
根据Cascade R-CNN的例子,我们在models/detectors找cascade_rcnn的py文件。
参考里面的参数时,直接打开对应的cascade_rcnn配置文件,在init中,里面的参数则
对应了配置文件中的字典名。下面两个截图分别是配置文件cascade_rcnn.py和model/detectors/cascade_rcnn.py中的类定义。
configs/cascade_rcnn.py:
# model settings
model = dict(
type='CascadeRCNN',
num_stages=3,
pretrained='torchvision://resnet50',
backbone=dict(
type='ResNet',
depth=50,
num_stages=4,
out_indices=(0, 1, 2, 3),
frozen_stages=1,
style='pytorch'),
neck=dict(
type='FPN',
in_channels=[256, 512, 1024, 2048],
out_channels=256,
num_outs=5),
rpn_head=dict(
type='RPNHead',
model/detectors/cascade_rcnn.py:
@DETECTORS.register_module
class CascadeRCNN(BaseDetector, RPNTestMixin):
def __init__(self,
num_stages,
backbone,
neck=None,
shared_head=None,
rpn_head=None,
bbox_roi_extractor=None,
bbox_head=None,
mask_roi_extractor=None,
mask_head=None,
train_cfg=None,
test_cfg=None,
pretrained=None):
assert bbox_roi_extractor is not None
assert bbox_head is not None
super(CascadeRCNN, self).__init__()
注意的是,在py配置文件中,好多py文件中都有type = 'CascadeRCNN',所以有些参数和属性对不上很正常(毕竟已经设置为None了),因为这个参数可能是其他的cascade R-CNN里面的字典。
所以,我们在训练时,测试时,就要给出配置文件,配置文件可以不同,但相同type
detector等文件是相同的,毕竟已经将数据和实现完全的分离了。
注意:无论训练/检测,都会build DETECTORS;
builder文件较为简单,因为train.py中,只出现了build_detector(),所以我们先记住里面的两个方法:build_detector和build()。
import:
# -*- coding: utf-8 -*-
from torch import nn
from mmdet.utils import build_from_cfg
#此处不会在执行registry而是直接进行sys.modules查询得到
from .registry import (BACKBONES, NECKS, ROI_EXTRACTORS, SHARED_HEADS, HEADS,
LOSSES, DETECTORS)
#上面的registry是在models文件夹下,registry类的具体实现是在mmdet/utils文件夹下
只需要看一下build()的两个参数:cfg, registry
build_detector()在train.py中的调用,我们就可以知道,cfg是py配置文件中的字典, 以registry是DETECTORS为例,cfg就是model字典 (后面注册表为BACKBONES、NECKS等时,就是配置文件中的其他的字典了,不是model) 。
build()方法中,主干是一个判断结构,其实就是判断传进来的cfg是字典列表还是单独的字典,来分情况处理。(以注册表DETECTORS为例,是一个单独的字典)
def build(cfg, registry, default_args=None):
if isinstance(cfg, list):
modules = [
build_from_cfg(cfg_, registry, default_args) for cfg_ in cfg
#build_from_cfg()返回值是一个带形参的类,返回时也就完成了实例化的过程。
]
#所以modules就是一个class类的列表
return nn.Sequential(*modules)
#nn.Sequential 一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数
else:
return build_from_cfg(cfg, registry, default_args) #Config dict
def build_detector(cfg, train_cfg=None, test_cfg=None):
return build(cfg, DETECTORS, dict(train_cfg=train_cfg, test_cfg=test_cfg))
#DETECTORS = Registry('detector'),创建一个名为DETECTORS的注册表Registry。
def build_backbone(cfg):
return build(cfg, BACKBONES)
def build_neck(cfg):
return build(cfg, NECKS)
def build_roi_extractor(cfg):
return build(cfg, ROI_EXTRACTORS)
def build_shared_head(cfg):
return build(cfg, SHARED_HEADS)
def build_head(cfg):
return build(cfg, HEADS)
def build_loss(cfg):
return build(cfg, LOSSES)
后面的几个build_XXXXX()的方法也就跟build_detector()相同咯。
还是以注册表DETECTORS为例,配置文件为cascade_rcnn_r50_fpn_1x.py来讲解:在model文件夹下的cascade_rcnn.py文件中,有类Cascade_RCNN()的定义,在配置文件中,对应的key被传入类中当做属性,这些属性被初始化的时候,调用对应的build_XXXXX(),由此创建它们对应的注册表。
再以NECK为例,调用build_neck(cfg);然后执行build(cfg, NECKS),这一步,形参用到NECKS,所以在Registry中,又多了一个名为NECKS的注册表了。然后将配置文件中,字典名为neck的,然后生成一个类(类名是neck字典中的type的值,该类在models/necks文件夹下),同时将该类添加到了注册表NECKS的_module_dict中。
#在model/detectors/cascade_rcnn.py中
if neck is not None:
self.neck = builder.build_neck(neck)
#再builder.py中
def build_neck(cfg):
return build(cfg, NECKS)
#在configs/cascade_rcnn_r50_fpn_1x.py中
neck=dict(
type='FPN',
in_channels=[256, 512, 1024, 2048],
out_channels=256,
num_outs=5),
到这,NECK的注册和数据读入,相信大家已经很清楚了,其他的注册表也是类似的。
搭建模型思路:
这是搭建模型的一个思路,虽然讲得篇幅很大,有点乱乱的感觉,但是看懂后,就会发现很简单。
mmdetection搭建模型用途:
mmdetection将配置文件中,字典名为:backbone、neck、roi_extractor、shared_head、head、loss、detector的字典,全部实例化成注册表(Registry),然后这些字典里的type,都被实例化成对应的类(module),并添加到注册表的属性_module_dict中,其他的字段,则为这个类的属性,由此完成模型的建立,实际上,就是将配置文件的字典数据保存到类(module)中,以便后面读取数据,加载数据。