计算机系统应用教程网站

网站首页 > 技术文章 正文

CV中的Attention和Self-Attention

btikc 2024-10-12 11:00:49 技术文章 10 ℃ 0 评论

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-poolingmax-pooling,以得到更精细的channel-wise attention。

Fcavg和Fcmax分别表示平均池特征和最大池特征,然后通过一个隐藏层的多层感知器(MLP),σ表示sigmoid函数。

Spatial Attention Module

Spatial attention关注“空间”的信息,是对Channel Attention的补充。

为了计算Spatial attention,在 Channel 轴上应用average-poolingmax-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 out


class 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检测器检测准确性还不能够超越之前的检测算法。

因此,如果是做研究,那么这是一个不错的主题;如果是要产品落地,那么直接拿来用可能就会被速度拖累。


公众号:AI约读社,欢迎关注

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表