【Faster R-CNN】5. Faster RCNN程式碼解析第四彈

1. 前言

經過前面三節,我們已經大概上講清楚了如何構造一個完整的 Faster RCNN 模型以及裡面的程式碼實現細節,這一節呢主要來解析一下工程中更外圍一點的東西,即train.pytrainer.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整體流程圖

這張圖把整個 Faster RCNN 的流程都解釋的比較清楚,注意一下圖中出現的Conv(512,512,3,1,1)類似的語句裡面的最後一個參數表示padding

3. 程式碼解析

這一節我們主要是對train.pytrainer.py的程式碼進行解析,我們首先來看trainer.py,這個腳本定義了類FasterRCNNTrainer ,在初始化的時候用到了之前定義的類FasterRCNNVGG16faster_rcnn。 此外在初始化中有引入了其他creator、vis、optimizer等。

另外,還定義了四個損失函數以及一個總的聯合損失函數:rpn_loc_lossrpn_cls_lossroi_loc_lossroi_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 微信:

二維碼