從編程實現角度學習Faster R-CNN(附極簡實現)

Faster R-CNN的極簡實現: github: simple-faster-rcnn-pytorch

本文插圖地址(含五幅高清矢量圖):draw.io

1 概述

在目標檢測領域, Faster R-CNN表現出了極強的生命力, 雖然是2015年的論文, 但它至今仍是許多目標檢測演算法的基礎,這在日新月異的深度學習領域十分難得。Faster R-CNN還被應用到更多的領域中, 比如人體關鍵點檢測、目標追蹤、 實例分割還有圖像描述等。

現在很多優秀的Faster R-CNN博客大都是針對論文講解,本文將嘗試從編程角度講解Faster R-CNN的實現。由於Faster R-CNN流程複雜,符號較多,容易混淆,本文以VGG16為例,所有插圖、數值皆是基於VGG16+VOC2007 。

1.1 目標

從編程實現角度角度來講, 以Faster R-CNN為代表的Object Detection任務,可以描述成:

給定一張圖片, 找出圖中的有哪些對象,以及這些對象的位置和置信概率

目標檢測任務

1.2 整體架構

Faster R-CNN的整體流程如下圖所示。

Faster R-CNN整體架構

從編程角度來說, Faster R-CNN主要分為四部分(圖中四個綠色框):

  • Dataset:數據,提供符合要求的數據格式(目前常用數據集是VOC和COCO)
  • Extractor: 利用CNN提取圖片特徵features(原始論文用的是ZF和VGG16,後來人們又用ResNet101)
  • RPN(Region Proposal Network): 負責提供候選區域rois(每張圖給出大概2000個候選框)
  • RoIHead: 負責對rois分類和微調。對RPN找出的rois,判斷它是否包含目標,並修正框的位置和座標

Faster R-CNN整體的流程可以分為三步:

  • 提特徵: 圖片(img)經過預訓練的網路(Extractor),提取到了圖片的特徵(feature
  • Region Proposal: 利用提取的特徵(feature),經過RPN網路,找出一定數量的rois(region of interests)。
  • 分類與回歸:將rois和圖像特徵features,輸入到RoIHead,對這些rois進行分類,判斷都屬於什麼類別,同時對這些rois的位置進行微調。

2 詳細實現

2.1 數據

對與每張圖片,需要進行如下數據處理:

  • 圖片進行縮放,使得長邊小於等於1000,短邊小於等於600(至少有一個等於)。
  • 對相應的bounding boxes 也也進行同等尺度的縮放。
  • 對於Caffe 的VGG16 預訓練模型,需要圖片位於0-255,BGR格式,並減去一個均值,使得圖片像素的均值為0。

最後返回四個值供模型訓練:

  • images : 3×H×W ,BGR三通道,寬W,高H
  • bboxes: 4×K , K個bounding boxes,每個bounding box的左上角和右下角的座標,形如(Y_min,X_min, Y_max,X_max),第Y行,第X列。
  • labels:K, 對應K個bounding boxes的label(對於VOC取值範圍為[0-19])
  • scale: 縮放的倍數, 原圖H ×W被resize到了HxW(scale=H/H )

需要注意的是,目前大多數Faster R-CNN實現都只支持batch-size=1的訓練(這個這個實現支持batch_size>1)。

2.2 Extractor

Extractor使用的是預訓練好的模型提取圖片的特徵。論文中主要使用的是Caffe的預訓練模型VGG16。修改如下圖所示:為了節省顯存,前四層卷積層的學習率設為0。Conv5_3的輸出作為圖片特徵(feature)。conv5_3相比於輸入,下採樣了16倍,也就是說輸入的圖片尺寸為3×H×W,那麼feature的尺寸就是C×(H/16)×(W/16)。VGG最後的三層全連接層的前兩層,一般用來初始化RoIHead的部分參數,這個我們稍後再講。總之,一張圖片,經過extractor之後,會得到一個C×(H/16)×(W/16)的feature map。

Extractor: VGG16

2.3 RPN

Faster R-CNN最突出的貢獻就在於提出了Region Proposal Network(RPN)代替了Selective Search,從而將候選區域提取的時間開銷幾乎降為0(2s -> 0.01s)。

2.3.1 Anchor

在RPN中,作者提出了anchor。Anchor是大小和尺寸固定的候選框。論文中用到的anchor有三種尺寸和三種比例,如下圖所示,三種尺寸分別是小(藍128)中(紅256)大(綠512),三個比例分別是1:1,1:2,2:1。3×3的組合總共有9種anchor。

Anchor

然後用這9種anchor在特徵圖(feature)左右上下移動,每一個特徵圖上的點都有9個anchor,最終生成了 (H/16)× (W/16)×9個anchor. 對於一個512×62×37的feature map,有 62×37×9~ 20000個anchor。 也就是對一張圖片,有20000個左右的anchor。這種做法很像是暴力窮舉,20000多個anchor,哪怕是蒙也能夠把絕大多數的ground truth bounding boxes蒙中。

2.3.2 訓練RPN

RPN的總體架構如下圖所示:

RPN架構

anchor的數量和feature map相關,不同的feature map對應的anchor數量也不一樣。RPN在Extractor輸出的feature maps的基礎之上,先增加了一個卷積(用來語義空間轉換?),然後利用兩個1x1的卷積分別進行二分類(是否為正樣本)和位置回歸。進行分類的卷積核通道數為9×2(9個anchor,每個anchor二分類,使用交叉熵損失),進行回歸的卷積核通道數為9×4(9個anchor,每個anchor有4個位置參數)。RPN是一個全卷積網路(fully convolutional network),這樣對輸入圖片的尺寸就沒有要求了。

接下來RPN做的事情就是利用(AnchorTargetCreator)將20000多個候選的anchor選出256個anchor進行分類和回歸位置。選擇過程如下:

  • 對於每一個ground truth bounding box (gt_bbox),選擇和它重疊度(IoU)最高的一個anchor作為正樣本
  • 對於剩下的anchor,從中選擇和任意一個gt_bbox重疊度超過0.7的anchor,作為正樣本,正樣本的數目不超過128個。
  • 隨機選擇和gt_bbox重疊度小於0.3的anchor作為負樣本。負樣本和正樣本的總數為256。

對於每個anchor, gt_label 要麼為1(前景),要麼為0(背景),而gt_loc則是由4個位置參數(tx,ty,tw,th)組成,這樣比直接回歸座標更好。

t_x = (x ? x_a)/w_a; t_y = (y ? y_a)/h_a; t_w = log(w/w_a); t_h = log(h/h_a); t_x^* = (x^* ? x_a)/w_a; t_y^* = (y^* ? y_a)/h_a; t_w^* = log(w^*/w_a); t_h^* = log(h^*/h_a);

計算分類損失用的是交叉熵損失,而計算回歸損失用的是Smooth_l1_loss. 在計算回歸損失的時候,只計算正樣本(前景)的損失,不計算負樣本的位置損失。

2.3.3 RPN生成RoIs

RPN在自身訓練的同時,還會提供RoIs(region of interests)給Fast RCNN(RoIHead)作為訓練樣本。RPN生成RoIs的過程(ProposalCreator)如下:

  • 對於每張圖片,利用它的feature map, 計算 (H/16)× (W/16)×9(大概20000)個anchor屬於前景的概率,以及對應的位置參數。
  • 選取概率較大的12000個anchor
  • 利用回歸的位置參數,修正這12000個anchor的位置,得到RoIs
  • 利用非極大值((Non-maximum suppression, NMS)抑制,選出概率最大的2000個RoIs

注意:在inference的時候,為了提高處理速度,12000和2000分別變為6000和300.

注意:這部分的操作不需要進行反向傳播,因此可以利用numpy/tensor實現。

RPN的輸出:RoIs(形如2000×4或者300×4的tensor)

2.4 RoIHead/Fast R-CNN

RPN只是給出了2000個候選框,RoI Head在給出的2000候選框之上繼續進行分類和位置參數的回歸。

2.4.1 網路結構

RoIHead網路結構

由於RoIs給出的2000個候選框,分別對應feature map不同大小的區域。首先利用ProposalTargetCreator 挑選出128個sample_rois, 然後使用了RoIPooling 將這些不同尺寸的區域全部pooling到同一個尺度(7×7)上。下圖就是一個例子,對於feature map上兩個不同尺度的RoI,經過RoIPooling之後,最後得到了3×3的feature map.

RoIPooling

RoI Pooling 是一種特殊的Pooling操作,給定一張圖片的Feature map (512×H/16×W/16) ,和128個候選區域的座標(128×4),RoI Pooling將這些區域統一下採樣到 (512×7×7),就得到了128×512×7×7的向量。可以看成是一個batch-size=128,通道數為512,7×7的feature map。

為什麼要pooling成7×7的尺度?是為了能夠共享權重。在之前講過,除了用到VGG前幾層的卷積之外,最後的全連接層也可以繼續利用。當所有的RoIs都被pooling成(512×7×7)的feature map後,將它reshape 成一個一維的向量,就可以利用VGG16預訓練的權重,初始化前兩層全連接。最後再接兩個全連接層,分別是:

  • FC 21 用來分類,預測RoIs屬於哪個類別(20個類+背景)
  • FC 84 用來回歸位置(21個類,每個類都有4個位置參數)

2.4.2 訓練

前面講過,RPN會產生大約2000個RoIs,這2000個RoIs不是都拿去訓練,而是利用ProposalTargetCreator 選擇128個RoIs用以訓練。選擇的規則如下:

  • RoIs和gt_bboxes 的IoU大於0.5的,選擇一些(比如32個)
  • 選擇 RoIs和gt_bboxes的IoU小於等於0(或者0.1)的選擇一些(比如 128-32=96個)作為負樣本

為了便於訓練,對選擇出的128個RoIs,還對他們的gt_roi_loc 進行標準化處理(減去均值除以標準差)

對於分類問題,直接利用交叉熵損失. 而對於位置的回歸損失,一樣採用Smooth_L1Loss, 只不過只對正樣本計算損失.而且是只對正樣本中的這個類別4個參數計算損失。舉例來說:

  • 一個RoI在經過FC 84後會輸出一個84維的loc 向量. 如果這個RoI是負樣本,則這84維向量不參與計算 L1_Loss
  • 如果這個RoI是正樣本,屬於label K,那麼它的第 K×4, K×4+1 ,K×4+2, K×4+3 這4個數參與計算損失,其餘的不參與計算損失。

2.4.3 生成預測結果

測試的時候對所有的RoIs(大概300個左右) 計算概率,並利用位置參數調整預測候選框的位置。然後再用一遍極大值抑制(之前在RPN的ProposalCreator用過)。

注意:

  • 在RPN的時候,已經對anchor做了一遍NMS,在RCNN測試的時候,還要再做一遍
  • 在RPN的時候,已經對anchor的位置做了回歸調整,在RCNN階段還要對RoI再做一遍
  • 在RPN階段分類是二分類,而Fast RCNN階段是21分類

2.5 模型架構圖

最後整體的模型架構圖如下:

整體網路結構

需要注意的是: 藍色箭頭的線代表著計算圖,梯度反向傳播會經過。而紅色部分的線不需要進行反向傳播(論文了中提到了ProposalCreator生成RoIs的過程也能進行反向傳播,但需要專門的演算法)。

3 概念對比

在Faster RCNN中有幾個概念,容易混淆,或者具有較強的相似性。在此我列出來並做對比,希望對你理解有幫助。

3.1 bbox anchor RoI loc

BBox:全稱是bounding box,邊界框。其中Ground Truth Bounding Box是每一張圖中人工標註的框的位置。一張圖中有幾個目標,就有幾個框(一般小於10個框)。Faster R-CNN的預測結果也可以叫bounding box,不過一般叫 Predict Bounding Box.

Anchor:錨?是人為選定的具有一定尺度、比例的框。一個feature map的錨的數目有上萬個(比如 20000)。

RoI:region of interest,候選框。Faster R-CNN之前傳統的做法是利用selective search從一張圖上大概2000個候選框框。現在利用RPN可以從上萬的anchor中找出一定數目更有可能的候選框。在訓練RCNN的時候,這個數目是2000,在測試推理階段,這個數目是300(為了速度)我個人實驗發現RPN生成更多的RoI能得到更高的mAP。

RoI不是單純的從anchor中選取一些出來作為候選框,它還會利用回歸位置參數,微調anchor的形狀和位置。

可以這麼理解:在RPN階段,先窮舉生成千上萬個anchor,然後利用Ground Truth Bounding Boxes,訓練這些anchor,而後從anchor中找出一定數目的候選區域(RoIs)。RoIs在下一階段用來訓練RoIHead,最後生成Predict Bounding Boxes。

loc: bbox,anchor和RoI,本質上都是一個框,可以用四個數(y_min, x_min, y_max, x_max)表示框的位置,即左上角的座標和右下角的座標。這裡之所以先寫y,再寫x是為了數組索引方便,但也需要千萬注意不要弄混了。 我在實現的時候,沒注意,導致輸入到RoIPooling的座標不對,浪費了好長時間。除了用這四個數表示一個座標之外,還可以用(y,x,h,w)表示,即框的中心座標和長寬。在訓練中進行位置回歸的時候,用的是後一種的表示。

3.2 四類損失

雖然原始論文中用的4-Step Alternating Training 即四步交替迭代訓練。然而現在github上開源的實現大多是採用近似聯合訓練(Approximate joint training),端到端,一步到位,速度更快。

在訓練Faster RCNN的時候有四個損失:

  • RPN 分類損失:anchor是否為前景(二分類)
  • RPN位置回歸損失:anchor位置微調
  • RoI 分類損失:RoI所屬類別(21分類,多了一個類作為背景)
  • RoI位置回歸損失:繼續對RoI位置微調

四個損失相加作為最後的損失,反向傳播,更新參數。

3.3 三個creator

在一開始閱讀源碼的時候,我常常把Faster RCNN中用到的三個Creator弄混。

  • AnchorTargetCreator : 負責在訓練RPN的時候,從上萬個anchor中選擇一些(比如256)進行訓練,以使得正負樣本比例大概是1:1. 同時給出訓練的位置參數目標。 即返回gt_rpn_locgt_rpn_label
  • ProposalTargetCreator: 負責在訓練RoIHead/Fast R-CNN的時候,從RoIs選擇一部分(比如128個)用以訓練。同時給定訓練目標, 返回(sample_RoI, gt_RoI_loc, gt_RoI_label
  • ProposalCreator: 在RPN中,從上萬個anchor中,選擇一定數目(2000或者300),調整大小和位置,生成RoIs,用以Fast R-CNN訓練或者測試。

其中AnchorTargetCreatorProposalTargetCreator是為了生成訓練的目標,只在訓練階段用到,ProposalCreator是RPN為Fast R-CNN生成RoIs,在訓練和測試階段都會用到。三個共同點在於他們都不需要考慮反向傳播(因此不同框架間可以共享numpy實現)

3.4 感受野與scale

從直觀上講,感受野receptive field)就是視覺感受區域的大小。在卷積神經網路中,感受野的定義是卷積神經網路每一層輸出的特徵圖(feature map)上的像素點在原始圖像上映射的區域大小。我的理解是,feature map上的某一點f對應輸入圖片中的一個區域,這個區域中的點發生變化,f可能隨之變化。而這個區域外的其它點不論如何改變,f的值都不會受之影響。VGG16的conv5_3的感受野為228,即feature map上每一個點,都包含了原圖一個228×228區域的信息。

Scale:輸入圖片的尺寸比上feature map的尺寸。比如輸入圖片是3×224×224,feature map 是 512×14×14,那麼scale就是 14/224=1/16。可以認為feature map中一個點對應輸入圖片的16個像素。由於相鄰的同尺寸、同比例的anchor是在feature map上的距離是一個點,對應到輸入圖片中就是16個像素。在一定程度上可以認為anchor的精度為16個像素。不過還需要考慮原圖相比於輸入圖片又做過縮放(這也是dataset返回的scale參數的作用,這個的scale指的是原圖和輸入圖片的縮放尺度,和上面的scale不一樣)。

4 實現方案

其實上半年好幾次都要用到Faster R-CNN,但是每回看到各種上萬行,幾萬行代碼,簡直無從下手。而且直到 @羅若天大神的ruotianluo/pytorch-faster-rcnn 之前,PyTorch的Faster R-CNN並未有合格的實現(速度和精度)。最早PyTorch實現的Faster R-CNN有longcw/faster_rcnn_pytorch 和 fmassa/fast_rcnn 後者是當之無愧的最簡實現(1,245行代碼,包括空行注釋,純Python實現),然而速度太慢,效果較差,fmassa最後也放棄了這個項目。前者又太過複雜,mAP也比論文中差一點(0.661VS 0.699)。當前github上的大多數實現都是基於py-faster-rcnn,RBG大神的代碼很健壯,考慮的很全面,支持很豐富,基本上git clone下來,準備一下數據模型就能直接跑起來。然而對我來說太過複雜,我的腦細胞比較少,上百個文件,動不動就好幾層的嵌套封裝,很容易令人頭大。

趁著最近時間充裕了一些,我決定從頭擼一個,剛開始寫沒多久,就發現chainercv內置了Faster R-CNN的實現,而且Faster R-CNN中用到的許多函數(比如對bbox的各種操作計算),chainercv都提供了內置支持(其實py-faster-rcnn也有封裝好的函數,但是chainercv的文檔寫的太詳細了!)。所以大多數函數都是直接copy&paste,把chainer的代碼改成pytorch/numpy,增加了一些可視化代碼等。不過cupy的內容並沒有改成THTensor。因為cupy現在已經是一個獨立的包,感覺比cffi好用(雖然我並不會C....)。

最終寫了一個簡單版本的Faster R-CNN,代碼地址在 github:simple-faster-rcnn-pytorch

這個實現主要有以下幾個特點:

  • 代碼簡單:除去空行,注釋,說明等,大概有2000行左右代碼,如果想學習如何實現Faster R-CNN,這是個不錯的參考。
  • 效果夠好:超過論文中的指標(論文mAP是69.9, 本程序利用caffe版本VGG16最低能達到0.70,最高能達到0.712,預訓練的模型在github中提供鏈接可以下載)
  • 速度足夠快:TITAN Xp上最快只要3小時左右(關閉驗證與可視化)就能完成訓練
  • 顯存佔用較小:3G左右的顯存佔用

^_^

這個項目其實寫代碼沒花太多時間,大多數時間花在調試上。有報錯的bug都很容易解決,最怕的是邏輯bug,只能一句句檢查,或者在ipdb中一步一步的執行,看輸出是否和預期一樣,還不一定找得出來。不過通過一步步執行,感覺對Faster R-CNN的細節理解也更深了。

寫完這個代碼,也算是基本掌握了Faster R-CNN。在寫代碼中踩了許多坑,也學到了很多,其中幾個收穫/教訓是:

  • 在復現別人的代碼的時候,不要自作聰明做什麼「改進」,先嚴格的按照論文或者官方代碼實現(比如把SGD優化器換成Adam,基本訓不動,後來調了一下發現要把學習率降10倍,但是效果依舊遠不如SGD)。
  • 不要偷懶,儘可能的「Match Everything」。由於torchvision中有預訓練好的VGG16,而caffe預訓練VGG要求輸入圖片像素在0-255之間(torchvision是0-1),BGR格式的,標準化只減均值,不除以標準差,看起來有點彆扭(總之就是要多寫幾十行代碼+專門下載模型)。然後我就用torchvision的預訓練模型初始化,最後用了一大堆的trick,各種手動調參,才把mAP調到0.7(正常跑,不調參的話大概在0.692附近)。某天晚上抱著試試的心態,睡前把VGG的模型改成caffe的,第二天早上起來一看輕輕鬆鬆0.705 ...
  • 有個小trick:把別人用其它框架訓練好的模型權重轉換成自己框架的,然後計算在驗證集的分數,如果分數相差無幾,那麼說明,相關的代碼沒有bug,就不用花太多時間檢查這部分代碼了。
  • 認真。那幾天常常一連幾個小時盯著屏幕,眼睛疼,很多單詞敲錯了沒發現,有些報錯了很容易發現,但是有些就。。。 比如計算分數的代碼就寫錯了一個單詞。然後我自己看模型的泛化效果不錯,但就是分數特別低,我還把模型訓練部分的代碼又過了好幾遍。。。
  • 紙上得來終覺淺, 絕知此事要coding。
  • 當初要是再仔細讀一讀 最近一點微小的工作和ruotianluo/pytorch-faster-rcnn 的readme,能少踩不少坑。

P.S. 在github上搜索faster rcnn,感覺有一半以上都是華人寫的。

最後,求Star github: simple-faster-rcnn-pytorch

推薦閱讀:

PhD Talk直播預告 | 亞馬遜高級應用科學家熊元駿:人類行為理解研究進展
CS231n課程筆記翻譯:圖像分類筆記(上)
車輛3D檢測:Deep MANTA論文閱讀筆記
AI為人民服務:視覺缺陷檢測
1.11【OpenCV圖像處理】形態學操作

TAG:计算机视觉 | PyTorch | 深度学习DeepLearning |