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

完整代碼及其數據,請移步小編的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

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