- 論文背景
- 引言(Introduction)
- 特征金字塔網路 FPN
- FPN網路建立
- Anchor錨框生成規則
- 實驗
- 代碼解讀
- 參考資料
本篇文章是論文閱讀筆記和網路理解心得總結而來,部分資料和圖參考論文和網路資料
論文背景
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
參考資料
- FPN(feature pyramid networks)演算法講解
- Mask RCNN 源代碼決議 (1) - 整體思路
- Mask RCNN 學習筆記
- 論文 - Feature Pyramid Networks for Object Detection (FPN)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/540148.html
標籤:其他
