『計算機視覺』FPN:feature pyramid networks for object detection

對用卷積神經網絡進行目標檢測方法的一種改進,經過提取多尺度的特徵信息進行融合,進而提升目標檢測的精度,特別是在小物體檢測上的精度。FPN是ResNet或DenseNet等通用特徵提取網絡的附加組件,能夠和經典網絡組合提高原網絡效果。html

1、問題背景

網絡的深度(對應到感覺野)與總stride一般是一對矛盾的東西,經常使用的網絡結構對應的總stride通常會比較大(如32),而圖像中的小物體甚至會小於stride的大小,形成的結果就是小物體的檢測性能急劇降低。python

傳統解決這個問題的思路包括:git

(1)多尺度訓練和測試,又稱圖像金字塔,以下圖(a)所示。目前幾乎全部在ImageNet和COCO檢測任務上取得好成績的方法都使用了圖像金字塔方法。然而這樣的方法因爲很高的時間及計算量消耗,難以在實際中應用。github

(2)特徵分層,即每層分別預測對應的scale分辨率的檢測結果。以下圖(c)所示。SSD檢測框架採用了相似的思想。這樣的方法問題在於直接強行讓不一樣層學習一樣的語義信息。而對於卷積神經網絡而言,不一樣深度對應着不一樣層次的語義特徵,淺層網絡分辨率高,學的更可能是細節特徵,深層網絡分辨率低,學的更可能是語義特徵。算法

 

於是,目前多尺度的物體檢測主要面臨的挑戰爲:網絡

1. 如何學習具備強語義信息的多尺度特徵表示?app

2. 如何設計通用的特徵表示來解決物體檢測中的多個子問題?如object proposal, box localization, instance segmentation.框架

3. 如何高效計算多尺度的特徵表示?ide

2、特徵金字塔網絡(Feature Pyramid Networks)

做者提出了FPN算法。作法很簡單,以下圖所示。把低分辨率、高語義信息的高層特徵和高分辨率、低語義信息的低層特徵進行自上而下的側邊鏈接,使得全部尺度下的特徵都有豐富的語義信息。post

圖中未註明的是融合以後的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一一對應。金字塔結構中全部層級共享分類層(迴歸層)。

3、fast rcnn中的特徵金字塔

Fast rcnn中的ROI Pooling層使用region proposal的結果和特徵圖做爲輸入。通過特徵金字塔,咱們獲得了許多特徵圖,做者認爲,不一樣層次的特徵圖上包含的物體大小也不一樣,所以,不一樣尺度的ROI,使用不一樣特徵層做爲ROI pooling層的輸入。大尺度ROI就用後面一些的金字塔層,好比P5;小尺度ROI就用前面一點的特徵層,好比P4。可是如何肯定不一樣的roi對應的不一樣特徵層呢?做者提出了一種方法:640?wx_fmt=png,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。

    • 640?wx_fmt=png

    • 其中w,h分別表示寬度和高度。k是分配RoI的level。640?wx_fmt=png是w,h=224,224時映射的level。

4、其餘問題

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能夠在幾乎不增長額外計算量狀況下解決多尺度檢測問題。

5、代碼層面看FPN

本部分截取自知乎文章:從代碼細節理解 FPN,做者使用Mask-RCNN的源碼輔助理解FPN結構,項目地址見MRCNN,關於MRCNN,文章『計算機視覺』RCNN學習_其三:Mask-RCNN會介紹。

一、 怎麼作的上採樣?

高層特徵怎麼上採樣和下一層的特徵融合的,代碼裏面能夠看到:

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 說明告訴咱們:

也就是說,這裏的實現使用的是最簡單的上採樣,沒有使用線性插值,沒有使用反捲積,而是直接複製。

二、 怎麼作的橫向鏈接?

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主幹網絡,更穩定;組合特徵。

三、 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的卷積,做用是消除上採樣帶來的混疊效應。

四、 如何肯定某個 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個融合了不一樣層級的特徵圖怎麼使用?

能夠看到,這裏只使用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。

六、 金字塔結構中全部層級共享分類層是怎麼回事?

先看代碼:

# 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中全部層的語義都類似。

七、 它的思想是什麼?

把高層的特徵傳下來,補充低層的語義,這樣就能夠得到高分辨率、強語義的特徵,有利於小目標的檢測。

八、 橫向鏈接起什麼做用?

若是不進行特徵的融合(也就是說去掉全部的1x1側鏈接),雖然理論上分辨率沒變,語義也加強了,可是AR降低了10%左右!做者認爲這些特徵上下采樣太屢次了,致使它們不適於定位。 Bottom-up的特徵包含了更精確的位置信息。

6、資源資料

Feature Pyramid Networks for Object DetectionCVPR 2017論文)

知乎:特徵金字塔網絡FPN

知乎:從代碼細節理解 FPN

FPN特徵金字塔網絡--論文解讀

詳解何愷明團隊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

相關文章
相關標籤/搜索