初識 FPN
FPN 全稱 Feature Pyramid Network,翻譯過來就是特徵金字塔網絡。何為特徵金字塔,深度卷積神經網絡(DCNN)提取的
不同尺度特徵組成的金字塔形狀。本文提出了一種新型的特徵融合方式,雖然距離論文提出的時間比較久了,但直到現在該結構仍較常用,尤其是在檢測小目標時值得一試。本篇論文的目的是為了合理利用特徵金字塔中不同尺度的語義信息。實際上在本篇文章之前,已經有很多特徵融合的方式,本文開篇就介紹了各種多尺度特徵的融合方式:
- (a) Featurized image pyramid,為了獲取不同尺度的特徵,這種方式需要將同一張圖片的不同尺寸分別輸入網絡,分別計算對應的 feature map 並預測結果,這種方式雖然可以提升預測精度但計算資源消耗太大,在實際工業應用中不太現實。
- (b) Single feature map,分類任務常用的網絡結構,深層特徵包含了豐富的語義信息適用於分類任務,由於分類任務對目標的位置信息並不敏感所以富含位置信息的淺層特徵沒用被再次使用,而這種結構也導致了分類網絡對小目標的檢測精度並不高。
- (c) Pyramid feature hierarchy,SSD 的多尺度特徵應用方式,在不同尺度的特徵上進行預測。關於這種方式作者在文中專門說了一段兒,意思是 SSD 中應用的淺層特徵還不夠"淺",而作者發現更淺層的特徵對檢測小目標來說非常重要。
- (d) Feature Pyramid Network,本篇的主角,一種新的特徵融合方式,在兼顧速度的同時提高了準確率,下面會介紹細節。
- (e) U-net 所採用的結構,與 (d) 的整體結構類似,但只在最後一層進行預測。
FPN 結構細節
FPN 的結構較為簡單,可以概括為:特徵提取,上採樣,特徵融合,多尺度特徵輸出。FPN 的輸入為任意大小的圖片,輸出為各尺度的 feature map。與 U-net 類似, FPN 的整個網絡結構分為自底向上 (Bottom-Up) 和自頂向下 (Top-Down) 兩個部分,Bottom-Up 是特徵提取過程,對應 Unet 中的 Encoder 部分,文中以 Resnet 作為 backbone,其中使用的 bottleneck 結構:
Top-Down 將最深層的特徵通過層層的上採樣,採樣至與 Bottom-Up 輸出對應的分辨率大小,與之融合後輸出 feature map,融合方式為對應位置相加,而 Unet 採用的融合方式為對應位置拼接,關於兩者的差異我之前在 Unet 這篇文章中提過,這裡就不再贅述。在下圖中放大的部分中,包含了 3 個步驟:1. 對上層輸出進行 2 倍的上採樣,2. 對 Bottom-Up 中與之對應的 feature map 的進行 1x1 卷積,以保證特徵 channels 相同,3. 將上面兩步的結果相加。
以上就是 FPN 的基本結構了,簡單且有效,這也符合何凱明大神一貫的作風,下面介紹代碼實現過程。
代碼實現
FPN 結構比較簡單且文中說明的很清楚,大家有空可以自己實現一下。下面是文章中對網絡結構的敘述以及 Pytorch 版本的實現,歡迎留言討論。
- Bottom-Up
This process is independent of the backbone convolutional architectures, and in this paper we present results using ResNets.
文中選擇 Resnet 作為 Bottom-Up,直接把 torchvision 中的 Resnet 拿來用:
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU(inplace=True) self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # Bottom-up stages self.layer1 = self._make_layer(block, 64, layers[0], stride=1) self.layer2 = self._make_layer(block, 128, layers[1], stride=2) self.layer3 = self._make_layer(block, 256, layers[2], stride=2) self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
- Top layer
To start the iteration, we simply attach a 1×1 convolutional layer on C5 to produce the coarsest resolution map.
We set d = 256 in this paper and thus all extra convolutional layers have 256-channel outputs.
對 C5(layer4 的輸出) 進行 1x1 的卷積確保特徵金字塔的每一層都是 256 個 channels。
self.toplayer = conv1x1(2048, 256)
- Top-Down
With a coarser-resolution feature map, we upsample the spatial resolution by a factor of 2 (using nearest neighbor upsampling for simplicity).
每次上採樣的倍數為 2,且使用 nearest 插值。
F.upsample(x, size=(H,W), mode='nearest')
The upsam3 pled map is then merged with the corresponding bottom-up map (which undergoes a 1×1 convolutional layer to reduce channel dimensions) by element-wise addition.
Bottom-Up 輸出的 C2,C3,C4 都需要進行 1x1 的卷積確保特徵金字塔的每一層都是 256 個 channels。
self.laterallayer1 = conv1x1(1024, 256) self.laterallayer2 = conv1x1( 512, 256) self.laterallayer3 = conv1x1( 256, 256)
Finally, we append a 3×3 convolution on each merged map to generate the final feature map, which is to reduce the aliasing effect of upsampling.
最終還需要一個 3x3 的卷積才能得到最後的 feature map,此舉是為了減小上採樣的影響。
# Final conv layers self.finalconv1 = conv3x3(256, 256) self.finalconv2 = conv3x3(256, 256) self.finalconv3 = conv3x3(256, 256)
至此,要用的基本模塊都有了,那麼整個前向傳播的過程:
def forward(self, x): # Bottom-Up c1 = self.relu(self.bn1(self.conv1(x))) c1 = self.maxpool(c1) c2 = self.layer1(c1) c3 = self.layer2(c2) c4 = self.layer3(c3) c5 = self.layer4(c4) # Top layer && Top-Down p5 = self.toplayer(c5) p4 = self._upsample_add(p5, self.laterallayer1(c4)) p3 = self._upsample_add(p4, self.laterallayer2(c3)) p2 = self._upsample_add(p3, self.laterallayer3(c2)) # Final conv layers p4 = self.finalconv1(p4) p3 = self.finalconv2(p3) p2 = self.finalconv3(p2) return p2, p3, p4, p5
論文中是將 FPN 作為一個結構嵌入到 Fast R-CNN 等網絡中來提升網絡的表現,那麼可否將 FPN 直接用於語義分割任務?答案是可以,一個思路是將 FPN 輸出的所有 feature map 相加為 1 層,上採樣至原圖分辨率可得輸出,也有不錯的效果。
以上代碼已經放在我的 github,歡迎 star:https://github.com/FroyoZzz/CV-Papers-Codes
最後,歡迎關注我的個人微信公眾號 [MachineLearning學習之路],CV 方向的童鞋不要錯過!