最先進的目標檢測網絡依賴於區域生成算法來假設目標位置。先前的SPPnet和Fast R-CNN都已經減少了檢測網絡的運行時間,但也暴露出區域建議計算是個瓶頸。這篇文章,引出一個區域生成網絡(RPN)和檢測網絡共享全圖的卷積特征,因此使得區域建議幾乎沒有任何開銷。RPN是一個在每一個位置同時預測目標邊界和目標分數的全卷積網絡。通過端到端的訓練RPN來生成高質量的區域建議來提供給Fast R-CNN作檢測使用。通過簡單的交替優化,RPN和Fast R-CNN可以共享特征訓練。
區域建議網絡
RPN輸入一個任意尺寸的圖像,並且輸出一組矩形目標建議,每一個都帶有一個目標分數。對這個過程使用全卷積網絡進行建模。因為最終的目標是和Fast R-CNN目標檢測網絡共享計算,所有假設兩個網絡都共享一組普通的卷積層。實驗中,調研了ZF(5層共享卷積)和VGG模型(13層共享卷積)。
為了生成區域建議,在最后的共享卷積層的卷積特征圖輸出上滑動一個小網絡。這個網絡被完全連接(注意,不是全連接)到輸入卷積特征圖的$n×n$空間窗口上。每個滑動窗口映射成一個低維的向量(256-d for ZF and 512-d for VGG)(這里的意思是,使用256或512個卷積核生成與被卷積特征圖同樣大的特征圖,只不過深度為256或者512)。這個向量被送到兩個同胞全連接層——邊框回歸層(reg)和邊框分類層(cls)。這里使用$n=3$,注意到輸入圖像的有效接受域是很大的(對於ZF和VGG來說分別是171和228)。在單個位置上解釋迷你版的網絡(圖1左)。注意因為迷你網絡以一種滑窗的方式操作,所以跨越所有空間位置。這個結構自然而然地通過一個$n×n$的卷積層實現,其后還跟着兩個同胞般的1x1卷積層(分別用於回歸和分類)。$n×n$卷積層的輸出使用ReLU。
圖1:左:區域建議網絡(RPN)。右:在PASCAL VOC 2007測試上用RPN建議的樣本檢測。該方法可以在大范圍的尺度和縱橫比上檢測目標。
Translation-Invariant Anchors
在每一個滑窗位置,同時預測$k$個區域建議,因此$reg$層有$4k$個輸出編碼$k$個邊框坐標。$cls$層輸出$2k$個分數來估計每個建議的目標/非目標的概率。$k$個建議相對於$k$個稱為錨點的參考框參數化。每個錨點在滑窗的中心,並且與尺度和縱橫比相關。這里使用3個尺度和3個縱橫比,在每個滑動位置上生成$k=9$個錨點。對於一個尺寸為$W×H$的卷積特征圖(約2400),總共有$W×H×k$個錨點。這個方法的一個重要屬性是,就錨點和與錨點相關的計算建議框函數而言,具有平移不變性。
相比而言,MultiBox method使用k-means來生成800個錨點,那不具有平移不變性。如果在一張圖像中平移一個目標,則建議框也應該平移,並且同樣能在每個位置上預測建議框。此外,因為MultiBox錨點不具有平移不變性,所有它要求一個(4+1)×800維的輸出層,而這篇論文的方法是一個(4+2)×9維的輸出層。這里的建議層有一個數量級的參數減少(使用GoogLeNet 的MultiBox有27百萬,而使用VGG-16的RPN有2.4百萬),因此在想PASCAL VOC這樣的小數據集上有更少擬合的風險。
A Loss Function for Learning Region Proposals
對於訓練RPN來說,為每一個錨點分配一個二元類標簽(是目標或不是)。為兩種錨點分配一個正標簽:(i)與真值框有最高IoU的錨點,(ii)與任何真值框有超過0.7的IoU。注意一個真值框可以將正標簽分配給多個錨點。如果錨點與所有真值框的IoU比例小於0.3,則賦給它一個負標簽。既不是正或也不是負的錨點沒有給訓練目標做任何貢獻。
根據這些定義,參照Fast R-CNN中的多任務損失最小化目標函數。一張圖像的損失函數定義為:
$L(\{p_{i}\},\{t_{i}\})=\frac{1}{N_{cls}}\sum_{i}L_{cls}(p_{i},p_{i}^*)+λ\frac{1}{N_{reg}}\sum_{i}p_{i}^*L_{reg}(t_{i},t_{i}^*)$. (1)
這里的$i$在一個批量中一個錨點的索引,$p_{i}$是錨點$i$是一個目標的概率。如果錨點是正的,標簽$p_{i}^{*}$是1,如果是負的則為0。$t_{i}$是代表邊框坐標4個參數的向量,$t_{i}^{*}$是與正錨點相關的真值。$p_{i}^{*}$用於只計算正錨點框損失,不計算負的。
RPN代碼示例
def rpn_graph(feature_map, anchors_per_location, anchor_stride): """Builds the computation graph of Region Proposal Network. feature_map: backbone features [batch, height, width, depth] anchors_per_location: number of anchors per pixel in the feature map anchor_stride: Controls the density of anchors. Typically 1 (anchors for every pixel in the feature map), or 2 (every other pixel). Returns: rpn_logits: [batch, H, W, 2] Anchor classifier logits (before softmax) rpn_probs: [batch, H, W, 2] Anchor classifier probabilities. rpn_bbox: [batch, H, W, (dy, dx, log(dh), log(dw))] Deltas to be applied to anchors. """ # TODO: check if stride of 2 causes alignment issues if the feature map # is not even. # Shared convolutional base of the RPN shared = KL.Conv2D(512, (3, 3), padding='same', activation='relu', strides=anchor_stride, name='rpn_conv_shared')(feature_map) # Anchor Score. [batch, height, width, anchors per location * 2]. x = KL.Conv2D(2 * anchors_per_location, (1, 1), padding='valid', activation='linear', name='rpn_class_raw')(shared) # Reshape to [batch, anchors, 2] rpn_class_logits = KL.Lambda( lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 2]))(x) # Softmax on last dimension of BG/FG. rpn_probs = KL.Activation( "softmax", name="rpn_class_xxx")(rpn_class_logits) # Bounding box refinement. [batch, H, W, anchors per location, depth] # where depth is [x, y, log(w), log(h)] x = KL.Conv2D(anchors_per_location * 4, (1, 1), padding="valid", activation='linear', name='rpn_bbox_pred')(shared) # Reshape to [batch, anchors, 4] rpn_bbox = KL.Lambda(lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 4]))(x) return [rpn_class_logits, rpn_probs, rpn_bbox]