從編程實現角度學習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主要分為四部分(圖中四個綠色框):
- 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。
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。
然後用這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的總體架構如下圖所示:
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)組成,這樣比直接回歸座標更好。
計算分類損失用的是交叉熵損失,而計算回歸損失用的是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 網路結構
由於RoIs給出的2000個候選框,分別對應feature map不同大小的區域。首先利用ProposalTargetCreator
挑選出128個sample_rois, 然後使用了RoIPooling 將這些不同尺寸的區域全部pooling到同一個尺度(7×7)上。下圖就是一個例子,對於feature map上兩個不同尺度的RoI,經過RoIPooling之後,最後得到了3×3的feature map.
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_loc
和gt_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訓練或者測試。
其中AnchorTargetCreator
和ProposalTargetCreator
是為了生成訓練的目標,只在訓練階段用到,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 |