目標檢測|YOLOv2原理與實現(附YOLOv3)
碼字不易,歡迎給個贊!
歡迎交流與轉載,文章會同步發布在公眾號:機器學習算法全棧工程師(Jeemy110)
前期文章:
小白將:目標檢測|YOLO原理與實現小白將:目標檢測|SSD原理與實現小白將:綜述|基於深度學習的目標檢測(一)
前言
在前面的一篇文章中,我們詳細介紹了YOLOv1的原理以及實現過程。這篇文章接着介紹YOLOv2的原理以及實現,YOLOv2的論文全名為YOLO9000: Better, Faster, Stronger,它斬獲了CVPR 2017 Best Paper Honorable Mention。在這篇文章中,作者首先在YOLOv1的基礎上提出了改進的YOLOv2,然后提出了一種檢測與分類聯合訓練方法,使用這種聯合訓練方法在COCO檢測數據集和ImageNet分類數據集上訓練出了YOLO9000模型,其可以檢測超過9000多類物體。所以,這篇文章其實包含兩個模型:YOLOv2和YOLO9000,不過后者是在前者基礎上提出的,兩者模型主體結構是一致的。YOLOv2相比YOLOv1做了很多方面的改進,這也使得YOLOv2的mAP有顯著的提升,並且YOLOv2的速度依然很快,保持着自己作為one-stage方法的優勢,YOLOv2和Faster R-CNN, SSD等模型的對比如圖1所示。這里將首先介紹YOLOv2的改進策略,並給出YOLOv2的TensorFlow實現過程,然后介紹YOLO9000的訓練方法。近期,YOLOv3也放出來了,YOLOv3也在YOLOv2的基礎上做了一部分改進,我們在最后也會簡單談談YOLOv3所做的改進工作。
圖1:YOLOv2與其它模型在VOC 2007數據集上的效果對比
YOLOv2的改進策略
YOLOv1雖然檢測速度很快,但是在檢測精度上卻不如R-CNN系檢測方法,YOLOv1在物體定位方面(localization)不夠准確,並且召回率(recall)較低。YOLOv2共提出了幾種改進策略來提升YOLO模型的定位准確度和召回率,從而提高mAP,YOLOv2在改進中遵循一個原則:保持檢測速度,這也是YOLO模型的一大優勢。YOLOv2的改進策略如圖2所示,可以看出,大部分的改進方法都可以比較顯著提升模型的mAP。下面詳細介紹各個改進策略。
圖2:YOLOv2相比YOLOv1的改進策略
Batch Normalization
Batch Normalization可以提升模型收斂速度,而且可以起到一定正則化效果,降低模型的過擬合。在YOLOv2中,每個卷積層后面都添加了Batch Normalization層,並且不再使用droput。使用Batch Normalization后,YOLOv2的mAP提升了2.4%。
High Resolution Classifier
目前大部分的檢測模型都會在先在ImageNet分類數據集上預訓練模型的主體部分(CNN特征提取器),由於歷史原因,ImageNet分類模型基本采用大小為 的圖片作為輸入,分辨率相對較低,不利於檢測模型。所以YOLOv1在采用
分類模型預訓練后,將分辨率增加至
,並使用這個高分辨率在檢測數據集上finetune。但是直接切換分辨率,檢測模型可能難以快速適應高分辨率。所以YOLOv2增加了在ImageNet數據集上使用
輸入來finetune分類網絡這一中間過程(10 epochs),這可以使得模型在檢測數據集上finetune之前已經適用高分辨率輸入。使用高分辨率分類器后,YOLOv2的mAP提升了約4%。
Convolutional With Anchor Boxes
在YOLOv1中,輸入圖片最終被划分為 網格,每個單元格預測2個邊界框。YOLOv1最后采用的是全連接層直接對邊界框進行預測,其中邊界框的寬與高是相對整張圖片大小的,而由於各個圖片中存在不同尺度和長寬比(scales and ratios)的物體,YOLOv1在訓練過程中學習適應不同物體的形狀是比較困難的,這也導致YOLOv1在精確定位方面表現較差。YOLOv2借鑒了Faster R-CNN中RPN網絡的先驗框(anchor boxes,prior boxes,SSD也采用了先驗框)策略。RPN對CNN特征提取器得到的特征圖(feature map)進行卷積來預測每個位置的邊界框以及置信度(是否含有物體),並且各個位置設置不同尺度和比例的先驗框,所以RPN預測的是邊界框相對於先驗框的offsets值(其實是transform值,詳細見Faster R_CNN論文),采用先驗框使得模型更容易學習。所以YOLOv2移除了YOLOv1中的全連接層而采用了卷積和anchor boxes來預測邊界框。為了使檢測所用的特征圖分辨率更高,移除其中的一個pool層。在檢測模型中,YOLOv2不是采用
圖片作為輸入,而是采用
大小。因為YOLOv2模型下采樣的總步長為
,對於
大小的圖片,最終得到的特征圖大小為
,維度是奇數,這樣特征圖恰好只有一個中心位置。對於一些大物體,它們中心點往往落入圖片中心位置,此時使用特征圖的一個中心點去預測這些物體的邊界框相對容易些。所以在YOLOv2設計中要保證最終的特征圖有奇數個位置。對於YOLOv1,每個cell都預測2個boxes,每個boxes包含5個值:
,前4個值是邊界框位置與大小,最后一個值是置信度(confidence scores,包含兩部分:含有物體的概率以及預測框與ground truth的IOU)。但是每個cell只預測一套分類概率值(class predictions,其實是置信度下的條件概率值),供2個boxes共享。YOLOv2使用了anchor boxes之后,每個位置的各個anchor box都單獨預測一套分類概率值,這和SSD比較類似(但SSD沒有預測置信度,而是把background作為一個類別來處理)。
使用anchor boxes之后,YOLOv2的mAP有稍微下降(這里下降的原因,我猜想是YOLOv2雖然使用了anchor boxes,但是依然采用YOLOv1的訓練方法)。YOLOv1只能預測98個邊界框( ),而YOLOv2使用anchor boxes之后可以預測上千個邊界框(
)。所以使用anchor boxes之后,YOLOv2的召回率大大提升,由原來的81%升至88%。
Dimension Clusters
在Faster R-CNN和SSD中,先驗框的維度(長和寬)都是手動設定的,帶有一定的主觀性。如果選取的先驗框維度比較合適,那么模型更容易學習,從而做出更好的預測。因此,YOLOv2采用k-means聚類方法對訓練集中的邊界框做了聚類分析。因為設置先驗框的主要目的是為了使得預測框與ground truth的IOU更好,所以聚類分析時選用box與聚類中心box之間的IOU值作為距離指標:
圖3為在VOC和COCO數據集上的聚類分析結果,隨着聚類中心數目的增加,平均IOU值(各個邊界框與聚類中心的IOU的平均值)是增加的,但是綜合考慮模型復雜度和召回率,作者最終選取5個聚類中心作為先驗框,其相對於圖片的大小如右邊圖所示。對於兩個數據集,5個先驗框的width和height如下所示(來源:YOLO源碼的cfg文件):
COCO: (0.57273, 0.677385), (1.87446, 2.06253), (3.33843, 5.47434), (7.88282, 3.52778), (9.77052, 9.16828)
VOC: (1.3221, 1.73145), (3.19275, 4.00944), (5.05587, 8.09892), (9.47112, 4.84053), (11.2364, 10.0071)
但是這里先驗框的大小具體指什么作者並沒有說明,但肯定不是像素點,從代碼實現上看,應該是相對於預測的特征圖大小( )。對比兩個數據集,也可以看到COCO數據集上的物體相對小點。這個策略作者並沒有單獨做實驗,但是作者對比了采用聚類分析得到的先驗框與手動設置的先驗框在平均IOU上的差異,發現前者的平均IOU值更高,因此模型更容易訓練學習。
圖3:數據集VOC和COCO上的邊界框聚類分析結果
New Network: Darknet-19
YOLOv2采用了一個新的基礎模型(特征提取器),稱為Darknet-19,包括19個卷積層和5個maxpooling層,如圖4所示。Darknet-19與VGG16模型設計原則是一致的,主要采用 卷積,采用
的maxpooling層之后,特征圖維度降低2倍,而同時將特征圖的channles增加兩倍。與NIN(Network in Network)類似,Darknet-19最終采用global avgpooling做預測,並且在
卷積之間使用
卷積來壓縮特征圖channles以降低模型計算量和參數。Darknet-19每個卷積層后面同樣使用了batch norm層以加快收斂速度,降低模型過擬合。在ImageNet分類數據集上,Darknet-19的top-1准確度為72.9%,top-5准確度為91.2%,但是模型參數相對小一些。使用Darknet-19之后,YOLOv2的mAP值沒有顯著提升,但是計算量卻可以減少約33%。
圖4:Darknet-19模型結構
Direct location prediction
前面講到,YOLOv2借鑒RPN網絡使用anchor boxes來預測邊界框相對先驗框的offsets。邊界框的實際中心位置 ,需要根據預測的坐標偏移值
,先驗框的尺度
以及中心坐標
(特征圖每個位置的中心點)來計算:
但是上面的公式是無約束的,預測的邊界框很容易向任何方向偏移,如當 時邊界框將向右偏移先驗框的一個寬度大小,而當
時邊界框將向左偏移先驗框的一個寬度大小,因此每個位置預測的邊界框可以落在圖片任何位置,這導致模型的不穩定性,在訓練時需要很長時間來預測出正確的offsets。所以,YOLOv2棄用了這種預測方式,而是沿用YOLOv1的方法,就是預測邊界框中心點相對於對應cell左上角位置的相對偏移值,為了將邊界框中心點約束在當前cell中,使用sigmoid函數處理偏移值,這樣預測的偏移值在(0,1)范圍內(每個cell的尺度看做1)。總結來看,根據邊界框預測的4個offsets
,可以按如下公式計算出邊界框實際位置和大小:
其中 為cell的左上角坐標,如圖5所示,在計算時每個cell的尺度為1,所以當前cell的左上角坐標為
。由於sigmoid函數的處理,邊界框的中心位置會約束在當前cell內部,防止偏移過多。而
和
是先驗框的寬度與長度,前面說過它們的值也是相對於特征圖大小的,在特征圖中每個cell的長和寬均為1。這里記特征圖的大小為
(在文中是
),這樣我們可以將邊界框相對於整張圖片的位置和大小計算出來(4個值均在0和1之間):
如果再將上面的4個值分別乘以圖片的寬度和長度(像素點值)就可以得到邊界框的最終位置和大小了。這就是YOLOv2邊界框的整個解碼過程。約束了邊界框的位置預測值使得模型更容易穩定訓練,結合聚類分析得到先驗框與這種預測方法,YOLOv2的mAP值提升了約5%。
圖5:邊界框位置與大小的計算示例圖
Fine-Grained Features
YOLOv2的輸入圖片大小為 ,經過5次maxpooling之后得到
大小的特征圖,並以此特征圖采用卷積做預測。
大小的特征圖對檢測大物體是足夠了,但是對於小物體還需要更精細的特征圖(Fine-Grained Features)。因此SSD使用了多尺度的特征圖來分別檢測不同大小的物體,前面更精細的特征圖可以用來預測小物體。YOLOv2提出了一種passthrough層來利用更精細的特征圖。YOLOv2所利用的Fine-Grained Features是
大小的特征圖(最后一個maxpooling層的輸入),對於Darknet-19模型來說就是大小為
的特征圖。passthrough層與ResNet網絡的shortcut類似,以前面更高分辨率的特征圖為輸入,然后將其連接到后面的低分辨率特征圖上。前面的特征圖維度是后面的特征圖的2倍,passthrough層抽取前面層的每個
的局部區域,然后將其轉化為channel維度,對於
的特征圖,經passthrough層處理之后就變成了
的新特征圖(特征圖大小降低4倍,而channles增加4倍,圖6為一個實例),這樣就可以與后面的
特征圖連接在一起形成
大小的特征圖,然后在此特征圖基礎上卷積做預測。在YOLO的C源碼中,passthrough層稱為reorg layer。在TensorFlow中,可以使用tf.extract_image_patches或者tf.space_to_depth來實現passthrough層:
out = tf.extract_image_patches(in, [1, stride, stride, 1], [1, stride, stride, 1], [1,1,1,1], padding="VALID") // or use tf.space_to_depth out = tf.space_to_depth(in, 2)
圖6:passthrough層實例
另外,作者在后期的實現中借鑒了ResNet網絡,不是直接對高分辨特征圖處理,而是增加了一個中間卷積層,先采用64個 卷積核進行卷積,然后再進行passthrough處理,這樣
的特征圖得到
的特征圖。這算是實現上的一個小細節。使用Fine-Grained Features之后YOLOv2的性能有1%的提升。
Multi-Scale Training
由於YOLOv2模型中只有卷積層和池化層,所以YOLOv2的輸入可以不限於 大小的圖片。為了增強模型的魯棒性,YOLOv2采用了多尺度輸入訓練策略,具體來說就是在訓練過程中每間隔一定的iterations之后改變模型的輸入圖片大小。由於YOLOv2的下采樣總步長為32,輸入圖片大小選擇一系列為32倍數的值:
,輸入圖片最小為
,此時對應的特征圖大小為
(不是奇數了,確實有點尷尬),而輸入圖片最大為
,對應的特征圖大小為
。在訓練過程,每隔10個iterations隨機選擇一種輸入圖片大小,然后只需要修改對最后檢測層的處理就可以重新訓練。
圖7:Multi-Scale Training
采用Multi-Scale Training策略,YOLOv2可以適應不同大小的圖片,並且預測出很好的結果。在測試時,YOLOv2可以采用不同大小的圖片作為輸入,在VOC 2007數據集上的效果如下圖所示。可以看到采用較小分辨率時,YOLOv2的mAP值略低,但是速度更快,而采用高分辨輸入時,mAP值更高,但是速度略有下降,對於 ,mAP高達78.6%。注意,這只是測試時輸入圖片大小不同,而實際上用的是同一個模型(采用Multi-Scale Training訓練)。
圖8:YOLOv2在VOC 2007數據集上的性能對比
總結來看,雖然YOLOv2做了很多改進,但是大部分都是借鑒其它論文的一些技巧,如Faster R-CNN的anchor boxes,YOLOv2采用anchor boxes和卷積做預測,這基本上與SSD模型(單尺度特征圖的SSD)非常類似了,而且SSD也是借鑒了Faster R-CNN的RPN網絡。從某種意義上來說,YOLOv2和SSD這兩個one-stage模型與RPN網絡本質上無異,只不過RPN不做類別的預測,只是簡單地區分物體與背景。在two-stage方法中,RPN起到的作用是給出region proposals,其實就是作出粗糙的檢測,所以另外增加了一個stage,即采用R-CNN網絡來進一步提升檢測的准確度(包括給出類別預測)。而對於one-stage方法,它們想要一步到位,直接采用“RPN”網絡作出精確的預測,要因此要在網絡設計上做很多的tricks。YOLOv2的一大創新是采用Multi-Scale Training策略,這樣同一個模型其實就可以適應多種大小的圖片了。
YOLOv2的訓練
YOLOv2的訓練主要包括三個階段。第一階段就是先在ImageNet分類數據集上預訓練Darknet-19,此時模型輸入為 ,共訓練160個epochs。然后第二階段將網絡的輸入調整為
,繼續在ImageNet數據集上finetune分類模型,訓練10個epochs,此時分類模型的top-1准確度為76.5%,而top-5准確度為93.3%。第三個階段就是修改Darknet-19分類模型為檢測模型,並在檢測數據集上繼續finetune網絡。網絡修改包括(網路結構可視化):移除最后一個卷積層、global avgpooling層以及softmax層,並且新增了三個
卷積層,同時增加了一個passthrough層,最后使用
卷積層輸出預測結果,輸出的channels數為:
,和訓練采用的數據集有關系。由於anchors數為5,對於VOC數據集輸出的channels數就是125,而對於COCO數據集則為425。這里以VOC數據集為例,最終的預測矩陣為
(shape為
),可以先將其reshape為
,其中
為邊界框的位置和大小
,
為邊界框的置信度,而
為類別預測值。
圖9:YOLOv2訓練的三個階段
圖10:YOLOv2結構示意圖
YOLOv2的網絡結構以及訓練參數我們都知道了,但是貌似少了點東西。仔細一想,原來作者並沒有給出YOLOv2的訓練過程的兩個最重要方面,即先驗框匹配(樣本選擇)以及訓練的損失函數,難怪Ng說YOLO論文很難懂,沒有這兩方面的說明我們確實不知道YOLOv2到底是怎么訓練起來的。不過默認按照YOLOv1的處理方式也是可以處理,我看了YOLO在TensorFlow上的實現darkflow(見yolov2/train.py),發現它就是如此處理的:和YOLOv1一樣,對於訓練圖片中的ground truth,若其中心點落在某個cell內,那么該cell內的5個先驗框所對應的邊界框負責預測它,具體是哪個邊界框預測它,需要在訓練中確定,即由那個與ground truth的IOU最大的邊界框預測它,而剩余的4個邊界框不與該ground truth匹配。YOLOv2同樣需要假定每個cell至多含有一個grounth truth,而在實際上基本不會出現多於1個的情況。與ground truth匹配的先驗框計算坐標誤差、置信度誤差(此時target為1)以及分類誤差,而其它的邊界框只計算置信度誤差(此時target為0)。YOLOv2和YOLOv1的損失函數一樣,為均方差函數。
但是我看了YOLOv2的源碼(訓練樣本處理與loss計算都包含在文件region_layer.c中,YOLO源碼沒有任何注釋,反正我看了是直搖頭),並且參考國外的blog以及allanzelener/YAD2K(Ng深度學習教程所參考的那個Keras實現)上的實現,發現YOLOv2的處理比原來的v1版本更加復雜。先給出loss計算公式:
我們來一點點解釋,首先 分別指的是特征圖(
)的寬與高,而
指的是先驗框數目(這里是5),各個
值是各個loss部分的權重系數。第一項loss是計算background的置信度誤差,但是哪些預測框來預測背景呢,需要先計算各個預測框和所有ground truth的IOU值,並且取最大值Max_IOU,如果該值小於一定的閾值(YOLOv2使用的是0.6),那么這個預測框就標記為background,需要計算noobj的置信度誤差。第二項是計算先驗框與預測寬的坐標誤差,但是只在前12800個iterations間計算,我覺得這項應該是在訓練前期使預測框快速學習到先驗框的形狀。第三大項計算與某個ground truth匹配的預測框各部分loss值,包括坐標誤差、置信度誤差以及分類誤差。先說一下匹配原則,對於某個ground truth,首先要確定其中心點要落在哪個cell上,然后計算這個cell的5個先驗框與ground truth的IOU值(YOLOv2中bias_match=1),計算IOU值時不考慮坐標,只考慮形狀,所以先將先驗框與ground truth的中心點都偏移到同一位置(原點),然后計算出對應的IOU值,IOU值最大的那個先驗框與ground truth匹配,對應的預測框用來預測這個ground truth。在計算obj置信度時,target=1,但與YOLOv1一樣而增加了一個控制參數rescore,當其為1時,target取預測框與ground truth的真實IOU值(cfg文件中默認采用這種方式)。對於那些沒有與ground truth匹配的先驗框(與預測框對應),除去那些Max_IOU低於閾值的,其它的就全部忽略,不計算任何誤差。這點在YOLOv3論文中也有相關說明:YOLO中一個ground truth只會與一個先驗框匹配(IOU值最好的),對於那些IOU值超過一定閾值的先驗框,其預測結果就忽略了。這和SSD與RPN網絡的處理方式有很大不同,因為它們可以將一個ground truth分配給多個先驗框。盡管YOLOv2和YOLOv1計算loss處理上有不同,但都是采用均方差來計算loss。另外需要注意的一點是,在計算boxes的
和
誤差時,YOLOv1中采用的是平方根以降低boxes的大小對誤差的影響,而YOLOv2是直接計算,但是根據ground truth的大小對權重系數進行修正:l.coord_scale * (2 - truth.w*truth.h)(這里w和h都歸一化到(0,1)),這樣對於尺度較小的boxes其權重系數會更大一些,可以放大誤差,起到和YOLOv1計算平方根相似的效果(參考YOLO v2 損失函數源碼分析)。
// box誤差函數,計算梯度 float delta_region_box(box truth, float *x, float *biases, int n, int index, int i, int j, int w, int h, float *delta, float scale, int stride) { box pred = get_region_box(x, biases, n, index, i, j, w, h, stride); float iou = box_iou(pred, truth); // 計算ground truth的offsets值 float tx = (truth.x*w - i); float ty = (truth.y*h - j); float tw = log(truth.w*w / biases[2*n]); float th = log(truth.h*h / biases[2*n + 1]); delta[index + 0*stride] = scale * (tx - x[index + 0*stride]); delta[index + 1*stride] = scale * (ty - x[index + 1*stride]); delta[index + 2*stride] = scale * (tw - x[index + 2*stride]); delta[index + 3*stride] = scale * (th - x[index + 3*stride]); return iou; }
最終的YOLOv2模型在速度上比YOLOv1還快(采用了計算量更少的Darknet-19模型),而且模型的准確度比YOLOv1有顯著提升,詳情見paper。
YOLOv2在TensorFlow上實現
這里參考YOLOv2在Keras上的復現(見yhcc/yolo2),使用TensorFlow實現YOLOv2在COCO數據集上的test過程。首先是定義YOLOv2的主體網絡結構Darknet-19:
def darknet(images, n_last_channels=425): """Darknet19 for YOLOv2""" net = conv2d(images, 32, 3, 1, name="conv1") net = maxpool(net, name="pool1") net = conv2d(net, 64, 3, 1, name="conv2") net = maxpool(net, name="pool2") net = conv2d(net, 128, 3, 1, name="conv3_1") net = conv2d(net, 64, 1, name="conv3_2") net = conv2d(net, 128, 3, 1, name="conv3_3") net = maxpool(net, name="pool3") net = conv2d(net, 256, 3, 1, name="conv4_1") net = conv2d(net, 128, 1, name="conv4_2") net = conv2d(net, 256, 3, 1, name="conv4_3") net = maxpool(net, name="pool4") net = conv2d(net, 512, 3, 1, name="conv5_1") net = conv2d(net, 256, 1, name="conv5_2") net = conv2d(net, 512, 3, 1, name="conv5_3") net = conv2d(net, 256, 1, name="conv5_4") net = conv2d(net, 512, 3, 1,