【计算机视觉】详解分类任务的视觉注意力:SENet、CBAM、SKNet (视觉注意力机制 (二))

目录

绪论

一、Squeeze-and-Excitation Networks (SENet)

二、Convolutional Block Attention Module (CBAM)

三、Selective Kernel Networks (SKNet)


绪论

        视觉注意力机制 (一) 主要关注了 视觉应用中的 Self-attention 机制及其应用 —— Non-local 网络模块 ,从最开始的了解什么是视觉注意力机制,到对自注意力机制的细节把握,再到 Non-local 模块的学习。而本文主要关注 视觉注意力机制 在 分类网络 中的经典应用 —— SENetSKNetCBAM

        通常,将软注意力机制中的模型结构分为三大注意力域来分析:空间域通道域混合域

  • 空间域 —— 对图片中的空间域信息进行某种空间变换,从而能将关键的信息提取出来。通常 先生成与特征图等尺寸 (size) 的空间掩码 (mask),然后对每个位置打分 (计算重要性),代表是 Spatial Attention Module。
  • 通道域 —— 给每个通道上的信号都增加一个权重,以代表该通道与关键信息的相关度,该权重越大,表示相关度越高。通常,先对通道生成掩码 (mask),然后对每个通道进行打分 (计算重要性),代表是 SENet, Channel Attention Module。
  • 混合域 —— 空间域的注意力是 忽略了通道域中的信息,将每个通道中的图片特征同等处理,这种做法会将空间域变换方法局限在原始图片特征提取阶段,应用在神经网络层其他层的 可解释性不强。而通道域的注意力是 对一个通道内的信息直接全局平均池化,而忽略每一个通道内的局部信息,这种做法其实也是比较暴力的行为。所以结合两种思路,就可以设计出混合域的注意力机制模型。同时对通道注意力和空间注意力进行评价打分,代表的有 BAM, CBAM。

        下面,将主要介绍视觉注意力机制在分类网络中的经典应用。


一、Squeeze-and-Excitation Networks (SENet)

  • 论文:https://arxiv.org/abs/1709.01507
  • 代码:https://github.com/hujie-frank/SENet

        SENet (Squeeze-and-Excitation Networks),由 Momenta 发表于 CVPR 2017,论文中的 SENet 赢得了 ImageNet 最后一届 (2017) 的图像识别冠军,论文的核心点在于 对 CNN 中的 feature channel (特征通道依赖性) 利用和创新提出的 SE 模块思想简单,易于实现,并且很容易可以加载到现有的网络模型框架中,并且在当前仍受到广泛应用!SENet 主要是 通过显式地建模通道之间的相互依赖关系,自适应地重新校准通道的特征响应,换句话说,就是 学习了通道之间的相关性,筛选出了针对通道的注意力,整个网络稍微增加了一点计算量,但是效果比较好。

        上图是 SENet 的 Block 单元,其中 F_{tr}(\cdot, \theta) 是传统卷积操作,设 xU 分别为 F_{tr} 的输入 (c_1 \times h \times w) 和 输出 (c_2 \times h \times w)。SENet 新增的部分是 U 后的结构:对 U 先做一个 Global Average Pooling F_{sq}(.) (作者称之为 Squeeze),输出的特征图 (1 \times 1 \times c_2) 再经过 两级全连接 F_{ex}(.) (作者称之为 Excitation,通常是降维再升维,后来有工作使用 1×1 卷积替换全连接层),最后用 Sigmoid (Self-gating Mechanism) 限制到 [0, 1] 的范围,把这个值作为 scale (相当于 通道重要性向量) 按元素一一对应乘到 Uc_2 个通道上,从而得到经过通道重要性加权的新特征图。

        这种结构的原理是 希望通过控制 scale 的大小,把重要的特征增强,不重要的特征减弱,从而让提取的特征指向性更强

        通俗地说就是:通过对卷积的到的 feature map 进行处理,得到一个 和通道数一样的一维向量作为每个通道的评价分数,然后将该分数 分别施加到对应的通道上,得到其结果,就在原有的基础上只添加了一个模块。

        这是文中给出的一个嵌入Inception 结构的一个例子。由(H×W×C)全局平均池化得到(1×1×C),即 S 步;接着利用两个全连接层和相应的激活函数建模通道之间的相关性,即 E 步。E 步中包含参数 r 的目的是为了减少全连接层的参数。输出特征通道的权重通过乘法逐通道加权到原来的特征上,得到(H×W×C)的数据,与输入形状完全相同。

        以下是基于 PyTorch 的代码实现:

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),                                 # ReLU 非线性激活函数
            nn.Linear(channel // reduction, channel, bias=False),  # 第二个全连接层 (升维)
            nn.Sigmoid()                                           # 非线性激活函数 + 数值范围约束 (0, 1)
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)  # 即上文所述的 U
        y = self.fc(y).view(b, c, 1, 1)  # reshape 张量以便于进行通道重要性加权的乘法操作

        return x * y.expand_as(x)  # 按元素一一对应相乘

二、Convolutional Block Attention Module (CBAM)

  • 地址:http://openaccess.thecvf.com/content_ECCV_2018/papers/Sanghyun_Woo_Convolutional_Block_Attention_ECCV_2018_paper.pdf

        在该论文中,作者研究了网络架构中的注意力,注意力不仅要告诉我们重点关注哪里,还要 提高关注点的表示。目标是通过使用注意机制来增加表现力,关注重要特征并抑制不必要的特征。为了强调空间和通道这两个维度上的有意义特征,作者 依次应用通道和空间注意模块,来分别在通道和空间维度上学习关注什么、在哪里关注此外,通过了解要强调或抑制的信息也有助于网络内的信息流动。

        上图为整个 CBAM 的示意图,先是通过注意力机制模块,然后是空间注意力模块,对于两个模块先后顺序对模型性能的影响,本文作者也给出了实验的数据对比,先通道再空间要比先空间再通道以及通道和空间注意力模块并行的方式效果要略胜一筹。

        那么这个通道注意力模块和空间注意力模块又是如何实现的呢?

  • 通道注意力模块

        这个部分大体上和 SENet 的注意力模块相同,主要的区别是 CBAM 在 S 步采取了全局平均池化以及全局最大池化,两种不同的池化意味着提取的高层次特征更加丰富。接着在 E 步同样通过两个全连接层和相应的激活函数建模通道之间的相关性,合并两个输出得到各个特征通道的权重。最后,得到特征通道的权重之后,通过乘法逐通道加权到原来的特征上,完成在通道维度上的原始特征重标定。

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)		
  • 空间注意力模块

        首先输入的是经过通道注意力模块的特征,同样利用了全局平均池化和全局最大池化,不同的是,这里是在通道这个维度上进行的操作,也就是说把所有输入通道池化成2个实数,由(h×w×c)形状的输入得到两个(h×w×1)的特征图。接着使用一个 7×7 的卷积核,卷积后形成新的(h×w×1)的特征图。最后也是相同的Scale操作,注意力模块特征与得到的新特征图相乘得到经过双重注意力调整的特征图。

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		

三、Selective Kernel Networks (SKNet)

  • 论文:https://arxiv.org/abs/1903.06586
  • 代码:https://github.com/implus/SKNet

        Selective Kernel Networks(SKNet)发表在 CVPR 2019,是 对 Momenta 发表于 CVPR 2018 上论文 SENet 的改进,且这篇的作者中也有 Momenta 的同学参与。

        SENet是 对特征图的通道注意力机制的研究,之前的 CBAM 提到了对特征图空间注意力机制的研究。这里 SKNet 针对卷积核的注意力机制 研究。

        不同大小的感受视野(卷积核)对于不同尺度(远近、大小)的目标会有不同的效果。尽管比如 Inception 这样的增加了多个卷积核来适应不同尺度图像,但是一旦训练完成后,参数就固定了,这样多尺度信息就会被全部使用了(每个卷积核的权重相同)。

        SKNet 提出了一种机制,即 卷积核的重要性 —— 不同的图像能够得到具有不同重要性的卷积核

        据作者说,该模块在超分辨率任务上有很大提升,并且论文中的实验也证实了在分类任务上有很好的表现。

        SKNet 对不同图像使用的卷积核权重不同,即一种针对不同尺度的图像动态生成卷积核整体结构如下图所示:

        网络主要由 Split、Fuse、Select 三部分组成。

  • Split 部分 是对原特征图经过 不同大小的卷积核 部分进行卷积的过程,这里可以有多个分支。

        对输入X使用不同大小卷积核分别进行卷积操作 (图中的卷积核 size 分别为 3×3 和 5×5 两个分支,但是可以有多个分支)。操作包括卷积、efficient grouped / depth-wise convolutions、BN。

  • Fuse 部分 是 计算每个卷积核权重 的部分。

        将两部分的特征图按元素求和

        U 通过全局平均池化(GAP)生成通道统计信息。得到的 Sc 维度为 C×1

        经过全连接生成紧凑的特征 z(维度为 d×1), δ 是 RELU 激活函数,B 表示批标准化(BN),z 的维度为卷积核的个数,W 维度为 d×C, d 代表全连接后的特征维度,L 在文中的值为 32,r 为压缩因子。

  • Select 部分 是根据不同权重卷积核计算后 得到的新的特征图 的过程。

        进行 Softmax 计算每个卷积核的权重,计算方式如下图所示。如果是两个卷积核,则 ac + bc = 1。z的维度为(d×1)A的维度为(C×d),B的维度为(C×d),则 a = A×z 的维度为1×C。

        Ac、Bc 为 A、B 的第 c 行数据(1×d)。ac 为 a 的第 c 个元素,这样分别得到了每个卷积核的权重。

        将权重应用到特征图上。其中 V = [V1,V2,...,VC],Vc 维度为(H×W),如果

        select 中 Softmax 部分可参考下图(3个卷积核)

        下图是针对 SKNet 总结的思维导图

        参考地址:https://blog.csdn.net/qq_34784753/article/details/89381947?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

        基于 PyTorch的代码实现 1:

class SKConv(nn.Module):
    def __init__(self, features, WH, M, G, r, stride=1, L=32):
        super(SKConv, self).__init__()
        d = max(int(features / r), L)
        self.M = M
        self.features = features
        self.convs = nn.ModuleList([])
        for i in range(M):
            # 使用不同kernel size的卷积
            self.convs.append(
                nn.Sequential(
                    nn.Conv2d(features,
                              features,
                              kernel_size=3 + i * 2,
                              stride=stride,
                              padding=1 + i,
                              groups=G), nn.BatchNorm2d(features),
                    nn.ReLU(inplace=False)))
            
        self.fc = nn.Linear(features, d)
        self.fcs = nn.ModuleList([])
        for i in range(M):
            self.fcs.append(nn.Linear(d, features))
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x):
        for i, conv in enumerate(self.convs):
            fea = conv(x).unsqueeze_(dim=1)
            if i == 0:
                feas = fea
            else:
                feas = torch.cat([feas, fea], dim=1)
        fea_U = torch.sum(feas, dim=1)
        fea_s = fea_U.mean(-1).mean(-1)
        fea_z = self.fc(fea_s)
        for i, fc in enumerate(self.fcs):
            print(i, fea_z.shape)
            vector = fc(fea_z).unsqueeze_(dim=1)
            print(i, vector.shape)
            if i == 0:
                attention_vectors = vector
            else:
                attention_vectors = torch.cat([attention_vectors, vector],
                                              dim=1)
        attention_vectors = self.softmax(attention_vectors)
        attention_vectors = attention_vectors.unsqueeze(-1).unsqueeze(-1)
        fea_v = (feas * attention_vectors).sum(dim=1)
        return fea_v	

        基于 PyTorch 的代码实现 2: 

import torch.nn as nn
import torch
from functools import reduce


class SKConv(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, M=2, r=16, L=32):
        '''
        :param in_channels:  输入通道维度
        :param out_channels: 输出通道维度   原论文中 输入输出通道维度相同
        :param stride:  步长,默认为1
        :param M:  分支数
        :param r: 特征Z的长度,计算其维度d 时所需的比率(论文中 特征S->Z 是降维,故需要规定 降维的下界)
        :param L:  论文中规定特征Z的下界,默认为32
        '''
        super(SKConv,self).__init__()
        d=max(in_channels//r, L)   # 计算向量Z 的长度d
        self.M=M
        self.out_channels=out_channels
        self.conv=nn.ModuleList()  # 根据分支数量 添加 不同核的卷积操作
        for i in range(M):
            # 为提高效率,原论文中 扩张卷积5x5为 (3X3,dilation=2)来代替。且论文中建议分组卷积G=32
            self.conv.append(nn.Sequential(nn.Conv2d(in_channels, out_channels, 3, stride, padding=1+i, dilation=1+i, groups=32, bias=False),
                                           nn.BatchNorm2d(out_channels),
                                           nn.ReLU(inplace=True)))
        self.global_pool=nn.AdaptiveAvgPool2d(1) # 自适应 pool 到指定维度 - GAP
        self.fc1=nn.Sequential(nn.Conv2d(out_channels, d, 1, bias=False),
                               nn.BatchNorm2d(d),
                               nn.ReLU(inplace=True))   # 降维
        self.fc2=nn.Conv2d(d, out_channels*M, 1, 1, bias=False)  # 升维
        self.softmax=nn.Softmax(dim=1) # 指定 dim=1 令两个 FCs 对应位置进行 softmax,保证 对应位置a+b+..=1

    def forward(self, input):
        batch_size=input.size(0)
        output=[]
        #the part of split
        for i,conv in enumerate(self.conv):
            #print(i,conv(input).size())
            output.append(conv(input))
        #the part of fusion
        U=reduce(lambda x, y: x+y, output) # 逐元素相加生成 混合特征 U
        s=self.global_pool(U)
        z=self.fc1(s)  # S->Z 降维
        a_b=self.fc2(z)  # Z->a, b 升维 - 论文用 conv 1x1 表示 FC。结果中前一半通道值为 a, 后一半为 b
        a_b=a_b.reshape(batch_size, self.M, self.out_channels, -1) # reshape 为两个 FCs 的值
        a_b=self.softmax(a_b)  # 令两个 FCs 对应位置进行 softmax
        #the part of selection
        a_b=list(a_b.chunk(self.M, dim=1))  # split to a 和 b - chunk 将 tensor 按指定维度切分成几块
        a_b=list(map(lambda x: x.reshape(batch_size, self.out_channels, 1, 1), a_b))  # reshape 所有分块,即扩展两维
        V=list(map(lambda x, y: x*y, output, a_b)) # 权重与对应不同卷积核输出的 U 逐元素相乘
        V=reduce(lambda x, y: x+y, V)  # 两个加权后的特征 逐元素相加
        return V


class SKBlock(nn.Module):
    '''
    基于Res Block构造的SK Block
    ResNeXt有  1x1Conv(通道数:x) +  SKConv(通道数:x)  + 1x1Conv(通道数:2x) 构成
    '''
    expansion=2 #指 每个block中 通道数增大指定倍数
    def __init__(self,inplanes,planes,stride=1,downsample=None):
        super(SKBlock,self).__init__()
        self.conv1=nn.Sequential(nn.Conv2d(inplanes,planes,1,1,0,bias=False),
                                 nn.BatchNorm2d(planes),
                                 nn.ReLU(inplace=True))
        self.conv2=SKConv(planes,planes,stride)
        self.conv3=nn.Sequential(nn.Conv2d(planes,planes*self.expansion,1,1,0,bias=False),
                                 nn.BatchNorm2d(planes*self.expansion))
        self.relu=nn.ReLU(inplace=True)
        self.downsample=downsample

    def forward(self, input):
        shortcut=input
        output=self.conv1(input)
        output=self.conv2(output)
        output=self.conv3(output)
        if self.downsample is not None:
            shortcut=self.downsample(input)
        output+=shortcut
        return self.relu(output)


class SKNet(nn.Module):
    '''
    参考 论文Table.1 进行构造
    '''
    def __init__(self,nums_class=1000,block=SKBlock,nums_block_list=[3, 4, 6, 3]):
        super(SKNet,self).__init__()
        self.inplanes=64
        # in_channel=3  out_channel=64  kernel=7x7 stride=2 padding=3
        self.conv=nn.Sequential(nn.Conv2d(3,64,7,2,3,bias=False),
                                nn.BatchNorm2d(64),
                                nn.ReLU(inplace=True))
        self.maxpool=nn.MaxPool2d(3,2,1) # kernel=3x3 stride=2 padding=1
        self.layer1=self._make_layer(block,128,nums_block_list[0],stride=1) # 构建表中 每个[] 的部分
        self.layer2=self._make_layer(block,256,nums_block_list[1],stride=2)
        self.layer3=self._make_layer(block,512,nums_block_list[2],stride=2)
        self.layer4=self._make_layer(block,1024,nums_block_list[3],stride=2)
        self.avgpool=nn.AdaptiveAvgPool2d(1) # GAP全局平均池化
        self.fc=nn.Linear(1024*block.expansion,nums_class) # 通道 2048 -> 1000
        self.softmax=nn.Softmax(-1) # 对最后一维进行softmax

    def forward(self, input):
        output=self.conv(input)
        output=self.maxpool(output)
        output=self.layer1(output)
        output=self.layer2(output)
        output=self.layer3(output)
        output=self.layer4(output)
        output=self.avgpool(output)
        output=output.squeeze(-1).squeeze(-1)
        output=self.fc(output)
        output=self.softmax(output)
        return output

    def _make_layer(self,block,planes,nums_block,stride=1):
        downsample=None
        if stride!=1 or self.inplanes!=planes*block.expansion:
            downsample=nn.Sequential(nn.Conv2d(self.inplanes,planes*block.expansion,1,stride,bias=False),
                                     nn.BatchNorm2d(planes*block.expansion))
        layers=[]
        layers.append(block(self.inplanes,planes,stride,downsample))
        self.inplanes=planes*block.expansion
        for _ in range(1,nums_block):
            layers.append(block(self.inplanes,planes))
        return nn.Sequential(*layers)


def SKNet50(nums_class=1000):
    return SKNet(nums_class, SKBlock, [3, 4, 6, 3])  # 论文通过[3, 4, 6, 3]搭配出SKNet50


def SKNet101(nums_class=1000):
    return SKNet(nums_class, SKBlock, [3, 4, 23, 3])


if __name__=='__main__':
    x = torch.rand(2, 3, 224, 224)
    model=SKNet50()
    y=model(x)
    print(y) # shape [2,1000]

参考资料:

视觉注意力机制 | 视觉注意力机制用于分类:SENet、CBAM、SKNet

[论文笔记]-SENet和SKNet(附代码) - 知乎

  • 38
    点赞
  • 367
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值