Tensorflow2.0用FPN(圖像金字塔網絡)提取特徵

一、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 的形狀轉變成經過 conv1conv2 之後的形狀)且卷積核個數與希望輸出的通道數相等。舉例來說:
在這裏插入圖片描述
上圖中,像情況(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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章