YOLO v2 原理總結

論文地址:YOLO9000: Better, Faster, Stronger 

預測更準確(Better)

1) Batch Normalization

       CNN在訓練過程中網絡每層輸入的分佈一直在改變, 會使訓練過程難度加大,但可以通過normalize每層的輸入解決這個問題。YOLO v2在每一個卷積層後添加batch normalization,通過這一方法,mAP獲得了2%的提升。batch normalization 也有助於規範化模型,可以在捨棄dropout優化後依然不會過擬合。

2)High Resolution Classifier 使用高分辨率圖像微調分類模型

mAP提升了3.7。

圖像分類的訓練樣本很多,而標註了邊框的用於訓練對象檢測的樣本相比而言就比較少了,因爲標註邊框的人工成本比較高。所以對象檢測模型通常都先用圖像分類樣本訓練卷積層,提取圖像特徵。但這引出的另一個問題是,圖像分類樣本的分辨率不是很高。所以YOLO v1使用ImageNet的圖像分類樣本採用 224*224 作爲輸入,來訓練CNN卷積層。然後在訓練對象檢測時,檢測用的圖像樣本採用更高分辨率的 448*448 的圖像作爲輸入。但這樣切換對模型性能有一定影響。

所以YOLO2在採用 224*224 圖像進行分類模型預訓練後,再採用 448*448 的高分辨率樣本對分類模型進行微調(10個epoch),使網絡特徵逐漸適應 448*448 的分辨率。然後再使用 448*448 的檢測樣本進行訓練,緩解了分辨率突然切換造成的影響。

3) Convolutional With Anchor Boxes

借鑑Faster RCNN的做法,YOLO2也嘗試採用先驗框(anchor)。在每個grid預先設定一組不同大小和寬高比的邊框,來覆蓋整個圖像的不同位置和多種尺度,這些先驗框作爲預定義的候選區在神經網絡中將檢測其中是否存在對象,以及微調邊框的位置。  

爲了引入anchor boxes來預測bounding boxes,作者使用了一下幾個技術手段:

(1)在網絡中果斷去掉了全連接層。

(2)去掉卷積網絡部分的最後一個池化層。這是爲了確保輸出的卷積特徵圖有更高的分辨率;

(3)縮減網絡輸入。讓圖片輸入分辨率從448*448降爲416 * 416,這一步的目的是爲了讓後面產生的卷積特徵圖寬、高都爲奇數,這樣就可以產生一個center cell。

加入了anchor boxes後,可以預料到的結果是召回率上升,準確率輕微下降。

4) Dimension Clusters (聚類提取先驗框尺度)

聚類提取先驗框尺度,結合下面的約束預測邊框的位置,使得mAP有4.8的提升。

之前先驗框都是手工設定的,YOLO2嘗試統計出更符合樣本中對象尺寸的先驗框,這樣就可以減少網絡微調先驗框到實際位置的難度。YOLO2的做法是對訓練集中標註的邊框進行聚類分析,以尋找儘可能匹配樣本的邊框尺寸。

聚類算法最重要的是選擇如何計算兩個邊框之間的“距離”,對於常用的歐式距離,大邊框會產生更大的誤差,但我們關心的是邊框的IOU。所以,YOLO2在聚類時採用以下公式來計算兩個邊框之間的“距離”。

centroid是聚類時被選作中心的邊框,box就是其它邊框,d就是兩者間的“距離”。IOU越大,“距離”越近。

5) Direct location prediction(約束預測邊框的位置)

       借鑑於Faster RCNN的先驗框方法,在訓練的早期階段,其位置預測容易不穩定。其位置預測公式爲:

其中, 是預測邊框的中心,是先驗框(anchor)的中心點座標,  是先驗框(anchor)的寬和高,是要學習的參數。 注意,YOLO論文中寫的是 ,根據Faster RCNN,應該是"+"。

由於 的取值沒有任何約束,因此預測邊框的中心可能出現在任何位置,訓練早期階段不容易穩定。

YOLO調整了預測公式,將預測邊框的中心約束在特定gird網格內。

其中, 是預測邊框的中心和寬高。是預測邊框的置信度,YOLO1是直接預測置信度的值,這裏對預測參數 進行σ變換後作爲置信度的值。是當前網格左上角到圖像左上角的距離,要先將網格大小歸一化,即令一個網格的寬=1,高=1。 是先驗框的寬和高。 σ是sigmoid函數。  是要學習的參數,分別用於預測邊框的中心和寬高,以及置信度。

參考上圖,由於σ函數將約束在(0,1)範圍內,所以根據上面的計算公式,預測邊框的藍色中心點被約束在藍色背景的網格內。約束邊框位置使得模型更容易學習,且預測更爲穩定。

6)passthrough層檢測細粒度特徵

passthrough層檢測細粒度特徵使mAP提升1。

對象檢測面臨的一個問題是圖像中對象會有大有小,輸入圖像經過多層網絡提取特徵,最後輸出的特徵圖中(比如YOLO2中輸入416*416經過卷積網絡下采樣最後輸出是13*13),較小的對象可能特徵已經不明顯甚至被忽略掉了。爲了更好的檢測出一些比較小的對象,最後輸出的特徵圖需要保留一些更細節的信息。

YOLO2引入一種稱爲passthrough層的方法在特徵圖中保留一些細節信息。具體來說,就是在最後一個pooling之前,特徵圖的大小是26*26*512,將其1拆4,直接傳遞(passthrough)到pooling後(並且又經過一組卷積)的特徵圖,兩者疊加到一起作爲輸出的特徵圖。

具體怎樣1拆4,圖中示例的是1個4*4拆成4個2*2。因爲深度不變,所以沒有畫出來。

另外,根據YOLO2的代碼,特徵圖先用1*1卷積從 26*26*512 降維到 26*26*64,再做1拆4並passthrough。

7)多尺度圖像訓練

多尺度圖像訓練對mAP有1.4的提升。

因爲去掉了全連接層,YOLO2可以輸入任何尺寸的圖像。因爲整個網絡下采樣倍數是32,作者採用了{320,352,...,608}等10種輸入圖像的尺寸,這些尺寸的輸入圖像對應輸出的特徵圖寬和高是{10,11,...19}。訓練時每10個batch就隨機更換一種尺寸,使網絡能夠適應各種大小的對象檢測。

速度更快(Faster)

爲了進一步提升速度,YOLO2提出了Darknet-19(有19個卷積層和5個MaxPooling層)網絡結構。DarkNet-19比VGG-16小一些,精度不弱於VGG-16,但浮點運算量減少到約1/5,以保證更快的運算速度。

YOLO2的訓練主要包括三個階段。第一階段就是先在ImageNet分類數據集上預訓練Darknet-19,此時模型輸入爲 224*224 ,共訓練160個epochs。然後第二階段將網絡的輸入調整爲 448*448 ,繼續在ImageNet數據集上finetune分類模型,訓練10個epochs,此時分類模型的top-1準確度爲76.5%,而top-5準確度爲93.3%。第三個階段就是修改Darknet-19分類模型爲檢測模型,移除最後一個卷積層、global avgpooling層以及softmax層,並且新增了三個 3*3*1024卷積層,同時增加了一個passthrough層,最後使用 1*1 卷積層輸出預測結果,輸出的channels數爲:num_anchors*(5+num_classes) ,和訓練採用的數據集有關係。由於anchors數爲5,對於VOC數據集(20種分類對象)輸出的channels數就是125,最終的預測矩陣T的shape爲 (batch_size, 13, 13, 125),可以先將其reshape爲 (batch_size, 13, 13, 5, 25) ,其中 T[:, :, :, :, 0:4] 爲邊界框的位置和大小 ,T[:, :, :, :, 4] 爲邊界框的置信度,而 T[:, :, :, :, 5:] 爲類別預測值。

對象檢測模型各層的結構如下:

看一下passthrough層。圖中第25層route 16,意思是來自16層的output,即26*26*512,這是passthrough層的來源(細粒度特徵)。第26層1*1卷積降低通道數,從512降低到64(這一點論文在討論passthrough的時候沒有提到),輸出26*26*64。第27層進行拆分(passthrough層)操作,1拆4分成13*13*256。第28層疊加27層和24層的輸出,得到13*13*1280。後面再經過3*3卷積和1*1卷積,最後輸出13*13*125。

 

YOLO2 輸入->輸出

綜上所述,雖然YOLO2做出了一些改進,但總的來說網絡結構依然很簡單,就是一些卷積+pooling,從416*416*3 變換到 13*13*5*25。稍微大一點的變化是:

  • 增加了batch normalization
  • 增加了一個passthrough層
  • 去掉了全連接層
  • 採用了5個先驗框

對比YOLO1的輸出張量,YOLO2的主要變化就是會輸出5個先驗框,且每個先驗框都會嘗試預測一個對象。輸出的 13*13*5*25 張量中,25維向量包含 20個對象的分類概率+4個邊框座標+1個邊框置信度。

 

YOLO2 誤差函數

誤差依然包括邊框位置誤差、置信度誤差、對象分類誤差。

公式中:

意思是預測邊框中,與真實對象邊框IOU最大的那個,其IOU<閾值Thresh,此係數爲1,即計入誤差,否則爲0,不計入誤差。YOLO2使用Thresh=0.6。

意思是前128000次迭代計入誤差。注意這裏是與先驗框的誤差,而不是與真實對象邊框的誤差。可能是爲了在訓練早期使模型更快學會先預測先驗框的位置。

意思是該邊框負責預測一個真實對象(邊框內有對象)。

各種 是不同類型誤差的調節係數。

import math
import torch
import torch.nn as nn


class YoloLoss(nn.modules.loss._Loss):
    # The loss I borrow from LightNet repo.
    def __init__(self, num_classes, anchors, reduction=32, coord_scale=1.0, noobject_scale=1.0,
                 object_scale=5.0, class_scale=1.0, thresh=0.6):
        super(YoloLoss, self).__init__()
        self.num_classes = num_classes
        self.num_anchors = len(anchors)
        self.anchor_step = len(anchors[0])
        self.anchors = torch.Tensor(anchors)
        self.reduction = reduction

        self.coord_scale = coord_scale
        self.noobject_scale = noobject_scale
        self.object_scale = object_scale
        self.class_scale = class_scale
        self.thresh = thresh

    def forward(self, output, target):
        """
        :param output:  [batch_size, n_anchor_box*(n_classes+5), grid_size, grid_size]
        :param target: 長度爲batch_size的list,元素的shape爲[n_targetbox, 5]
        :return:
        """
        batch_size = output.data.size(0)
        height = output.data.size(2)
        width = output.data.size(3)

        # Get x,y,w,h,conf,cls
        output = output.view(batch_size, self.num_anchors, -1, height * width) # [batch_size, n_anchor_box, n_class+5, grid_size*grid_size]
        coord = torch.zeros_like(output[:, :, :4, :]) # [batch_size, n_anchor_box, 4, grid_size*grid_size]
        coord[:, :, :2, :] = output[:, :, :2, :].sigmoid()  
        coord[:, :, 2:4, :] = output[:, :, 2:4, :]
        conf = output[:, :, 4, :].sigmoid() # [batch_size, n_anchor_box, grid_size*grid_size]
        cls = output[:, :, 5:, :].contiguous().view(batch_size * self.num_anchors, self.num_classes,   # [batch_size*n_anchor_box*grid_size*grid_size, n_classes]
                                                    height * width).transpose(1, 2).contiguous().view(-1,
                                                                                                      self.num_classes)

        # Create prediction boxes
        pred_boxes = torch.FloatTensor(batch_size * self.num_anchors * height * width, 4) # [batch_size*n_anchor_box*grid_size*grid_size, 4]
        lin_x = torch.range(0, width - 1).repeat(height, 1).view(height * width) # [grid_size*grid_size]
        lin_y = torch.range(0, height - 1).repeat(width, 1).t().contiguous().view(height * width) # [grid_size*grid_size]
        anchor_w = self.anchors[:, 0].contiguous().view(self.num_anchors, 1)  # [n_anchor_box, 1]
        anchor_h = self.anchors[:, 1].contiguous().view(self.num_anchors, 1)  # [n_anchor_box, 1]

        if torch.cuda.is_available():
            pred_boxes = pred_boxes.cuda()
            lin_x = lin_x.cuda()
            lin_y = lin_y.cuda()
            anchor_w = anchor_w.cuda()
            anchor_h = anchor_h.cuda()

        pred_boxes[:, 0] = (coord[:, :, 0].detach() + lin_x).view(-1)
        pred_boxes[:, 1] = (coord[:, :, 1].detach() + lin_y).view(-1)
        pred_boxes[:, 2] = (coord[:, :, 2].detach().exp() * anchor_w).view(-1)
        pred_boxes[:, 3] = (coord[:, :, 3].detach().exp() * anchor_h).view(-1)
        pred_boxes = pred_boxes.cpu()  # [batch_size*n_anchor_box*grid_size*grid_size, 4]

        # Get target values
        coord_mask, conf_mask, cls_mask, tcoord, tconf, tcls = self.build_targets(pred_boxes, target, height, width)
        coord_mask = coord_mask.expand_as(tcoord) # [batch_size, n_anchor_box, 4, grid_size*grid_size]
        tcls = tcls[cls_mask].view(-1).long()
        cls_mask = cls_mask.view(-1, 1).repeat(1, self.num_classes)

        if torch.cuda.is_available():
            tcoord = tcoord.cuda()
            tconf = tconf.cuda()
            coord_mask = coord_mask.cuda()
            conf_mask = conf_mask.cuda()
            tcls = tcls.cuda()
            cls_mask = cls_mask.cuda()

        conf_mask = conf_mask.sqrt()
        cls = cls[cls_mask].view(-1, self.num_classes)

        # Compute losses
        mse = nn.MSELoss(size_average=False)
        ce = nn.CrossEntropyLoss(size_average=False)
        self.loss_coord = self.coord_scale * mse(coord * coord_mask, tcoord * coord_mask) / batch_size
        self.loss_conf = mse(conf * conf_mask, tconf * conf_mask) / batch_size
        self.loss_cls = self.class_scale * 2 * ce(cls, tcls) / batch_size
        self.loss_tot = self.loss_coord + self.loss_conf + self.loss_cls

        return self.loss_tot, self.loss_coord, self.loss_conf, self.loss_cls

    def build_targets(self, pred_boxes, ground_truth, height, width):
        """

        :param pred_boxes: [batch_size*grid_size*grid_size*n_anchorbox, 4]
        :param ground_truth: 長度爲batch_size的list,元素的shape爲[n_targetbox, 5]
        :param height: grid_size
        :param width: grid_size
        :return:
        """
        batch_size = len(ground_truth)

        conf_mask = torch.ones(batch_size, self.num_anchors, height * width, requires_grad=False) * self.noobject_scale
        coord_mask = torch.zeros(batch_size, self.num_anchors, 1, height * width, requires_grad=False)
        cls_mask = torch.zeros(batch_size, self.num_anchors, height * width, requires_grad=False).byte()
        tcoord = torch.zeros(batch_size, self.num_anchors, 4, height * width, requires_grad=False)
        tconf = torch.zeros(batch_size, self.num_anchors, height * width, requires_grad=False)
        tcls = torch.zeros(batch_size, self.num_anchors, height * width, requires_grad=False)

        for b in range(batch_size):
            if len(ground_truth[b]) == 0:
                continue

            # Build up tensors
            cur_pred_boxes = pred_boxes[
                             b * (self.num_anchors * height * width):(b + 1) * (self.num_anchors * height * width)] # [n_anchorbox*grid_size*grid_size, 4]
            if self.anchor_step == 4:
                anchors = self.anchors.clone()
                anchors[:, :2] = 0
            else:
                anchors = torch.cat([torch.zeros_like(self.anchors), self.anchors], 1) # [n_ancorbox, 4]  x,y,w,h
            gt = torch.zeros(len(ground_truth[b]), 4) # [n_target_box, 4] x,y,w,h
            for i, anno in enumerate(ground_truth[b]):
                gt[i, 0] = (anno[0] + anno[2] / 2) / self.reduction
                gt[i, 1] = (anno[1] + anno[3] / 2) / self.reduction
                gt[i, 2] = anno[2] / self.reduction
                gt[i, 3] = anno[3] / self.reduction

            # Set confidence mask of matching detections to 0
            iou_gt_pred = bbox_ious(gt, cur_pred_boxes) # [n_target_box, grid_size*grid_size*n_anchorbox]
            mask = (iou_gt_pred > self.thresh).sum(0) >= 1 # [n_anchorbox*grid_size*grid_size]
            conf_mask[b][mask.view_as(conf_mask[b])] = 0 # conf_mask [batch_size, n_anchor_box, grid_size*grid_size]

            # Find best anchor for each ground truth
            gt_wh = gt.clone()
            gt_wh[:, :2] = 0
            iou_gt_anchors = bbox_ious(gt_wh, anchors) # [n_target_box, n_anchor_box]
            _, best_anchors = iou_gt_anchors.max(1) # [n_target_box], 值爲iou最大的anchor box 的index

            # Set masks and target values for each ground truth
            for i, anno in enumerate(ground_truth[b]):
                gi = min(width - 1, max(0, int(gt[i, 0])))
                gj = min(height - 1, max(0, int(gt[i, 1])))
                best_n = best_anchors[i]
                iou = iou_gt_pred[i][best_n * height * width + gj * width + gi]
                coord_mask[b][best_n][0][gj * width + gi] = 1
                cls_mask[b][best_n][gj * width + gi] = 1
                conf_mask[b][best_n][gj * width + gi] = self.object_scale
                tcoord[b][best_n][0][gj * width + gi] = gt[i, 0] - gi
                tcoord[b][best_n][1][gj * width + gi] = gt[i, 1] - gj
                tcoord[b][best_n][2][gj * width + gi] = math.log(max(gt[i, 2], 1.0) / self.anchors[best_n, 0])
                tcoord[b][best_n][3][gj * width + gi] = math.log(max(gt[i, 3], 1.0) / self.anchors[best_n, 1])
                tconf[b][best_n][gj * width + gi] = iou
                tcls[b][best_n][gj * width + gi] = int(anno[4])

        return coord_mask, conf_mask, cls_mask, tcoord, tconf, tcls


def bbox_ious(boxes1, boxes2):
    b1x1, b1y1 = (boxes1[:, :2] - (boxes1[:, 2:4] / 2)).split(1, 1)
    b1x2, b1y2 = (boxes1[:, :2] + (boxes1[:, 2:4] / 2)).split(1, 1)
    b2x1, b2y1 = (boxes2[:, :2] - (boxes2[:, 2:4] / 2)).split(1, 1)
    b2x2, b2y2 = (boxes2[:, :2] + (boxes2[:, 2:4] / 2)).split(1, 1)

    dx = (b1x2.min(b2x2.t()) - b1x1.max(b2x1.t())).clamp(min=0)
    dy = (b1y2.min(b2y2.t()) - b1y1.max(b2y1.t())).clamp(min=0)
    intersections = dx * dy

    areas1 = (b1x2 - b1x1) * (b1y2 - b1y1)
    areas2 = (b2x2 - b2x1) * (b2y2 - b2y1)
    unions = (areas1 + areas2.t()) - intersections

    return intersections / unions

參考:

1. <機器愛學習>YOLOv2 / YOLO9000 深入理解

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