FPN(Features Pyramid Networks) 特徵金字塔網絡是從backbone CNN中提取特徵用於後續目標檢測和語義分割等問題。一個top-down結構結合lateral連接來建立所有不同尺度特徵的high-level語義特徵。
背景
(a)使用原始圖像去建立特徵金字塔,特徵相互獨立地在不同尺度上的圖像進行計算,所以非常慢,使得此方法不能用於實際的應用。
(b)近期的detection系統選擇僅僅使用單尺度的特徵來進行快速的detection,即backbone的最後輸出特徵。
(c)一個可選的方法是重用ConvNet計算的金字塔層次特徵。
(d)FPN比(b)和(c)都要快但是更準確。
上圖中,越厚的邊緣代表語義更強的特徵。
深度Convnet通過逐層計算feature hierarchy,因爲sub-sampling使得feature hierarchy本身具有金字塔的形狀,但是不同分辨率的feature-maps具有因深度引起的較大語義差距。高像素feature-map具有low-level特徵,會影響目標識別的特徵能力。SSD就使用了圖 ( c ) 的方法,爲了避免使用low-level特徵,SSD從高層開始進行金字塔的建立(e.g.conv4_3 of VGGNets)並且添加了幾個新層。這種方法錯過了高分辨率feature-map的使用,作者認爲這些高分辨率feature-map對於檢測small目標是重要的。
FPN的目標是特徵金字塔的所有尺度特徵都具有很強的語義性。爲了完成這個目的,我們依靠一個架構來結合低分辨率,語義強的特徵與高分辨率,語義弱的特徵,通過一個top-down通路和lateral鏈接的方式,(d)所示。
相似的結構也採用了top-down和跳連,如圖2的Top,但是他們的目標是產生一個單一的high-level和fine resolution的feature map來預測。FPN對每一層的特徵都進行獨立地預測。
解釋一下fine resolution:
參考鏈接:https://www.cnblogs.com/kk17/p/9807571.html
CNN 抽取 semantic feature 導致的 coarse representation雖然具有強的語義性,但和 detect small objects 需要 fine resolution 之間的矛盾,small objects 因爲 small,很難在 coarse representation 還有很好的表示,很可能就被忽略了。Backbone的stride太大導致最終的表示是coarse resolution表示,小目標容易被忽略。
FPN方法
給定一個單尺度的圖像,輸出的不同level features的size是成比例的。這個處理過程是和backbone無關的,本文以ResNet爲例。如上的構建涉及3個部分:top-down通路,bottom-up通路。lateral連接。
bottom-up通路
就是圖中左側一個ConvNet前向計算的過程,產生了不同尺度的feature map。就ResNet來說,每一個階段最後一個殘差塊的輸出爲:分別對應的Conv2,Conv3,Conv4,Conv5。作者沒有包括Conv1的feature map,由於佔用內存太大。
top-down通路和lateral連接
top-down通路是右側自上而下的通路,該通路逐漸升採樣spatially coarser但是語義stronger的特徵。這些特徵會通過lateral連接將bottom-up的特徵傳來進行merge。bottom-up傳來的特徵是low-level的語義,但是其激活能夠更準確地定位因爲被subsample的次數更少。
虛擬框展示了block來產生top-down特徵。對於一個coarser-resolution feature map,首先升採樣到2倍(使用了最近鄰插值for simplicity)。之中升採樣的特徵需要被merge到對應的bottom-up特徵(經歷一個1x1卷積層來減少channel)通過element-wise addition。這個過程需要不斷迭代直到finest resolution map產生。
迭代開始前,使用一個1x1卷積層在來產生coarsest resolution map。使用一個3x3卷積對於merge之後的特徵圖來減少升採樣的重疊效應。最終的特徵集對應的spatial size。
因爲每一層的特徵是使用shared 分類器和迴歸器,作者固定每個feature map的channel維度,因此其他的多餘的卷積也是256-channel輸出。這些層都沒有非線性,實驗上有微小的影響。
代碼:
使用代碼來解析上述的過程:
# Bottom-up
self.RCNN_layer0 = nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool)
self.RCNN_layer1 = nn.Sequential(resnet.layer1)
self.RCNN_layer2 = nn.Sequential(resnet.layer2)
self.RCNN_layer3 = nn.Sequential(resnet.layer3)
self.RCNN_layer4 = nn.Sequential(resnet.layer4)
c1 = self.RCNN_layer0(im_data)
c2 = self.RCNN_layer1(c1)
c3 = self.RCNN_layer2(c2)
c4 = self.RCNN_layer3(c3)
c5 = self.RCNN_layer4(c4)
# Top layer
self.RCNN_toplayer = nn.Conv2d(2048, 256, kernel_size=1, stride=1, padding=0) # reduce channel
# Smooth layers
self.RCNN_smooth1 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.RCNN_smooth2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.RCNN_smooth3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
# Lateral layers
self.RCNN_latlayer1 = nn.Conv2d(1024, 256, kernel_size=1, stride=1, padding=0)
self.RCNN_latlayer2 = nn.Conv2d( 512, 256, kernel_size=1, stride=1, padding=0)
self.RCNN_latlayer3 = nn.Conv2d( 256, 256, kernel_size=1, stride=1, padding=0)
def _upsample_add(self, x, y):
_,_,H,W = y.size()
return F.upsample(x, size=(H,W), mode='bilinear') + y
# Top-down
p5 = self.RCNN_toplayer(c5)
p4 = self._upsample_add(p5, self.RCNN_latlayer1(c4))
p4 = self.RCNN_smooth1(p4)
p3 = self._upsample_add(p4, self.RCNN_latlayer2(c3))
p3 = self.RCNN_smooth2(p3)
p2 = self._upsample_add(p3, self.RCNN_latlayer3(c2))
p2 = self.RCNN_smooth3(p2)