源码文件
不论是在训练脚本文件 train_net.py
还是在测试脚本文件 test_net.py
中, 都调用了 build_detection_model(cfg)
函数来创建模型, 该函数封装了模型定义的内部细节, 使得我们可以通过配置文件轻松的组合出不同类型的模型, 为了能够更好的了解模型的内部细节, 我们有必要知道这些模型是如何被定义, 又是如何组合到一起的, 为此我们需要对 MaskrcnnBenchmark 的 modeling 文件夹进行解析, 该文件夹的结构及文件关系如下所示(位于 ./maskrcnn_benchmark/modeling/
文件夹下):
- backbone
- detector
- roi_heads
- box_head
- mask_head
- inference.py
- loss.py
- mask_head.py
- roi_mask_feature_extractors.py
- rpn
- balanced_positive_negative_sampler.py
- box_coder.py
- matcher.py
- poolers.py
- registry.py
- utils.py
下面, 我们根据各个文件和函数之间的逻辑关系(而不是上面的文件顺序), 对 MaskrcnnBenchmark 的模型定义模块展开详细的解析和讨论. 想要透彻了解此部分的代码, 只需要按照本文的顺序仔细阅读即可.
detector 模型定义入口
第一部分是 detector
文件夹, 该文件夹中的两个文件定义了是整个 modeling
模块的入口. 文件解析如下
detectors.py 文件解析
第一个文件 detectors.py
中的代码只有短短几行, 其主要功能就是根据给定的配置信息实例化一个 class GeneralizedRCNN
的对象, 代码如下所示:
1 | # ./maskrcnn_benchmark/modeling/detector/detectors.py |
上面的代码利用配置信息 cfg
实例化了一个 class GeneralizedRCNN
类, 该类定义在 ./maskrcnn_benchmark/modeling/detector/generalized_rcnn.py
文件中, 关于该文件的解析请看下一节.
generalized_rcnn.py 文件解析
该文件定义了 MaskrcnnBenchmark 的 GeneralizedRCNN 类, 用于表示各种组合后的目标检测模型, 代码解析如下:
1 | import torch |
上面的代码中, to_image_list
函数位于 MaskrcnnBenchmark 的结构模块当中, 具体解析可以看structures. 另外, 可以看出, MaskrcnnBenchmark 模型的创建主要依赖于三个函数, 即 build_backbone(cfg)
, build_rpn(cfg)
, build_roi_heads(cfg)
. 下面, 我们就按照模型定义的顺序, 分别讲解这三个函数的内部实现
backbone 模型骨架定义
modeling/
文件夹下面的 backbone/
文件夹定义了有关模型骨架的相关代码, 该文件夹中总共三个主要的文件, 分别为:
backbone.py
fpn.py
resnet.py
backbone.py 文件解析
我们在定义骨架网络时使用到的 build_backbone(cfg)
函数, 正位于 ./maskrcnn_benchmark/modeling/backbone/backbone.py
文件中, 因此, 我们首先来看看该文件的内部实现.
1 | from collections import OrderedDict # 导入有序字典 |
上面两个函数分别定义了创建 ResNet 和 FPN 的代码逻辑, 下面我们就用这两个函数来进行模型创建, 代码解析如下:
1 | def build_backbone(cfg): |
resnet.py 网络主体(特征提取器)
在上面一节中的 backbone.py
文件中的两个函数 build_resnet_backbone()
和 build_resnet_fpn_backbone()
都使用了 body = resnet.ResNet(cfg)
来创建网络的主体, 这部分的代码定义位于 ./maskrcnn_benchmark/modeling/backbone/resnet.py
文件中, 下面我们就该该文件进行解析, 由于该文件篇幅较多, 因此我们先来看一下文件的整体结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62# ./maskrcnn_benchmark/modeling/backbone/resnet.py
# 导入各种包及函数
# ...
from maskrcnn_benchmark.layers ipmort FrozenBatchNorm2d
# ...
# ResNet stage specification
StageSpec = #...
# ResNet
class ResNet(nn.Module):
def __init__(self, cfg):
super(ResNet, self).__init__()
# 初始化
# ...
def _freeze_backbone(self, freeze_at):
# 将指定的参数置为: requires_grad = False
# ...
def forward(self, x):
# 定义 resnet 的前向传播过程
# ...
# ResNetHead
class ResNetHead(nn.Module):
def __init__(...):
# 初始化
# ...
def foward(self, x):
# 定义 ResNetHead 的前向传播过程
# ...
def _make_stage(...):
# 创建 ResNet 的 residual-block
# ...
class BottleneckWithFixedBatchNorm(nn.Module):
# 使用固定的BN
def __init__(...):
# 初始化
# ...
def forward(self, x):
# 定义前向传播过程
# ...
class StemWithFixedBatchNorm(nn.Module):
def __init__(self, cfg):
# 初始化
# ...
def forward(self, x):
# 定义前向传播过程
# ...
_TRANSFORMATION_MODULES = Registry({..})
_STEM_MODULES = Registry({..})
_STAGE_SPECS = Registry({..})
ResNet Stage Specification
文件的开头定义了 ResNet 的不同 stage 下的 block 的定义, 使用了 namedtuple
数据结构(命名元组, 可以用名字访问元素)来实现, 如下所示:
1 | StageSpec = namedtuple( |
ResNet 类
为了使阅读代码时不被搞混, 我们首先将文件最后的注册的各个模块贴出来, 这些模块会通过配置文件中的字符串信息来决定调用哪一个类或者参数, 代码如下所示:1
2
3
4
5
6
7
8
9
10
11
12_TRANSFORMATION_MODULES = Registry({
"BottleneckWithFixedBatchNorm": BottleneckWithFixedBatchNorm
})
_STEM_MODULES = Registry({"StemWithFixedBatchNorm": StemWithFixedBatchNorm})
_STAGE_SPECS = Registry({
"R-50-C4": ResNet50StagesTo4,
"R-50-C5": ResNet50StagesTo5,
"R-50-FPN": ResNet50FPNStagesTo5,
"R-101-FPN": ResNet101FPNStagesTo5,
})
当定义完各个 ResNet 模型的 stages 的卷积层数量后, 我们再来看一看 ResNet 类的实现, 代码解析如下所示:
1 | # ./maskrcnn_benchmark/modeling/backbone/resnet.py |
ResNetHead 类
接下来, 我们来看看 ResNetHead
类的实现, 代码解析如下所示:
1 | class ResNetHead(nn.Module): |
make_stage
在上面两个类中, 都使用了 _make_stage()
函数来创建对应的 stage, 下面, 我们就来看看该函数的具体实现, 代码解析如下所示:
1 | # ./maskrcnn_benchmark/modeling/backbone/resnet.py |
StemWithFixedBatchNorm 类
该类负责构建 ResNet 的 stem 模块, 也可以认为是 ResNet 的第一阶段(或者说是第零阶段), 在 ResNet 50 中, 该阶段主要包含一个 7×7 大小的卷积核, 在 MaskrcnnBenchmark 的实现中, 为了可以方便的复用实现各个 stage 的代码, 它将第二阶段最开始的 3×3 的 max pooling 层也放到了 stem 中的 forward
函数中实现(一般不带参数网络层的都放在 forward
中), 该类的实现代码解析如下:
1 | # ./maskrcnn_benchmark/modeling/backbone/resnet.py |
上面代码中的 Conv2d
是封装在 ./maskrcnn_benchmark/layers/misc.py
文件中的 class Conv2d(nn.Conv2d)
类, 它会根据 tensor 的 numel
参数决定其返回值, 当 x.numel()>0
时, 与普通的 torch.nn.Conv2d()
函数没有区别. 另外还使用了 ./maskrcnn_benchmark/layers/batch_norm.py
文件中定义的 class FrozenBatchNorm2d(nn.Module)
类, 该类主要实现了 BN 层的功能, 只不过其中的参数都是固定的, 而非可更新的.
BottleneckWithFixedBatchNorm 类
创建完 stem(stage1) 以后, 接下来就是需要创建 resnet 的 stage2~5, 根据 resnet 的特点我们可以知道, resnet2~5 阶段的整体结构是非常相似的, 都是有最基础的 resnet bottleneck block 堆叠形成的, 不同 stage 的 bottleneck block 的数量不同, 对于 resnet50 来说, 每一个阶段的 bottleneck block 的数量分别为 3,4,6,3, 并且各个相邻 stage 之间的通道数都是两倍的关系, 所以可以很容易的从一个 stage 的通道数推知另一个 stage 的通道数, 关于 bottleneck block 的代码解析如下所示:
1 | # ./maskrcnn_benchmark/modeling/backbone/resnet.py |
fpn.py 特征金字塔网络
对于 ResNet-50-C4 来说, 只需要上面的 ResNet 模型即可完成特征提取任务, 但是对于 ResNet-50-FPN 来说, 我们还需要实现 FPN 网络以获得更强的特征提取能力, 在 backbone.py
文件中的 build_resnet_fpn_backbone(cfg)
函数中, 就使用了 fpn = fpn_module.FPN(...)
来创建一个 FPN 类的实例对象, 并且利用 nn.Sequential()
将 ResNet 和 FPN 组合在一起形成一个模型, 并将其返回, 下面, 我们就来看看 FPN 网络的具体实现, 实例代码位于 ./maskrcnn_benchmark/modeling/backbone/fpn.py
文件中, 解析如下:
1 | # ./maskrcnn_benchmark/modeling/backbone/fpn.py |
roi_heads
在detector/generalized_rcnn.py
文件中, 模型定义如下所示:1
2
3
4
5
6def __init__(self, cfg):
super(GeneralizedRCNN, self).__init__()
self.backbone = build_backbone(cfg)
self.rpn = build_rpn(cfg, self.backbone.out_channels)
self.roi_heads = build_roi_heads(cfg, self.backbone.out_channels)
所以, 当使用 backbone 和 rpn 构建后特征图谱的生成结构以后, 我们就需要在特征图谱上划分相应的 RoI, 该模块的定义入口就是roi_heads/roi_heads.py
中build_roi_heads
函数, 下面我们对该文件进行解析
roi_heads
首先是入口函数build_roi_heads
1 | def build_roi_heads(cfg, in_channels): |
上面在构建roi
时, 根据种类的不同分别使用了build_roi_box_head
, build_roi_mask_head
, 以及build_roi_keypoint_head
, 同时, 利用CombinedROIHeads
将它们结合在一起, 前三个是函数, CombinedROIHeads
是一个类. 下面我们逐个介绍
box_head
build_roi_box_head()
该函数位于roi_heads/box_head/box_head.py
文件中, 我们来看一下该函数的实现:
1 | def build_roi_box_head(cfg, in_channels): |
该函数返回了ROIBoxHead
的实例, 该类同样定义在roi_heads/box_head/box_head.py
文件中, 实现如下:
1 | class ROIBoxHead(torch.nn.Module): |
可以看出, ROIBoxHead 主要由 4 个部分组成, 分别是:feature_extractor
, predictor
, post_processor
, 以及loss_evaluator
. 其中, feature_extractor
主要是提取各个 RoI 的特征, predictor
是对每个 RoI 进行预测, 得到class_logits
和box_regression
, 然后, 利用post_processor
和loss_evaluator
计算分类器和回归器的损失, 这四个部分的分别位于roi_heads/box_head/
目录下的不同文件中, 下面我们一一对其进行分析.
make_roi_box_feature_extractor
该函数位于roi_heads/box_head/roi_box_feature_extractors.py
文件中, 函数定义如下:
1 | def make_roi_box_feature_extractor(cfg, in_channels): |
上面的代码表示, 该函数会根据用户配置文件中指定的cfg.MODEL.ROI_BOX_HEAD.FEATURE_EXTRACTOR
来调用不同的函数, 对应了不同的 RoIHEAD, roi_box_feature_extractors.py
文件中定义了三种不同的 ROI_BOX_HEAD.FEATURE_EXTRACTOR, 分别如下所示:
ResNet50Conv5ROIFeatureExtractor
1 |
|
FPN2MLPFeatureExtractor
1 |
|
FPNXconv1fcFeatureExtractor
1 |
|
mask_head
接下来是build_roi_mask_head
函数, 该函数位于roi_heads/mask_head/mask_head.py
文件中, 定义如下:
1 | def build_roi_mask_head(cfg, in_channels): |
ROIMaskHead 定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44class ROIMaskHead(torch.nn.Module):
def __init__(self, cfg, in_channels):
super(ROIMaskHead, self).__init__()
self.cfg = cfg.clone()
self.feature_extractor = make_roi_mask_feature_extractor(cfg, in_channels)
self.predictor = make_roi_mask_predictor(
cfg, self.feature_extractor.out_channels)
self.post_processor = make_roi_mask_post_processor(cfg)
self.loss_evaluator = make_roi_mask_loss_evaluator(cfg)
def forward(self, features, proposals, targets=None):
"""
Arguments:
features (list[Tensor]): feature-maps from possibly several levels
proposals (list[BoxList]): proposal boxes
targets (list[BoxList], optional): the ground-truth targets.
Returns:
x (Tensor): the result of the feature extractor
proposals (list[BoxList]): during training, the original proposals
are returned. During testing, the predicted boxlists are returned
with the `mask` field set
losses (dict[Tensor]): During training, returns the losses for the
head. During testing, returns an empty dict.
"""
if self.training:
# during training, only focus on positive boxes
all_proposals = proposals
proposals, positive_inds = keep_only_positive_boxes(proposals)
if self.training and self.cfg.MODEL.ROI_MASK_HEAD.SHARE_BOX_FEATURE_EXTRACTOR:
x = features
x = x[torch.cat(positive_inds, dim=0)]
else:
x = self.feature_extractor(features, proposals)
mask_logits = self.predictor(x)
if not self.training:
result = self.post_processor(mask_logits, proposals)
return x, result, {}
loss_mask = self.loss_evaluator(proposals, mask_logits, targets)
return x, all_proposals, dict(loss_mask=loss_mask)
同样可以看到, ROIMaskHead 由四部分组成, 分别为feature_extractor
, predictor
, post_processor
以及loss_evaluator
, 他们分别定义在下面的四个文件当中, 具体细节可查看源代码.
roi_mask_feature_extractors.py, roi_mask_predictors.py, inference.py, loss.py
keypoint_head
函数build_roi_keypoint_head
定义在roi_heads/keypoint_head/keypoint_head.py
, 具体如下所示:
1 | def build_roi_keypoint_head(cfg, in_channels): |