本篇文章是論文閱讀筆記和網路理解心得總結而來,部分資料和圖參考論文和網路資料
論文背景
FPN
(feature pyramid networks) 是何凱明等作者提出的適用於多尺度目標檢測演算法。原來多數的 object detection 演算法(比如 faster rcnn)都是隻採用頂層特徵做預測,但我們知道低層的特徵語義資訊比較少,但是目標位置準確;高層的特徵語義資訊比較豐富,但是目標位置比較粗略。另外雖然也有些演算法採用多尺度特徵融合的方式,但是一般是採用融合後的特徵做預測,而本文不一樣的地方在於預測是在不同特徵層獨立進行的。
引言(Introduction)
從上圖可以看出,(a)使用影像金字塔構建特徵金字塔。每個影像尺度上的特徵都是獨立計算的,速度很慢。(b)最近的檢測系統選擇(比如 Faster RCNN)只使用單一尺度特徵進行更快的檢測。(c)另一種方法是重用 ConvNet(卷積層)計算的金字塔特徵層次結構(比如 SSD),就好像它是一個特徵化的影像金字塔。(d)我們提出的特徵金字塔網路(FPN)與(b)和(c)類似,但更準確。在該圖中,特徵對映用藍色輪廓表示,較粗的輪廓表示語義上較強的特徵。
特徵金字塔網路 FPN
作者提出的 FPN
結構如下圖:這個金字塔結構包括一個自底向上的線路,一個自頂向下的線路和橫向連線(lateral connections)。
自底向上其實就是卷積網路的前向過程。在前向過程中,feature map
的大小在經過某些層後會改變,而在經過其他一些層的時候不會改變,作者將不改變 feature map 大小的層歸為一個 stage,因此這裡金字塔結構中每次抽取的特徵都是每個 stage
的最後一個層的輸出。在程式碼中我們可以看到共有C1、C2、C3、C4、C5
五個特徵圖,C1
和 C2
的特徵圖大小是一樣的,所以,FPN
的建立也是基於從 C2
到 C5
這四個特徵層上。
自頂向下的過程採用上取樣(upsampling
)進行,而橫向連線則是將上取樣的結果和自底向上生成的相同大小的 feature map
進行融合(merge)。在融合之後還會再採用 3*3
的卷積核對每個融合結果進行卷積,目的是消除上取樣的混疊效應(aliasing effect)。並假設生成的 feature map 結果是 P2,P3,P4,P5,和原來自底向上的卷積結果 C2,C3,C4,C5一一對應。
這裡貼一個 ResNet
的結構圖:論文中作者採用 conv2_x,conv3_x,conv4_x 和 conv5_x
的輸出,對應 C1,C2,C3,C4,C5
,因此類似 Conv2
就可以看做一個stage。
FPN網路建立
這裡自己沒有總結,因為已經有篇博文總結得很不錯了,在這。
透過 ResNet50 網路,得到圖片不同階段的特徵圖,最後利用 C2,C3,C4,C5 建立特徵圖金字塔結構:
- 將 C5 經過 256 個 1*1 的卷積核操作得到:32*32*256,記為 P5;
- 將 P5 進行步長為 2 的上取樣得到 64*64*256,再與 C4 經過的 256 個 1*1 卷積核操作得到的結果相加,得到 64*64*256,記為 P4;
- 將 P4 進行步長為 2 的上取樣得到 128*128*256,再與 C3 經過的 256 個 1*1 卷積核操作得到的結果相加,得到 128*128*256,記為 P3;
- 將 P3 進行步長為 2 的上取樣得到 256*256*256,再與 C2 經過的 256 個 1*1 卷積核操作得到的結果相加,得到 256*256*256,記為 P2;
- 將 P5 進行步長為 2 的最大池化操作得到:16*16*256,記為 P6;
結合從 P2 到 P6 特徵圖的大小,如果原圖大小 1024*1024, 那各個特徵圖對應到原圖的步長
依次為 [P2,P3,P4,P5,P6]=>[4,8,16,32,64]。
Anchor錨框生成規則
當 Faster RCNN
採用 FPN
的網路作 backbone 後,錨框的生成規則也會有所改變。基於上一步得到的特徵圖 [P2,P3,P4,P5,P6],再介紹下采用 FPN
的 Faster RCNN(或者 Mask RCNN)網路中 Anchor 錨框的生成,根據原始碼中介紹的規則,與之前 Faster-RCNN 中的生成規則有一點差別。
- 遍歷
P2 到 P6
這五個特徵層,以每個特徵圖上的每個畫素點都生成Anchor
錨框; - 以 P2 層為例,P2 層的特徵圖大小為 256*256,相對於原圖的步長為4,這樣 P2上的每個畫素點都可以生成一個基於座標陣列 [0,0,3,3] 即 4*4 面積為 16 大小的Anchor錨框,當然,可以設定一個比例 SCALE,將這個基礎的錨框放大或者縮小,比如,這裡設定 P2 層對應的縮放比例為 16,那邊生成的錨框大小就是長和寬都擴大16倍,從 4*4 變成 64*64,面積從 16 變成 4096,當然在保證面積不變的前提下,長寬比可以變換為 32*128、64*64 或 128*32,這樣以長、寬比率 RATIO = [0.5,1,2] 完成了三種變換,這樣一個畫素點都可以生成3個Anchor錨框。在 Faster-RCNN 中可以將
Anchor scale
也可以設定為多個值,而在MasK RCNN 中則是每一特徵層只對應著一個Anchor scale
即對應著上述所設定的 16; - 以
P2
層每個畫素點位中心,對應到原圖上,則可生成 256*256*3(長寬三種變換) = 196608 個錨框; - 以
P3
層每個畫素點為中心,對應到原圖上,則可生成 128*128*3 = 49152 個錨框; - 以
P4
層每個畫素點為中心,對應到原圖上,則可生成 64*64*3 = 12288 個錨框; - 以
P5
層每個畫素點為中心,對應到原圖上,則生成 32*32*3 = 3072 個錨框; - 以
P6
層每個畫素點為中心,對應到原圖上,則生成 16*16*3 = 768 個錨框。
從 P2 到 P6 層一共可以在原圖上生成 \(196608 + 49152 + 12288 + 3072 + 768 = 261888\) 個 Anchor
錨框。
實驗
看看加入FPN 的 RPN 網路的有效性,如下表 Table1。網路這些結果都是基於 ResNet-50。評價標準採用 AR,AR 表示 Average Recall,AR 右上角的 100 表示每張影像有 100 個 anchor,AR 的右下角 s,m,l 表示 COCO 資料集中 object 的大小分別是小,中,大。feature 列的大括號 {} 表示每層獨立預測。
從(a)(b)(c)的對比可以看出 FPN
的作用確實很明顯。另外(a)和(b)的對比可以看出高層特徵並非比低一層的特徵有效。
(d)表示只有橫向連線,而沒有自頂向下的過程,也就是僅僅對自底向上(bottom-up)的每一層結果做一個 1*1 的橫向連線和 3*3 的卷積得到最終的結果,有點像 Fig1
的(b)。從 feature 列可以看出預測還是分層獨立的。作者推測(d)的結果並不好的原因在於在自底向上的不同層之間的 semantic gaps 比較大。
(e)表示有自頂向下的過程,但是沒有橫向連線,即向下過程沒有融合原來的特徵。這樣效果也不好的原因在於目標的 location 特徵在經過多次降取樣和上取樣過程後變得更加不準確。
(f)採用 finest level 層做預測(參考 Fig2 的上面那個結構),即經過多次特徵上取樣和融合到最後一步生成的特徵用於預測,主要是證明金字塔分層獨立預測的表達能力。顯然 finest level 的效果不如 FPN 好,原因在於 PRN 網路是一個視窗大小固定的滑動視窗檢測器,因此在金字塔的不同層滑動可以增加其對尺度變化的魯棒性。另外(f)有更多的 anchor,說明增加 anchor 的數量並不能有效提高準確率。
程式碼解讀
這裡給出一個基於 Pytorch
的 FPN
網路的程式碼,來自這裡。
## ResNet的block
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, in_planes, planes, stride=1):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, self.expansion*planes, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(self.expansion*planes)
self.shortcut = nn.Sequential()
if stride != 1 or in_planes != self.expansion*planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion*planes)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += self.shortcut(x)
out = F.relu(out)
return out
class FPN(nn.Module):
def __init__(self, block, num_blocks):
super(FPN, self).__init__()
self.in_planes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
# Bottom-up layers, backbone of the network
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
# Top layer
# 我們需要在C5後面接一個1x1, 256 conv,得到金字塔最頂端的feature
self.toplayer = nn.Conv2d(2048, 256, kernel_size=1, stride=1, padding=0) # Reduce channels
# Smooth layers
# 這個是上面引文中提到的抗aliasing的3x3卷積
self.smooth1 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.smooth2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.smooth3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
# Lateral layers
# 為了匹配channel dimension引入的1x1卷積
# 注意這些backbone之外的extra conv,輸出都是256 channel
self.latlayer1 = nn.Conv2d(1024, 256, kernel_size=1, stride=1, padding=0)
self.latlayer2 = nn.Conv2d( 512, 256, kernel_size=1, stride=1, padding=0)
self.latlayer3 = nn.Conv2d( 256, 256, kernel_size=1, stride=1, padding=0)
def _make_layer(self, block, planes, num_blocks, stride):
strides = [stride] + [1]*(num_blocks-1)
layers = []
for stride in strides:
layers.append(block(self.in_planes, planes, stride))
self.in_planes = planes * block.expansion
return nn.Sequential(*layers)
## FPN的lateral connection部分: upsample以後,element-wise相加
def _upsample_add(self, x, y):
'''Upsample and add two feature maps.
Args:
x: (Variable) top feature map to be upsampled.
y: (Variable) lateral feature map.
Returns:
(Variable) added feature map.
Note in PyTorch, when input size is odd, the upsampled feature map
with `F.upsample(..., scale_factor=2, mode='nearest')`
maybe not equal to the lateral feature map size.
e.g.
original input size: [N,_,15,15] ->
conv2d feature map size: [N,_,8,8] ->
upsampled feature map size: [N,_,16,16]
So we choose bilinear upsample which supports arbitrary output sizes.
'''
_,_,H,W = y.size()
return F.upsample(x, size=(H,W), mode='bilinear') + y
def forward(self, x):
# Bottom-up
c1 = F.relu(self.bn1(self.conv1(x)))
c1 = F.max_pool2d(c1, kernel_size=3, stride=2, padding=1)
c2 = self.layer1(c1)
c3 = self.layer2(c2)
c4 = self.layer3(c3)
c5 = self.layer4(c4)
# Top-down
# P5: 金字塔最頂上的feature
p5 = self.toplayer(c5)
# P4: 上一層 p5 + 側邊來的 c4
# 其餘同理
p4 = self._upsample_add(p5, self.latlayer1(c4))
p3 = self._upsample_add(p4, self.latlayer2(c3))
p2 = self._upsample_add(p3, self.latlayer3(c2))
# Smooth
# 輸出做一下smooth
p4 = self.smooth1(p4)
p3 = self.smooth2(p3)
p2 = self.smooth3(p2)
return p2, p3, p4, p5