引言:
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系列文章:
- mmdetection源碼筆記(一):train.py解讀
- mmdetection源碼筆記(二):創建網絡模型之registry.py和builder.py解讀(上)
- mmdetection源碼筆記(二):cascade_rcnn.py搭建模型過程中各個module的forward()的代碼解讀(下)(待完成)
- mmdetection源碼筆記(三):創建數據集模型之datasets/coco.py和的解讀datasets/custom.py(未完結)
- mmdetection源碼筆記(四):訓練模型之train_detector()的解讀(待完成)
- mmdetection源碼筆記(五):測試之test()部分的解讀(待完成)