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

https://www.sohu.com/a/215920394_717210 轉

本文原載於知乎專欄「人工智障的深度瞎學之路」,AI 研習社獲得授權轉載。

Faster R-CNN 的極簡實現: github: simple-faster-rcnn-pytorch(http://t.cn/RHCDoPv )

本文插圖地址(含五幅高清矢量圖):draw.io(http://t.cn/RQzroe3 )

1 概述

在目標檢測領域, Faster R-CNN 表現出了極強的生命力, 雖然是 2015 年的論文(https://arxiv.org/abs/1506.01497),但它至今仍是許多目標檢測算法的基礎,這在日新月異的深度學習領域十分難得。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 的訓練(http://t.cn/RQzdbYt 和http://t.cn/R5vaVPi 實現支持 batch_size>1)。

2.2 Extractor

Extractor 使用的是預訓練好的模型提取圖片的特徵。論文中主要使用的是 Caffe 的預訓練模型 VGG16。修改如下圖所示:爲了節省顯存,前四層卷積層的學習率設爲 0。Conv4_3 的輸出作爲圖片特徵(feature)。conv4_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) 組成,這樣比直接回歸座標更好。

計算分類損失用的是交叉熵損失,而計算迴歸損失用的是 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 的過程也能進行反向傳播,但需要專門的算法,https://arxiv.org/abs/1512.04412)。

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

趁着最近時間充裕了一些,我決定從頭擼一個,剛開始寫沒多久,就發現 chainercv(http://t.cn/RN2kZoJ ) 內置了 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(http://t.cn/RHCDoPv )

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

  • 代碼簡單:除去空行,註釋,說明等,大概有 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。

  • 當初要是再仔細讀一讀 最近一點微小的工作(http://t.cn/RQzgn7p )和 ruotianluo/pytorch-faster-rcnn(http://t.cn/RQzgGEo )的 readme,能少踩不少坑。

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

最後,求 Star github: simple-faster-rcnn-pytorch(http://t.cn/RHCDoPv )

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章