對用卷積神經網絡進行目標檢測方法的一種改進,通過提取多尺度的特征信息進行融合,進而提高目標檢測的精度,特別是在小物體檢測上的精度。FPN是ResNet或DenseNet等通用特征提取網絡的附加組件,可以和經典網絡組合提升原網絡效果。
一、問題背景
網絡的深度(對應到感受野)與總stride通常是一對矛盾的東西,常用的網絡結構對應的總stride一般會比較大(如32),而圖像中的小物體甚至會小於stride的大小,造成的結果就是小物體的檢測性能急劇下降。
傳統解決這個問題的思路包括:
(1)多尺度訓練和測試,又稱圖像金字塔,如下圖(a)所示。目前幾乎所有在ImageNet和COCO檢測任務上取得好成績的方法都使用了圖像金字塔方法。然而這樣的方法由於很高的時間及計算量消耗,難以在實際中應用。
(2)特征分層,即每層分別預測對應的scale分辨率的檢測結果。如下圖(c)所示。SSD檢測框架采用了類似的思想。這樣的方法問題在於直接強行讓不同層學習同樣的語義信息。而對於卷積神經網絡而言,不同深度對應着不同層次的語義特征,淺層網絡分辨率高,學的更多是細節特征,深層網絡分辨率低,學的更多是語義特征。
因而,目前多尺度的物體檢測主要面臨的挑戰為:
1. 如何學習具有強語義信息的多尺度特征表示?
2. 如何設計通用的特征表示來解決物體檢測中的多個子問題?如object proposal, box localization, instance segmentation.
3. 如何高效計算多尺度的特征表示?
二、特征金字塔網絡(Feature Pyramid Networks)
作者提出了FPN算法。做法很簡單,如下圖所示。把低分辨率、高語義信息的高層特征和高分辨率、低語義信息的低層特征進行自上而下的側邊連接,使得所有尺度下的特征都有豐富的語義信息。
圖中未注明的是融合之后的feat還需要進行一次3*3卷積
作者的算法結構可以分為三個部分:自下而上的卷積神經網絡(上圖左),自上而下過程(上圖右)和特征與特征之間的側邊連接。
自下而上的部分其實就是卷積神經網絡的前向過程。在前向過程中,特征圖的大小在經過某些層后會改變,而在經過其他一些層的時候不會改變,作者將不改變特征圖大小的層歸為一個階段,因此每次抽取的特征都是每個階段的最后一個層的輸出,這樣就能構成特征金字塔。具體來說,對於ResNets,作者使用了每個階段的最后一個殘差結構的特征激活輸出。將這些殘差模塊輸出表示為{C2, C3, C4, C5},對應於conv2,conv3,conv4和conv5的輸出。
自上而下的過程采用上采樣進行。上采樣幾乎都是采用內插值方法,即在原有圖像像素的基礎上在像素點之間采用合適的插值算法插入新的元素,從而擴大原圖像的大小。通過對特征圖進行上采樣,使得上采樣后的特征圖具有和下一層的特征圖相同的大小。
根本上來說,側邊之間的橫向連接是將上采樣的結果和自下而上生成的特征圖進行融合。我們將卷積神經網絡中生成的對應層的特征圖進行1×1的卷積操作,將之與經過上采樣的特征圖融合,得到一個新的特征圖,這個特征圖融合了不同層的特征,具有更豐富的信息。 這里1×1的卷積操作目的是改變channels,要求和后一層的channels相同。在融合之后還會再采用3*3的卷積核對每個融合結果進行卷積,目的是消除上采樣的混疊效應,如此就得到了一個新的特征圖。這樣一層一層地迭代下去,就可以得到多個新的特征圖。假設生成的特征圖結果是P2,P3,P4,P5,它們和原來自底向上的卷積結果C2,C3,C4,C5一一對應。金字塔結構中所有層級共享分類層(回歸層)。
三、fast rcnn中的特征金字塔
Fast rcnn中的ROI Pooling層使用region proposal的結果和特征圖作為輸入。經過特征金字塔,我們得到了許多特征圖,作者認為,不同層次的特征圖上包含的物體大小也不同,因此,不同尺度的ROI,使用不同特征層作為ROI pooling層的輸入。大尺度ROI就用后面一些的金字塔層,比如P5;小尺度ROI就用前面一點的特征層,比如P4。但是如何確定不同的roi對應的不同特征層呢?作者提出了一種方法:,224是ImageNet的標准輸入,k0是基准值,設置為5,代表P5層的輸出(原圖大小就用P5層),w和h是ROI區域的長和寬,假設ROI是112 * 112的大小,那么k = k0-1 = 5-1 = 4,意味着該ROI應該使用P4的特征層。k值做取整處理。這意味着如果RoI的尺度變小(比如224的1/2),那么它應該被映射到一個精細的分辨率水平。
與RPN一樣,FPN每層feature map加入3*3的卷積及兩個相鄰的1*1卷積分別做分類和回歸的預測。在RPN中,實驗對比了FPN不同層feature map卷積參數共享與否,發現共享仍然能達到很好性能,說明特征金字塔使得不同層學到了相同層次的語義特征。
-
用於RPN的FPN:用FPN替換單一尺度的FMap。它們對每個級都有一個單一尺度的anchor(不需要多級作為其FPN)。它們還表明,金字塔的所有層級都有相似的語義層級。
-
Faster RCNN:他們以類似於圖像金字塔輸出的方式觀察金字塔。因此,使用下面這個公式將RoI分配到特定level。
-
-
其中w,h分別表示寬度和高度。k是分配RoI的level。
是w,h=224,224時映射的level。
-
四、其他問題
Q1:不同深度的feature map為什么可以經過upsample后直接相加?
答:作者解釋說這個原因在於我們做了end-to-end的training,因為不同層的參數不是固定的,不同層同時給監督做end-to-end training,所以相加訓練出來的東西能夠更有效地融合淺層和深層的信息。
Q2:為什么FPN相比去掉深層特征upsample(bottom-up pyramid)對於小物體檢測提升明顯?(RPN步驟AR從30.5到44.9,Fast RCNN步驟AP從24.9到33.9)
答:作者在poster里給出了這個問題的答案
對於小物體,一方面我們需要高分辨率的feature map更多關注小區域信息,另一方面,如圖中的挎包一樣,需要更全局的信息更准確判斷挎包的存在及位置。
Q3:如果不考慮時間情況下,image pyramid是否可能會比feature pyramid的性能更高?
答:作者覺得經過精細調整訓練是可能的,但是image pyramid(金字塔)主要的問題在於時間和空間占用太大,而feature pyramid可以在幾乎不增加額外計算量情況下解決多尺度檢測問題。
五、代碼層面看FPN
本部分截取自知乎文章:從代碼細節理解 FPN,作者使用Mask-RCNN的源碼輔助理解FPN結構,項目地址見MRCNN,關於MRCNN,文章『計算機視覺』RCNN學習_其三:Mask-RCNN會介紹。
1、 怎么做的上采樣?
高層特征怎么上采樣和下一層的特征融合的,代碼里面可以看到:
P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5)
C5是 resnet最頂層的輸出,它會先通過一個1*1的卷積層,同時把通道數轉為256,得到FPN 的最上面的一層 P5。
KL.UpSampling2D(size=(2, 2),name="fpn_p5upsampled")(P5)
Keras 的 API 說明告訴我們:

也就是說,這里的實現使用的是最簡單的上采樣,沒有使用線性插值,沒有使用反卷積,而是直接復制。
2、 怎么做的橫向連接?
P4 = KL.Add(name="fpn_p4add") ([KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5), KL.Conv2D(256,(1, 1), name='fpn_c4p4')(C4)])
這里可以很明顯的看到,P4就是上采樣之后的 P5加上1*1 卷積之后的 C4,這里的橫向連接實際上就是像素加法,先把 P5和C4轉換到一樣的尺寸,再直接進行相加。
注意這里對從 resnet抽取的特征圖做的是 1*1 的卷積:
1x1的卷積我認為有三個作用:使bottom-up對應層降維至256;緩沖作用,防止梯度直接影響bottom-up主干網絡,更穩定;組合特征。
3、 FPN自上而下的網絡結構代碼怎么實現?
# 先從 resnet 抽取四個不同階段的特征圖 C2-C5。 _, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,stage5=True, train_bn=config.TRAIN_BN) # Top-down Layers 構建自上而下的網絡結構 # 從 C5開始處理,先卷積來轉換特征圖尺寸 P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5) # 上采樣之后的P5和卷積之后的 C4像素相加得到 P4,后續的過程就類似了 P4 = KL.Add(name="fpn_p4add")([ KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5), KL.Conv2D(256, (1, 1),name='fpn_c4p4')(C4)]) P3 = KL.Add(name="fpn_p3add")([ KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4), KL.Conv2D(256, (1, 1), name='fpn_c3p3')(C3)]) P2 = KL.Add(name="fpn_p2add")([ KL.UpSampling2D(size=(2, 2),name="fpn_p3upsampled")(P3), KL.Conv2D(256, (1, 1), name='fpn_c2p2')(C2)]) # P2-P5最后又做了一次3*3的卷積,作用是消除上采樣帶來的混疊效應 # Attach 3x3 conv to all P layers to get the final feature maps. P2 = KL.Conv2D(256, (3, 3), padding="SAME", name="fpn_p2")(P2) P3 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p3")(P3) P4 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p4")(P4) P5 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p5")(P5) # P6 is used for the 5th anchor scale in RPN. Generated by # subsampling from P5 with stride of 2. P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2,name="fpn_p6")(P5) # 注意 P6是用在 RPN 目標區域提取網絡里面的,而不是用在 FPN 網絡 # Note that P6 is used in RPN, but not in the classifier heads. rpn_feature_maps = [P2, P3, P4, P5, P6] # 最后得到了5個融合了不同層級特征的特征圖列表;
注意 P6是用在 RPN 目標區域提取網絡里面的,而不是用在 FPN 網絡;
另外這里 P2-P5最后又做了一次3*3的卷積,作用是消除上采樣帶來的混疊效應。
4、 如何確定某個 ROI 使用哪一層特征圖進行 ROIpooling ?
看代碼:
# Assign each ROI to a level in the pyramid based on the ROI area. # 這里的 boxes 是 ROI 的框,用來計算得到每個 ROI 框的面積 y1, x1, y2, x2 = tf.split(boxes, 4, axis=2) h = y2 - y1 w = x2 - x1 # Use shape of first image. Images in a batch must have the same size. # 這里得到原圖的尺寸,計算原圖的面積 image_shape = parse_image_meta_graph(image_meta)['image_shape'][0] # Equation 1 in the Feature Pyramid Networks paper. Account for # the fact that our coordinates are normalized here. # e.g. a 224x224 ROI (in pixels) maps to P4 # 原圖面積 image_area = tf.cast(image_shape[0] * image_shape[1], tf.float32) # 分兩步計算每個 ROI 框需要在哪個層的特征圖中進行 pooling roi_level = log2_graph(tf.sqrt(h * w) / (224.0 / tf.sqrt(image_area))) roi_level = tf.minimum(5, tf.maximum( 2, 4 + tf.cast(tf.round(roi_level), tf.int32)))
不同尺度的ROI,使用不同特征層作為ROI pooling層的輸入,大尺度ROI就用后面一些的金字塔層,比如P5;小尺度ROI就用前面一點的特征層,比如P4。那怎么判斷ROI改用那個層的輸出呢?論文的 K 使用如下公式,代碼做了一點更改,替換為roi_level:
# 代碼里面的計算替換為以下計算方式: roi_level = min(5, max(2, 4 + log2(sqrt(w * h) / ( 224 / sqrt(image_area)) ) ) )
224是ImageNet的標准輸入,k0是基准值,設置為5,代表P5層的輸出(原圖大小就用P5層),w和h是ROI區域的長和寬,image_area是輸入圖片的長乘以寬,即輸入圖片的面積,假設ROI是112 * 112的大小,那么k = k0-1 = 5-1 = 4,意味着該ROI應該使用P4的特征層。k值會做取整處理,防止結果不是整數。
5、 上面得到的5個融合了不同層級的特征圖怎么使用?
可以看到,這里只使用2-5四個特征圖:
for i, level in enumerate(range(2, 6)): # 先找出需要在第 level 層計算ROI ix = tf.where(tf.equal(roi_level, level)) level_boxes = tf.gather_nd(boxes, ix) # Box indicies for crop_and_resize. box_indices = tf.cast(ix[:, 0], tf.int32) # Keep track of which box is mapped to which level box_to_level.append(ix) # Stop gradient propogation to ROI proposals level_boxes = tf.stop_gradient(level_boxes) box_indices = tf.stop_gradient(box_indices) # Crop and Resize # From Mask R-CNN paper: "We sample four regular locations, so # that we can evaluate either max or average pooling. In fact, # interpolating only a single value at each bin center (without # pooling) is nearly as effective." # # Here we use the simplified approach of a single value per bin, # which is how it's done in tf.crop_and_resize() # Result: [batch * num_boxes, pool_height, pool_width, channels] # 使用 tf.image.crop_and_resize 進行 ROI pooling pooled.append(tf.image.crop_and_resize( feature_maps[i], level_boxes, box_indices, self.pool_shape, method="bilinear"))
對每個 box,都提取其中每一層特征圖上該box對應的特征,然后組成一個大的特征列表pooled。
6、 金字塔結構中所有層級共享分類層是怎么回事?
先看代碼:
# ROI Pooling # Shape: [batch, num_boxes, pool_height, pool_width, channels] # 得到經過 ROI pooling 之后的特征列表 x = PyramidROIAlign([pool_size, pool_size], name="roi_align_classifier")([rois, image_meta] + feature_maps) # 將上面得到的特征列表送入 2 個1024通道數的卷積層以及 2 個 rulu 激活層 # Two 1024 FC layers (implemented with Conv2D for consistency) x = KL.TimeDistributed(KL.Conv2D(1024, (pool_size, pool_size), padding="valid"), name="mrcnn_class_conv1")(x) x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn1')(x, training=train_bn) x = KL.Activation('relu')(x) x = KL.TimeDistributed(KL.Conv2D(1024, (1, 1)), name="mrcnn_class_conv2")(x) x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn2')(x, training=train_bn) x = KL.Activation('relu')(x) shared = KL.Lambda(lambda x: K.squeeze(K.squeeze(x, 3), 2), name="pool_squeeze")(x) # 分類層 # Classifier head mrcnn_class_logits = KL.TimeDistributed(KL.Dense(num_classes), name='mrcnn_class_logits')(shared) mrcnn_probs = KL.TimeDistributed(KL.Activation("softmax"), name="mrcnn_class")(mrcnn_class_logits) # BBOX 的位置偏移回歸層 # BBox head # [batch, boxes, num_classes * (dy, dx, log(dh), log(dw))] x = KL.TimeDistributed(KL.Dense(num_classes * 4, activation='linear'), name='mrcnn_bbox_fc')(shared) # Reshape to [batch, boxes, num_classes, (dy, dx, log(dh), log(dw))] s = K.int_shape(x) mrcnn_bbox = KL.Reshape((s[1], num_classes, 4), name="mrcnn_bbox")(x)
這里的PyramidROIAlign得到的 x就是上面一步得到的從每個層的特征圖上提取出來的特征列表,這里對這個特征列表先接兩個1024通道數的卷積層,再分別送入分類層和回歸層得到最終的結果。
也就是說,每個 ROI 都在P2-P5中的某一層得到了一個特征,然后送入同一個分類和回歸網絡得到最終結果。
FPN中每一層的heads 參數都是共享的,作者認為共享參數的效果也不錯就說明FPN中所有層的語義都相似。
7、 它的思想是什么?
把高層的特征傳下來,補充低層的語義,這樣就可以獲得高分辨率、強語義的特征,有利於小目標的檢測。8、 橫向連接起什么作用?
如果不進行特征的融合(也就是說去掉所有的1x1側連接),雖然理論上分辨率沒變,語義也增強了,但是AR下降了10%左右!作者認為這些特征上下采樣太多次了,導致它們不適於定位。 Bottom-up的特征包含了更精確的位置信息。六、資源資料
Feature Pyramid Networks for Object Detection(CVPR 2017論文)
詳解何愷明團隊4篇大作 | 從特征金字塔網絡、Mask R-CNN到學習分割一切
源碼資料:
-
官方:Caffe2
https://github.com/facebookresearch/Detectron/tree/master/configs/12_2017_baselines
-
Caffe
https://github.com/unsky/FPN
-
PyTorch
https://github.com/kuangliu/pytorch-fpn (just the network)
-
MXNet
https://github.com/unsky/FPN-mxnet
-
Tensorflow
https://github.com/yangxue0827/FPN_Tensorflow