cascade_rcnn.py
文件在moels/detections
文件夹下。本次对文件cascade_rcnn.py
的代码解读,是根据py配置文件configs/cascade_rcnn_r50_fpn_1x.py
的数据信息进行讲解的。
moels/detectionscascade_rcnn.py文件中
主要的内容如下:
共七个部分,本篇文章主要对前四个部分的代码精度,这四个步骤中的__init__()
和forward_train()
是module类的最主要的两个部分,也是定义网络的最关键的部分。
自定义一个模型就是通过继承nn.Module类来实现,在__init__()
构造函数中申明各个层的定义,在forward()
中实现层之间的连接关系,实际上就是前向传播的过程。
注:后面三个部分,博主后续会继续阅读代码,在对这三个部分进行补充。
首先,看本篇讲解时,先了解一下下篇文章,该文章讲解了创建模型的过程,尤其以detection为例,讲解了mmdetection通过注册表的形式,实例化了类名为DETECTION的Rigistry类,并且在其module_dict属性中,保存了detection的module类,和其对应的类名。通过这篇文章,可以了解mmdetection如何注册和创建模型的。
其次,了解一下torch.nn.module
(有pytorch基础也行,博主刚开始看mmdetection时,没有pytorch一点基础,然后看到forward()函数时,找了好几个文件夹,看他在哪里调用的…,后面才知道,forward()是自定义层的前向计算,自动执行的( 也就是对输入自动进行处理)),推荐下篇文章:
@DETECTORS.register_module
#在build_from_cfg()中,实例化detector,然后在通过形参的方式,将类和类名送入了方法register_module中。
class CascadeRCNN(BaseDetector, RPNTestMixin):
# 参数来自cascade_rcnn_r50_fpn_1x.py
def __init__(self,
num_stages, # 3
backbone, # ResNet
neck=None, # FPN
shared_head=None,
rpn_head=None, # RPNHead
bbox_roi_extractor=None, # SingleRoIExtractor
bbox_head=None, # SharedFCBBoxHead * 3 (三阶段)
mask_roi_extractor=None,
mask_head=None,
train_cfg=None, # assigner : MaxIoUAssigner ; sampler : RandomSampler
test_cfg=None, # skip
pretrained=None): # modelzoo://resnet50
assert bbox_roi_extractor is not None
assert bbox_head is not None
super(CascadeRCNN, self).__init__()
self.num_stages = num_stages
self.backbone = builder.build_backbone(backbone) # build backbone and Registry
#同上,创建模型,对各个组件(比如backbone、neck、bbox_head等字典数据,构建成module类)分别创建module类模型
if neck is not None:
self.neck = builder.build_neck(neck)
if rpn_head is not None:
self.rpn_head = builder.build_head(rpn_head)
if shared_head is not None:
self.shared_head = builder.build_shared_head(shared_head)
if bbox_head is not None:
self.bbox_roi_extractor = nn.ModuleList()
#ModuleList() 能够像列表一样索引 , [module1 , module2 , module3 ....]
#type='SingleRoIExtractor'
self.bbox_head = nn.ModuleList()
#SharedFCBBoxHead * 3 ; 三个字典构成list列表,字典的type一样,但是里面的其他字段不一样
if not isinstance(bbox_roi_extractor, list):
bbox_roi_extractor = [
bbox_roi_extractor for _ in range(num_stages)
# cascade rcnn, 1 stage + 3 stage , 3 include 3 times detection
]
if not isinstance(bbox_head, list): # bbox_head is list, so skip
bbox_head = [bbox_head for _ in range(num_stages)]
assert len(bbox_roi_extractor) == len(bbox_head) == self.num_stages
for roi_extractor, head in zip(bbox_roi_extractor, bbox_head):
self.bbox_roi_extractor.append(
builder.build_roi_extractor(roi_extractor)) # build bbox_roi_extractor
self.bbox_head.append(builder.build_head(head)) # build bbox_head
if mask_head is not None: # 配置文件是cascade rcnn,没有涉及到mask部分,不过mask也是一样的,build都是相同目的
self.mask_head = nn.ModuleList()
if not isinstance(mask_head, list):
mask_head = [mask_head for _ in range(num_stages)]
assert len(mask_head) == self.num_stages
for head in mask_head:
self.mask_head.append(builder.build_head(head)) # build mask_head
if mask_roi_extractor is not None: # 配置文件中也没有 mask_roi_extractor -> None ,所以跳到下面的else部分。
#该部分类似于build.py文件中的build()方法,本质都是build模型,只是对多个字典还是单个字典进行分别处理而已。
self.share_roi_extractor = False
self.mask_roi_extractor = nn.ModuleList()
if not isinstance(mask_roi_extractor, list):
mask_roi_extractor = [
mask_roi_extractor for _ in range(num_stages)
]
assert len(mask_roi_extractor) == self.num_stages
for roi_extractor in mask_roi_extractor:
self.mask_roi_extractor.append(
builder.build_roi_extractor(roi_extractor)) # build mask_roi_extractor
else:
self.share_roi_extractor = True # share_roi_extractor = True
self.mask_roi_extractor = self.bbox_roi_extractor # mask_roi_extractor = bbox_roi_extractor
self.train_cfg = train_cfg # train_cfg字典
self.test_cfg = test_cfg # test_cfg字典
# 以上都是在建模型的过程,换句话说就是将config配置文件中的字典映射成module,将数据进行保存到module的属性中。这些module类都是torch.nn.module的子类。
self.init_weights(pretrained=pretrained) # 初始化detector的权值。
# 初始化权值过程
def init_weights(self, pretrained=None): # pretrained= modelzoo://resnet50
super(CascadeRCNN, self).init_weights(pretrained)
self.backbone.init_weights(pretrained=pretrained) # backbone.init_weights()
if self.with_neck:
if isinstance(self.neck, nn.Sequential): # nn.Sequential ?
for m in self.neck:
m.init_weights() # neck.init_weights()
else:
self.neck.init_weights()
if self.with_rpn: # true
self.rpn_head.init_weights() # rpn_head.init_weights()
if self.with_shared_head:
self.shared_head.init_weights(pretrained=pretrained) # hared_head.init_weights()
for i in range(self.num_stages):
if self.with_bbox:
self.bbox_roi_extractor[i].init_weights()
self.bbox_head[i].init_weights()
if self.with_mask:
if not self.share_roi_extractor:
self.mask_roi_extractor[i].init_weights()
self.mask_head[i].init_weights()
def extract_feat(self, img):
x = self.backbone(img) # 经过backbone的前向计算 提取特征
if self.with_neck: #如果有neck特征处理的话,将提取处的特征,进行对应的特征处理。
x = self.neck(x)
我们上面说,实例化一个module类的时候,会自动执行forward()方法 ,计算结果。
那为什么实例化一个类的时候就可以调用forward?原来是实例化的时候会调用__call__方法,然后在这个方法里面调用forward方法。
在Python中,一个特殊的魔术方法可以让类的实例的行为表现的像函数一样,你可以调用他们,将一个函数当做一个参数传到另外一个函数中等等。这是一个非常强大的特性让Python编程更加舒适甜美。 __call_(self, [args…])
允许一个类的实例像函数一样被调用。实质上说,这意味着 x() 与 x.__call_() 是相同的。注意 _call_ 参数可变。这意味着你可以定义 _call_ 为其他你想要的函数,无论有多少个参数。
然而在本py文件中,并没有forward()方法。
网上说,当继承nn.module时,必须实现forward()方法,那这里为什么没有实现?我查看了其父类BaseDetector,发现,在父类BaseDetector中,实现了forward(),所以子类CascadeRCNN是继承的BaseDetector,而BaseDetector继承nn.module,所以在BaseDetector中实现forward()应该也是可以的,所以调用CascadeRCNN的时候,也就会调用父类的forward()(子类没有重写覆盖父类的forward()方法),在父类BaseDetector的forward()中,调用了forward_train()(其在父类中是抽象方法)。所以,可以理解为,forward_train()的作用就是CascadeRCNN的前向传播计算。
大体上思路:input -> backbone -> neck -> head -> cls and pred
结合以上的思路,我们捋一捋forward()的实现过程:
extract_feat()
;它包含了backbone + neck 两个部分 , 计算了前向的backbone传播和FPN。即调用了self.backbone(img)
和self.neck(x)
。rpn_head(x)
实现。rpn_head(x)
在models/anchor_head/rpn_head.py
中,RPN的目标是得到候选框,所以这里就还要用到anchor_head.py
中的另一个函数get_bboxs()
,该函数在models/anchor_head/anchor_head.py
中,前者是后者的子类。sampler_result
进行训练。主要是调用了bbox_assigner.assign()
和bbox_sampler.sample()
。roi_layers
用的是RoIAlign
(由配置文件可以知道具体用的是什么类型的ROI处理),RoI的结果就可以送到bbox head了。调用的函数是bbox_roi_extractor()
。bbox_head
。configs/cascade_rcnn_r50_fpn_1x.py
的,但是其处理和bbox head是一样的。(bbox_head 输出:bbox_cls + bbox_pred;而mask_head 输出:mask_pred)以上就是下面forward的大致处理过程,里面涉及到很多的函数操作,这里先不抠细节进行详细讲解,后面会花点时间,挨个对各个部分进行详细的代码解读。然后在来对本篇文章不正确的地方进行修改。forward_train()的代码如下:
# 在这里实现层之间的连接关系,其实就是所谓的前向传播(训练过程的前向传播计算 )
# 实现父类的抽象方法 forward_train() ,该方法在父类的forward()中被调用执行 。
def forward_train(self,
img,
img_meta,
gt_bboxes,
gt_labels,
gt_bboxes_ignore=None,
gt_masks=None,
proposals=None):
#提取特征,包含了backbone + neck 两个部分 , 计算了前向的backbone传播和FPN
x = self.extract_feat(img) # 执行extract_feat() 的 forward()
# 从RPN开始有loss了
#开始计算loss, include rpn_loss 、 bbox_loss 、mask_loss
losses = dict()
#rpn输出了一堆候选框
if self.with_rpn:
rpn_outs = self.rpn_head(x) # x 为提取的特征,将特征输入到rpn_head(),进行处理,输出bbox
# tuple可以直接作加法,相当于元组合并
rpn_loss_inputs = rpn_outs + (gt_bboxes, img_meta, #计算rpn_loss时的输入
self.train_cfg.rpn)
rpn_losses = self.rpn_head.loss( #rpn_head.loss() 计算loss
*rpn_loss_inputs, gt_bboxes_ignore=gt_bboxes_ignore)
losses.update(rpn_losses) # 字典的合并方法
proposal_cfg = self.train_cfg.get('rpn_proposal', # proposal_cfg is a dict.
self.test_cfg.rpn)
proposal_inputs = rpn_outs + (img_meta, proposal_cfg) #将RPN输出的box和相关参数信息输入proposal
proposal_list = self.rpn_head.get_bboxes(*proposal_inputs) #获得回归候选框
else:
# 直接指定proposals
proposal_list = proposals
#上一步rpn输出了一堆候选框,但是在将这些候选框拿去训练之前还需要分为正负样本。assigners就是完成这个工作的
for i in range(self.num_stages): # num_stages = 3 。 cascade rcnn 1 stage + 3 stage,三次循环
self.current_stage = i # 3 stage rcnn for detect
rcnn_train_cfg = self.train_cfg.rcnn[i] # 不同stage ,rcnn的参数不一样
lw = self.train_cfg.stage_loss_weights[i] # stage_loss_weights=[1, 0.5, 0.25])
# assign gts and sample proposals 分正负样本,采样候选框 assign() and sample()
sampling_results = []
if self.with_bbox or self.with_mask: # if include bbox or mask -> true
bbox_assigner = build_assigner(rcnn_train_cfg.assigner) # build assigner -> MaxIoUAssigner
bbox_sampler = build_sampler( # build_sampler -> RandomSampler
rcnn_train_cfg.sampler, context=self)
num_imgs = img.size(0) # img.size(0) 估摸着是图片的数量吧
if gt_bboxes_ignore is None:
gt_bboxes_ignore = [None for _ in range(num_imgs)] # 生成 num_imgs 个none值
# start assign and sample (file in max_iou_assigner.py and random_sampler.py)
for j in range(num_imgs):
assign_result = bbox_assigner.assign( #bbox_assigner.assign()
proposal_list[j], gt_bboxes[j], gt_bboxes_ignore[j],
gt_labels[j])
#Sample positive and negative bboxes.
sampling_result = bbox_sampler.sample( #bbox_sampler.sample()
assign_result,
proposal_list[j],
gt_bboxes[j],
gt_labels[j],
feats=[lvl_feat[j][None] for lvl_feat in x])
sampling_results.append(sampling_result) #sample results ( list of proposals bbox )
# ROI_pooling 过程
# bbox head forward and loss
bbox_roi_extractor = self.bbox_roi_extractor[i] # i stage bbox_roi_extractor
bbox_head = self.bbox_head[i]
rois = bbox2roi([res.bboxes for res in sampling_results])
# deal with proposals bbox to roi *** bbox2roi() how to work ?***
bbox_feats = bbox_roi_extractor(x[:bbox_roi_extractor.num_inputs], # x extract_feat 提取的特征
rois)
if self.with_shared_head: #false
bbox_feats = self.shared_head(bbox_feats)
cls_score, bbox_pred = bbox_head(bbox_feats) #bbox_head()处理,分类得分score and 框预测pred
bbox_targets = bbox_head.get_target(sampling_results, gt_bboxes,
gt_labels, rcnn_train_cfg) #获得 gt 框??
loss_bbox = bbox_head.loss(cls_score, bbox_pred, *bbox_targets) #计算 bbox_loss
for name, value in loss_bbox.items():
losses['s{}.{}'.format(i, name)] = (
value * lw if 'loss' in name else value) #lw(loss_weight)=[1, 0.5, 0.25]
#同样mask部分和bbox一样,只是参数不一样,同样也要,ROI_pooling and head --> mask_pred (also have mask_loss)
# mask head forward and loss
if self.with_mask:
if not self.share_roi_extractor: # share_roi_extractor -> None -> = True
mask_roi_extractor = self.mask_roi_extractor[i]
pos_rois = bbox2roi( # bbox2roi(res.pos_bboxes)
[res.pos_bboxes for res in sampling_results])# sampling_results 中的 postive sample ?
mask_feats = mask_roi_extractor(
x[:mask_roi_extractor.num_inputs], pos_rois)
if self.with_shared_head:
mask_feats = self.shared_head(mask_feats)
else:
# reuse positive bbox feats
pos_inds = []
device = bbox_feats.device # ????
for res in sampling_results:
pos_inds.append(
torch.ones( # torch.ones() 返回一个全为1 的张量
res.pos_bboxes.shape[0], # pos_bboxes.shape[0] 定义了输出形状
device=device,
dtype=torch.uint8))
pos_inds.append(
torch.zeros( # zeros
res.neg_bboxes.shape[0], # neg_bboxes.shape[0] 定义了输出形状
device=device,
dtype=torch.uint8))
pos_inds = torch.cat(pos_inds) # 连接操作
mask_feats = bbox_feats[pos_inds] # 此时,bbox中的对象上的值为1,非对象区域(背景)为0
# 这样就生成了 mask 区域 ??
mask_head = self.mask_head[i]
mask_pred = mask_head(mask_feats) # mask_head() 做预测 -> pred
mask_targets = mask_head.get_target(sampling_results, gt_masks,
rcnn_train_cfg)
pos_labels = torch.cat(
[res.pos_gt_labels for res in sampling_results])
loss_mask = mask_head.loss(mask_pred, mask_targets, pos_labels)
for name, value in loss_mask.items():
losses['s{}.{}'.format(i, name)] = (
value * lw if 'loss' in name else value)
# refine bboxes
if i < self.num_stages - 1: # num_stages = 3 , so when stage = 1
pos_is_gts = [res.pos_is_gt for res in sampling_results]
roi_labels = bbox_targets[0] # bbox_targets is a tuple
with torch.no_grad(): # 不需要计算梯度,也不会进行反向传播
proposal_list = bbox_head.refine_bboxes( # refine_bboxes() function???(后续再对其详细的解读)
rois, roi_labels, bbox_pred, pos_is_gts, img_meta)
# for 循环结束
return losses # forward() end
后面还有三个函数,这里先不对其讲解,后面看到这一块的内容时,博主再来对其细化。本篇文章的内容是博主刚刚阅读mmdetection代码后,按照自己的理解做的笔记,如有错误的地方,还请指出,相互学习,共同进步。
mmdetection系列文章: