Detectron 2 源码阅读笔记
Most part of the begining of this notes is inherited from https://www.cnblogs.com/marsggbo/p/11677086.html.
1. 代码结构概览
核心部分
- configs: 储存各种网络的yaml配置文件;
- datasets: 存放数据集的地方;
- deteectron2:运行代码的核心组建;
- tools:提供了运行代码的入口以及一切可视化的代码问价。
Tutorial部分
- demo:demo;
- docs:docs;
- tests:tests;
- projects:提供了真实的项目代码示例;
2. 代码逻辑分析
2.1 超参数配置
Detectron2中的参数配置使用了yacs这个库,这个库能够很好地重用和拼接超参数文件配置。我们先看一下detrctron2/config/
的文件结构:
- compat.py: 应该是对之前的Detectron库的兼容吧,可忽略。
- config.py: 定义了一个
CfgNode
类,这个类继承自fvcore
库(fb写的一个共公共库,提供一些共享的函数,方便各种不同项目使用)中定义的CfgNode
,总之就是不断继承。。。继承关系是这样的:detrctron2.config.CfgNode->fcvore.common.config.CfgNode->yacs.config.CfgNode->dict
。另外该文件还提供了get_cfg()
方法,该方法会返回一个含有默认配置的CfgNode
,而这些默认的配置值在下面的default.py
中定义了,之所以这样做是因为要配置的默认值太多了,所以为了文档清晰才写到了一个新的文件中去,不过,yacs库的作者也建议这样做。 - default.py: 如上面所说,该文件定义了各种参数的默认值。
了解配置函数的方法后我们再回到tools/train_net.py
,我们一行一行的来理解。
tools/train_net.py
1 | from detectron2.config import get_cfg |
cfg = get_cfg()
: 获取已经配置好默认参数的cfgcfg.merge_from_file(args.config_file)
:config_file
是指定的yaml配置文件,通过merge_from_file
这个函数会将yaml文件中指定的超参数对默认值进行覆盖。cfg.merge_from_list(args.opts)
:merge_from_list
作用同上面的类似,只不过是通过命令行的方式覆盖。cfg.freeze()
:freeze
函数的作用是将超参数值冻结,避免被程序不小心修改。default_setup(cfg, args)
:default_setup
是detectron2/engine/default.py
中提供的一个默认配置函数,具体是怎么配置的这里不详细说明了。不过需要知道的值这个文件中还提供了很多其他的配置函数,例如还提供了两个类:DefaultPredictor
和DefaultTrainer
。
2.2 Trainer结构
既然上面提到了DefaultTrainer
,那么我们就从这个类入手了解一下detectron2.engine
,其代码结构如下:
train_loop.py
:这个函数主要作用是提供了三个重要的类:HookBase
: 这是一个Hook的基类,用于指定在训练前后或者每一个step前后需要做什么事情,所以根据特定的需求需要对如下四种方法做不同的定义:before_train
,after_train
,before_step
,after_step
。以before_step
。TrainerBase
: 该类中定义的函数可以归纳成三种,并初始化_hooks
为[]
:register_hooks
:这个很好理解,就是将用户定义的一些hooks
进行注册,说大白话就是把若干个Hook
放在一个list
里面去。之后只需要遍历这个list
依次执行就可以了。register_hooks(hooks)
会将输入的hooks
中的trainer
设置为当前的类实例的弱引用,并将其添加到当前类实例的self._hooks
中去;
- 第二类其实就是上面提到的遍历
hook list
并执行hook
,不过这个遍历有四种,分别是before_train
,after_train
,before_step
,after_step
。还有一个就是run_step
,这个函数其实就是平常我们在编写训练过程的代码,例如读数据,训练模型,获取损失值,求导数,反向梯度更新等,只不过在这个类里面没有定义。 - 第三类就是
train
函数,它有两个参数,分别是开始的迭代数和最大的迭代数。之后就是重复依次执行第二类中的函数指定迭代次数。
SimpleTrainer
:其实就是继承自TrainerBase
,然后定义了run_step
等方法。我们后面也可以继承这个类做进一步的自定义。- 初始化基类,并赋予其
model
、data_loader
、_data_loader_iter
、optimizer
; - 这在随后的训练过程中会被继承为默认的训练类
DefaultTrainer
。
- 初始化基类,并赋予其
defaults.py
: 上面已介绍,提供了两个类:DefaultPredictor
和DefaultTrainer
,这个DefaultTrainer
就继承自SimpleTrainer
,所以存在如下继承关系:detectron2.engine.default.DefaultTrainer->detectron2.engine.train_loop.SimpleTrainer->detectron2.engine.train_loop.TrainerBase
hooks.py
:定义了很多继承自train_loop.HookBase的Hook
。CallBackHook
: Create a hook using callback functions provided by the user.IterationTimer
:Track the time spent for each iteration (each run_step call in the trainer). Print a summary in the end of training.;PeriodicWriter
:Write events to EventStorage periodically.PeriodicCheckpointer
:Same as :class:detectron2.checkpoint.PeriodicCheckpointer
, but as a hook.LRScheduler
:A hook which executes a torch builtin LR scheduler and summarizes the LR. It is executed after every iteration.AutogradProfiler
:A hook which runstorch.autograd.profiler.profile
.EvalHook
: Run an evaluation function periodically, and at the end of training.PreciseBN
: The standard implementation of BatchNorm uses EMA in inference, which is sometimes suboptimal. This class computes the true average of statistics rather than the moving average, and put true averages to every BN layer in the given model.
launch.py
: 前面提到过,可以理解成代码启动器,可以根据命令决定是否采用分布式训练(或者单机多卡)或者单机单卡训练。
整个训练过程按照如下流程进行:
1 | self.before_train() |
小结:
2.3 Trainer解析
1 | def main(args): |
2.3.1 build_*方法
build_*
主要来自于engine.defaults.DefaultTrainer
类,继承自engine.train_loop.SimpleTrainer
类。而detectron2.engine.default.DefaultTrainer
在其__init__(self, cfg)
函数中定义了解析cfg。如下面代码所示,cfg会作为参数倍若干个build_*
方法解析,得到解析后的model,optimizer
,data_loader
等。
1 | from detectron2.modeling import build_model |
下面我们以DefaultTrainer.build_model
为例来介绍注册机制,该方法调用了detectron2/modeling/meta_arch/build.py
的build_model
函数,其源代码如下:
1 | from detectron2.utils.registry import Registry |
meta_arch = cfg.MODEL.META_ARCHITECTURE
: 根据超参数获得网络结构的名字return META_ARCH_REGISTRY.get(meta_arch)(cfg)
:META_ARCH_REGISTRY
是一个Registry
类(这个在后面会详细介绍),可以将这一行代码拆成如下几个步骤:
1 | model = META_ARCH_REGISTRY.get(meta_arch) |
2.3.2 注册机制Registry
假如你想自己实现一个新的backbone网络,那么你可以这样做:
首先在detectron2中定义好如下(实际上已经定义了):
1 | # detectron2/modeling/backbone/build.py |
之后在你创建的新的文件下按如下方式创建你的backbone
1 | # detectron2/modeling/backbone/your_backbone.py |
Registry
源代码如下(有删减):
1 | class Registry(object): |
- 首先是
__init__
部分:self._name
则是你要注册的名字,例如对于完整的模型而言,name
一般取META_ARCH
。当然如果你需要自定义backbone
网络,你也可以定义一个Registry('BACKBONE')
;self._obj_map
:其实就是一个字典。以模型为例,key
就是你的模型名字,而value
就是对应的模型类。这样你在传参时只需要修改一下模型名字就能使用不同的模型了。具体实现方法就是后面这几个函数;
register
: 可以看到该方法定义了注册的两种方式,一种是当obj==None
的时候,使用装饰器的方式注册,另外一种就是直接将obj作为参数调用_do_register
进行注册。_do_register
:真正注册的函数,可以看到它首先会判断name是否已经存在于self._obj_map
了。什么意思呢?还是以backbone为例,我们定义了一个BACKBONE_REGISTRY = Registry('BACKBONE')
,然后又定义了很多种backbone
,而这些backbone
都使用@BACKBONE_REGISTRY.register()
的方式注册到了BACKBONE_REGISTRY._obj_map
中了,所以才取名为Registry
。
2.4 Detectron2 整体代码框架
整体框架如下图所示:
3. Dataset
构建data_loader
原理步骤
1 | # engine/default.py |
函数调用关系如下图:
结合前面两篇文章的内容可以看到detectron2
在构建model,optimizer
和data_loader
的时候都是在对应的build.py
文件里实现的。我们看一下build_detection_train_loader
是如何定义的(对应上图中紫色方框内的部分(自下往上的顺序)):
1 | def build_detection_train_loader(cfg, mapper=None): |
后面的采样器和data_loader可以参阅https://www.cnblogs.com/marsggbo/p/11308889.html一文弄懂Pytorch的DataLoader, DataSet, Sampler之间的关系。
3.1 获取dataset_dicts
get_detection_dataset_dicts(dataset_names)
函数需要传递的一个重要参数是dataset_names
,这个参数其实就是一个字符串,用来指定数据集的名称。通过这个字符串,该函数会调用data/catalog.py
的DatasetCatalog
类来进行解析得到一个包含数据信息的字典。
解析的原理是:DatasetCatalog
有一个字典_REGISTERED
,默认已经注册好了例如coco
, voc
这些数据集的信息。如果你想要使用你自己的数据集,那么你需要在最开始前你需要定义你的数据集名字以及定义一个函数(这个函数不需要传参,而且最后会返回一个dict
,该dict
包含你的数据集信息),举个栗子:
1 | from detectron2.data import DatasetCatalog |
当然,如果你的数据集已经是COCO的格式了,那么你也可以使用如下方法进行注册:
1 | from detectron2.data.datasets import register_coco_instances |
最后,get_detection_dataset_dicts
会返回一个包含若干个dict
的list
,之所以是lis
t是因为参数dataset_names
也是一个list
,这样我们就可以制定多个names
来同时对数据进行读取。
3.2 解析成DatasetFromList
DatasetFromList(dataset_dict)
函数定义在detectron2/data/common.py
中,它其实就是一个torch.utils.data.Dataset
类,其源码如下
1 | class DatasetFromList(data.Dataset): |
3.3 将DatasetFromList
转化为MapDataset
其实DatsetFromList
和MapDatase
t都是torch.utils.data.Dataset
的子类,那他们的区别是什么呢?很简单,区别就是后者使用了mapper
。
在解释mapper
是什么之前我们首先要知道的是,在detectron2
中,一张图片对应的是一个dict
,那么整个数据集就是listdict
。之后我们再看DatsetFromList
,它的__getitem__
函数非常简单,它只是简单粗暴地就返回了指定idx的元素。显然这样是不行的,因为在把数据扔给模型训练之前我们肯定还要对数据做一定的处理,而这个工作就是由mapper
来做的,默认情况下使用的是detectron2/data/dataset_mapper.py
中定义的DatasetMapper
,如果你需要自定义一个mapper
也可以参考这个写。
1 | DatasetMapper(cfg, is_train=True) |
我们继续了解一下DatasetMapper
的实现原理,首先看一下官方给的定义:
1 | """ |
简单概括就是这个类是可调用的(callable),所以在下面的源码中可以看到定义了__call__
方法。
该类主要做了这三件事:
1 | The callable currently does the following: |
其源码如下(有删减):
1 | class DatasetMapper: |
MapDataset
:
1 | class MapDataset(data.Dataset): |
self._fallback_candidates
是一个set
,它的特点是其中的元素是独一无二的,定义这个的作用是记录可正常读取的数据索引,因为有的数据可能无法正常读取,所以这个时候我们就可以把这个坏数据的索引从_fallback_candidates
中剔除,并随机采样一个索引来读取数据。__getitem__
中的逻辑就是首先读取指定索引的数据,如果正常读取就把该所索引值加入到_fallback_candidates
中去;反之,如果数据无法读取,则将对应索引值删除,并随机采样一个数据,并且尝试3次,若3次后都无法正常读取数据,则报错,但是好像也没有退出程序,而是继续读数据,可能是以为总有能正常读取的数据吧;
3. Debug
3.1 Using ground.py
4. Training
4.1 在COCO数据集上训练
Using 1 image per batch, cause the desktop can’t afford 2 images per batch.
1 | python tools/train_net.py \ |
Training parameters:
- max_mem: 2638M;
- 89999 iterations;
- The training log is saved to log_1127_first_training.txt;
4.2 训练流程
主要训练函数为tools/train_net.py
中的main
函数,launch
函数将训练过程分布化,在当前阶段暂时不予深究。
5. Validation
python demo/demo.py
–config-file configs/COCO-Keypoints/keypoint_rcnn_R_50_FPN_1x.yaml
–input comunity/1574912817253.png
–opts MODEL.WEIGHTS weights/COCO-Keypoints/model_final_04e291.pkl