fasterrcnn_resnet50_fpn - 从torchvision源码理解Faster R-CNN原理
PyTorch的torchvision包中实现了Faster R-CNN。本文结合对torchvision源码的阅读,深入理解Faster R-CNN的内部原理,以便进行开发利用。
fasterrcnn_resnet50_fpn - 从torchvision源码理解Faster R-CNN原理
1 接口层
外部调用
根据PyTorch的torchvision库的文档,Faster
R-CNN模型对象可以直接通过fasterrcnn_resnet50_fpn
函数来构造。
具体地,官方文档给出了训练时和预测时的调用样例:
torchvision.models.detection.fasterrcnn_resnet50_fpn
1 | True) model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained= |
- 其中,不论是训练,还是预测,模型的输入都是list容器,表示的是若干个图片(与目标框和类别)。
fasterrcnn_resnet50_fpn
fasterrcnn_resnet50_fpn
函数在torchvision.models.detection.faster_rcnn
包中实现,文档见torchvision.models.detection.fasterrcnn_resnet50_fpn。
1 | def fasterrcnn_resnet50_fpn(pretrained=False, progress=True, |
该函数的实现中,首先进行参数检查:
- 检查
trainable_backbone_layers
参数,必须在0~5之间,表示从最后一层开始计数,有几层在训练中是可优化的; - 检查
pretrained
和pretrained_backbone
参数,如果整个模型都设为预训练的,那就当然没必要再单独下载预训练的backbone
了,把整个Faster R-CNN模型都载入预训练参数即可。
Faster
R-CNN模型是FasterRCNN
类的实例。实例化时,传入指定的backbone
作为FasterRCNN
的backbone。
resnet_fpn_backbone
backbone通过对外开放的resnet_fpn_backbone
函数来构造。
resnet_fpn_backbone
函数在torchvision.models.detection.backbone_utils
包中实现。
1 | def resnet_fpn_backbone( |
首先,根据传入参数选出对应的resnet模型进行实例化。
随后,检查trainable_layers
参数的合法值范围,并通过parameter.requires_grad_(False)
来freeze除此以外的其他层。
默认未定义extra_blocks
的时候,会在feature
map结尾添加一个maxpool2d层,该LastLevelMaxPool
类实现并不复杂:
1 | # defined in torchvision.ops.feature_pyramid_network |
根据官方文档torch.nn.functional.max_pool2d可进一步查阅torch.nn.MaxPool2d,实际上F.max_pool2d(x[-1], 1, 2, 0)
表示:
- 输入input为
x[-1]
; - 池化窗口大小kernel_size为1;
- 步长stride为2;
- 边界填充padding为0。
关于卷积类的操作可以结合可视化理解:
然后,处理其他传参:
return_layers
,这是一个dict,与传入的backbone相配合,key是backbone的module name,value是用户定义的返回名;in_channels_list
,这是一个list,与传入的backbone和return_layers
相配合,是backbone返回的每一层feature map的通道数;out_channels
,一个整数,FPN中的通道数。
2 实现层
我们以一个例子贯穿始终:
1 | import torch |
我们使用预训练模型,并模拟输入两张图片。均为3通道,一张\(300 \times 400\)的\(H \times W\)分辨率,一张\(500 \times 400\)。
FasterRCNN
FasterRCNN
类在torchvision.models.detection.faster_rcnn.py
中实现。
1 | class FasterRCNN(GeneralizedRCNN): |
FasterRCNN
的代码看起来很长,实际上主要是文档注释。
FasterRCNN
的实现只有__init__
函数,因为FasterRCNN
继承自GeneralizedRCNN
,主要结构和计算流的实现都在父类中实现了,该子类的实现实际上只需要做一些参数检查和子类的具体子结构的实例化。
FasterRCNN的__init__
函数的主要就是在做参数检查和一些实例化准备工作,其结果就是将准备好的backbone、rpn、roi_heads和transform对象传递给父类(GeneralizedRCNN)的初始化函数,由此构建一个FasterRCNN实例对象。
GeneralizedRCNN
GeneralizedRCNN
在torchvision.models.detection.generalized_rcnn.py
中实现,负责以父类的形式定义RCNN架构的整体计算。
1 | class GeneralizedRCNN(nn.Module): |
在__init__
中,GenerailizedRCNN把R-CNN架构定义为4个组成部分:
transform
:一个变换模型,用于对图像和其他输入进行变换;backbone
:一个特征提取模型,输入的是进过变换处理的图像张量,输出的是取得的图像特征features;rpn
:一个RPN模型,输入包含——图像images、backbone提取出的图像特征features以及训练时输入的包含bbox ground truth的targets,输出包含——预测的区域proposals和相应的损失;roi_heads
:一个RoIHeads模型,输入包含——backbone输出的features,RPN输出的proposals,以及图像尺寸和训练时的targets。
该类的__forward__
计算流差不多就是这四部分依次执行的过程,除了一些参数检查,训练时和预测时对输入的区分以外,主要代码逻辑可以概括为:
1 | def forward(self, images, targets=None): |
GeneralizedRCNNTransform
Faster
R-CNN模型对输入图像的预处理由torchvision.models.detection.transform
包的GeneralizedRCNNTransform
类实现。
1 | class GeneralizedRCNNTransform(nn.Module): |
对输入图像的初步转换处理在forward
前向传播函数中实现,主要实现normalize和resize操作:
self.normalize
:初始参数在FasterRCNN的初始化中被设为image_mean = [0.485, 0.456, 0.406]
和image_std = [0.229, 0.224, 0.225]
;self.resize
:初始参数在FasterRCNN的初始化中被设为min_size=800, max_size=1333
;self.batch_images
,对一个batch的图像做了Padding,使其输出的张量尺寸一致。
根据该转换模块的默认值,结合本节开头的例子:
- 经过resize处理后,因为最小尺寸必须为800,因此\(300 \times 400\)的图片1转换为了\(800 \times 1066\),\(400 \times 500\)的图片2转换为了\(1000 \times 800\);
- 因为batch处理转tensors时加padding的缘故,两个图片的张量尺寸被统一为\(1024 \times 1088\)。
BackboneWithFPN
BackboneWithFPN
在torchvision.models.detection.backbone_utils
中实现,其作用就是以ResNet模型中提取出的一些中间层作为backbone,在backbone后面继续接上一个FPN。
1 | class BackboneWithFPN(nn.Module): |
该类的实现很简单,就像是一个组合,把backbone和FPN装起来:
- 把从backbone中取出的(用于提供feature maps)中间层作为模型的body;
- 构造出FPN(FeaturePyramidNetworkj)作为模型的fpn;
然后数据流定义很简洁,就是输入数据x先经过body,再经过fpn,就完成了。
有了backbone+FPN的模型,就可以进一步构造Faster R-CNN模型了。
torchvision
的Faster
R-CNN的backbone负责提取图像特征,具体实现由ResNet中间层衔接FPN组成。
ResNet
class ResNet(nn.Module)
ResNet在torchvision.models.resnet
包中实现,属于卷积神经网络实现的范畴,本文不再赘述。
有了resnet作为backbone,就可以通过resnet_fpn_backbone
构造一个在resnet后面接上FPN的模型,具体地,是构造BackboneWithFPN
类的对象。
torchvision
实现中:
- 默认后三层,即ResNet的layer4, layer3, layer2为可训练层,其余freeze;
- 默认返回后四层的feature map,即layer1, layer2, layer3, layer4,命名index依次为0, 1, 2, 3,每层输出feature map的通道数依次为256, 512, 1024, 2048。
本节的例子经过ResNet部分的计算后,从输入的\(2 \times 3 \times 1024 \times 1088\)的tensor,转换为了一个有序字典OrderedDict:
'0': shape[2, 256, 256, 272]
,源自ResNet的layer1;'1': shape[2, 512, 128, 136]
,源自ResNet的layer2;'2': shape[2, 1024, 64, 68]
,源自ResNet的layer3;'3': shape[2, 2048, 32, 34]
,源自ResNet的layer4;
FPN(FeaturePyramidNetwork)
FeaturePyramidNetwork
在torchvision.ops.feature_pyramid_network
包中实现。FPN实现了金字塔结构的特征提取,低层的卷积感受野小,其特征代表小目标的特征,而高层的卷积感受野大,因此其特征适合表示大目标特征。在目标检测中运用FPN,在低层配合小尺寸anchor,在高层配合大尺寸anchors,有利于同时有效检测小目标和大目标。
1 | class FeaturePyramidNetwork(nn.Module): |
按原论文的思路,FPN第n层输出feature map \(P_n\)的是把两者进行合并:
- lateral:CNN的第n层feature map \(C_n\),做1×1卷积;
- top-down upsampling:FPN的n+1层feature map \(P_{n+1}\)做2×上采样(长宽各2倍)变成第n层的尺寸;
此后,采用3×3卷积对合并后的feature map进行卷积处理,以便消除上采样操作造成的失真效应(aliasing effect)。
此时,形成的每层的最终的feature map就是最终的feature map \(P_n\),例如:从ResNet的2~5层feature map \(\{C_2, C_3, C_4, C_5\}\)经过FPN取得\(\{P_2, P_3, P_4, P_5\}\),对应的两者的空域尺寸(spatial size)是相同的。
在torchvision
的具体实现中:
self.inner_blocks
就是FPN的所有1×1卷积;self.layer_blocks
就是FPN合并后需要用到的3×3卷积;
这两者都是nn.ModuleList()
,在__init__
初始化时,在一个n次(n个feature
map)的for循环中进行初始化,都填入nn.Conv2d
对象,设置为统一的out_channels
。
在__forward__
定义的计算流中,核心代码逻辑可以概括为:
1 | def forward(self, x: Dict[str, Tensor]): |
具体步骤是从后往前计算每一层的result,即论文中的\(P_n\):
inner_lateral
就是CNN的feature map经过1×1卷积计算的结果,该卷积通过self.get_result_from_inner_blocks(x[idx], idx)
实现;inner_top_down
就是从后一层\(P_{n+1}\)上采样出来的结果,该上采样通过插值实现F.interpolate(last_inner, size=feat_shape, mode="nearest")
;last_inner
就是两者合并的结果,通过element-wise addition实现;- 在加入
results
前,还需要用3×3卷积计算一下,即self.get_result_from_layer_blocks(last_inner, idx)
。
最后,如果还有额外计算块的话,就再算一遍,取得这层的结果也加入。
在具体实现中,在FPN尾部增加了LastLevelMaxPool
,并将其计算结果命名为pool
加入了names
。
本节的例子经过FPN部分的计算后,从ResNet输出的4个通道数不同的feature maps,转换为了各层通道数一致的一个有序字典OrderedDict:
'0': shape[2, 256, 256, 272]
,源自ResNet的layer1;'1': shape[2, 256, 128, 136]
,源自ResNet的layer2;'2': shape[2, 256, 64, 68]
,源自ResNet的layer3;'3': shape[2, 256, 32, 34]
,源自ResNet的layer4;'pool': shape[2, 256, 16, 17]
,源自FPN作为extra_blocks
的LastLevelMaxPool
。
RegionProposalNetwork
RegionProposalNetwork
在torchvision.models.detection.rpn
包中实现。
RegionProposalNetwork
的实现比较长,主要看__init__
和__forward__
就可以了。
1 | class RegionProposalNetwork(torch.nn.Module): |
主要看__forward__
中的计算流,RPN的完整过程
- 把输入的特征features输入到RPNHead(
self.head
)中,输出object/non-object分类分值(objectness
)和bbox回归数值(pred_bbox_deltas
); self.anchor_generator
为当前输入的图像和feature map生成anchors
;self.box_coder.decode
把bbox回归数值pred_bbox_deltas
算到锚框anchors
上,得到预测出的候选框proposals
;- 计算出的
proposals
可能很多且相互密集重叠,那么就通过self.filter_proposals
做一遍过滤,输出候选框proposals
和与之对应的分值scores
; - 如果是训练时,当然在RPN阶段需要根据预测出的
proposals
与候选框真值之间的误差来计算损失。
AnchorGenerator
AnchorGenerator
在torchvision.models.detection.anchor_utils
中实现,其作用是根据预定义的anchor的sizes和aspect_ratios,针对图像到feature
map的尺寸比例,计算feature map对应的anchors。
1 | class AnchorGenerator(nn.Module): |
这部分代码也有点长,主要原理也还是看__forward__
计算流即可,这里面有一系列预备的计算,随后就是两层的for循环,表示:每一个图片可以传入\(n\)个(尺寸不同的)feature
map,每一个feature map上都有\(k_i\)个anchor,那么每个图片就有\(K = \sum_{i=1}^{n}k_i\)个anchors。
其中,每个滑窗位置上有\(A\)个anchor,第\(i\)个feature map上有\(L_i\)个滑窗位置,则该层feature map上有\(k_i = A L_i\)个anchors。
两层循环,外层遍历images,内层遍历feature maps,由此输出所有图片的feature map上的anchors。
在具体实现中,AnchorGenerator:
self.set_cell_anchors
函数负责为每一层feature map生成self.cell_anchors
,这个Cell Anchors的尺寸基于的是输入图片tensor的尺寸,;self.cached_grid_anchors
函数内会进一步调用self.grid_anchors
函数,该函数负责根据feature map的网格尺寸以及该feature map相较于输入图片tensor的步长,计算出anchors_over_all_feature_maps
,它的尺寸则是基于输入图片tensor的尺寸。- 双重for循环,输入\(N\)个图片,就相应地将anchors复制出\(N\)份。
- 最后
torch.cat
拉平每个图片上不同feature map上的所有\(K\)个anchors,形成一个长度为\(N\)的list,每个元素是\(K \times 4\)的anchors张量。
对应到例子,torchvision
实现默认为:
- 3种aspect ratio,分别为0.5, 1.0, 2.0;
- 每层feature map对应1个scale,5层feature map分别为16, 32, 64, 128, 256。
因此,cell_anchors
中,每层feature map都是3个anchor
cells:
'0'
: shape[3, 4]'1'
: shape[3, 4]'2'
: shape[3, 4]'3'
: shape[3, 4]'4'
: shape[3, 4]
结合例子来算,把cell_anchors
算到输入图像张量的每一个滑窗位置上,就可以算出所有位置上的所有anchors_over_all_feature_maps
:
'0'
: shape[208896, 4],\(208896 = 256 \times 272 \times 3\);'1'
: shape[52224, 4],\(52224 = 128 \times 136 \times 3\);'2'
: shape[13056, 4],\(13056 = 64 \times 68 \times 3\);'3'
: shape[3264, 4],\(3264 = 32 \times 34 \times 3\);'4'
: shape[816, 4],\(816 = 16 \times 17 \times 3\);
最后返回的anchors
会为输入的每个图片复制一份,并通过torch.cat
拉平:
- shape[278256, 4], \(278256 = 208896 + 52224 + 13056 + 3264 + 816\);
- shape[278256, 4], \(278256 = 208896 + 52224 + 13056 + 3264 + 816\);
RPNHead
RPNHead
在torchvision.models.detection.rpn
包中实现。RPNHead被用于以滑窗的形式在特征提取出的feature
map上滑动并计算每个anchor的bbox回归值和object/non-object二分类。
1 | class RPNHead(nn.Module): |
可以看到,RPNHead的结构不复杂,就是三个卷积:
self.conv
:3×3卷积,对输入的feature map做卷积处理;self.cls_logits
:1×1卷积,对处理后的feature map \(t\)做卷积,取得object/non-object的分类数值;self.bbox_reg
:1×1卷积,对处理后的feature map \(t\)做卷积,取得bbox坐标值的回归数值。
在forward
前向传播计算的时候,输入的x是一个List[Tensor]
,即FPN的输出。值得注意的是,for
循环遍历的并不是每一张图片,而是FPN输出的每一层特征。
在本例中,RPNHead的两个卷积分支输出了两个List[Tensor]
:
logits
(objectness
):
'0'
: shape[2, 3, 252, 272];'1'
: shape[2, 3, 128, 136];'2'
: shape[2, 3, 64, 68];'3'
: shape[2, 3, 32, 34];'4'
: shape[2, 3, 16, 17];
bbox_regs
(pred_bbox_deltas
):
'0'
: shape[2, 12, 252, 272];'1'
: shape[2, 12, 128, 136];'2'
: shape[2, 12, 64, 68];'3'
: shape[2, 12, 32, 34];'4'
: shape[2, 12, 16, 17];
因为每个滑窗位置对应三种ratios
,即3个anchors,所以logits
是3个值,而bbox_regs
因为坐标乘4,所以是12个值。
BoxCoder
BoxCoder
在torchvision.models.detection._utils
中实现。
1 | class BoxCoder(object): |
RPN中self.box_coder
使用BoxCoder作为bbox的编解码器:
1 | proposals = self.box_coder.decode(pred_bbox_deltas.detach(), anchors) |
通过BoxCoder实例,将RPNHead回归出的pred_bbox_deltas
与RPN的锚框anchors
做解码计算,把回归出的偏移值加到基准anchors位置上,解码输出候选框proposals
。
在本例中,RPN的forward
对解码出的原始proposals
做了维度整理proposals = proposals.view(num_images, -1, 4)
,得到的proposals
是:
- shape[2, 278256, 4]
filter_proposals
filter_proposals
是一个对RPN
Head生成的候选框proposals
的过滤操作,在RPN类RegionProposalNetwork
中作为成员函数实现。
1 | class RegionProposalNetwork(torch.nn.Module): |
对proposals
的过滤操作分几个阶段实现:
- 首先,根据
objectness
分值和num_anchors_per_level
来在每层选出top_n_idx(pre_nms_top_n)
用于在NMS前先筛选一下proposals
; - 此后,进入for循环,遍历batch中的每张图片,
- 先做一些裁边界、去小框的处理;
- 然后再做(类间)NMS;
- 保留当前图片所有结果的
post_nms_top_n
的目标作为返回结果。
最后将这么多筛选操作筛选出的final_boxes
和final_scores
返回(boxes
是筛选后的proposals
,scores
是筛选后的objectness
)。
在本例中,有两张图片,每张图片上有278256个anchors
,因此产生278256个proposals
和objectness
,进过筛选处理后:
final_boxes
:- shape[1000, 4]
- shape[1000, 4]
final_scores
:- shape 1000
- shape 1000
因为FasterRCNN中默认值rpn_post_nms_top_n_test=1000
,所以在eval模式(即test,
infer情况)下,例子中的两张图片都各筛选出了top-1000个boxes。
RoIHeads
RoIHeads
在torchvision.models.detection.roi_heads
包中实现。
1 | class RoIHeads(torch.nn.Module): |
主要看__forward__
函数的实现,虽然很长,但是如果只考虑Faster
R-CNN需要的部分(不考虑用于Mask
R-CNN的图像分割分支),其实可以概括为:
1 | def forward(self, |
Faster R-CNN的RoIHeads主要包含几个步骤:
- RoI
Pool:由
box_features = self.box_roi_pool(features, proposals, image_shapes)
执行,Faster R-CNN的RoI Pool的具体实现是torchvision.ops.poolers
包中的MultiScaleRoIAlign
类。因为目标的形状不尽相同,所以涉及到的特征窗口就不尽相同。RoI Pool的目的在于通过把尺寸不定的RoI window划分为固定的网格做池化,来把输入的变长的RoI特征池化为定长的特征输出,方便后续的特征处理。 - MLP
Head:由
box_features = self.box_head(box_features)
执行,Faster R-CNN的MLP Head的具体实现是torchvision.models.detection.faster_rcnn
中的TwoMLPHead
类。MLP Head承接RoI Pool池化出的定长特征向量,并通过MLP做非线性计算,输出最终特征用于后续的任务(分类、回归等)。 - Predictor:由
class_logits, box_regression = self.box_predictor(box_features)
执行,Faster R-CNN的Predictor的具体实现是torchvision.models.detection.faster_rcnn
中的FastRCNNPredictor
类。上一步MLP操作输出的特征作为最后的特征,交给Predictor去做具体任务的预测,例如:目标分类,bbox位置和尺寸值的回归预测。 - Postprocess
Detections:由
boxes, scores, labels = self.postprocess_detections(class_logits, box_regression, proposals, image_shapes)
,该函数是RoIHeads类的一个成员函数。
MultiScaleRoIAlign
torchvision
采用torchvision.ops.poolers
包中的MultiScaleRoIAlign
作为Faster
R-CNN的RoI Pool的实现。
1 | class MultiScaleRoIAlign(nn.Module): |
实际上Faster R-CNN论文发表时并没有RoI Align技术,当时仍然沿用的是Fast R-CNN中的RoI Pool。RoI Pool指的是对RoI内的特征做池化,取得一个小的feature map,即,把原来形状不定的\(h \times w\)(\(h, w\)均为变量)的RoI窗口内的特征池化为统一的\(H \times W\)(\(H, W\)均为常量)的小feature map。
RoI Align其实是Mask R-CNN论文中提出的概念。RoI Align觉得RoI Pool的处理太粗糙了,存在量化(Quantization)的问题,计算feature map上的窗口坐标的时候就舍入取整了,窗口内划分bins的时候又舍入取整了,这样就很不精确。这样的量化处理,用作分类任务倒还影响不大,但是用作图像分割这种像素级精度的任务时就是个问题了。
RoI Align对RoI Pool的改进及其二次插值的数学计算原理可以仔细阅读这篇文章:
MultiScaleRoIAlign核心的RoI
Align操作是通过调用torchvision.ops.roi_align
包的roi_align
函数实现的,而该函数实际上也只是执行了对底层torch.ops.torchvision.roi_align
函数的调用。
在本例中,输入的两张图片经过RPN处理后,各得到1000个boxes,即共2000个boxes。经过RoIPool / RoIAlign处理后,输出为:
box_features
(results
):shape[2000, 256, 7, 7]
表示2000个boxes,都被池化为了\(C, H, W = 256, 7, 7\)的特征。
TwoMLPHead
torchvision
采用torchvision.models.detection.faster_rcnn
包中的TwoMLPHead
作为MLP
Head的实现。
1 | class TwoMLPHead(nn.Module): |
这部分并不复杂,实际上就是实现了两层的MLP,名为fc6
和fc7
。
在本例中,RoIAlign输出的box_features
原shape[2000, 256,
7, 7],在TwoMLPHead中:
- 首先经过flatten处理,变为shape[2000, 12544];
- 进过双层MLP处理后,变为shape[2000, 1024]。
返回的是如上非线性转换后的box_features
特征,此时shape[2000,
1024]。
FastRCNNPredictor
torchvision
采用torchvision.models.detection.faster_rcnn
包中的FastRCNNPredictor
作为Predictor的实现。
1 | class FastRCNNPredictor(nn.Module): |
这部分也并不复杂,实际上就是同论文中描述的一样,通过MLP实现了两个预测分支:
self.cls_score
分支预测目标的分类分值scores
;self.bbox_pred
分支回归目标对应各个分类的目标框回归值。
在torchvision
的预训练模型中,FastRCNNPredictor的num_classes
是91,即能识别含背景在内的91个类。
在本例中,两个分支根据RoIAlign和TwoMLPHead提取出的特征,分别预测输出:
class_logits
(socres
):shape[2000, 91];box_regression
(bbox_deltas
):shape[2000, 364]。
意思是输入的2张图片上共2000个框(1000个/图片),这2000个框都做了分类预测,并且为每个类分别计算了目标框的回归修正值。
postprocess_detections
在模型的Predictor完成预测后,还需要做后续的一些处理,该部分的处理在torchvision.models.detection.roi_heads.RoIHeads
的postprocess_detections
函数中实现。
1 | class RoIHeads(torch.nn.Module): |
后处理在for循环中,遍历每一张图片:
- 去除背景类的框;
- 去除低分框;
- 去除空框(尺寸极小的无意义小框);
- 对它的boxes和scores做(类内)NMS;
- 保留
self.detections_per_img
个的top-k个目标。
因为本例输入的是随机值填充的模拟图片,所以在去除低分框的环节,2000个候选框就因为没有实际的目标而被全部滤除了。
GeneralizedRCNNTransform.postprocess
Faster
R-CNN模型的后期处理由torchvision.models.detection.transform
包的GeneralizedRCNNTransform
类实现。
1 | class GeneralizedRCNNTransform(nn.Module): |
其实对于目标检测而言,实际上只对boxes
的坐标做了resize的操作。因为GeneralizedRCNNTransform
在对输入图像做预处理的时候,有进行尺寸转换,而且转tensor的时候又增加了padding是同一batch的图像张量能够保持尺寸一致。所以输出结果的时候,还是要把在tensor上的坐标转换为原始图像尺度上的坐标。
3 总结
最后总览一下整个模型的实现结构,只需通过简单的print
:
1 | print(model) |
,即可输出结果:
1 | FasterRCNN( |
模型结构可以总结为层次结构:
- Faster R-CNN
- (transform): GeneralizedRCNNTransform
- (backbone): BackboneWithFPN
- (body): IntermediateLayerGetter
- (fpn): FeaturePyramidNetwork
- (rpn): RegionProposalNetwork
- (anchor_generator): AnchorGenerator
- (head): RPNHead
- (roi_heads): RoIHeads
- (box_roi_pool): MultiScaleRoIAlign
- (box_head): TwoMLPHead
- (box_predictor): FastRCNNPredictor