卷積神經網路學習筆記——SENet

戰爭熱誠發表於2021-01-23

完整程式碼及其資料,請移步小編的GitHub地址

  傳送門:請點選我

  如果點選有誤:https://github.com/LeBron-Jian/DeepLearningNote

  這裡結合網路的資料和SENet論文,捋一遍SENet,基本程式碼和圖片都是來自網路,這裡表示感謝,參考連結均在後文。下面開始。

  SENet論文寫的很好,有想法的可以去看一下,我這裡提供翻譯地址:

深度學習論文翻譯解析(十六):Squeeze-and-Excitation Networks

 

  在深度學習領域,CNN分類網路的發展對其他計算機視覺任務如目標檢測和語義分割都起到至關重要的作用(檢測和分割模型通常都是構建在 CNN 分類網路之上)。提到CNN分類網路,我們之前已經學習了 AlexNet,VGGNet,InceptionNet,ResNet,DenseNet等,他們的效果已經被充分驗證,而且被廣泛的應用在各類計算機視覺任務上。這裡我們再學習一個網路(SENet),SENet 以極大的優勢獲得了最後一屆 ImageNet 2017 競賽 Image Classification 任務的冠軍,和ResNet的出現類似,都很大程度上減少了之前模型的錯誤率,並且複雜度低,新增引數和計算量小。下面就來具體學習一下SENet。

1,SENet 簡介

   SENet的全稱是Squeeze-and-Excitation Networks,中文可以翻譯為壓縮和激勵網路。 Squeeze-and-Excitation(SE) block 並不是一個完整的網路結構,而是一個子結構,可以嵌到其他分類或檢測模型中,作者採用 SENet block 和 ResNeXt結合在 ILSVRC 2017 的分類專案中拿到第一,在ImageNet資料集上將 top-5 error 降低到 2.251%,原先的最好成績是 2.991%。

  作者在文中將 SENet block 插入到現有的多種分類網路中,都取得了不錯的效果。SENet的核心思想在於通過網路根據 loss 去學習特徵權重,使得有效的  feature map 權重大,無效或效果小的 feature map 權重小的方式訓練 模型達到更好的結果。當然,SE block 嵌入在原有的一些分類網路中不可避免地增加了一些引數和計算量,但是在效果面前還是可以接受的。

  也許通過給某一層特徵配備權重的想法很多人都有,那為什麼只有 SENet 成功了? 個人認為主要原因在於權重具體怎麼訓練得到。就像有些是直接根據 feature map 的數值分佈來判斷;有些可能也利用了loss來指導權重的訓練,不過全域性資訊該怎麼獲取和利用也是因人而已。

2,SENet的主體思路

2.1 中心思想

  對於CNN網路來說,其核心計算是卷積運算元,其通過卷積核從輸入特徵圖學習到新特徵圖。從本質上講,卷積是對一個區域性區域進行特徵融合,這包括空間上(W和H維度)以及通道間(C 維度)的特徵融合,而對於卷積操作,很大一部分工作是提高感受野,即空間上融合更多特徵,或者是提取多尺度空間資訊,而SENet網路的創新點在於關注 channel 之間的關係,希望模型可以自動學習到不同 channel 特徵的重要程度。為此,SENet 提出了 Squeeze-and-Excitation(SE)模組。

  中心思想:對於每個輸出 channel,預測一個常數權重,對每個 channel 加權一下,本質上,SE模組是在 channel 維度上做 attention 或者 gating 操作,這種注意力機制讓模型可以更加關注資訊量最大的 channel 特徵,而抑制那些不重要的 channel 特徵。SENet 一個很大的優點就是可以很方便地整合到現有網路中,提升網路效能,並且代價很小。

  如下就是 SENet的基本結構:

  原來的任意變換,將輸入 X 變為輸出 U,每個通道的重要程度不同,有的通道更有用,有的通道則不太有用。

  對於每一輸出通道,先 global average pool,每個通道得到 1個標量,C個通道得到C個數,然後經過 FC-ReLU-FC-Sigmoid 得到 C個0~1 之間的標量,作為通道的加權,然後原來的輸出通道每個通道用對應的權重進行加權(對應通道的每個元素與權重分別相乘),得到新的加權後的特徵,作者稱為 feature recalibration。

  第一步每個通道 H*W 個數全域性平均池化得到一個標量,稱之為 Squeeze,然後兩個 FC得到0~1之間的一個權重值,對原始的每個 H*W 的每個元素乘以對應通道的權重,得到新的 feature map ,稱之為 Excitation。任意的原始網路結構,都可以通過這個 Squeeze-Excitation的方式進行 feature recalibration,採用了改方式的網路,即 SENet版本。

  上面的模組很通用,也可以很容易的和現有網路整合,得到對應的 SENet版本,提升現有網路效能,SENet泛指所有的採用了上述結構地網路。另外,SENet也可以特指作者 ILSVRC 2017奪冠中採用的 SE-ResNeXt-152(64*4d)。

  SENet和ResNet很相似,但比ResNet做的更多,ResNet只是增加了一個 skip connection,而SENet在相鄰兩層之間加入了處理,使得 channel 之間的資訊互動稱為可能,進一步提高了網路的準確率。

  我們從最基本的卷積操作開始學習。近些年來,卷積神經網路在很多領域上都取得了巨大的突破。而卷積核作為卷積神經網路的核心,通常被看作是在區域性感受野上,將空間上(Spatial)的資訊和特徵維度上(channel-wise)的資訊進行聚合的資訊聚合體。卷積神經網路由一系列卷積層,非線性層和下采樣層構成,這樣他們能夠從全域性感受野上去捕獲影像的特徵來進行影像的描述。

   然而去學到一個效能非常強勁的網路是相當困難的,其難點來自於很多方面。最近很多工作被提出來從空間維度層面來提升網路的效能,如 Inception 結構中嵌入了多尺度資訊,聚合多種不同感受野上的特徵來獲得效能增益;在 Inside-Outside 網路中考慮了空間中的上下文資訊;還有將 Attention 機制引入到空間維度上等等。這些工作都獲得了相當不錯的成果。

   我們可以看到,已經有很多工作在空間維度上來提升網路的效能。那麼很自然的想到,網路是否可以從其他層面來考慮去提升效能,比如考慮特徵通道之間的關係?我們的工作就是基於這一點並提出了 Squeeze-and-Excitation  Networks(簡稱:SENet)。在我們提出的結構中,Squeeze和Excitation 是兩個非常關鍵的操作,所以我們以此來命名。我們的動機是希望顯式的建模特徵通道之間的相互依賴關係。另外,我們並不打算引入一個新的空間維度來進行特徵通道間的融合,而是採用了一種全新的“特徵重標定”策略。具體來說,就是通過學習的方式來自動獲取到每個特徵通道的重要程度,然後依照這個重要程度去提升有用的特徵並抑制對當前任務用處不大的特徵。

   上圖是我們提出的 SE 模組的示意圖。給定一個輸入 x,其特徵通道數為c1,通過一系列卷積等一般變換後得到一個特徵通道數為 c2 的特徵。與傳統的CNN不一樣的是,接下來我們通過三個操作來重標定前面得到的特徵。

2.2  SE模組

  SE模組主要包含 Squeeze 和 Excitation 兩個操作,可以適用於任何對映:

  以卷積為例,卷積核為 V=[v1, v2, .... vn],其中 Vc 表示第 c 個卷積核,那麼輸出 u=[u1, u2,...,uc]為:

  其中 * 代表卷積操作,而 Vcs 代表一個 3D卷積核,其輸入一個 channel 上的空間特徵,它學習特徵空間關係,但是由於對各個 channel 的卷積結果做了 sum,所以 channel 特徵關係與卷積核學習到的空間關係混合在一起。而SE模組就是為了抽離這種混雜,使得模型直接學習到 channel 特徵關係。

2.3  Squeeze操作

  首先是 Squeeze 操作,我們順著空間維度來進行特徵壓縮,原始 feature map 的維度為 H*W*C,其中H是高度(height), W 是寬度(Width), C是通道數(Channel)。Squeeze做的事情是把 H*W*C 壓縮為1*1*C,相當於將每個二維的特徵通道(即H*W)變成一個實數(即變為一維了),實際中一般是用 global average pooling 實現的。H*W 壓縮成一維後,相當於這一維度獲得了之前H*W全域性的視野,感受野區域更廣,所以這個實數某種程度上具有全域性的感受野,並且輸出的維度和輸入的特徵通道數相匹配。它表徵著在特徵通道上響應的全域性分佈,而且使得靠近輸入的層也可以獲得全域性的感受野,這一點在很多工中都是非常有用的。

  由於卷積只是在一個區域性空間內進行操作, U 很難獲得足夠的資訊來提取 channel 之間的關係,對於網路中前面的層這更嚴重,因為感受野比較小。為此SENet 提出了 Squeeze操作,將一個 channel 上整個空間特徵編碼為一個全域性特徵,採用 global average pooling 來實現(原則上也可以採用更復雜的 聚合策略):

2.4  Excitation 操作

   其次是 Excitation 操作,它是一個類似於迴圈神經網路中門的機制。通過引數 w 來為每個特徵通道生成權重,其中引數 w 被學習用來顯式的建模特徵通道間的相關性。得到Squeeze 的 1*1*C 的表示後,加入一個 FC 全連線層(Fully Connected),對每個通道的重要性進行預測,得到不同 channel的重要性大小後再作用(激勵)到之前的  feature map 的對應  channel上,再進行後續操作。

  Sequeeze操作得到了全域性描述特徵,我們接下來需要另外一種運算來抓取 channel 之間的關係。這個操作需要滿足兩個準則:首先要靈活,它要可以學習到各個 channel之間的非線性關係;第二點是學習的關係不是互斥的,因為這裡允許多 channel 特徵,而不是 one-hot 形式。基於此,這裡採用了 Sigmoid形式的 gating 機制:

  其中:

  為了降低模型複雜度以及提升泛化能力,這裡採用包含兩個全連線層的 bottleneck結構,其中第一個 FC 層起到降維的作用,降維繫數為 r 是個超引數,然後採用 ReLU啟用。最後的 FC層恢復原始的維度。

  最後將學習到的各個 channel的啟用值(Sigmoid啟用,值0~1)乘以 U 上的原始特徵:

  其中整個操作可以看成學習到了各個channel的權重係數,從而使得模型對各個 channel 的特徵更有辨識能力,這應該也算是一種 attention機制。

  最後一個是 Reweight 的操作,我們將 Excitation 的輸出的權重看做是進過特徵選擇後的每個特徵通道的重要性,然後通過乘法逐通道加權到先前的特徵上,完成在通道維度上的對原始特徵的重標定。

3,SE模組的應用

3.1  SE模組在 Inception 和 ResNet 上的應用

  SE模組的靈活性在於它可以直接應用現有的網路結構中。這裡以 Inception和ResNet為例。對於 Inception網路,沒有殘差網路,這裡對整個Inception模組應用SE模組。對於ResNet,SE模組嵌入到殘差結構中的殘差學習分支中,具體如下圖所示:

   上左圖是將 SE 模組嵌入到 Inception 結構的一個示例。方框旁邊的維度資訊代表該層的輸出。

  這裡我們使用 global average pooling 作為 Squeeze 操作。緊接著兩個 Fully Connected 層組成一個 Bottleneck 結構去建模通道間的相關性,並輸出和輸入特徵同樣數目的權重。我們首先將特徵維度降低到輸入的 1/16,然後經過 ReLU 啟用後再通過一個 Fully Connected 層升回到原來的維度。這樣做比直接用一個 Fully Connected 層的好處在於:1)具有更多的非線性,可以更好地擬合通道間複雜的相關性;2)極大地減少了引數量和計算量。然後通過一個 Sigmoid 的門獲得 0~1 之間歸一化的權重,最後通過一個 Scale 的操作來將歸一化後的權重加權到每個通道的特殊上。

  除此之外,SE模組還可以嵌入到含有 skip-connections 的模組中。上右圖是將 SE嵌入到 ResNet模組中的一個例子,操作過程基本和 SE-Inception 一樣,只不過是在 Addition前對分支上 Residual 的特徵進行了特徵重標定。如果對 Addition 後主支上的特徵進行重標定,由於在主幹上存在 0~1 的 scale 操作,在網路較深 BP優化時就會在靠近輸入層容易出現梯度消散的情況,導致模型難以優化。

  目前大多數的主流網路都是基於這兩種類似的單元通過 repeat 方式疊加來構造的。由此可見,SE模組可以嵌入到現在幾乎所有的網路結構中。通過在原始網路結構的 building block 單元中嵌入 SE模組,我們可以獲得不同種類的 SENet。如SE-BN-Inception,SE-ResNet,SE-ReNeXt,SE-Inception-ResNet-v2等等。

   從上面的介紹中可以發現,SENet構造非常簡單,而且很容易被部署,不需要引入新的函式或者層。除此之外,它還在模型和計算複雜度上具有良好的特性。拿 ResNet-50 和 SE-ResNet-50 對比舉例來說,SE-ResNet-50 相對於 ResNet-50有著 10% 模型引數的增長。額外的模型引數都存在於 Bottleneck 設計的兩個 Fully Connected 中,由於 ResNet 結構中最後一個 stage 的特徵通道數目為 2048,導致模型引數有著較大的增長,實現發現移除掉最後一個 stage 中 3個 build block 上的 SE設定,可以將 10%引數量的增長減少到 2%。此時模型的精度幾乎無損失。

  另外,由於在現有的  GPU 實現中,都沒有對 global pooling 和較小計算量的 Fully Connected 進行優化,這導致了在 GPU 上的執行時間 SE-ResNet-50 相對於 ResNet-50 有著約 10% 的增長。儘管如此,其理論增長的額外計算量僅僅不到1%,這與其在 CPU 執行時間上的增長相匹配(~2%)。可以看出,在現有網路架構中嵌入 SE 模組而導致額外的引數和計算量的增長微乎其微。

  增加了SE模組後,模型引數以及計算量都會增加,下面以SE-ResNet-50為例,對模型引數增加量為:

  其中 r 為降維繫數,S表示 stage數量,Cs 為第 s 個 stage的通道數,Ns 為第 s 個 stage的重複 block量。當 r=16時,SE-ResNet-50只增加了約 10%的引數量,但是計算量(GFLOPS)卻增加不到 1%。

3.2  SE模組在ResNet網路上的模型效果

  SE模組很容易嵌入到其他網路中,作者為了驗證 SE模組的作用,在其他流行網路如 ResNet和VGG中引入 SE模組,測試其在 ImageNet 上的效果。

   在訓練中,我們使用了一些常見的資料增強方法和 Li Shen 提出的均衡資料策略。為了提高訓練效率,我們使用了我們自己優化的分散式訓練系統 ROCS,並採用了更大的 batch-size 和初始學習率。所有的模型都是從頭開始訓練的。

  接下來,為了驗證SENets 的有效性,我們將在 ImageNet 資料集上進行實驗,並從兩個方面來進行論證。一個是效能的增益 vs 網路的深度;另一個是將 SE 嵌入到現有的不同網路中進行結果對比。另外,我們也會展示在 ImageNet 競賽中的結果。 

  首先,我們來看一下網路的深度對SE的影響。上表分別展示了 ResNet-50,ResNet-101,ResNet-152和嵌入SE模型的結果。第一欄 Original 是原作者實現的記過,為了公平的比較,我們在ROCS 上重新進行了實驗得到了 Our re-implementation 的結果(PS:我們衝實現的精度往往比原paper中要高一些)。最後一欄 SE-module 是指嵌入了 SE模組的結果,它的訓練引數和第二欄 Our re-implementation 一致。括號中的紅色數值是指相對於 Our re-implementation 的精度提升的幅值。

  從上表可以看出,SE-ResNets 在各種深度上都遠遠超過其對應的沒有 SE 的結構版本的精度,這說明無論網路的深度如何,SE模組都能夠給網路帶來效能上的增益。值得一提的是,SE-ResNet-50 可以達到和 ResNet-101 一樣的精度;更甚,SE-ResNet-101 遠遠地超過了更深的 ResNet-152。

  上圖展示了 ResNet-50 和 ResNet-152 以及他們對應的嵌入 SE模組的網路在 ImageNet 上的訓練過程,可以明顯的看出加入了 SE 模組的網路收斂到更低的錯誤率上。

  另外,為了驗證 SE模組的泛化能力,我們也在除 ResNet之外的結構上進行了實驗。從上表可以看出,將 SE模組嵌入到 ResNeXt,BN-Inception,Inception-ResNet-v2 上均獲得了不菲的增益效果。由此看出,SE的增益效果不僅僅侷限於某些特殊的網路結構,它具有很強的泛化性。

  上圖展示的是 SE 嵌入在 ResNeXt-50 和 Inception-ResNet-v2 的訓練過程對比。

  在上表中我們列出了一些最新的在 ImageNet 分類上的網路的結果。其中我們的 SENet 實質上是一個 SE-ResNeXt-152(64*4d),在ResNeXt-152 上嵌入 SE模組,並作出一些其他修改和訓練優化上的小技巧,這些我們會在後面介紹。

  最後,在 ILSVRC 2017 競賽中,我們的融合模型在測試集上獲得了 2.251~ top-5 錯誤率。對比於去年第一名的結果 2.991%,我們獲得了將近 25% 的精度提升。

4,總結

  1,SE模組主要為了提升模型對 channel 特徵的敏感性,這個模組是輕量級的,而且可以應用在現有的網路結構中,只需要增加較少的計算量就可以帶來效能的提升。

  2,提升很大,並且代價很小,通過對通道進行加權,強調有效資訊,抑制無效資訊,注意力機制,並且是一個通用的方法,應該在 Inception,Inception-ResNet, ResNeXt, ResNet 都能有所提升,適用範圍很廣。

  3,思路很清晰簡潔,實現很簡單,用起來也很方便,各種試驗都證明了其有效性,各種任務都可以嘗試一下,效果應該不會太差。

5,Keras 實現 SENet

5.1  Keras 實現SE-Inception Net

  首先,先看SE-Inception Net架構的原理圖:

  圖中是將SE模組嵌入到Inception結構的一個示例。方框旁邊的維度資訊代表該層的輸出。這裡我們使用 global average pooling 作為 Squeeze 操作。緊接著兩個 Fully Connected 層組成一個 Bottleneck 結構去建模通道間的相關性,並輸出和輸入特徵同樣數目的權重。

  我們首先將特徵維度降低到輸入的 1/16,然後經過 ReLU 啟用後再通過一個 Fully Connected 層升回到原來的維度。這樣做比直接用一個 Fully Connected層的好處在於:

  • 1,具有更多的非線性,可以更好地擬合通道間複雜的相關性
  • 2,極大地減少了引數量和計算量。然後通過一個 Sigmoid的門獲得 0~1 之間歸一化的權重,最後通過一個 Scale的操作來將歸一化後的權重加權到每個通道的特徵上。

 

  程式碼如下(這裡 r = 16):

def build_SE_model(nb_classes, input_shape=(256, 256, 3)):
    inputs_dim = Input(input_shape)
    x = Inception(include_top=False, weights='imagenet', input_shape=None,
        pooling=max)(inputs_dim)

    squeeze = GlobalAveragePooling2D()(x)

    excitation = Dense(units=2048//16)(squeeze)
    excitation = Activation('relu')(excitation)
    excitation = Dense(units=2048)(excitation)
    excitation = Activation('sigmoid')(excitation)
    excitation = Reshape((1, 1, 2048))(excitation)

    scale = multiply([x, excitation])

    x = GlobalAveragePooling2D()(scale)
    dp_1 = Dropout(0.3)(x)
    fc2 = Dense(nb_classes)(dp_1)
    # 此處注意,為Sigmoid函式
    fc2 = Activation('sigmoid')(fc2)
    model = Model(inputs=inputs_dim, outputs=fc2)
    return model


if __name__ == '__main__':
    model =build_model(nb_classes, input_shape=(im_size1, im_size2, channels))
    opt = Adam(lr=2*1e-5)
    model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
    model.fit()

   注意:

  1,multiply([x, excitation]) 中的 x 的 shape 為(10, 10,  2048),Excitation 的  shape 為(1, 1, 2048) ,應保持他們的最後一維即 2048 相同。例如:如果用 DenseNet201,它的最後一層卷積出來的結果為(8,  8,  1920)(不包括全連線層),Excitation的 Reshape為(1,  1,  1920)。

  2, fc2 = Activation('sigmoid')(fc2) ,此處注意,為Sigmoid函式。

 

5.2  Keras 實現SE-ResNeXt Net

  下面看一下 SEResNet 架構圖:

  ResNeXt是 ResNet的改進版本。這裡參考了網友實現的 ResNeXt,程式碼如下:

from __future__ import print_function
from __future__ import absolute_import

import warnings
import numpy as np

from keras.models import Model
from keras.layers import Input
from keras.layers import Lambda
from keras.layers import Reshape

from keras.layers import Conv2D
from keras.layers import Activation
from keras.layers import AveragePooling2D
from keras.layers import GlobalAveragePooling2D
from keras.layers import BatchNormalization
from keras.layers import Dense

from keras.layers import Concatenate, concatenate
from keras.layers import Add, add
from keras.layers import Multiply, multiply

from keras import backend as K


class SEResNeXt(object):
    def __init__(self, size=96, num_classes=10, depth=64, reduction_ratio=4, num_split=8, num_block=3):
        self.depth = depth  # number of channels
        self.ratio = reduction_ratio  # ratio of channel reduction in SE module
        self.num_split = num_split  # number of splitting trees for ResNeXt (so called cardinality)
        self.num_block = num_block  # number of residual blocks
        if K.image_data_format() == 'channels_first':
            self.channel_axis = 1
        else:
            self.channel_axis = 3
        self.model = self.build_model(Input(shape=(size,size,3)), num_classes)

    def conv_bn(self, x, filters, kernel_size, stride, padding='same'):
        '''
        Combination of Conv and BN layers since these always appear together.
        '''
        x = Conv2D(filters=filters, kernel_size=[kernel_size, kernel_size],
                   strides=[stride, stride], padding=padding)(x)
        x = BatchNormalization()(x)
        
        return x
    
    def activation(self, x, func='relu'):
        '''
        Activation layer.
        '''
        return Activation(func)(x)
    
    def channel_zeropad(self, x):
        '''
        Zero-padding for channle dimensions.
        Note that padded channles are added like (Batch, H, W, 2/x + x + 2/x).
        '''
        shape = list(x.shape)
        y = K.zeros_like(x)
        
        if self.channel_axis == 3:
            y = y[:, :, :, :shape[self.channel_axis] // 2]
        else:
            y = y[:, :shape[self.channel_axis] // 2, :, :]
        
        return concatenate([y, x, y], self.channel_axis)
    
    def channel_zeropad_output(self, input_shape):
        '''
        Function for setting a channel dimension for zero padding.
        '''
        shape = list(input_shape)
        shape[self.channel_axis] *= 2

        return tuple(shape)
    
    def initial_layer(self, inputs):
        '''
        Initial layers includes {conv, BN, relu}.
        '''
        x = self.conv_bn(inputs, self.depth, 3, 1)
        x = self.activation(x)
        
        return x
    
    def transform_layer(self, x, stride):
        '''
        Transform layer has 2 {conv, BN, relu}.
        '''
        x = self.conv_bn(x, self.depth, 1, 1)
        x = self.activation(x)
        
        x = self.conv_bn(x, self.depth, 3, stride)
        x = self.activation(x)
        
        return x
        
    def split_layer(self, x, stride):
        '''
        Parallel operation of transform layers for ResNeXt structure.
        '''
        splitted_branches = list()
        for i in range(self.num_split):
            branch = self.transform_layer(x, stride)
            splitted_branches.append(branch)
        
        return concatenate(splitted_branches, axis=self.channel_axis)
    
    def squeeze_excitation_layer(self, x, out_dim):
        '''
        SE module performs inter-channel weighting.
        '''
        squeeze = GlobalAveragePooling2D()(x)
        
        excitation = Dense(units=out_dim // self.ratio)(squeeze)
        excitation = self.activation(excitation)
        excitation = Dense(units=out_dim)(excitation)
        excitation = self.activation(excitation, 'sigmoid')
        excitation = Reshape((1,1,out_dim))(excitation)
        
        scale = multiply([x,excitation])
        
        return scale
    
    def residual_layer(self, x, out_dim):
        '''
        Residual block.
        '''
        for i in range(self.num_block):
            input_dim = int(np.shape(x)[-1])
            
            if input_dim * 2 == out_dim:
                flag = True
                stride = 2
            else:
                flag = False
                stride = 1
            
            subway_x = self.split_layer(x, stride)
            subway_x = self.conv_bn(subway_x, out_dim, 1, 1)
            subway_x = self.squeeze_excitation_layer(subway_x, out_dim)
            
            if flag:
                pad_x = AveragePooling2D(pool_size=(2,2), strides=(2,2), padding='same')(x)
                pad_x = Lambda(self.channel_zeropad, output_shape=self.channel_zeropad_output)(pad_x)
            else:
                pad_x = x
            
            x = self.activation(add([pad_x, subway_x]))
                
        return x
    
    def build_model(self, inputs, num_classes):
        '''
        Build a SENet model.
        '''
        x = self.initial_layer(inputs)
        
        x = self.residual_layer(x, out_dim=64)
        x = self.residual_layer(x, out_dim=128)
        x = self.residual_layer(x, out_dim=256)
        
        x = GlobalAveragePooling2D()(x)
        x = Dense(units=num_classes, activation='softmax')(x)
        
        return Model(inputs, x)

 

6,SE模組的 Pytorch實現

  SE模組是非常簡單的,實現起來也比較容易,這裡給出Pytorch版本的實現(地址:https://zhuanlan.zhihu.com/p/65459972/)。

  程式碼如下:

class SELayer(nn.Module):
    def __init__(self, channel, reduction=16):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channel // reduction, channel, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y.expand_as(x)

   對於SE-ResNet模型,只需要將SE模組加入到殘差單元就可以:

class SEBottleneck(nn.Module):
        expansion = 4

        def __init__(self, inplanes, planes, stride=1, downsample=None, reduction=16):
            super(SEBottleneck, self).__init__()
            self.conv1 = nn.Conv2d(inplanes, 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, planes * 4, kernel_size=1, bias=False)
            self.bn3 = nn.BatchNorm2d(planes * 4)
            self.relu = nn.ReLU(inplace=True)
            self.se = SELayer(planes * 4, reduction)
            self.downsample = downsample
            self.stride = stride

        def forward(self, x):
            residual = x

            out = self.conv1(x)
            out = self.bn1(out)
            out = self.relu(out)

            out = self.conv2(out)
            out = self.bn2(out)
            out = self.relu(out)

            out = self.conv3(out)
            out = self.bn3(out)
            out = self.se(out)

            if self.downsample is not None:
                residual = self.downsample(x)

            out += residual
            out = self.relu(out)

            return out

 

 

 

參考地址:https://www.sohu.com/a/161633191_465975

https://blog.csdn.net/u014380165/article/details/78006626

https://zhuanlan.zhihu.com/p/65459972/

https://blog.csdn.net/qq_38410428/article/details/87979417

https://github.com/yoheikikuta/senet-keras

https://github.com/moskomule/senet.pytorch

相關文章