计算机视觉中的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

        整篇文章的立意还是很清楚的,实现起来不难,但是整个结构的出处,或者来源点还没有完全弄清,弄清后补充。

       

        最后的最后,总结一下,这几篇文章各有立意,各有改进,精读一下很有必要。

        代码还会不断完善改进,一旦完成会更新博客。如果有同样感兴趣的,欢迎与我联系。如有理解不到位,也欢迎指出。

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