1、SE
SENet是Squeeze-and-Excitation Networks的简称,拿到了ImageNet2017分类比赛冠军,其效果得到了认可,其提出的SE模块思想简单,易于实现,并且很容易可以加载到现有的网络模型框架中。SENet主要是学习了channel之间的相关性,筛选出了针对通道的注意力,稍微增加了一点计算量,但是效果比较好。
通过上图可以理解他的实现过程,通过对卷积的到的feature map进行处理,得到一个和通道数一样的一维向量作为每个通道的评价分数,然后将改分数分别施加到对应的通道上,得到其结果,就在原有的基础上只添加了一个模块。
首先是Squeeze操作,我们顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使的靠近输入的层也可以获得全局感受野,这一点在很多任务中都是非常有用的。
其次是Excitation操作,它是一个类似于循环神经网络门的机制。通过参数来为每个特征通道生成权重,其中参数被学习用来显式地建模特征通道间的相关性。
最后是一个Reweight的操作,我们将Excitation的输出的权重看做是进过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。
上左图是将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等等。
用pytorch实现resnet model:
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添加到分类网络,分类网络现在一般都是成一个block一个block,se模块就可以加到一个block结束的位置,进行一个信息refine。这里用了一些STOA的分类模型如:resnet50,resnext50,bn-inception等网络。通过添加SE模块,能使模型提升0.5-1.5%,效果还可以,增加的计算量也可以忽略不计。在轻量级网络MobileNet,ShuffleNet上也进行了实验,可以提升的点更多一点大概在1.5-2%。
SE添加到目标检测网络,主要还是将SE模块添加到backbone部分,优化学习到的内容。目标检测数据集使用的是benchmark MSCOCO, 使用的Faster R-CNN作为目标。检测器,使用backbone从ResNet50替换为SE-ResNet50以后带了了两个点的AP提升,确实有效果。
论文地址:https://arxiv.org/abs/1709.01507
代码参考地址:https://github.com/moskomule/senet.pytorch
核心代码:https://github.com/pprp/SimpleCVReproduction/blob/master/attention/SE/senet.py
SENet的变种:
SKNet:https://zhuanlan.zhihu.com/p/60187262
GCNet:https://zhuanlan.zhihu.com/p/64988633
2、 CBAM(Convolutional Block Attention Module)
论文:https://arxiv.org/abs/1807.06521
代码:https://github.com/Jongchan/attention-module
之前有一篇论文提出了SENet,在feature map的通道上进行attention生成,然后与原来的feature map相乘。这篇文章指出,该种attention方法只关注了通道层面上哪些层会具有更强的反馈能力,但是在空间维度上并不能体现出attention的意思。CBAM作为本文的亮点,将attention同时运用在channel和spatial两个维度上,CBAM与SE Module一样,可以嵌入了目前大部分主流网络中,在不显著增加计算量和参数量的前提下能提升网络模型的特征提取能力。 近几年,随着CNN的兴起,很多结构新颖、有效的网络结构被提出,比如ResNet、ResNext等;最近,大部分论文证明了在网络结构中引入attention机制可以提升网络模型的特征表达能力。attention不止能告诉网络模型该注意什么,同时也能增强特定区域的表征。本文的CBAM在channel和spatial两个维度上引入了attention机制。
由于卷积运算通过将多通道和空间信息混合在一起来提取信息特征,而在论文中采用他们的模块来强调沿着这两个主要维度的有意义的特征:通道和空间轴。这样每个分支都可以分别在通道和空间轴上学习到"what"和"where"。
F
∈
R
C
×
H
×
W
F∈R^{C×H×W}
F∈RC×H×W,
F
F
F是input feature map作为输入,其中⊕表示逐元素乘法,
M
c
∈
R
C
×
1
×
1
M_c∈R^{C×1×1}
Mc∈RC×1×1,
M
c
M_c
Mc表示在channel维度上做attention提取的操作
M
s
∈
R
1
×
H
×
W
M_s∈R^{1×H×W}
Ms∈R1×H×W,
M
s
M_s
Ms表示的是在spatial维度上做attention提取的操作
① Channel Attention Module算法原理
为了有效地计算通道注意力,我们压缩输入特征图谱的空间维度。为了聚集空间信息首先将feature map在spatial维度上进行压缩,得到一个一维矢量以后再进行操作。对输入feature map进行spatial维度压缩时,a使用的是average pooling(平均池化)和max pooling(最大值池化),通过两个pooling函数以后总共可以得到两个一维矢量,这样做的好处是:average pooling有效地学习目标物体,而最大池化收集了关于独特对象特征的另一个重要线索,以推断出通道方面的注意力。此外,我在网上学习时看到过另一种说法:average pooling对feature map上的每一个像素点都有反馈,而 max pooling在进行梯度反向传播计算只有feature map中响应最大的地方有梯度的反馈,能作为GAP的一个补充。
在得到两个一维矢量后,将其放入一个共享网络中,共享网络是由一个隐藏层和多层感知机(MLP)组成。为了减少参数开销,隐藏的激活大小设置为 R C ⁄ r × 1 × 1 R^{C⁄r×1×1} RC⁄r×1×1,其中r是压缩率。在将共享网络应用于矢量之后,我们使用逐元素求和来合并输出特征向量。其中σ表示Sigmoid函数,即再经过激活函数后最后得到的结果 M c ∈ R C × 1 × 1 M_c∈R^{C×1×1} Mc∈RC×1×1
值得注意的是,多层感知机模型中W0和W1之间的feature需要使用ReLU作为激活函数去处理
② Spatial attention module
首先沿通道轴应用平均池化和最大池化操作(此处注意点:这里的压缩变成了通道层面上的压缩,不再是最开始的feature map,这里面的
F
=
M
c
F=M_c
F=Mc),接着对输入特征分别在通道维度上做了avg和max操作。最后得到了两个二维的feature,将其按通道维度拼接在一起得到一个通道数为2的feature map,之后使用一个包含单个卷积核的隐藏层对其进行卷积操作,要保证最后得到的feature在spatial维度上与输入的feature map一致。
其中 σ σ σ表示sigmoid函数, f 7 × 7 f^{7×7} f7×7表示卷积操作,卷积核大小为7×7
给定输入图像,两个注意力模块,通道和空间,计算互补注意力,分别关注“什么”和“何处”。 考虑到这一点,可以以并行或顺序方式放置两个模块。我们发现顺序排列比并行排列能得到更好的结果。对于顺序过程的安排,我们的实验结果表明,通道的第一顺序略好于空间第一顺序。
Pytorch代码:
class Channel_Attention(nn.Module):
def __init__(self, channel, r):
super(Channel_Attention, self).__init__()
self.__avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.__max_pool = nn.AdaptiveMaxPool2d((1, 1))
self.__fc = nn.Sequential(
nn.Conv2d(channel, channel//r, 1, bias=False),
nn.ReLU(True),
nn.Conv2d(channel//r, channel, 1, bias=False),
)
self.__sigmoid = nn.Sigmoid()
def forward(self, x):
y1 = self.__avg_pool(x)
y1 = self.__fc(y1)
y2 = self.__max_pool(x)
y2 = self.__fc(y2)
y = self.__sigmoid(y1+y2)
return x * y
class Spartial_Attention(nn.Module):
def __init__(self, kernel_size):
super(Spartial_Attention, self).__init__()
assert kernel_size % 2 == 1, "kernel_size = {}".format(kernel_size)
padding = (kernel_size - 1) // 2
self.__layer = nn.Sequential(
nn.Conv2d(2, 1, kernel_size=kernel_size, padding=padding),
nn.Sigmoid(),
)
def forward(self, x):
avg_mask = torch.mean(x, dim=1, keepdim=True)
max_mask, _ = torch.max(x, dim=1, keepdim=True)
mask = torch.cat([avg_mask, max_mask], dim=1)
mask = self.__layer(mask)
return x * mask
OR:
class ChannelAttention(nn.Module):
def __init__(self, in_planes, rotio=16):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.sharedMLP = nn.Sequential(
nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False), nn.ReLU(),
nn.Conv2d(in_planes // rotio, in_planes, 1, bias=False))
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = self.sharedMLP(self.avg_pool(x))
maxout = self.sharedMLP(self.max_pool(x))
return self.sigmoid(avgout + maxout)
class SpatialAttention(nn.Module):
def __init__(self, kernel_size=7):
super(SpatialAttention, self).__init__()
assert kernel_size in (3,7), "kernel size must be 3 or 7"
padding = 3 if kernel_size == 7 else 1
self.conv = nn.Conv2d(2,1,kernel_size, padding=padding, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = torch.mean(x, dim=1, keepdim=True)
maxout, _ = torch.max(x, dim=1, keepdim=True)
x = torch.cat([avgout, maxout], dim=1)
x = self.conv(x)
return self.sigmoid(x)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.ca = ChannelAttention(planes)
self.sa = SpatialAttention()
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.ca(out) * out # 广播机制
out = self.sa(out) * out # 广播机制
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
Keras代码:
def CBAM(input, channel, ratio):
# channel attention
avg_pool = GlobalAveragePooling2D()(input)
avg_pool = Dense(channel // ratio, avtivation='relu')(avg_pool)
max_pool = GlobalAveragePooling2D()(input)
max_pool = Dense(channel // ratio, avtivation='relu')(max_pool)
avg_pool = Dense(channel, avtivation=None)(avg_pool)
max_pool = Dense(channel, avtivation=None)(max_pool)
mask = Add()([avg_pool, max_pool])
mask = Activation('sigmoid')(mask)
x = multiply([input,mask])
# spatial attention
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])
# x = Conv2D(8, (1, 1), padding='same', activation='tanh')(x)
mask = Conv2D(1, (1, 1), padding='same', activation='sigmoid')(x)
output = multiply([x, mask])
return output
3、SA-NET
论文地址:https://arxiv.org/pdf/2102.00240.pdf
Github地址:https://github.com/wofmanaf/SA-Net/blob/main/models/sa_resnet.py
Shuffle Attention(SA)模块采用Shuffle单元有效地结合了两种类型的注意机制(空间注意力和通道注意力)。具体而言,SA首先将通道维分组为多个子特征,然后再并行处理它们。然后,对于每个子特征,SA利用shuffle单元在空间和通道维度上描绘特征依赖性。之后,将所有子特征汇总在一起,并使用“通道混洗”运算符来启用不同子特征之间的信息通信。
主要动机来源于:
① ShuffleNet中的channel shuffle可以高效的保证通道信息交互
② SGE中将特征按照通道维度分组
因此,结合上述两个动机,提出了一种更轻量级且更高效的shuffle注意力(SA)模块,该模块将通道维度划分为子特征。对于每个子特征,SA均采用shuffle单元同时构建通道注意力和空间注意力。对于每个注意力模块,在所有位置上都设计了一个注意力mask,以抑制可能出现的噪声并突出显示正确的语义特征区域。
针对深层CNN引入了一个轻量级但有效的注意力模块SA,该模块将通道维度分为多个子特征,然后利用shuffle单元整合互补通道,并每个子特征的空间关注模块
该结构很简单,流程如下:
① 对输入特征X按照通道维度分组X1…Xg,每个子特征的通道数位c/g
② 对每个子特征继续split成两份,分别提取通道注意力和空间注意力
③ 后处理融合
代码,使用parameter代替卷积比较新颖:
class sa_layer(nn.Module):
"""Constructs a Channel Spatial Group module.
Args:
k_size: Adaptive selection of kernel size
"""
def __init__(self, channel, groups=64):
super(sa_layer, self).__init__()
self.groups = groups
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.cweight = Parameter(torch.zeros(1, channel // (2 * groups), 1, 1))
self.cbias = Parameter(torch.ones(1, channel // (2 * groups), 1, 1))
self.sweight = Parameter(torch.zeros(1, channel // (2 * groups), 1, 1))
self.sbias = Parameter(torch.ones(1, channel // (2 * groups), 1, 1))
self.sigmoid = nn.Sigmoid()
self.gn = nn.GroupNorm(channel // (2 * groups), channel // (2 * groups))
@staticmethod
def channel_shuffle(x, groups):
b, c, h, w = x.shape
x = x.reshape(b, groups, -1, h, w)
x = x.permute(0, 2, 1, 3, 4)
# flatten
x = x.reshape(b, -1, h, w)
return x
def forward(self, x):
b, c, h, w = x.shape
x = x.reshape(b * self.groups, -1, h, w)
x_0, x_1 = x.chunk(2, dim=1)
# channel attention
xn = self.avg_pool(x_0)
xn = self.cweight * xn + self.cbias
xn = x_0 * self.sigmoid(xn)
# spatial attention
xs = self.gn(x_1)
xs = self.sweight * xs + self.sbias
xs = x_1 * self.sigmoid(xs)
# concatenate along channel axis
out = torch.cat([xn, xs], dim=1)
out = out.reshape(b, -1, h, w)
out = self.channel_shuffle(out, 2)
return out
ECA:
论文:https://arxiv.org/abs/1910.03151
代码:https://github.com/BangguWu/ECANet
参考:https://blog.csdn.net/tjut_zdd/article/details/102600401
SGE:
论文:https://arxiv.org/abs/1905.09646
代码:https://github.com/implus/PytorchInsight
参考:https://blog.csdn.net/ITOMG/article/details/93652288
4、Triplet
论文:https://arxiv.org/pdf/2010.03045.pdf
代码:https://github.com/LandskapeAI/triplet-attention
triplet attention,是一种通过使用三分支结构捕获跨维度交互来计算注意力权重的新方法。对于输入张量,triplet attention通过旋转操作,然后使用残差变换建立维度间的依存关系,并以可忽略的计算开销对通道间和空间信息进行编码。
作者观察到CBAM中的通道注意力方法虽然提供了显着的性能改进,却不是因为跨通道交互。然而,作者展示了捕获通道交互时对性能会产生有利的影响。此外,CBAM在计算通道注意力时结合了降维功能。这在捕获通道之间的非线性局部依赖关系方面是多余的。
因此,本文提出了可以有效解决跨维度交互的triplet attention。相较于以往的注意力方法,主要有两个优点:
① 可以忽略的计算开销
② 强调了多维交互而不降低维度的重要性,因此消除了通道和权重之间的间接对应
如上图所示,Triplet Attention主要包含3个分支,其中两个分支分别用来捕获通道C维度和空间维度W/H之间的跨通道交互,剩下的一个分支就是传统的通道注意力权重的计算。
网络结构:
具体的网络结构如上图所示:
① 第一个分支:通道注意力计算分支,输入特征经过Z-Pool,再接着7 x 7卷积,最后Sigmoid激活函数生成通道注意力权重
② 第二个分支:通道C和空间W维度交互捕获分支,输入特征先经过permute,变为H X C X W维度特征,接着在H维度上进行Z-Pool,后面操作类似。最后需要经过permuter变为C X H X W维度特征,方便进行element-wise相加
③ 第三个分支:通道C和空间H维度交互捕获分支,输入特征先经过permute,变为W X H X C维度特征,接着在W维度上进行Z-Pool,后面操作类似。最后需要经过permuter变为C X H X W维度特征,方便进行element-wise相加
最后对3个分支输出特征进行相加求Avg
Z-Pool
对输入进行MaxPooling和AvgPooling,输出2 X H X W特征
代码:
class ZPool(nn.Module):
def forward(self, x):
return torch.cat( (torch.max(x,1)[0].unsqueeze(1), torch.mean(x,1).unsqueeze(1)), dim=1 )
class AttentionGate(nn.Module):
def __init__(self):
super(AttentionGate, self).__init__()
kernel_size = 7
self.compress = ZPool()
self.conv = BasicConv(2, 1, kernel_size, stride=1, padding=(kernel_size-1) // 2, relu=False)
def forward(self, x):
x_compress = self.compress(x)
x_out = self.conv(x_compress)
scale = torch.sigmoid_(x_out)
return x * scale
class TripletAttention(nn.Module):
def __init__(self, no_spatial=False):
super(TripletAttention, self).__init__()
self.cw = AttentionGate()
self.hc = AttentionGate()
self.no_spatial=no_spatial
if not no_spatial:
self.hw = AttentionGate()
def forward(self, x):
x_perm1 = x.permute(0,2,1,3).contiguous()
x_out1 = self.cw(x_perm1)
x_out11 = x_out1.permute(0,2,1,3).contiguous()
x_perm2 = x.permute(0,3,2,1).contiguous()
x_out2 = self.hc(x_perm2)
x_out21 = x_out2.permute(0,3,2,1).contiguous()
if not self.no_spatial:
x_out = self.hw(x)
x_out = 1/3 * (x_out + x_out11 + x_out21)
else:
x_out = 1/2 * (x_out11 + x_out21)
return x_out
模块复杂度,几乎无增长:
ImageNet:参数量不涨,但是Flops比别人多
5、DCANet
论文:https://arxiv.org/abs/2007.05099
DCANet用增强其他Attention模块能力的方式来改进的,可以让注意力模块之间的信息流动更加充分,提升注意力学习的能力。目前文章还没有被接收
DCANet(Deep Connected Attention Network)就是用来提升attention模块能力的。主要做法是:将相邻的Attention Block互相连接,让信息在Attention模块之间流动。
注意力机制可以分为三部分:通道注意力机制、空间注意力机制和self-attention,也有说法是可以将self-attention视为通道注意力和空间注意力的混合。DCANet设计也参考了残差网络的思想。Attention模块之间的连接方式也已经有一部分的研究,比如RA-CNN是专门用于细粒度图像识别的网络架构,网络可以不断生成具有鉴别力的区域,从而实现从粗糙到细节的识别。GANet中,高层的attention特征会送往底层的特征来指导注意力学习。
attention方面的设计思路,首先将他们分为三个阶段,分别是
① Extraction: 特征提取阶段
② Transformation:转换阶段,将信息进行处理或者融合
③ Fusion: 融合阶段,将得到的信息进行融合到主分支中
下图从左到右分别是SEblock、GEblock、GCblock、SKblock,他们都可以归结以上三个阶段
Extraction
这个阶段是从特征图上进行特征提取,规定特征图为:
经过特征提取器g(wg是这个特征提取操作的参数,G是输出结果):
Transformation
这个阶段处理上一个阶段得到的聚集的信息,然后将他们转化到非线性注意力空间中。规定转换t为(wt是参数,T是这个阶段的输出结果):
Fusion
这个阶段整合上个阶段获取的特征图,可以表达为:
其中代表最终这一层的输出结果,代表特征融合方式,比如dot-product,summation等操作。
Attention Connection
这是本文的核心,特征不仅仅要从当前层获取,还要从上一层获取,这时就是需要两层信息的融合,具体融合方式提出了两种:
Direct Connection: 通过相加的方式进行相连。
weighted Connection: 加权的方式进行相连。
可以看到,上边通路是两个通道注意力模块,两个模块之间会有一个Attention Connection让两者相连,让后边的模块能利用上之前模块的信息。下边通路是两个空间注意力模块,原理同上,空间注意力模块可以有也可以没有。
参考博客:
https://www.cnblogs.com/pprp/p/12128520.html
https://www.sohu.com/a/161793789_642762
https://blog.csdn.net/weixin_45250844/article/details/103500784
https://blog.csdn.net/qq_39027890/article/details/86656182
https://blog.csdn.net/weixin_42096202/article/details/108970057
https://jishuin.proginn.com/p/763bfbd3abed等