mmdetection源码笔记(二):创建网络模型之cascade_rcnn.py的解读(中)

引言:

cascade_rcnn.py文件在moels/detections文件夹下。本次对文件cascade_rcnn.py的代码解读,是根据py配置文件configs/cascade_rcnn_r50_fpn_1x.py的数据信息进行讲解的。

moels/detectionscascade_rcnn.py文件中

主要的内容如下:

  • __init__() :module的构造函数。
  • init_weights() :backbone为cascade rcnn的初始化权重方法,在__init__()调用进行初始化。
  • extract_feat() :提取img特征,主要实现了backbone和neck的forward()的前向计算。
  • forward_train() :在这里实现层之间的连接关系,其实就是所谓的前向传播。当执行model(x)(该model为module的子类)的时候,底层自动调用forward方法计算结果。
  • simple_test() :检测过程的前向传播forward调用的函数,通过最原始的nn.Module到父类BaseDetector的forward,继续由底层 ,层层向上调用到这里。
  • aug_test() :Test with augmentations。
  • show_result() :

共七个部分,本篇文章主要对前四个部分的代码精度,这四个步骤中的__init__()forward_train()是module类的最主要的两个部分,也是定义网络的最关键的部分。
自定义一个模型就是通过继承nn.Module类来实现,在__init__()构造函数中申明各个层的定义,在forward()中实现层之间的连接关系,实际上就是前向传播的过程。

注:后面三个部分,博主后续会继续阅读代码,在对这三个部分进行补充。

首先,看本篇讲解时,先了解一下下篇文章,该文章讲解了创建模型的过程,尤其以detection为例,讲解了mmdetection通过注册表的形式,实例化了类名为DETECTION的Rigistry类,并且在其module_dict属性中,保存了detection的module类,和其对应的类名。通过这篇文章,可以了解mmdetection如何注册和创建模型的。

  • mmdetection源码笔记(二):创建网络模型之registry.py和builder.py解读(上)

其次,了解一下torch.nn.module(有pytorch基础也行,博主刚开始看mmdetection时,没有pytorch一点基础,然后看到forward()函数时,找了好几个文件夹,看他在哪里调用的…,后面才知道,forward()是自定义层的前向计算,自动执行的( 也就是对输入自动进行处理)),推荐下篇文章:

  • PyTorch之前向传播函数forward

__init__()

@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的权值。

init_weights()

# 初始化权值过程
    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()

extract_feat()

 	def extract_feat(self, img):
        x = self.backbone(img)  # 经过backbone的前向计算  提取特征
        if self.with_neck:      #如果有neck特征处理的话,将提取处的特征,进行对应的特征处理。
            x = self.neck(x)  

forward_train()

我们上面说,实例化一个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中,前者是后者的子类。
  • 提取框框后,直接送入训练?不行,上一步rpn输出了一堆候选框,但是在将这些候选框拿去训练之前还需要分为正负样本。assigners就是完成这个工作的。将proposal分为正负样本过后,通过sampler对这些proposal进行采样得到sampler_result进行训练。主要是调用了bbox_assigner.assign()bbox_sampler.sample()
  • 现在bbox已经处理好了,当然得到的那些框还不能直接送到bbox head,在此之前还要做一次RoI Pooling,将不同大小的框映射成固定大小。roi_layers用的是RoIAlign(由配置文件可以知道具体用的是什么类型的ROI处理),RoI的结果就可以送到bbox head了。调用的函数是bbox_roi_extractor()
  • bbox head部分和之前的rpn部分的操作差不多,主要是针对每个框进行分类和坐标修正。之前rpn分为前景和背景两类,这里分为N+1类(实际类别 + 背景)。调用的是bbox_head
  • mask_head部分这里没有将,因为主要是依据配置文件configs/cascade_rcnn_r50_fpn_1x.py的,但是其处理和bbox head是一样的。(bbox_head 输出:bbox_cls + bbox_pred;而mask_head 输出:mask_pred)
  • 最最最重要的是loss的计算,它从RPN阶段,就开始有loss了。

以上就是下面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系列文章:

  • mmdetection源码笔记(一):train.py解读
  • mmdetection源码笔记(二):创建网络模型之registry.py和builder.py解读(上)
  • mmdetection源码笔记(二):cascade_rcnn.py搭建模型过程中各个module的forward()的代码解读(下)(待完成)
  • mmdetection源码笔记(三):创建数据集模型之datasets/coco.py的解读(上)
  • mmdetection源码笔记(三):创建数据集模型之datasets/custom.py的解读(下)
  • mmdetection源码笔记(四):训练模型之train_detector()的解读
  • mmdetection源码笔记(五):测试之test.py的解读

你可能感兴趣的:(mmdetection源码笔记)