CV中的Attention和Self-Attention
1 Attention 和 Self-Attention
Attention的核心思想是:从关注全部到关注重点。
Attention 机制很像人类看图片的逻辑,当我们看一张图片的时候,我们并没有看清图片的全部内容,而是将注意力集中在了图片的焦点上。大家看下面这张图自行体会:
对于CV中早期的Attention,通常是在通道或者空间计算注意力分布,例如:SENet,CBAM。
而Self-attention(NLP中往往称为Scaled-Dot Attention)的结构有三个分支:query、key和value。计算时通常分为三步:
- 第一步是将query和每个key进行相似度计算得到权重,常用的相似度函数有点积,cos相似度,拼接,感知机等;
- 第二步一般是使用一个softmax函数对这些权重进行归一化;
- 第三步将权重和相应的键值value进行加权求和得到最后的attention。
假设输入的feature maps的大小Batch_size×Channels×Width×Height,那么通过三个1×1卷积(分别是query_conv , key_conv 和 value_conv)就可以得到query、key和value:
- query:在query_conv卷积中,输入为B×C×W×H,输出为B×C/8×W×H;
- key:在key_conv卷积中,输入为B×C×W×H,输出为B×C/8×W×H;
- value:在value_conv卷积中,输入为B×C×W×H,输出为B×C×W×H。
后续的操作可以查看下面代码及注释:
class Self_Attn(nn.Module): ''' Self attention Layer''' def __init__(self,in_dim,activation): super(Self_Attn,self).__init__() self.chanel_in = in_dim self.activation = activation self.query_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//8 , kernel_size= 1) self.key_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//8 , kernel_size= 1) self.value_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim , kernel_size= 1) self.gamma = nn.Parameter(torch.zeros(1)) self.softmax = nn.Softmax(dim=-1) def forward(self,x): ''' inputs : x : input feature maps( B * C * W * H) returns : out : self attention value + input feature attention: B * N * N (N is Width*Height) ''' m_batchsize,C,width ,height = x.size() proj_query = self.query_conv(x).view(m_batchsize,-1,width*height).permute(0,2,1) # B*N*C/8 proj_key = self.key_conv(x).view(m_batchsize,-1,width*height) # B*C*N/8 energy = torch.bmm(proj_query,proj_key) # batch的matmul B*N*N attention = self.softmax(energy) # B * (N) * (N) proj_value = self.value_conv(x).view(m_batchsize,-1, width*height) # B * C * N out = torch.bmm(proj_value,attention.permute(0,2,1) ) # B*C*N out = out.view(m_batchsize,C,width,height) # B*C*H*W out = self.gamma*out + x return out,attention
2 【CVPR 2017】SENet
论文:Squeeze-and-Excitation Networks
代码:hujie-frank/SENet
由Momenta研发的网路SENet,获得ImageNet 2017 Image Classification 冠军。将top-5 error从2.991%降到2.251%。
SENet是早期Attention,核心思想是学习 feature Channel 间的关系,以凸显feature Channel 不同的重要度(也就是注意力分布),进而提高模型表现。
上图是SE Module 的示意图。给定一个输入 x,其特征通道数为 c_1,通过一系列卷积等一般变换后得到一个特征通道数为 c_2 的特征。与传统的 CNN 不一样的是,接下来通过三个操作来重标定前面得到的特征。
首先是 Squeeze 操作,从空间维度来进行特征压缩,将h*w*c的特征变成一个1*1*c的特征,得到向量某种程度上具有全域性的感受野,并且输出的通道数和输入的特征通道数相匹配,它表示在特征通道上响应的全域性分布。公式非常简单,就是一个 global average pooling:
其次是 Excitation 操作,通过引入 w 参数来为每个特征通道生成权重,其中引数 w 是可学习的,并通过一个 Sigmoid 的门获得 0~1 之间归一化的权重,完成显式地建模特征通道间的相关性。公式如下:
最后是一个 Scale 的操作,将 Excitation 的输出的权重看做是经过选择后的每个特征通道的重要性,然后通过channel-wise multiplication 主通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。公式如下:
介绍完具体的公式实现,下面介绍下SE block如何运用到具体的网络中。
代码:
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)
虽然没有看到query、key和value的影子,但是其体现了不同channel应有不同权重,是早期的attention。
3【ECCV 2018】CBAM
论文:CBAM: Convolutional Block Attention Module
代码:https://github.com/luuuyi/CBAM.PyTorch
这是2018年ECCV的一篇论文,引文超过1000篇。
CBAM可以无缝地集成到任何CNN架构中,开销不会很大,而且可以与基本CNN网络一起进行端到端的训练。与SENet类似,CBAM 也是早期的Attention,没有通过复杂的相似度计算得到注意力分布。
CBAM: General Architecture
CBAM依次推导出尺寸为C×1×1的一维通道注意图Mc和尺寸为1×H×W的二维空间注意图Ms:
其中⨂表示element-wise的乘法,F''是最终的优化输出。
实验表明,sequential arrangement 比parallel arrangement效果,并且channel-first 顺序略优于 spatial-first.。
ResBlock中的CBAM示例如下所示:
Channel Attention Module
Channel Attention集中在输入图像的“channel”上。
为了有效地计算channel attention,对输入特征映射的空间维度进行压缩。
对于空间信息的聚合,通常同时采用 average-pooling 和 max-pooling,以得到更精细的channel-wise attention。
Fcavg和Fcmax分别表示平均池特征和最大池特征,然后通过一个隐藏层的多层感知器(MLP),σ表示sigmoid函数。
Spatial Attention Module
Spatial attention关注“空间”的信息,是对Channel Attention的补充。
为了计算Spatial attention,在 Channel 轴上应用average-pooling 和 max-pooling,然后将它们连接起来生成一个有效的特征。
然后利用卷积层生成一个R×H×W的空间注意映射Ms(F),该映射对强调或抑制的位置进行编码。
具体地说,通过两次池生成两个映射:1×H×W的Fsavg和1×H×W的Fsmax。σ表示sigmoid函数,f7×7表示滤波器尺寸为7×7的卷积运算。
代码如下:
def conv3x3(in_planes, out_planes, stride=1): '3x3 convolution with padding' return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False)class ChannelAttention(nn.Module): def __init__(self, in_planes, ratio=16): super(ChannelAttention, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) # 压缩空间 self.max_pool = nn.AdaptiveMaxPool2d(1) self.fc1 = nn.Conv2d(in_planes, in_planes // 16, 1, bias=False) self.relu1 = nn.ReLU() self.fc2 = nn.Conv2d(in_planes // 16, in_planes, 1, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.fc2(self.relu1(self.fc1(self.avg_pool(x)))) max_out = self.fc2(self.relu1(self.fc1(self.max_pool(x)))) out = avg_out + max_out # [b, C, 1, 1] return self.sigmoid(out)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.conv1 = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) # 压缩通道 max_out, _ = torch.max(x, dim=1, keepdim=True) # 压缩通道 x = torch.cat([avg_out, max_out], dim=1) # [b, 1, h, w] x = self.conv1(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 outclass Bottleneck(nn.Module): expansion = 4 def __init__(self, inplanes, planes, stride=1, downsample=None): super(Bottleneck, 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.ca = ChannelAttention(planes * 4) 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.relu(out) out = self.conv3(out) out = self.bn3(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
4 【CVPR2018 Non-local】
论文地址:https://arxiv.org/abs/1711.07971
再次回顾下Self-attention
Self-attention结构自上而下分为三个分支,分别是query、key和value。计算时通常分为三步:
- 第一步是将query和每个key进行相似度计算得到权重,常用的相似度函数有点积,cos相似度,拼接,感知机等;
- 第二步一般是使用一个softmax函数对这些权重进行归一化;
- 第三步将权重和相应的键值value进行加权求和得到最后的attention。
Non-local就是CV中的self-attetion。其计算公式如下:
- x是输入信号,CV中使用的一般是 feature map
- i 代表的是输出位置,如空间、时间或者时空的索引,j 代表全局响应
- f 函数式计算i和j的相似度
- g 函数计算feature map在j位置的表示
- 最终的y是通过响应因子 C(x) 进行标准化处理以后得到的
non-local结构图如下,可以看到non-local的原理与self-attention运行原理一样,通过 3 个1*1的卷积构建了query,key 和 value。
5 【CVPR 2019】DANet
论文题目:Dual Attention Network for Scene Segmentation
论文地址:https://arxiv.org/abs/1809.02983
DANet结构如上图,包含了Position Attention Module 和 Channel Attention Module,和CBAM相似,只是在spatial和channel维度利用self-attention思想建立全局上下文关系。如下所示:
6 总结
Self-attention能够捕捉全局的特征,因此,也在计算机视觉领域大放异彩,如 Detr,Sparse R-CNN等等,不过需要指出的是:Self-attention 也是有缺陷的,如:计算量大,并且这类Set Prediction检测器检测准确性还不能够超越之前的检测算法。
因此,如果是做研究,那么这是一个不错的主题;如果是要产品落地,那么直接拿来用可能就会被速度拖累。