1. 程式人生 > >Yolov1原理及實現

Yolov1原理及實現

以下部分來自於對原論文的翻譯

一、引言

目前的檢測系統通過重用分類器來執行檢測。為了檢測目標,這些系統為該目標提供一個分類器,在測試影象的不同的位置和不同的尺度上對其進行評估。像deformable parts models(DPM)這樣的系統使用滑動視窗方法,其分類器在整個影象上均勻間隔的位置上執行。最近的方法,如R-CNN使用region proposal策略,首先在影象中生成潛在的邊界框(bounding box),然後在這些框上執行分類器。在分類之後,執行用於細化邊界框的後處理,消除重複的檢測,並根據場景中的其它目標為邊界框重新打分。這些複雜的流程是很慢,很難優化的,因為每個獨立的部分都必須單獨進行訓練。

我們將目標檢測看作是一個單一的迴歸問題,直接從影象畫素得到邊界框座標和類別概率。使用我們的系統——You Only Look Once(YOLO),便能得到影象上的物體是什麼和物體的具體位置。YOLO非常簡單(見下圖),它僅用單個卷積網路就能同時預測多個邊界框和它們的類別概率。YOLO在整個影象上訓練,並能直接優化檢測效能。與傳統的目標檢測方法相比,這種統一的模型下面所列的一些優點。

第一,YOLO速度非常快由於我們將檢測視為迴歸問題,所以我們不需要複雜的流程。測試時,我們在一張新影象上簡單的執行我們的神經網路來預測檢測結果。在Titan X GPU上不做批處理的情況下,YOLO的基礎版本以每秒45幀的速度執行,而快速版本執行速度超過150fps。這意味著我們可以在不到25毫秒的延遲內實時處理流媒體視訊。此外,YOLO實現了其它實時系統兩倍以上的平均精度。


第二,YOLO是在整個影象上進行推斷的與基於滑動視窗和候選框的技術不同,YOLO在訓練期間和測試時都會顧及到整個影象,所以它隱式地包含了關於類的上下文資訊以及它們的外觀。Fast R-CNN是一種很好的檢測方法,但由於它看不到更大的上下文,會將背景塊誤檢為目標。與Fast R-CNN相比,YOLO的背景誤檢數量少了一半。

第三,YOLO能學習到目標的泛化表徵把在自然影象上進行訓練的模型,用在藝術影象進行測試時,YOLO大幅優於DPM和R-CNN等頂級的檢測方法。由於YOLO具有高度泛化能力,因此在應用於新領域或碰到意外的輸入時不太可能出故障。

YOLO在精度上仍然落後於目前最先進的檢測系統。雖然它可以快速識別影象中的目標,但它在定位某些物體尤其是小的物體上精度不高。我們在實驗中會進一步探討精度/時間的權衡。我們所有的訓練和測試程式碼都是開源的,而且各種預訓練模型也都可以下載。



二、檢測

我們將目標檢測的獨立部分整合到單個神經網路中。我們的網路使用整個影象的特徵來預測每個邊界框。它還可以同時預測一張影象中的所有類別的所有邊界框。這意味著我們的網路對整張影象和影象中的所有目標進行推斷。YOLO設計可實現端到端訓練和實時的速度,同時保持較高的平均精度。我們的系統將輸入影象分成S×S的網格。如果目標的中心落入某個網格單元中,那麼該網格單元就負責檢測該目標。

每個網格單元都會預測B個邊界框和這些框的置信度分數(confidence scores)。這些置信度分數反映了該模型對那個框內是否包含目標的信心,以及它對自己的預測的準確度的估量。在形式上,我們將置信度定義為 P_{r}(Object)*IOU_{pred}^{truth} 。如果該單元格中不存在目標,則置信度分數應為零。否則,我們希望置信度分數等於預測框(predict box)與真實標籤框(ground truth)之間聯合部分的交集(IOU)。每個邊界框包含5個預測:x,y,w,h和置信度。(xy)座標表示邊界框的中心相對於網格單元的邊界的值,而寬度和高度則是相對於整張影象來預測的。置信度預測表示預測框與任意實際邊界框之間的IOU。

每個網格單元還預測C個條件類別概率 P_{r}(Class|Object),這些概率以包含目標的網格單元為條件。不管邊界框的的數量B是多少,每個網格單元我們只預測一組類別概率。在測試時,我們把條件類概率和每個框的預測的置信度值相乘,


它給出了每個框特定類別的置信度分數。這些分數體現了該類出現在框中的概率以及預測框擬合目標的程度。為了在Pascal VOC上評估YOLO,我們使用S=7,B=2。Pascal VOC有20個標註類,所以C=20。我們最終的預測是7×7×30的張量。


2.1 網路設計

我們將此模型作為卷積神經網路來實現,並在Pascal VOC檢測資料集上進行評估。網路的初始卷積層從影象中提取特徵,而全連線層負責預測輸出概率和座標。我們的網路架構受影象分類模型GoogLeNet的啟發。我們的網路有24個卷積層,後面是2個全連線層。我們只使用1×1降維層,後面是3×3卷積層,這與Lin等人類似,而不是GoogLeNet使用的Inception模組。完整的網路如圖所示。我們還訓練了快速版本的YOLO,旨在推動快速目標檢測的界限。快速YOLO使用具有較少卷積層(9層而不是24層)的神經網路,在這些層中使用較少的濾波器。除了網路規模之外,基本版YOLO和快速YOLO的所有訓練和測試引數都是相同的。我們網路的最終輸出是7×7×30的預測張量。

2.2 訓練

我們在ImageNet的1000類競賽資料集上預訓練我們的卷積層。對於預訓練,我們使用圖3中的前20個卷積層,接著是平均池化層和全連線層。我們對這個網路進行了大約一週的訓練,並且在ImageNet 2012驗證集上獲得了單一裁剪影象88%的top-5準確率,與Caffe模型池中的GoogLeNet模型相當。我們使用Darknet框架進行所有的訓練和推斷。然後我們轉換模型來執行檢測訓練。Ren等人表明,預訓練網路中增加捲積層和連線層可以提高效能[29]。按照他們的方法,我們添加了四個卷積層和兩個全連線層,這些層的權重都用隨機值初始化。檢測通常需要細粒度的視覺資訊,因此我們將網路的輸入解析度從224×224改為448×448

模型的最後一層預測類概率和邊界框座標。我們通過影象寬度和高度來規範邊界框的寬度和高度,使它們落在0和1之間。我們將邊界框x和y座標引數化為特定網格單元位置的偏移量,所以它們的值被限定在在0和1之間。模型的最後一層使用線性啟用函式,而所有其它的層使用下面的leaky rectified activation:


我們對模型輸出的平方和誤差進行優化。我們選擇使用平方和誤差,是因為它易於優化,但是它並不完全符合最大化平均精度(average precision)的目標。它給分類誤差與定位誤差的權重是一樣的,這點可能並不理想。另外,每個影象都有很多網格單元並沒有包含任何目標,這將這些單元格的“置信度”分數推向零,通常壓制了包含目標的單元格的梯度。這可能導致模型不穩定,從而導致訓練在早期就發散。為了彌補平方和誤差的缺陷,我們增加了邊界框座標預測的損失,並減少了不包含目標的框的置信度預測的損失。 我們使用兩個引數λcoord和λnoobj來實現這一點。 我們設定λcoord= 5和λnoobj= .5。

平方和誤差對大框和小框的誤差權衡是一樣的,而我們的錯誤指標應該要體現出,大框的小偏差的重要性不如小框的小偏差的重要性。為了部分解決這個問題,我們直接預測邊界框寬度和高度的平方根,而不是寬度和高度。YOLO為每個網格單元預測多個邊界框。在訓練時,每個目標我們只需要一個邊界框預測器來負責。若某預測器的預測值與目標的實際值的IOU值最高,則這個預測器被指定為“負責”預測該目標。這導致邊界框預測器的專業化。每個預測器可以更好地預測特定大小,方向角,或目標的類別,從而改善整體召回率。在訓練期間,我們優化以下多部分損失函式:


其中 1_{i}^{obj} 表示目標是否出現在網格單元i中, 1_{ij}^{obj} 表示單元格i中的第j個邊界框預測器“負責”該預測。注意,如果目標存在於該網格單元中(前面討論的條件類別概率),則損失函式僅懲罰分類錯誤。如果預測器“負責”實際邊界框(即該網格單元中具有最高IOU的預測器),則它也僅懲罰邊界框座標錯誤。

我們用Pascal VOC 2007和2012的訓練集和驗證資料集進行了大約135個迭代的網路訓練。因為我們僅在Pascal VOC 2012上進行測試,所以我們的訓練集裡包含了Pascal VOC 2007的測試資料。在整個訓練過程中,我們使用的批量大小是64,動量為0.9,衰減率是0.0005。我們的學習率計劃如下:在第一個迭代週期,我們將學習率從 10^{-3} 慢慢地提高到 10^{-2} 。如果從大的學習率開始訓練,我們的模型通常會由於不穩定的梯度而發散。我們繼續以 10^{-2} 進行75個週期的訓練,然後以 10^{-3} 進行30個週期的訓練,最後以 10^{-4} 進行30個週期的訓練。為避免過擬合,我們使用了Dropout和大量的資料增強。 在第一個連線層之後的dropout層的丟棄率設定為0.5,以防止層之間的相互適應。 對於資料增強,我們引入高達20%的原始影象大小的隨機縮放和平移。我們還在HSV色彩空間中以高達1.5的因子隨機調整影象的曝光度和飽和度。

2.3 推斷

就像在訓練中一樣,預測測試影象的檢測只需要一次網路評估。在Pascal VOC上,每張影象上網路預測98個邊界框和每個框的類別概率。YOLO在測試時非常快,因為它只需要一次網路評估,這與基於分類器的方法不同。網格設計強化了邊界框預測中的空間多樣性。通常一個目標落在哪一個網格單元中是很明顯的,而網路只能為每個目標預測一個邊界框。然而,一些大的目標或接近多個網格單元的邊界的目標能被多個網格單元定位。非極大值抑制可以用來修正這些多重檢測。非最大抑制對於YOLO的效能的影響不像對於R-CNN或DPM那樣重要,但也能增加2−3%的mAP。

2.4 缺陷

YOLO給邊界框預測強加空間約束,因為每個網格單元只預測兩個框和只能有一個類別。這個空間約束限制了我們的模型可以預測的鄰近目標的數量。我們的模型難以預測群組中出現的小物體(比如鳥群)。由於我們的模型學習是從資料中預測邊界框,因此它很難泛化到新的、不常見的長寬比或配置的目標。我們的模型也使用相對較粗糙的特徵來預測邊界框,因為輸入影象在我們的架構中歷經了多個下采樣層。

最後,我們的訓練基於一個逼近檢測效能的損失函式,這個損失函式無差別地處理小邊界框與大邊界框的誤差。大邊界框的小誤差通常是無關要緊的,但小邊界框的小誤差對IOU的影響要大得多。我們的主要錯誤來自於不正確的定位。

三、實驗

此次我們復現的是yolo-small 模型,也就是dark-net19。話不多說,程式碼如下:

# -*- coding: utf-8 -*-

import tensorflow as tf
import numpy as np
import cv2


# leaky_relu啟用函式
def leaky_relu(x, alpha=0.1):
    return tf.maximum(alpha * x, x)


class Yolo(object):
    def __init__(self, weights_file, input_image, verbose=True):
        # 後面程式列印描述功能的標誌位
        self.verbose = verbose

        # 檢測超引數
        self.S = 7  # cell數量
        self.B = 2  # 每個網格的邊界框數
        self.classes = ["aeroplane", "bicycle", "bird", "boat", "bottle",
                        "bus", "car", "cat", "chair", "cow", "diningtable",
                        "dog", "horse", "motorbike", "person", "pottedplant",
                        "sheep", "sofa", "train", "tvmonitor"]
        self.C = len(self.classes)  # 類別數

        self.x_offset = np.transpose(np.reshape(np.array([np.arange(self.S)] * self.S * self.B),
                                                [self.B, self.S, self.S]), [1, 2, 0])
        self.y_offset = np.transpose(self.x_offset, [1, 0, 2])  # 改變陣列的shape

        self.threshold = 0.2  # 類別置信度分數閾值
        self.iou_threshold = 0.4  # IOU閾值,小於0.4的會過濾掉

        self.max_output_size = 10  # NMS選擇的邊界框的最大數量

        self.sess = tf.Session()
        self._build_net()  # 【1】搭建網路模型(預測):模型的主體網路部分,這個網路將輸出[batch,7*7*30]的張量
        self._build_detector()  # 【2】解析網路的預測結果:先判斷預測框類別,再NMS
        self._load_weights(weights_file)  # 【3】匯入權重檔案
        self.detect_from_file(image_file=input_image)  # 【4】從預測輸入圖片,並可視化檢測邊界框、將obj的分類結果和座標儲存成txt。

    # 【1】搭建網路模型(預測):模型的主體網路部分,這個網路將輸出[batch,7*7*30]的張量
    def _build_net(self):
        # 列印狀態資訊
        if self.verbose:
            print("Start to build the network ...")

        # 輸入、輸出用佔位符,因為尺寸一般不會改變
        self.images = tf.placeholder(tf.float32, [None, 448, 448, 3])  # None表示不確定,為了自適應batchsize

        # 搭建網路模型
        net = self._conv_layer(self.images, 1, 64, 7, 2)
        net = self._maxpool_layer(net, 1, 2, 2)
        net = self._conv_layer(net, 2, 192, 3, 1)
        net = self._maxpool_layer(net, 2, 2, 2)
        net = self._conv_layer(net, 3, 128, 1, 1)
        net = self._conv_layer(net, 4, 256, 3, 1)
        net = self._conv_layer(net, 5, 256, 1, 1)
        net = self._conv_layer(net, 6, 512, 3, 1)
        net = self._maxpool_layer(net, 6, 2, 2)
        net = self._conv_layer(net, 7, 256, 1, 1)
        net = self._conv_layer(net, 8, 512, 3, 1)
        net = self._conv_layer(net, 9, 256, 1, 1)
        net = self._conv_layer(net, 10, 512, 3, 1)
        net = self._conv_layer(net, 11, 256, 1, 1)
        net = self._conv_layer(net, 12, 512, 3, 1)
        net = self._conv_layer(net, 13, 256, 1, 1)
        net = self._conv_layer(net, 14, 512, 3, 1)
        net = self._conv_layer(net, 15, 512, 1, 1)
        net = self._conv_layer(net, 16, 1024, 3, 1)
        net = self._maxpool_layer(net, 16, 2, 2)
        net = self._conv_layer(net, 17, 512, 1, 1)
        net = self._conv_layer(net, 18, 1024, 3, 1)
        net = self._conv_layer(net, 19, 512, 1, 1)
        net = self._conv_layer(net, 20, 1024, 3, 1)
        net = self._conv_layer(net, 21, 1024, 3, 1)
        net = self._conv_layer(net, 22, 1024, 3, 2)
        net = self._conv_layer(net, 23, 1024, 3, 1)
        net = self._conv_layer(net, 24, 1024, 3, 1)
        net = self._flatten(net)
        net = self._fc_layer(net, 25, 512, activation=leaky_relu)
        net = self._fc_layer(net, 26, 4096, activation=leaky_relu)
        net = self._fc_layer(net, 27, self.S * self.S * (self.B * 5 + self.C))

        # 網路輸出,[batch,7*7*30]的張量
        self.predicts = net

    # 【2】解析網路的預測結果:先判斷預測框類別,再NMS
    def _build_detector(self):
        # 原始圖片的寬和高
        self.width = tf.placeholder(tf.float32, name='img_w')
        self.height = tf.placeholder(tf.float32, name='img_h')

        # 網路迴歸[batch,7*7*30]:
        idx1 = self.S * self.S * self.C
        idx2 = idx1 + self.S * self.S * self.B
        # 1.類別概率[:,:7*7*20]  20維
        class_probs = tf.reshape(self.predicts[0, :idx1], [self.S, self.S, self.C])
        # 2.置信度[:,7*7*20:7*7*(20+2)]  2維
        confs = tf.reshape(self.predicts[0, idx1:idx2], [self.S, self.S, self.B])
        # 3.邊界框[:,7*7*(20+2):]  8維 -> (x,y,w,h)
        boxes = tf.reshape(self.predicts[0, idx2:], [self.S, self.S, self.B, 4])

        # 將x,y轉換為相對於影象左上角的座標
        # w,h的預測是平方根乘以影象的寬度和高度
        boxes = tf.stack([(boxes[:, :, :, 0] + tf.constant(self.x_offset, dtype=tf.float32)) / self.S * self.width,
                          (boxes[:, :, :, 1] + tf.constant(self.y_offset, dtype=tf.float32)) / self.S * self.height,
                          tf.square(boxes[:, :, :, 2]) * self.width,
                          tf.square(boxes[:, :, :, 3]) * self.height], axis=3)

        # 類別置信度分數:[S,S,B,1]*[S,S,1,C]=[S,S,B,類別置信度C]
        scores = tf.expand_dims(confs, -1) * tf.expand_dims(class_probs, 2)

        scores = tf.reshape(scores, [-1, self.C])  # [S*S*B, C]
        boxes = tf.reshape(boxes, [-1, 4])  # [S*S*B, 4]

        # 只選擇類別置信度最大的值作為box的類別、分數
        box_classes = tf.argmax(scores, axis=1)  # 邊界框box的類別
        box_class_scores = tf.reduce_max(scores, axis=1)  # 邊界框box的分數

        # 利用類別置信度閾值self.threshold,過濾掉類別置信度低的
        filter_mask = box_class_scores >= self.threshold
        scores = tf.boolean_mask(box_class_scores, filter_mask)
        boxes = tf.boolean_mask(boxes, filter_mask)
        box_classes = tf.boolean_mask(box_classes, filter_mask)

        # NMS (不區分不同的類別)
        # 中心座標+寬高box (x, y, w, h) -> xmin=x-w/2 -> 左上+右下box (xmin, ymin, xmax, ymax),因為NMS函式是這種計算方式
        _boxes = tf.stack([boxes[:, 0] - 0.5 * boxes[:, 2], boxes[:, 1] - 0.5 * boxes[:, 3],
                           boxes[:, 0] + 0.5 * boxes[:, 2], boxes[:, 1] + 0.5 * boxes[:, 3]], axis=1)
        nms_indices = tf.image.non_max_suppression(_boxes, scores,
                                                   self.max_output_size, self.iou_threshold)
        self.scores = tf.gather(scores, nms_indices)
        self.boxes = tf.gather(boxes, nms_indices)
        self.box_classes = tf.gather(box_classes, nms_indices)

    # 【3】匯入權重檔案
    def _load_weights(self, weights_file):
        # 列印狀態資訊
        if self.verbose:
            print("Start to load weights from file:%s" % (weights_file))

        # 匯入權重
        saver = tf.train.Saver()  # 初始化
        saver.restore(self.sess, weights_file)  # saver.restore匯入/saver.save儲存

    # 【4】從預測輸入圖片,並可視化檢測邊界框、將obj的分類結果和座標儲存成txt。
    # image_file是輸入圖片檔案路徑;
    # deteted_boxes_file="boxes.txt"是最後座標txt;detected_image_file="detected_image.jpg"是檢測結果視覺化圖片
    def detect_from_file(self, image_file, imshow=True, deteted_boxes_file="boxes.txt",
                         detected_image_file="detected_image.jpg"):
        # read image
        image = cv2.imread(image_file)
        img_h, img_w, _ = image.shape
        scores, boxes, box_classes = self._detect_from_image(image)
        predict_boxes = []
        for i in range(len(scores)):
            # 預測框資料為:[概率,x,y,w,h,類別置信度]
            predict_boxes.append((self.classes[box_classes[i]], boxes[i, 0],
                                  boxes[i, 1], boxes[i, 2], boxes[i, 3], scores[i]))
        self.show_results(image, predict_boxes, imshow, deteted_boxes_file, detected_image_file)

    ################# 對應【1】:定義conv/maxpool/flatten/fc層#############################################################
    # 卷積層:x輸入;id:層數索引;num_filters:卷積核個數;filter_size:卷積核尺寸;stride:步長
    def _conv_layer(self, x, id, num_filters, filter_size, stride):

        # 通道數
        in_channels = x.get_shape().as_list()[-1]
        # 均值為0標準差為0.1的正態分佈,初始化權重w;shape=行*列*通道數*卷積核個數
        weight = tf.Variable(
            tf.truncated_normal([filter_size, filter_size, in_channels, num_filters], mean=0.0, stddev=0.1))
        bias = tf.Variable(tf.zeros([num_filters, ]))  # 列向量

        # padding, 注意: 不用padding="SAME",否則可能會導致座標計算錯誤
        pad_size = filter_size // 2  # 除法運算,保留商的整數部分
        pad_mat = np.array([[0, 0], [pad_size, pad_size], [pad_size, pad_size], [0, 0]])
        x_pad = tf.pad(x, pad_mat)
        conv = tf.nn.conv2d(x_pad, weight, strides=[1, stride, stride, 1], padding="VALID")
        output = leaky_relu(tf.nn.bias_add(conv, bias))

        # 列印該層資訊
        if self.verbose:
            print('Layer%d:type=conv,num_filter=%d,filter_size=%d,stride=%d,output_shape=%s'
                  % (id, num_filters, filter_size, stride, str(output.get_shape())))

        return output

    # 池化層:x輸入;id:層數索引;pool_size:池化尺寸;stride:步長
    def _maxpool_layer(self, x, id, pool_size, stride):
        output = tf.layers.max_pooling2d(inputs=x,
                                         pool_size=pool_size,
                                         strides=stride,
                                         padding='SAME')
        if self.verbose:
            print('Layer%d:type=MaxPool,pool_size=%d,stride=%d,out_shape=%s'
                  % (id, pool_size, stride, str(output.get_shape())))
        return output

    # 扁平層:因為接下來會連線全連線層,例如[n_samples, 7, 7, 32] -> [n_samples, 7*7*32]
    def _flatten(self, x):
        tran_x = tf.transpose(x, [0, 3, 1, 2])  # [batch,行,列,通道數channels] -> [batch,通道數channels,列,行]
        nums = np.product(x.get_shape().as_list()[1:])  # 計算的是總共的神經元數量,第一個表示batch數量所以去掉
        return tf.reshape(tran_x, [-1, nums])  # [batch,通道數channels,列,行] -> [batch,通道數channels*列*行],-1代表自適應batch數量

    # 全連線層:x輸入;id:層數索引;num_out:輸出尺寸;activation:啟用函式
    def _fc_layer(self, x, id, num_out, activation=None):
        num_in = x.get_shape().as_list()[-1]  # 通道數/維度
        #