計算機視覺中的attention

        首先,本文從計算機視覺領域介紹attention機制,主要是在圖像上的應用,會從總體motivation,具體的文章,以及相應的代碼實現進行描述。

        本文背景部分,大量參考了 https://mp.weixin.qq.com/s/KKlmYOduXWqR74W03Kl-9A, 特此感謝。

一、背景

1.1 直觀理解

       引用一下示例,從感知上理解一下計算機視覺中的注意力機制。 比如天空一隻鳥飛過去的時候,往往你的注意力會追隨着鳥兒,天空在你的視覺系統中,自然成爲了一個背景(background)信息。

                                                              

                                                             

        而當我們在用深度神經網絡處理計算視覺問題時,步驟首先是對圖像中的特徵進行提取,這些特徵在神經網絡“眼裏”沒有差異,神經網絡並不會過多關注某個“區域”,但我們人在處理時,人類注意力是會集中在這張圖片的一個區域內,而其他的信息受關注度會相應降低。

        計算機視覺(computer vision)中的注意力機制(attention)的基本思想就是想讓系統學會注意力——能夠忽略無關信息而關注重點信息

1.2 理論研究基礎

        注意力機制最早的應用在機器翻譯上。之所以它這麼受歡迎,是因爲Attention給模型賦予了區分辨別的能力,例如,在機器翻譯、語音識別應用中,爲句子中的每個詞賦予不同的權重,使神經網絡模型的學習變得更加靈活(soft),同時Attention本身可以做爲一種對齊關係,解釋翻譯輸入/輸出句子之間的對齊關係,解釋模型到底學到了什麼知識,爲我們打開深度學習的黑箱,提供了一個窗口。

        想從根本上理解它的提出,可以看張俊林的專欄 https://zhuanlan.zhihu.com/p/37601161,寫的非常具體,另外,引用一下https://www.jianshu.com/p/c94909b835d6 的一個直觀例子。

        A是encoder, B是decoder,A網絡接收了一個四個字的句子,對每個字都產生了一個輸出向量,v1,v2,v3,v4。B網絡,在第一個B產生的hidden state(稱其爲h1)除了傳給下一個cell外,還傳到了A網絡,這裏就是Attention發揮作用的地方。首先,h1 分別與v1,v2,v3,v4做點積,產生了四個數,稱其爲m1,m2,m3,m4,由於是點積,這四個數均爲標量。然後傳到softmax層,產生a1,a2,a3, a4概率分佈。然後將a1,a2,a3, a4 與 v1,v2,v3,v4分別相乘,再相加,得到得到一個vector,稱其爲Attention vector。Attention vector 將作爲輸入傳到B網絡的第二個cell中,參與預測。

        以上就是Attention機制的基本思想了。我們看到,Attention vector 實際上融合了v1,v2,v3,v4的信息,具體的融合是用一個概率分佈來達到的,而這個概率分佈又是通過B網絡上一個cell的hidden state與v1,v2,v3,v4進行點乘得到的。
Attention vector實際上達到了讓B網絡聚焦於A網絡輸出的某一部分的作用。這個概率分佈,實際上就是一個權重,對輸入進行一定的篩選。

        同樣的,在計算機視覺領域,也是類似。注意力機制被引入來進行視覺信息處理。注意力是一種機制,或者方法論,並沒有嚴格的數學定義。比如,傳統的局部圖像特徵提取、顯著性檢測、滑動窗口方法等都可以看作一種注意力機制。在神經網絡中,注意力模塊通常是一個額外的神經網絡,能夠硬性選擇輸入的某些部分,或者給輸入的不同部分分配不同的權重。

        從分類來看,可分爲hard-attention和soft-attention。

1. Hard-attention,就是0/1問題,哪些區域是被attentioned,哪些區域不關注

2. Soft-attention,[0,1]間連續分佈問題,每個區域被關注的程度高低,用0~1的score表示

        由於兩者特性不同,在解決方法上也不同。hard-attention 主要利用強化學習,而 soft-attention則利用梯度下降即可求解。因而計算機視覺領域,soft-attention 使用的相對較多。本文中剩下的文章也都基於soft-attention。

二、主要文章及其代碼實現

        在主流的注意力機制應用中,有從channel 特徵尺度上加上注意力,有從spatial 空間加上注意力,更多的是將兩者進行結合。本人是研究醫學圖像語義分割,因而將視覺注意力和經典的Unet進行結合,在2D圖像上,探究其有效性。以下幾篇文章都很經典,基本都在頂會上,這裏將其attention 部分進行提煉解析。文章更爲詳細的內容晚些補上。

 [1] Squeeze-and-Excitation Network: CVPR 2017 

        作者主要在channel上增加了attention,作者稱其爲“Squeeze-and-Excitation”網絡塊。作者的定位是通過精確的建模卷積特徵各個通道之間的作用關係來改善網絡模型的表達能力。爲了達到這個期望,作者提出了一種能夠讓網絡模型對特徵進行校準的機制,使網絡從全局信息出發來選擇性的放大有價值的特徵通道並且抑制無用的特徵通道。個人觀點,這篇文章利用了注意力機制,對無效信息進行抑制,強調在信息篩選上,對傳統的卷積操作進行了改進。這樣設計帶來的有效性還是非常玄妙的,但是結果不錯,能在很多應用上有所提高。

       下面,具體介紹一下:

        1. 對於任意給定的信息進入網絡模塊後進行操作爲:,因此 X--> U 可表示一次或多次卷積操作)。

        2. 對於第一個Squeeze操作,其實就是做了一個global average pooling (GAP),將各個特徵圖全局感受野的空間信息置入特徵圖。稱爲descriptor,後面的操作可根據這個 descriptor 獲取全局的感受野,避免了由於卷積核尺寸造成的信息不足。爲了加強理解GAP, 給出tensorflow的代碼,實際上就是將width and height 信息全部進行平均,從而得到一個全局的信息:

"""
input: net, shape=[b,h,w,c],b is batch_size, h is height, w is width and c is channel
"""
x = tf.reduce_mean(x, [1, 2], name='average_pooling', keep_dims=True)

        3. 隨後再接着進行excitation操作,在excitation操作中,每層卷積操作之後都接着一個樣例特化(sample-specific)激活函數,基於通道之間的依賴關係對每各個通道過一種篩選機制(self-gating mechanism)操作,以此來對各個通道進行權值評比(excitation) 。這裏採用了bottleneck,具有更多非線性,並減少參數來和計算量,可以比單層的性能更優秀。

                               

                        

        4. 最後將得到的權值和卷積後的 U 相乘,得到輸出,作爲下一層的輸入。

        優點很明顯,整個個SE網絡模型通過不斷堆疊SE網絡模塊進行構造,SE網絡模塊能夠在一個網絡模型中的任意深度位置進行插入替換 。SE網絡模塊會不斷的特化,並且以一個高度特化類別的方式對所在不同深度的SE網絡模塊的輸入進行相應,因此在整個網絡模型中,特徵組圖的調整的優點能夠通過SE網絡模塊不斷地累計。

        下面是 keras 寫的se模塊代碼。

def activation(x,func='relu'):
    return Activation(func)(x)
def squeeze_excitation_layer(x, out_dim, ratio=4):
    squeeze = GlobalAveragePooling2D()(x)
    excitation = Dense(units=out_dim // ratio)(squeeze)
    excitation = activation(excitation)
    excitation = Dense(units=out_dim)(excitation)
    excitation = activation(excitation, 'sigmoid')
    excitation = Reshape((1, 1, out_dim))(excitation)
    scale = multiply([x, excitation])
    return scale

       完整實驗請看github/下一篇。 

[2] CBAM: Convolutional Block Attention Module ECCV 2018

        這個ECCV2018的一篇文章。針對SE的一個補充和改進。

                   

def cbam(x,ratio = 4):
    x = channel_attention(x,ratio)
    x = spatial_attention(x)
    return x

        這是整體的框架圖,motivation 很直觀,和SE相比,channel attention部分基本類似,主要增加了spatial attention, 作者認爲這樣可以更好的將信息進行整合。主要亮點將attention同時運用在channel 和 spatial兩個維度上,CBAM與SE Module一樣,可以嵌入了目前大部分主流網絡中,在不顯著增加計算量和參數量的前提下能提升網絡模型的特徵提取能力。主要attention的作用,仍舊是對信息進行一個篩選與整合

       

        首先,先看一下channel attention部分。在SE中,僅僅使用了一個AvgPool,這裏進行了改進,同時用了MaxPool 和 AvgPool, 兩者通過同一個MLP(多層感知機)共享參數。這樣做的原因是通過兩個pooling函數以後總共可以得到兩個一維矢量。global average pooling對feature map上的每一個像素點都有反饋,而global max pooling在進行梯度反向傳播計算只有feature map中響應最大的地方有梯度的反饋,能作爲一個補充。

        其實客觀來看,除了增加MaxPool 作爲補充,它和SE基本是一樣的,MLP在代碼中也可以看到,取了bottelneck形狀。下面一起寫一下keras代碼。

def channel_attention(x,ratio):
    channel = x._keras_shape[-1]
    shared_layer_one = Dense(channel//ratio,activation='relu',kernel_initializer='he_normal',use_bias=True,
                             bias_initializer='zeros')
    shared_layer_two = Dense(channel,kernel_initializer='he_normal',use_bias=True,bias_initializer='zeros')

    avg_pool = GlobalAveragePooling2D()(x)
    avg_pool = shared_layer_one(avg_pool)
    avg_pool = shared_layer_two(avg_pool)

    max_pool = GlobalMaxPooling2D()(x)
    max_pool = shared_layer_one(max_pool)
    max_pool = shared_layer_two(max_pool)

    a= Add()([avg_pool,max_pool])
    a = activation(a,'sigmoid')
    return multiply([x,a])

        接着看一下第二個spatial attention 部分。

        其實操作上更爲簡單一些,使用average pooling和max pooling對輸入feature map 在通道層面上進行壓縮操作,對輸入特徵分別在通道維度上做了mean和max操作。最後得到了兩個二維的 feature,將其按通道維度拼接在一起得到一個通道數爲2的feature map,之後使用一個包含單個卷積核的隱藏層對其進行卷積操作,要保證最後得到的feature在spatial 維度上與輸入的feature map一致。先上代碼。

def spatial_attention(x):
    avg_pool = Lambda(lambda x:K.mean(x,axis=3,keepdims=True))(x)
    max_pool = Lambda(lambda x:K.max(x,axis=3,keepdims=True))(x)
    concat = Concatenate(axis=3)([avg_pool,max_pool])
    o = Conv2D(filters=1,kernel_size=7,strides=1,padding='same',activation='sigmoid',kernel_initializer='he_normal',
               use_bias=False)(concat)
    return multiply([x,o])

        總體來說,文章中的一句原話說的很好,作者認爲通道注意力關注的是:what,然而空間注意力關注的是:Where。這樣兩方面結合,能更好地指明信息豐富的區域。另外,文章中這種對稱的結構設計以及效果,還是讓人覺得很不錯的。文章中其實還有一些對整體結構的具體分析值得學習。篇幅限制,就不展開了。 完整實驗請看github/下一篇。 

[3] Dual Attention Network for Scene Segmentation:AAAI 2019

        這篇文章是AAAI 剛錄取的文章。但是整體想法其實和self-attention GAN 以及 Non-local 的操作十分類似。它通過self attention 機制來捕獲上下文依賴。這樣一個結構可以自適應地整合局部特徵和全局依賴。self-attention GAN 和Non-local 有興趣的可以自己看看論文。這裏主要介紹這一篇文章的主要做法和意義。

 

overview.png

         主要有兩個 attention 模塊。其中,position attention module 選擇性地通過所有位置的加權求和聚集每個特徵的位置。 channel attention module 通過所以 channel 的 feature map中的特徵選擇地強調某個特徵圖。最後將兩者模塊的輸出求和,兩個分支並聯,得到最後的特徵表達。

        在這篇文章中加入attention的motivation有一些不同。它們是對空間和通道維度的語義相互關聯進行建模。相對於前面的強調信息流,對有效信息/區域的提取,這裏更加強調特徵或者是語義之間的關聯。position注意力模塊通過對所有位置的特徵加權總和選擇性地聚集每個位置的特徵。無論距離遠近,相似的特徵都會相互關聯。通道注意力模塊通過整合所有通道圖中的相關特徵,有選擇地強調相互關聯的通道圖。

                                  

        首先先看PAM,特徵圖A(C*H*W)分別通過三個卷積層得到3個特徵圖B,C,D,然後reshape爲C*N,其中N=H*W,之後將reshape 之後的B的轉置與reshape之後的C相乘,再通過softmax得到spatial attention map S(N*N),接着把S的轉置與D做乘積再乘以尺度係數α,再reshape回原來的形狀,最後與A相加得到最後的輸出E。其中α初始化爲0,是一個可學習的變量,逐漸學習分配到更大的權重。其中我們可以看出E的每個位置的值是原始特徵每個位置的加權求和得到。

        這一部分實際上和 self attention gan 一模一樣。通過 (H*W)*(H*W),建立起了每個位置的像素點之間的聯繫。

def hw_flatten(x):
    return K.reshape(x, shape=[K.shape(x)[0], K.shape(x)[1]*K.shape(x)[2], K.shape(x)[3]])

def pam(x):
    f = K.conv2d(x, kernel= kernel_f, strides=(1, 1), padding='same')  # [bs, h, w, c']
    g = K.conv2d(x, kernel= kernel_g, strides=(1, 1), padding='same')  # [bs, h, w, c']
    h = K.conv2d(x, kernel= kernel_h, strides=(1, 1), padding='same')  # [bs, h, w, c]
    s = K.batch_dot(hw_flatten(g), K.permute_dimensions(hw_flatten(f), (0, 2, 1))) #[bs, N, N]
    beta = K.softmax(s, axis=-1)  # attention map
    o = K.batch_dot(beta, hw_flatten(h))  # [bs, N, C]
    o = K.reshape(o, shape=K.shape(x))  # [bs, h, w, C]
    x =  gamma * o + x
    return x

 

                                            

        在CAM 模塊中,分別對A做reshape和(reshape 與 transpose),將得到的兩個特徵圖相乘,再通過softmax得到 channel attention map X(C*C),接着把X與A做乘積再乘以尺度係數 β,再reshape回原來的形狀,最後與A相加得到最後的輸出E。其中,β初始化爲0,並逐漸的學習分配到更大的權重。這裏的β和之前一樣,是可學習的參數。

def hw_flatten(x):
    return K.reshape(x, shape=[K.shape(x)[0], K.shape(x)[1]*K.shape(x)[2], K.shape(x)[3]])

def cam(x):
    f =  hw_flatten(x) # [bs, h*w, c]
    g = hw_flatten(x) # [bs, h*w, c]
    h =  hw_flatten(x) # [bs, h*w, c]
    s = K.batch_dot(K.permute_dimensions(hw_flatten(g), (0, 2, 1)), hw_flatten(f))
    beta = K.softmax(s, axis=-1)  # attention map
    o = K.batch_dot(hw_flatten(h),beta)  # [bs, N, C]
    o = K.reshape(o, shape=K.shape(x))  # [bs, h, w, C]
    x = gamma * o + x
    return x

        通過看代碼,有一點值得注意,作者加了這一部分,防止訓練時梯度爆炸。我們的代碼沒加。

"""官方pytorch版本""""
energy_new = torch.max(energy, -1, keepdim=True)[0].expand_as(energy)-energy

        最後,將兩個模塊的輸出執行 element-wise sum 進行特徵融合。

        這一個整個模塊在原文中放在ResNet之後,作爲額外的模塊更好地輔助恢復原文信息。作爲實驗,還沒想好怎麼和unet結合,後續有進一步的實驗會補充上來,也希望有興趣的可以和我交流!完整版請見github。

[4] Attention U-Net: Learning Where to Look for the Pancreas: MIDL 2018

        最後介紹一下Attention Unet, 它是加了一個gated 模塊,對信息流進行一個篩選。文章的意思是,它是用於醫學成像的新型注意門(AG)模型,該模型自動學習聚焦於不同形狀和大小的目標結構。用AG訓練的模型隱含地學習抑制輸入圖像中的不相關區域,同時突出顯示對特定任務有用的顯著特徵。在醫學圖像尤其是病竈中很直觀,可以用 AG 避免級聯的定位模塊。

        具有AG的CNN模型可以通過類似於FCN模型的訓練的方式訓練,並且AG自動學習專注於目標結構而無需額外的監督。在測試時,這些門會動態地隱式生成軟區域提議,並突出顯示對特定任務有用的顯着特徵。

                        

        爲了減少附加的級聯定位模塊,作者增加了注意係數,,識別顯着圖像區域和修剪特徵響應,以僅保留與特定任務相關的激活。這個是最終目的,接下來看看是如何實現的。

        輸入包括兩個方面,一是針對每個像素矢量的 ,對其計算單個標量注意值,其中對應於層 l 中的特徵圖的數量。另一個是門控矢量用於每個像素 i 以確定聚焦區域。門控向量包含上下文信息,以減少較低級別的特徵響應。這裏使用加性注意來獲得門控係數。核心公式如下,第一個Φ 代表 relu,第二個 σ 代表了 sigmoid 函數。最後resample到原始尺寸。

                                             

        根據pytorch所改寫keras代碼如下。 

def attention_block(x, gating, inter_shape):
    shape_x = K.int_shape(x)
    shape_g = K.int_shape(gating)

    theta_x = layers.Conv2D(inter_shape, (2, 2), strides=(2, 2), padding='same')(x)  # 16
    shape_theta_x = K.int_shape(theta_x)

    phi_g = layers.Conv2D(inter_shape, (1, 1), padding='same')(gating)
    
    upsample_g = layers.Conv2DTranspose(inter_shape, (3, 3),strides=(shape_theta_x[1] // shape_g[1], shape_theta_x[2] // shape_g[2]),padding='same')(phi_g)  # 16

    concat_xg = layers.add([upsample_g, theta_x])
    act_xg = layers.Activation('relu')(concat_xg)
    psi = layers.Conv2D(1, (1, 1), padding='same')(act_xg)
    sigmoid_xg = layers.Activation('sigmoid')(psi)
    shape_sigmoid = K.int_shape(sigmoid_xg)
    upsample_psi = layers.UpSampling2D(size=(shape_x[1] // shape_sigmoid[1], shape_x[2] // shape_sigmoid[2]))(sigmoid_xg)  # 32

    upsample_psi = expend_as(upsample_psi, shape_x[3])

    y = layers.multiply([upsample_psi, x])

    result = layers.Conv2D(shape_x[3], (1, 1), padding='same')(y)
    result_bn = layers.BatchNormalization()(result)
    return result_bn

        整篇文章的立意還是很清楚的,實現起來不難,但是整個結構的出處,或者來源點還沒有完全弄清,弄清後補充。

       

        最後的最後,總結一下,這幾篇文章各有立意,各有改進,精讀一下很有必要。

        代碼還會不斷完善改進,一旦完成會更新博客。如果有同樣感興趣的,歡迎與我聯繫。如有理解不到位,也歡迎指出。

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