一、FPN 的作用
當我們在使用卷積神經網絡的提取圖像特徵的時候,最後一個 feature map 的長寬會比原始圖片小很多,比如原始圖片大小爲 100x100,feature map 大小爲 10x10,這就說明,其實我們是在用 feature map 中的一個特徵點來表示原始圖片中一個 10x10 的像素區域。然而,在目標檢測中,我們可能要對原始圖片中的一個 1x1 的像素點中包含的物體進行檢測,這樣的話我們就很有可能將這個小物體忽略掉。於是,爲了解決多尺度檢測的問題,引入了特徵金字塔網絡,它可以在這張原始圖片的不同尺度上提取特徵,從而得到原始圖片的更多、更細節的信息。
二、 FPN 的原理
在進行多尺度檢測時,有以下幾種特徵提取的方法:
圖(a)被稱爲 featurized image pyramid,它先將原始圖片進行多尺度放縮,然後在不同尺度上提取特徵,這樣就解決了上面介紹的多尺度問題了。但是這樣相當於訓練了多個模型,計算量巨大。
圖(b)就是 CNN,有多尺度問題。
圖(c)重利用了前向過程計算出的來自多層的多尺度特徵圖,因此這種形式是不消耗額外的資源的。這種網絡就是 SSD 方法所使用的,但是 SSD 爲了避免使用低級的特徵,放棄了淺層的 feature map,而是從 conv4_3 開始建立金字塔,而且加入了一些新的層。因此 SSD 放棄了重利用更高分辨率的 feature map,但是這些 feature map 對檢測小目標非常重要。這就是 SSD 與 FPN 的區別。
圖(4)就是 FPN 的結構了,FPN 是爲了自然地利用 CNN 層級特徵的金字塔形式,同時生成在所有尺度上都具有強語義信息的特徵金字塔。所以 FPN 的結構設計了 top-down 結構和橫向連接,以此融合具有高分辨率的淺層 feature map 和具有豐富語義信息的深層 feature map。這樣就實現了從單尺度的單張輸入圖像,快速構建在所有尺度上都具有強語義信息的特徵金字塔,同時不產生明顯的代價。
1、down-top
自下而上的計算過程實際上就是 CNN 計算,feature map 經過卷積覈計算越來越小。
2、top-down 和橫向連接
自上而下的計算過程就是把更抽象,語義更強的高層特徵圖進行上取樣,然後把該特徵橫向連接至前一層特徵,因此高層特徵可以得到加強。
下圖顯示連接細節。把高層特徵做兩次上採樣(最鄰近上採樣法),然後將其和對應的前一層特徵結合(前一層要經過 1x1 的卷積核才能用,目的是改變改變形狀到和上採樣後的形狀相同),結合方式就是做像素間的加法。
具體的網絡結構爲:
其中 down-top 過程的 feature map 用 C 表示;down-top 過程的 feature map 用 M 表示;最終得到的特徵用 P 表示。
三、代碼實現
1、BasicBlock 模塊
這個模塊和殘差網絡中的 Residual + ResnetBlock 模塊很像,它主要用在 down-top 過程中。具體實現過程如下:
import tensorflow as tf
class BasicBlock(tf.keras.Model):
def __init__(self, in_channels, out_channels, strides=1):
super(BasicBlock, self).__init__()
self.conv1 = tf.keras.layers.Conv2D(out_channels, kernel_size=3, strides=strides,
padding="same", use_bias=False)
self.bn1 = tf.keras.layers.BatchNormalization()
self.conv2 = tf.keras.layers.Conv2D(out_channels, kernel_size=3, strides=1,
padding="same", use_bias=False)
self.bn2 = tf.keras.layers.BatchNormalization()
if strides != 1 or in_channels != out_channels:
self.shortcut = tf.keras.Sequential([
tf.keras.layers.Conv2D(out_channels, kernel_size=1,
strides=strides, use_bias=False),
tf.keras.layers.BatchNormalization()]
)
else:
self.shortcut = lambda x,_: x
def call(self, x, training=False):
# if training: print("=> training network ... ")
out = tf.nn.relu(self.bn1(self.conv1(x), training=training))
out = self.bn2(self.conv2(out), training=training)
out += self.shortcut(x, training)
return tf.nn.relu(out)
在上述代碼中,我們定義了一個 shortcut,其中的卷積核是 1x1 的,步長與 conv1 中的步長相等(保證將輸入 x 的形狀轉變成經過 conv1 和 conv2 之後的形狀)且卷積核個數與希望輸出的通道數相等。舉例來說:
上圖中,像情況(1)這種,就不需要 shortcut;像情況(2)這種,就需要 shortcut 對輸入進行處理。
2、實現 FPN
class FPN(tf.keras.Model):
def __init__(self, block, num_blocks):
super(FPN, self).__init__()
self.in_channels = 64
self.conv1 = tf.keras.layers.Conv2D(64, 7, 2, padding="same", use_bias=False)
self.bn1 = tf.keras.layers.BatchNormalization()
# Bottom --> up layers
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)
# Smooth layers
self.smooth1 = tf.keras.layers.Conv2D(256, 3, 1, padding="same")
self.smooth2 = tf.keras.layers.Conv2D(256, 3, 1, padding="same")
self.smooth3 = tf.keras.layers.Conv2D(256, 3, 1, padding="same")
self.smooth4 = tf.keras.layers.Conv2D(256, 3, 1, padding="same")
# Lateral layers
self.lateral_layer1 = tf.keras.layers.Conv2D(256, 1, 1, padding="valid")
self.lateral_layer2 = tf.keras.layers.Conv2D(256, 1, 1, padding="valid")
self.lateral_layer3 = tf.keras.layers.Conv2D(256, 1, 1, padding="valid")
self.lateral_layer4 = tf.keras.layers.Conv2D(256, 1, 1, padding="valid")
def _make_layer(self, block, out_channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels
return tf.keras.Sequential(layers)
def _upsample_add(self, x, y):
_, H, W, C = y.shape
return tf.image.resize(x, size=(H, W), method="bilinear")
def call(self, x, training=False):
C1 = tf.nn.relu(self.bn1(self.conv1(x), training=training))
C1 = tf.nn.max_pool2d(C1, ksize=3, strides=2, padding="SAME")
# Bottom --> up
C2 = self.layer1(C1, training=training)
C3 = self.layer2(C2, training=training)
C4 = self.layer3(C3, training=training)
C5 = self.layer4(C4, training=training)
# Top-down
M5 = self.lateral_layer1(C5)
M4 = self._upsample_add(M5, self.lateral_layer2(C4))
M3 = self._upsample_add(M4, self.lateral_layer3(C3))
M2 = self._upsample_add(M3, self.lateral_layer4(C2))
# Smooth
P5 = self.smooth1(M5)
P4 = self.smooth2(M4)
P3 = self.smooth3(M3)
P2 = self.smooth4(M2)
return P2, P3, P4, P5
舉例來說:
四、 參考資料
1、Feature Pyramid Networks for Object Detection 總結
2、目標檢測FPN