【Faster R-CNN】5. Faster RCNN程式碼解析第四彈
1. 前言
經過前面三節,我們已經大概上講清楚了如何構造一個完整的 Faster RCNN 模型以及裡面的程式碼實現細節,這一節呢主要來解析一下工程中更外圍一點的東西,即train.py
和trainer.py
,這將教會我們如何使用已經搭建好的 Faster RCNN 網路。解析程式碼地址為://github.com/BBuf/simple-faster-rcnn-explain 。
2. 回顧
首先從三年一夢這個部落客的部落格裡面看到了一張對 Faster RCNN 全過程總結的圖,地址為://www.cnblogs.com/king-lps/p/8995412.html 。它是針對 Chainner 實現的一個 Faster RCNN 工程所做的流程圖,但我研究了一下過程和本文介紹的陳雲大佬的程式碼流程完全一致,所以在這裡貼一下這張圖,再熟悉一下 Faster RCNN 的整體流程。
這張圖把整個 Faster RCNN 的流程都解釋的比較清楚,注意一下圖中出現的Conv(512,512,3,1,1)
類似的語句裡面的最後一個參數表示padding
。
3. 程式碼解析
這一節我們主要是對train.py
和trainer.py
的程式碼進行解析,我們首先來看trainer.py
,這個腳本定義了類FasterRCNNTrainer ,在初始化的時候用到了之前定義的類FasterRCNNVGG16 為faster_rcnn
。 此外在初始化中有引入了其他creator、vis、optimizer
等。
另外,還定義了四個損失函數以及一個總的聯合損失函數:rpn_loc_loss
、rpn_cls_loss
、roi_loc_loss
、roi_cls_loss
,total_loss
。
首先來看一下FasterRCNNTrainer類的初始化函數:
class FasterRCNNTrainer(nn.Module):
def __init__(self, faster_rcnn):
# 繼承父模組的初始化
super(FasterRCNNTrainer, self).__init__()
self.faster_rcnn = faster_rcnn
# 下面2個參數是在_faster_rcnn_loc_loss調用用來計算位置損失函數用到的超參數
self.rpn_sigma = opt.rpn_sigma
self.roi_sigma = opt.roi_sigma
# target creator create gt_bbox gt_label etc as training targets.
# 用於從20000個候選anchor中產生256個anchor進行二分類和位置回歸,也就是
# 為rpn網路產生的預測位置和預測類別提供真正的ground_truth標準
self.anchor_target_creator = AnchorTargetCreator()
# AnchorTargetCreator和ProposalTargetCreator是為了生成訓練的目標
# (或稱ground truth),只在訓練階段用到,ProposalCreator是RPN為Fast
# R-CNN生成RoIs,在訓練和測試階段都會用到。所以測試階段直接輸進來300
# 個RoIs,而訓練階段會有AnchorTargetCreator的再次干預
self.proposal_target_creator = ProposalTargetCreator()
# (0., 0., 0., 0.)
self.loc_normalize_mean = faster_rcnn.loc_normalize_mean
# (0.1, 0.1, 0.2, 0.2)
self.loc_normalize_std = faster_rcnn.loc_normalize_std
# SGD
self.optimizer = self.faster_rcnn.get_optimizer()
# 可視化,vis_tool.py
self.vis = Visualizer(env=opt.env)
# 混淆矩陣,就是驗證預測值與真實值精確度的矩陣ConfusionMeter
# (2)括弧里的參數指的是類別數
self.rpn_cm = ConfusionMeter(2)
# roi的類別有21種(20個object類+1個background)
self.roi_cm = ConfusionMeter(21)
# 平均損失
self.meters = {k: AverageValueMeter() for k in LossTuple._fields} # average loss
接下來是Forward
函數,因為只支援 batch_size 等於 1 的訓練,因此 n=1。每個 batch 輸入一張圖片,一張圖片上所有的 bbox 及 label,以及圖片經過預處理後的 scale。
然後對於兩個分類損失(RPN 和 ROI Head)都使用了交叉熵損失,而回歸損失則使用了smooth_l1_loss
。
還需要注意的一點是例如 ROI 回歸輸出的是128\times 84,然而真實位置參數是128\times 4和真實標籤128\times 1,我們需要利用真實標籤將回歸輸出索引為128\times 4,然後在計算過程中只計算前景類的回歸損失。具體實現與 Fast-RCNN 略有不同(\sigma設置不同)。
程式碼解析如下:
def forward(self, imgs, bboxes, labels, scale):
# 獲取batch個數
n = bboxes.shape[0]
if n != 1:
raise ValueError('Currently only batch size 1 is supported.')
_, _, H, W = imgs.shape
# (n,c,hh,ww)
img_size = (H, W)
# vgg16 conv5_3之前的部分提取圖片的特徵
features = self.faster_rcnn.extractor(imgs)
# rpn_locs的維度(hh*ww*9,4),rpn_scores維度為(hh*ww*9,2),
# rois的維度為(2000,4),roi_indices用不到,anchor的維度為
# (hh*ww*9,4),H和W是經過數據預處理後的。計算(H/16)x(W/16)x9
# (大概20000)個anchor屬於前景的概率,取前12000個並經過NMS得到2000個
# 近似目標框G^的坐標。roi的維度為(2000,4)
rpn_locs, rpn_scores, rois, roi_indices, anchor = \
self.faster_rcnn.rpn(features, img_size, scale)
# Since batch size is one, convert variables to singular form
# bbox維度(N, R, 4)
bbox = bboxes[0]
# labels維度為(N,R)
label = labels[0]
#hh*ww*9
rpn_score = rpn_scores[0]
# hh*ww*9
rpn_loc = rpn_locs[0]
# (2000,4)
roi = rois
# Sample RoIs and forward
# 調用proposal_target_creator函數生成sample roi(128,4)、
# gt_roi_loc(128,4)、gt_roi_label(128,1),RoIHead網路
# 利用這sample_roi+featue為輸入,輸出是分類(21類)和回歸
# (進一步微調bbox)的預測值,那麼分類回歸的groud truth就
# 是ProposalTargetCreator輸出的gt_roi_label和gt_roi_loc。
sample_roi, gt_roi_loc, gt_roi_label = self.proposal_target_creator(
roi,
at.tonumpy(bbox),
at.tonumpy(label),
self.loc_normalize_mean,
self.loc_normalize_std)
# NOTE it's all zero because now it only support for batch=1 now
sample_roi_index = t.zeros(len(sample_roi))
# roi回歸輸出的是128*84和128*21,然而真實位置參數是128*4和真實標籤128*1
roi_cls_loc, roi_score = self.faster_rcnn.head(
features,
sample_roi,
sample_roi_index)
# ------------------ RPN losses -------------------#
# 輸入20000個anchor和bbox,調用anchor_target_creator函數得到
# 2000個anchor與bbox的偏移量與label
gt_rpn_loc, gt_rpn_label = self.anchor_target_creator(
at.tonumpy(bbox),
anchor,
img_size)
gt_rpn_label = at.totensor(gt_rpn_label).long()
gt_rpn_loc = at.totensor(gt_rpn_loc)
# 下面分析_fast_rcnn_loc_loss函數。rpn_loc為rpn網路回歸出來的偏移量
# (20000個),gt_rpn_loc為anchor_target_creator函數得到2000個anchor
# 與bbox的偏移量,rpn_sigma=1.
rpn_loc_loss = _fast_rcnn_loc_loss(
rpn_loc,
gt_rpn_loc,
gt_rpn_label.data,
self.rpn_sigma)
# NOTE: default value of ignore_index is -100 ...
# rpn_score為rpn網路得到的(20000個)與anchor_target_creator
# 得到的2000個label求交叉熵損失
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label.cuda(), ignore_index=-1)
_gt_rpn_label = gt_rpn_label[gt_rpn_label > -1] #不計算背景類
_rpn_score = at.tonumpy(rpn_score)[at.tonumpy(gt_rpn_label) > -1]
self.rpn_cm.add(at.totensor(_rpn_score, False), _gt_rpn_label.data.long())
# ------------------ ROI losses (fast rcnn loss) -------------------#
# roi_cls_loc為VGG16RoIHead的輸出(128*84), n_sample=128
n_sample = roi_cls_loc.shape[0]
# roi_cls_loc=(128,21,4)
roi_cls_loc = roi_cls_loc.view(n_sample, -1, 4)
roi_loc = roi_cls_loc[t.arange(0, n_sample).long().cuda(), \
at.totensor(gt_roi_label).long()]
# proposal_target_creator()生成的128個proposal與bbox求得的偏移量
# dx,dy,dw,dh
gt_roi_label = at.totensor(gt_roi_label).long()
# 128個標籤
gt_roi_loc = at.totensor(gt_roi_loc)
# 採用smooth_l1_loss
roi_loc_loss = _fast_rcnn_loc_loss(
roi_loc.contiguous(),
gt_roi_loc,
gt_roi_label.data,
self.roi_sigma)
# 求交叉熵損失
roi_cls_loss = nn.CrossEntropyLoss()(roi_score, gt_roi_label.cuda())
self.roi_cm.add(at.totensor(roi_score, False), gt_roi_label.data.long())
# 四個loss加起來
losses = [rpn_loc_loss, rpn_cls_loss, roi_loc_loss, roi_cls_loss]
losses = losses + [sum(losses)]
return LossTuple(*losses)
下面我們來解析一下程式碼中的_fast_rcnn_loc_loss
函數,它用到了 smooth_l1_loss。其中in_weight
代表權重,只將那些不是背景的 Anchor/ROIs 的位置放入到損失函數的計算中來,方法就是只給不是背景的 Anchor/ROIs 的in_weight
設置為 1,這樣就可以完成loc_loss
的求和計算。
程式碼解析如下:
# 輸入分別為rpn回歸框的偏移量和anchor與bbox的偏移量以及label
def _fast_rcnn_loc_loss(pred_loc, gt_loc, gt_label, sigma):
in_weight = t.zeros(gt_loc.shape).cuda()
# Localization loss is calculated only for positive rois.
# NOTE: unlike origin implementation,
# we don't need inside_weight and outside_weight, they can calculate by gt_label
in_weight[(gt_label > 0).view(-1, 1).expand_as(in_weight).cuda()] = 1
# sigma設置為1
loc_loss = _smooth_l1_loss(pred_loc, gt_loc, in_weight.detach(), sigma)
# Normalize by total number of negtive and positive rois.
# 除去背景類
loc_loss /= ((gt_label >= 0).sum().float()) # ignore gt_label==-1 for rpn_loss
return loc_loss
接下來就是train_step
函數,整個函數實際上就是進行了一次參數的優化過程,首先self.optimizer.zero_grad()
將梯度數據全部清零,然後利用剛剛介紹self.forward(imgs,bboxes,labels,scales)
函數將所有的損失計算出來,接著依次進行losses.total_loss.backward()
反向傳播計算梯度,self.optimizer.step()
進行一次參數更新過程,self.update_meters(losses)
就是將所有損失的數據更新到可視化介面上,最後將losses
返回。程式碼如下:
def train_step(self, imgs, bboxes, labels, scale):
self.optimizer.zero_grad()
losses = self.forward(imgs, bboxes, labels, scale)
losses.total_loss.backward()
self.optimizer.step()
self.update_meters(losses)
return losses
接下來還有一些函數比如save()
,load()
,update_meters()
,reset_meters()
,get_meter_data()
等。其中save()
和load()
就是根據輸入參數來選擇保存和解析model
模型或者config
設置或者other_info
其他vis_info
可視化參數等等,程式碼如下:
# 模型保存
def save(self, save_optimizer=False, save_path=None, **kwargs):
save_dict = dict()
save_dict['model'] = self.faster_rcnn.state_dict()
save_dict['config'] = opt._state_dict()
save_dict['other_info'] = kwargs
save_dict['vis_info'] = self.vis.state_dict()
if save_optimizer:
save_dict['optimizer'] = self.optimizer.state_dict()
if save_path is None:
timestr = time.strftime('%m%d%H%M')
save_path = 'checkpoints/fasterrcnn_%s' % timestr
for k_, v_ in kwargs.items():
save_path += '_%s' % v_
save_dir = os.path.dirname(save_path)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
t.save(save_dict, save_path)
self.vis.save([self.vis.env])
return save_path
# 模型載入
def load(self, path, load_optimizer=True, parse_opt=False, ):
state_dict = t.load(path)
if 'model' in state_dict:
self.faster_rcnn.load_state_dict(state_dict['model'])
else: # legacy way, for backward compatibility
self.faster_rcnn.load_state_dict(state_dict)
return self
if parse_opt:
opt._parse(state_dict['config'])
if 'optimizer' in state_dict and load_optimizer:
self.optimizer.load_state_dict(state_dict['optimizer'])
return self
而update_meters
,reset_meters
以及get_meter_data()
就是負責將數據向可視化介面更新傳輸獲取以及重置的函數。
OK,trainer.py
大概就解析到這裡,接下來我們來看看train.py
,詳細解釋如下:
def train(**kwargs):
# opt._parse(kwargs)#將調用函數時候附加的參數用,
# config.py文件裡面的opt._parse()進行解釋,然後
# 獲取其數據存儲的路徑,之後放到Dataset裡面!
opt._parse(kwargs)
dataset = Dataset(opt)
print('load data')
# #Dataset完成的任務見第二次推文數據預處理部分,
# 這裡簡單解釋一下,就是用VOCBboxDataset作為數據
# 集,然後依次從樣例資料庫中讀取圖片出來,還調用了
# Transform(object)函數,完成影像的調整和隨機翻轉工作
dataloader = data_.DataLoader(dataset, \
batch_size=1, \
shuffle=True, \
# pin_memory=True,
num_workers=opt.num_workers)
testset = TestDataset(opt)
# 將數據裝載到dataloader中,shuffle=True允許數據打亂排序,
# num_workers是設置數據分為幾批處理,同樣的將測試數據集也
# 進行同樣的處理,然後裝載到test_dataloader中
test_dataloader = data_.DataLoader(testset,
batch_size=1,
num_workers=opt.test_num_workers,
shuffle=False, \
pin_memory=True
)
# 定義faster_rcnn=FasterRCNNVGG16()訓練模型
faster_rcnn = FasterRCNNVGG16()
print('model construct completed')
# 設置trainer = FasterRCNNTrainer(faster_rcnn).cuda()將
# FasterRCNNVGG16作為fasterrcnn的模型送入到FasterRCNNTrainer
# 中並設置好GPU加速
trainer = FasterRCNNTrainer(faster_rcnn).cuda()
if opt.load_path:
trainer.load(opt.load_path)
print('load pretrained model from %s' % opt.load_path)
trainer.vis.text(dataset.db.label_names, win='labels')
best_map = 0
lr_ = opt.lr
# 用一個for循環開始訓練過程,而訓練迭代的次數
# opt.epoch=14也在config.py文件中預先定義好,屬於超參數
for epoch in range(opt.epoch):
# 首先在可視化介面重設所有數據
trainer.reset_meters()
for ii, (img, bbox_, label_, scale) in tqdm(enumerate(dataloader)):
scale = at.scalar(scale)
# 然後從訓練數據中枚舉dataloader,設置好縮放範圍,
# 將img,bbox,label,scale全部設置為可gpu加速
img, bbox, label = img.cuda().float(), bbox_.cuda(), label_.cuda()
# 調用trainer.py中的函數trainer.train_step
# (img,bbox,label,scale)進行一次參數迭代優化過程
trainer.train_step(img, bbox, label, scale)
# 判斷數據讀取次數是否能夠整除plot_every
# (是否達到了畫圖次數),如果達到判斷debug_file是否存在,
# 用ipdb工具設置斷點,調用trainer中的trainer.vis.
# plot_many(trainer.get_meter_data())將訓練數據讀取並
# 上傳完成可視化
if (ii + 1) % opt.plot_every == 0:
if os.path.exists(opt.debug_file):
ipdb.set_trace()
# plot loss
trainer.vis.plot_many(trainer.get_meter_data())
# plot groud truth bboxes
ori_img_ = inverse_normalize(at.tonumpy(img[0]))
gt_img = visdom_bbox(ori_img_,
at.tonumpy(bbox_[0]),
at.tonumpy(label_[0]))
# 將每次迭代讀取的圖片用dataset文件裡面的inverse_normalize()
# 函數進行預處理,將處理後的圖片調用Visdom_bbox可視化
trainer.vis.img('gt_img', gt_img)
# plot predicti bboxes
# 調用faster_rcnn的predict函數進行預測,
# 預測的結果保留在以_下劃線開頭的對象裡面
_bboxes, _labels, _scores = trainer.faster_rcnn.predict([ori_img_], visualize=True)
pred_img = visdom_bbox(ori_img_,
at.tonumpy(_bboxes[0]),
at.tonumpy(_labels[0]).reshape(-1),
at.tonumpy(_scores[0]))
# 利用同樣的方法將原始圖片以及邊框類別的
# 預測結果同樣在可視化工具中顯示出來
trainer.vis.img('pred_img', pred_img)
# rpn confusion matrix(meter)
# 調用trainer.vis.text將rpn_cm也就是
# RPN網路的混淆矩陣在可視化工具中顯示出來
trainer.vis.text(str(trainer.rpn_cm.value().tolist()), win='rpn_cm')
# roi confusion matrix
# 可視化ROI head的混淆矩陣
trainer.vis.img('roi_cm', at.totensor(trainer.roi_cm.conf, False).float())
# 調用eval函數計算map等指標
eval_result = eval(test_dataloader, faster_rcnn, test_num=opt.test_num)
# 可視化map
trainer.vis.plot('test_map', eval_result['map'])
# 設置學習的learning rate
lr_ = trainer.faster_rcnn.optimizer.param_groups[0]['lr']
log_info = 'lr:{}, map:{},loss:{}'.format(str(lr_),
str(eval_result['map']),
str(trainer.get_meter_data()))
# 將損失學習率以及map等資訊及時顯示更新
trainer.vis.log(log_info)
# 用if判斷語句永遠保存效果最好的map
if eval_result['map'] > best_map:
best_map = eval_result['map']
best_path = trainer.save(best_map=best_map)
if epoch == 9:
# if判斷語句如果學習的epoch達到了9就將學習率*0.1
# 變成原來的十分之一
trainer.load(best_path)
trainer.faster_rcnn.scale_lr(opt.lr_decay)
lr_ = lr_ * opt.lr_decay
# 判斷epoch==13結束訓練驗證過程
if epoch == 13:
break
在train.py
裡面還有一個函數為eval()
,具體解釋如下:
def eval(dataloader, faster_rcnn, test_num=10000):
# 預測框的位置,預測框的類別和分數
pred_bboxes, pred_labels, pred_scores = list(), list(), list()
# 真實框的位置,類別,是否為明顯目標
gt_bboxes, gt_labels, gt_difficults = list(), list(), list()
# 一個for循環,從 enumerate(dataloader)裡面依次讀取數據,
# 讀取的內容是: imgs圖片,sizes尺寸,gt_boxes真實框的位置
# gt_labels真實框的類別以及gt_difficults
for ii, (imgs, sizes, gt_bboxes_, gt_labels_, gt_difficults_) in tqdm(enumerate(dataloader)):
sizes = [sizes[0][0].item(), sizes[1][0].item()]
# 用faster_rcnn.predict(imgs,[sizes]) 得出預測的pred_boxes_,
# pred_labels_,pred_scores_預測框位置,預測框標記以及預測框
# 的分數等等
pred_bboxes_, pred_labels_, pred_scores_ = faster_rcnn.predict(imgs, [sizes])
gt_bboxes += list(gt_bboxes_.numpy())
gt_labels += list(gt_labels_.numpy())
gt_difficults += list(gt_difficults_.numpy())
pred_bboxes += pred_bboxes_
pred_labels += pred_labels_
pred_scores += pred_scores_
if ii == test_num: break
# 將pred_bbox,pred_label,pred_score ,gt_bbox,gt_label,gt_difficult
# 預測和真實的值全部依次添加到開始定義好的列表裡面去,如果迭代次數等於測
# 試test_num,那麼就跳出循環!調用 eval_detection_voc函數,接收上述的
# 六個列表參數,完成預測水平的評估!得到預測的結果
result = eval_detection_voc(
pred_bboxes, pred_labels, pred_scores,
gt_bboxes, gt_labels, gt_difficults,
use_07_metric=True)
return result
關於如何計算 map 我就不再贅述了,感興趣可以去看我這篇推文,自認為寫的是很清楚的,也有源碼解釋:目標檢測演算法之常見評價指標(mAP)的詳細計算方法及程式碼解析 。
4. 總結
今天是 5/5 號,也是五一的最後一天假期,算是完成了對 Faster RCNN 程式碼的全部解讀,另外不久後我也修改一些內容並將整理一個 PDF 版本(包括 NMS 和 mAP 的計算也準備放到 PDF 里),並且目前所有的程式碼注釋都放在了這個 GitHub 工程://github.com/BBuf/simple-faster-rcnn-explain 。
5. 參考
歡迎關注 GiantPandaCV, 在這裡你將看到獨家的深度學習分享,堅持原創,每天分享我們學習到的新鮮知識。( • ̀ ω•́ )✧
有對文章相關的問題,或者想要加入交流群,歡迎添加 BBuf 微信: