目標檢測之YOLOv2,最詳細的代碼解析
一、前言
最近一直在研究深度學習在目標檢測的應用,看完了YOLOv2的paper和YAD2K的實現源碼,來總結一下自己的收獲,以便於加深理解。
二、關於目標檢測
目標檢測可簡單划分成兩個任務,一個是分類,一個是確定bounding boxes。目前目標檢測領域的深度學習方法主要分為兩類:two stage的目標檢測算法;one stage的目標檢測算法。前者是先由算法生成一系列作為樣本的候選框,再通過卷積神經網絡進行樣本分類;后者則不用產生候選框,直接將目標邊框定位的問題轉化為回歸問題處理。正是由於兩種方法的差異,在性能上也有不同,前者在檢測准確率和定位精度上占優,后者在算法速度上占優。YOLO(You Only Look Once )則是一種one stage的目標檢測算法,目前已經迭代發布了三個版本YOLOv1、YOLOv2、YOLOv3。本文着重介紹的是YOLOv2。
三、YOLOv2的改進
作者在論文中主要總結了關於YOLOv2的三個方面改進:Better、Faster、Stronger。這不是本片文章我想分享的主要內容,因為有太多博主已經寫的很透徹了,所以這部分我就只是很簡單的稍微敘述了作者的思想,公式比較難編輯也基本沒寫。可以看下我黑體字的概括,如果想要了解更多的細節,可以搜搜別的博客看看。

1、Better
-
(1)batch Normalization
每個卷積層后均使用batch Normalization
采用Batch Normalization可以提升模型收斂速度,而且可以起到一定正則化效果,降低模型的過擬合。在YOLOv2中,每個卷積層后面都添加了Batch Normalization層,並且不再使用droput。使用Batch Normalization后,YOLOv2的mAP提升了2.4%。Bacth_Normalizing -
(2)High ResolutionClassifier
預訓練分類模型采用了更高分辨率的圖片
YOLOv1先在ImageNet(224x224)分類數據集上預訓練模型的主體部分(大部分目標檢測算法),獲得較好的分類效果,然后再訓練網絡的時候將網絡的輸入從224x224增加為448x448。但是直接切換分辨率,檢測模型可能難以快速適應高分辨率。所以YOLOv2增加了在ImageNet數據集上使用448x448的輸入來finetune分類網絡這一中間過程(10 epochs),這可以使得模型在檢測數據集上finetune之前已經適用高分辨率輸入。使用高分辨率分類器后,YOLOv2的mAP提升了約4%。YOLOv2訓練的三個階段 -
(3)Convolutional With Anchor Boxes
使用了anchor boxes去預測bounding boxes,去掉了最后的全連接層,網絡僅采用了卷積層和池化層
在YOLOv1中,輸入圖片最終被划分為7x7的gird cell,每個單元格預測2個邊界框。YOLOv1最后采用的是全連接層直接對邊界框進行預測,其中邊界框的寬與高是相對整張圖片大小的,而由於各個圖片中存在不同尺度和長寬比(scales and ratios)的物體,YOLOv1在訓練過程中學習適應不同物體的形狀是比較困難的,這也導致YOLOv1在精確定位方面表現較差。YOLOv2則引入了一個anchor boxes的概念,這樣做的目的就是得到更高的召回率,yolov1只有98個邊界框,yolov2可以達到1000多個(論文中的實現是845個)。還去除了全連接層,保留一定空間結構信息,網絡僅由卷積層和池化層構成。輸入由448x448變為416x416,下采樣32倍,輸出為13x13x5x25。采用奇數的gird cell 是因為大圖像的中心往往位於圖像中間,為了避免四個gird cell參與預測,我們更希望用一個gird cell去預測。結果mAP由69.5下降到69.2,下降了0.3,召回率由81%提升到88%,提升7%。盡管mAP下降,但召回率的上升意味着我們的模型有更大的提升空間。 -
(4)Dimension Clusters(關於anchor boxes的第一個問題:如何確定尺寸)
利用Kmeans聚類,解決了anchor boxes的尺寸選擇問題
在Faster R-CNN和SSD中,先驗框的維度(長和寬)都是手動設定的,帶有一定的主觀性。如果選取的先驗框維度比較合適,那么模型更容易學習,從而做出更好的預測。因此,YOLOv2采用k-means聚類方法對訓練集中的邊界框做了聚類分析。比較了復雜度和精確度后,選用了K值為5。因為設置先驗框的主要目的是為了使得預測框與ground truth的IOU更好,所以聚類分析時選用box與聚類中心box之間的IOU值作為距離指標:距離公式Dimension_Clusters.png -
(5)Direction locationprediction(關於anchor boxes的第二個問題:如何確定位置)
引入Sigmoid函數預測offset,解決了anchor boxes的預測位置問題,采用了新的損失函數
作者借鑒了RPN網絡使用的anchor boxes去預測bounding boxes相對於圖片分辨率的offset,通過(x,y,w,h)四個維度去確定anchor boxes的位置,但是這樣在早期迭代中x,y會非常不穩定,因為RPN是一個區域預測一次,但是YOLO中是169個gird cell一起預測,處於A gird cell 的x,y可能會跑到B gird cell中,到處亂跑,導致不穩定。作者巧妙的引用了sigmoid函數來規約x,y的值在(0,1)輕松解決了這個offset的問題。關於w,h的也改進了YOLOv1中平方差的差的平方的方法,用了RPN中的log函數。 -
(6)Fine-Grained Features
采用了passthrough層,去捕捉更細粒度的特征
YOLOv2提出了一種passthrough層來利用更精細的特征圖,Fine-Grained Features之后YOLOv2的性能有1%的提升。 -
(7)Multi-Scale Training
采用不同尺寸的圖片訓練,提高魯棒性
由於YOLOv2模型中只有卷積層和池化層,所以YOLOv2的輸入可以不限於416x416大小的圖片。為了增強模型的魯棒性,YOLOv2采用了多尺度輸入訓練策略,具體來說就是在訓練過程中每間隔一定的iterations之后改變模型的輸入圖片大小。由於YOLOv2的下采樣總步長為32,輸入圖片大小選擇一系列為32倍數的值:{320,352,384,...,608},輸入圖片最小為320x320,此時對應的特征圖大小為10x10(不是奇數了,確實有點尷尬),而輸入圖片最大為 608x608,對應的特征圖大小為19x19。在訓練過程,每隔10個iterations隨機選擇一種輸入圖片大小,然后只需要修改對最后檢測層的處理就可以重新訓練。采用Multi-Scale Training策略,YOLOv2可以適應不同大小的圖片,並且預測出很好的結果。
2、Faster
大多數檢測框架依賴於VGG-16作為的基本特征提取器。VGG-16是一個強大的,准確的分類網絡,但它是不必要的復雜。在單張圖像224×224分辨率的情況下VGG-16的卷積層運行一次前饋傳播需要306.90億次浮點運算。YOLO框架使用基於Googlenet架構的自定義網絡。這個網絡比VGG-16更快,一次前饋傳播只有85.2億次的操作。然而,它的准確性比VGG-16略差。在ImageNet上,對於單張裁剪圖像,224×224分辨率下的top-5准確率,YOLO的自定義模型獲得了88.0%,而VGG-16則為90.0%。YOLOv2使用Darknet-19網絡,有19個卷積層和5個最大池化層。相比YOLOv1的24個卷積層和2個全連接層精簡了網絡。

3、Stronger
這里作者的想法也很新穎,解決了2個不同數據集相互排斥(mutualy exclusive)的問題。作者提出了WordTree,使用該樹形結構成功的解決了不同數據集中的排斥問題。使用該樹形結構進行分層的預測分類,在某個閾值處結束或者最終達到葉子節點處結束。下面這副圖將有助於WordTree這個概念的理解。

四、YAD2K代碼解析
YAD2K用了90%的Keras和10%Tensorflow實現的YOLOv2。下面主要分析一下/yad2k/models/keras_yolo.py
這個文件里的代碼。
提示:其實boxes的坐標是[y,x,h,w]而不是[x,y,w,h]。
流程:數據先經過preprocess_true_boxes()函數處理,然后做一些處理輸入到模型,損失函數是yolo_loss(),網絡最后一個卷積層的輸出作為函數yolo_head()的輸入,然后再使用函數yolo_eval(),得到結果。
1、preprocess_true_boxes()
這個函數是得到detectors_mask(最佳預測的anchor boxes,每一個true boxes都對應一個anchor boxes),matching_true_boxes(用於后面和pred_boxes做差求loss)代碼后都給了比較詳細的注釋
def preprocess_true_boxes(true_boxes, anchors, image_size): """ 參數 -------------- true_boxes : 實際框的位置和類別,我們的輸入。二個維度: 第一個維度:一張圖片中有幾個實際框 第二個維度: [x, y, w, h, class],x,y 是框中心點坐標,w,h 是框的寬度和高度。x,y,w,h 均是除以圖片 分辨率得到的[0,1]范圍的比值。 anchors : 實際anchor boxes 的值,論文中使用了五個。[w,h],都是相對於gird cell 的比值。二個維度: 第一個維度:anchor boxes的數量,這里是5 第二個維度:[w,h],w,h,都是相對於gird cell長寬的比值。 [1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52] image_size : 圖片的實際尺寸。這里是416x416。 Returns -------------- detectors_mask : 取值是0或者1,這里的shape是[13,13,5,1],四個維度。 第一個維度:true_boxes的中心位於第幾行(y方向上屬於第幾個gird cell) 第二個維度:true_boxes的中心位於第幾列(x方向上屬於第幾個gird cell) 第三個維度:哪個anchor box 第四個維度:0/1。1的就是用於預測改true boxes 的 anchor boxes matching_true_boxes: 這里的shape是[13,13,5,5],四個維度。 第一個維度:true_boxes的中心位於第幾行(y方向上屬於第幾個gird cel) 第二個維度:true_boxes的中心位於第幾列(x方向上屬於第幾個gird cel) 第三個維度:第幾個anchor box 第四個維度:[x,y,w,h,class]。這里的x,y表示offset,是相當於gird cell的,w,h是取了log函數的, class是屬於第幾類。后面的代碼會詳細看到 """ height, width = image_size num_anchors = len(anchors) assert height % 32 == 0, '輸入的圖片的高度必須是32的倍數,不然會報錯。' assert width % 32 == 0, '輸入的圖片的寬度必須是32的倍數,不然會報錯。' conv_height = height // 32 '進行gird cell划分' conv_width = width // 32 '進行gird cell划分' num_box_params = true_boxes.shape[1] detectors_mask = np.zeros( (conv_height, conv_width, num_anchors, 1), dtype=np.float32) matching_true_boxes = np.zeros( (conv_height, conv_width, num_anchors, num_box_params), dtype=np.float32) '確定detectors_mask和matching_true_boxes的維度,用0填充' for box in true_boxes: '遍歷實際框' box_class = box[4:5] '提取類別信息,屬於哪類' box = box[0:4] * np.array( [conv_width, conv_height, conv_width, conv_height]) '換算成相對於gird cell的值' i = np.floor(box[1]).astype('int') '(y方向上屬於第幾個gird cell)' j = np.floor(box[0]).astype('int') '(x方向上屬於第幾個gird cell)' best_iou = 0 best_anchor = 0 '計算anchor boxes 和 true boxes的iou,找到最佳預測的一個anchor boxes' for k, anchor in enumerate(anchors): # Find IOU between box shifted to origin and anchor box. box_maxes = box[2:4] / 2. box_mins = -box_maxes anchor_maxes = (anchor / 2.) anchor_mins = -anchor_maxes intersect_mins = np.maximum(box_mins, anchor_mins) intersect_maxes = np.minimum(box_maxes, anchor_maxes) intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.) intersect_area = intersect_wh[0] * intersect_wh[1] box_area = box[2] * box[3] anchor_area = anchor[0] * anchor[1] iou = intersect_area / (box_area + anchor_area - intersect_area) if iou > best_iou: best_iou = iou best_anchor = k if best_iou > 0: detectors_mask[i, j, best_anchor] = 1 '找到最佳預測anchor boxes' adjusted_box = np.array( [ box[0] - j, box[1] - i, 'x,y都是相對於gird cell的位置,左上角[0,0],右下角[1,1]' np.log(box[2] / anchors[best_anchor][0]), '對應實際框w,h和anchor boxes w,h的比值取log函數' np.log