FPN详述
簡介
為了使用更多的語義信息,目標檢測模型一般在卷積神經網絡最后一層的特征圖上進行后續操作(隨著不斷地下采樣,語義信息更豐富,空間信息更稀少),而這一層對應的下采樣率一般是比較大的,如16或者32。這就造成原始圖像上地小目標在特征圖上有效信息較少,小物體檢測性能急劇下降,這就是目標檢測中的多尺度問題,這個問題很直觀,小目標在原圖中對應的像素本就不多,下采樣后很難找到對應信息。
為了解決多尺度問題,關鍵在于如何獲取多尺度的特征,從數據準備的角度出發有圖像金字塔這一策略,它的想法就是將圖像調整為多個尺度,不同尺度圖像對應不同的特征,這個思路的缺點就是計算量非常之大。不過,它也為后來的一些方法提供了思考,要知道,卷積神經網絡層層遞進,尺度和語義信息不斷變化的過程就是一個金字塔結構,如何能夠有效融合不同層卷積生成的特征圖,應該可以較好地改善多尺度檢測問題,FPN(Feature Pyramid Network)就是以此為出發點誕生的工作,極大地推動了后來目標檢測的發展。
-
論文標題
Feature Pyramid Networks for Object Detection
-
論文地址
http://arxiv.org/abs/1612.03144
-
論文源碼
https://github.com/jwyang/fpn.pytorch(非官方)
網絡結構
FPN的結構如下圖所示,主要有自下而上、自上而下、橫向連接和卷積融合四個操作流程,我們一個個來看。不過,在此之前先對下圖做一些必要的說明,圖中最左側的一列表示輸入圖像經過多個stage的卷積層不斷下采樣修改特征圖的過程,其中C1為最淺幾層,空間尺度大語義信息極少,一般不做考慮。
首先,來看最左側的自下而上的操作,這個流是一個普通的ResNet網絡,用來提取語義信息,C1至C5代表網絡的5個stage(即不同的卷積層),每個stage內部特征圖尺寸不變,從C1到C5特征圖尺寸遞減。由這個卷積網絡提取特征的過程,就是自下而上的過程。
接著,我們來看中間那個自上而下操作,首先,對最小的特征圖進行1x1卷積降維通道得到P5,然后對P5依此進行上采樣得到P4、P3和P2,這個步驟是為了保證P4、P3和P2與C4、C3和C2長寬相同,以方便后面的逐元素相加。需要注意的是,這里的上采樣實際上是最近鄰上采樣(臨近元素復制),而不是線性插值。
然后,就是所謂的橫向連接(Lateral Connection),這個步驟的目的就是將上采樣后的高語義特征與下采樣之前對應大小的定位細節特征進行融合。不過,上采樣的尺寸雖然和淺層特征圖一樣,但是通道數是不同的,因此為了相加,需要將C2、C3和C4通過1x1卷積調整和P2、P3和P4通道數目一致,均為256。繼而逐元素相加得到真正的P2、P3和P4,這個過程就叫做橫向連接。
最后就是卷積融合的過程,得到相加的P2、P3和P4之后,再通過3x3卷積對這三個特征圖進行變換以消除上采樣過程帶來的重疊效應,生成最終的P2、P3和P4,它們與P5一同輸出進行后續任務。
至此,FPN網絡結構最核心的部分就講完了,不過實際目標檢測需要在特征圖上進行RoI特征提取(通過anchor等),而FPN有四個特征圖,因此選用哪個就是值得考慮的。FPPN的方案是,對于不同尺寸的RoI區域,使用不同的特征圖,大尺寸RoI需要更加精細的語義信息,即使深層特征圖也會有較多信息保留,如在P5上進行;而小尺寸RoI因為尺寸小,高語義信息很難定位,因此采樣淺層特征圖,如P2。
實驗
在檢測網絡中,將neck換為FPN,獲得了非常恐怖的效果提升,包括后來的anchor-free的發展,也與FPN的成功是分不開的。
代碼實現
FPN實現起來不是很難,很多工具箱都做了封裝,這里就給出一個比較使用的PyTorch實現版本。
import torch.nn as nn import torch.nn.functional as Fclass Bottleneck(nn.Module):expansion = 4def __init__(self, in_planes, planes, stride=1, downsample=None):super(Bottleneck, self).__init__()self.bottleneck = nn.Sequential(nn.Conv2d(in_planes, planes, 1, bias=False),nn.BatchNorm2d(planes),nn.ReLU(inplace=True),nn.Conv2d(planes, planes, 3, stride, 1, bias=False),nn.BatchNorm2d(planes),nn.ReLU(inplace=True),nn.Conv2d(planes, self.expansion * planes, 1, bias=False),nn.BatchNorm2d(self.expansion * planes),)self.relu = nn.ReLU(inplace=True)self.downsample = downsampledef forward(self, x):identity = xout = self.bottleneck(x)if self.downsample is not None:identity = self.downsample(x)out += identityout = self.relu(out)return outclass FPN(nn.Module):def __init__(self, layers):super(FPN, self).__init__()self.inplanes = 64self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False)self.bn1 = nn.BatchNorm2d(64)self.relu = nn.ReLU(inplace=True)self.maxpool = nn.MaxPool2d(3, 2, 1)self.layer1 = self._make_layer(64, layers[0])self.layer2 = self._make_layer(128, layers[1], 2)self.layer3 = self._make_layer(256, layers[2], 2)self.layer4 = self._make_layer(512, layers[3], 2)self.toplayer = nn.Conv2d(2048, 256, 1, 1, 0)self.smooth1 = nn.Conv2d(256, 256, 3, 1, 1)self.smooth2 = nn.Conv2d(256, 256, 3, 1, 1)self.smooth3 = nn.Conv2d(256, 256, 3, 1, 1)self.latlayer1 = nn.Conv2d(1024, 256, 1, 1, 0)self.latlayer2 = nn.Conv2d(512, 256, 1, 1, 0)self.latlayer3 = nn.Conv2d(256, 256, 1, 1, 0)def _make_layer(self, planes, blocks, stride=1):downsample = Noneif stride != 1 or self.inplanes != Bottleneck.expansion * planes:downsample = nn.Sequential(nn.Conv2d(self.inplanes, Bottleneck.expansion * planes, 1, stride, bias=False),nn.BatchNorm2d(Bottleneck.expansion * planes))layers = []layers.append(Bottleneck(self.inplanes, planes, stride, downsample))self.inplanes = planes * Bottleneck.expansionfor i in range(1, blocks):layers.append(Bottleneck(self.inplanes, planes))return nn.Sequential(*layers)def _upsample_add(self, x, y):_, _, H, W = y.shapereturn F.upsample(x, size=(H, W), mode='bilinear') + ydef forward(self, x):c1 = self.maxpool(self.relu(self.bn1(self.conv1(x))))c2 = self.layer1(c1)c3 = self.layer2(c2)c4 = self.layer3(c3)c5 = self.layer4(c4)p5 = self.toplayer(c5)p4 = self._upsample_add(p5, self.latlayer1(c4))p3 = self._upsample_add(p4, self.latlayer2(c3))p2 = self._upsample_add(p3, self.latlayer3(c2))p4 = self.smooth1(p4)p3 = self.smooth2(p3)p2 = self.smooth3(p2)return p2, p3, p4, p5if __name__ == '__main__':fpn = FPN([3, 4, 6, 3])print(fpn)總結
FPN通過金字塔結構將深層語義信息回傳補充淺層語義信息,從而獲得了高分辨率、強語義的多層特征,在小目標檢測、實例分割領域有著不俗的表現。本文涉及的代碼都可以在我的Github找到,歡迎star或者fork。
總結
- 上一篇: TrackFormer解读
- 下一篇: GFocal解读