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如何註冊和創建模型的。

其次,瞭解一下torch.nn.module(有pytorch基礎也行,博主剛開始看mmdetection時,沒有pytorch一點基礎,然後看到forward()函數時,找了好幾個文件夾,看他在哪裏調用的…,後面才知道,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系列文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章