注意力残差网络_git


注意力机制在卷积网络的优化中,以及被广泛的使用。下面介绍几种非常著名的,应用于特征提取网络的注意力机制。

  • SEnet

SEnet(Squeeze-and-Excitation Network)考虑了特征通道之间的关系,在特征通道上加入了注意力机制。

SEnet通过学习的方式自动获取每个特征通道的重要程度,并且利用得到的重要程度来提升特征并抑制对当前任务不重要的特征。SEnet通过Squeeze模块和Exciation模块实现所述功能。


注意力残差网络_注意力残差网络_02


如上图所示,首先作者通过squeeze操作,对空间维度进行压缩,直白的说就是对每个特征图做全局池化,平均成一个实数值。该实数从某种程度上来说具有全局感受野。作者提到该操作能够使得靠近数据输入的特征也可以具有全局感受野,这一点在很多的任务中是非常有用的。

紧接着就是excitaton操作,由于经过squeeze操作后,网络输出了1*1*C大小的特征图,作者利用权重w来学习C个通道直接的相关性。在实际应用时有的框架使用全连接,有的框架使用1*1的卷积实现,从参数计算角度我更加推荐使用1*1卷积,也就是下面代码中的fc2操作。该过程中作者先对C个通道降维再扩展回C通道。好处就是一方面降低了网络计算量,一方面增加了网络的非线性能力。

最后一个操作时将exciation的输出看作是经过特征选择后的每个通道的重要性,通过乘法加权的方式乘到先前的特征上,从事实现提升重要特征,抑制不重要特征这个功能。

SEnet的实现代码如下:


#https://github.com/Amanbhandula/AlphaPose/blob/master/train_sppe/src/models/layers/SE_module.py
class SELayer(nn.Module):
    def __init__(self, channel, reduction=1):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc1 = nn.Sequential(
            nn.Linear(channel, channel / reduction),
            nn.ReLU(inplace=True),
            nn.Linear(channel / reduction, channel),
            nn.Sigmoid())
        self.fc2 = nn.Sequential(
            nn.Conv2d(channel , channel / reduction, 1, bias=False)
            nn.ReLU(inplace=True),
            nn.Conv2d(channel , channel / reduction, 1, bias=False)
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc1(y).view(b, c, 1, 1)
        return x * y


  • CBAM

CBAM(Convolutional Block Attention Module)结合了特征通道和特征空间两个维度的注意力机制。

CBAM通过学习的方式自动获取每个特征通道的重要程度,和SEnet类似。此外还通过类似的学习方式自动获取每个特征空间的重要程度。并且利用得到的重要程度来提升特征并抑制对当前任务不重要的特征。


注意力残差网络_卷积_03


CBAM提取特征通道注意力的方式基本和SEnet类似,如下ChannelAttention中的代码所示,其在SEnet的基础上增加了max_pool的特征提取方式,其余步骤是一样的。将通道注意力提取厚的特征作为空间注意力模块的输入。


注意力残差网络_卷积_04


CBAM提取特征空间注意力的方式:经过ChannelAttention后,最终将经过通道重要性选择后的特征图送入特征空间注意力模块,和通道注意力模块类似,空间注意力是以通道为单位进行最大和平均迟化,并将两者的结果进行concat,之后再一个卷积降成1*w*h的特征图空间权重,再将该权重和输入特征进行点积,从而实现空间注意力机制。


注意力残差网络_2d_05


CBAM的实现代码如下:


#https://github.com/luuuyi/CBAM.PyTorch/blob/master/model/resnet_cbam.py
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
        return self.sigmoid(out)
#https://github.com/luuuyi/CBAM.PyTorch/blob/master/model/resnet_cbam.py
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)
        x = self.conv1(x)
        return self.sigmoid(x)


  • GSoP-Net


注意力残差网络_git_06


给定一个输入张量,降维后,GSoP块先进行协方差矩阵计算,然后进行线性卷积和非线性激活的两个连续运算,得到输出张量,输出张量沿通道维数对原始输入进行缩放,一定程度上也是一种通道注意力的体现,但与SEnet不同的是该模块提出了2维平均池化,通过协方差的形式体现了通道与通道之间的关系。


注意力残差网络_2d_07


如上图所示,通过GSop模块的不同位置,作者提供了两个模型称为GSopNet1和GSopNet2。GSopNet2中还涉及到修改和推进(平方根和带参数)涉及到一些统计和线代上的知识。

GSoP-Net的实现代码如下:


# https://github.com/ZilinGao/Global-Second-order-Pooling-Convolutional-Networks/blob/master/torchvision/models/resnet.py
# init
if GSoP_mode == 1:
    self.avgpool = nn.AvgPool2d(14, stride=1)
    self.fc = nn.Linear(512 * block.expansion, num_classes)
    print("GSoP-Net1 generating...")
else :
    self.isqrt_dim = 256
    self.layer_reduce = nn.Conv2d(512 * block.expansion, self.isqrt_dim, kernel_size=1, stride=1, padding=0, bias=False)
    self.layer_reduce_bn = nn.BatchNorm2d(self.isqrt_dim)
    self.layer_reduce_relu = nn.ReLU(inplace=True)
    self.fc = nn.Linear(int(self.isqrt_dim * (self.isqrt_dim + 1) / 2), num_classes)
    print("GSoP-Net2 generating...")

# forward
if self.GSoP_mode == 1:
    x = self.avgpool(x)
else :
    x = self.layer_reduce(x)
    x = self.layer_reduce_bn(x)
    x = self.layer_reduce_relu(x)

    x = MPNCOV.CovpoolLayer(x)
    x = MPNCOV.SqrtmLayer(x, 3)
    x = MPNCOV.TriuvecLayer(x)


  • AA-Net


注意力残差网络_注意力残差网络_08


SENet对通道重新加权,CBAM独立地重新加权通道和空间位置。与这些方法不同,AA-Net使用可以共同参与空间和特征子空间的注意机制(每个头对应于特征子空间),引入额外的特征映射而不是精炼它们。 AA-Net的核心思想是使用自注意力机制,首先通过矩阵运算获得注意力权重图,通过多Head操作赋值多个空间,在多个空间内进行注意力点乘,实现自注意力机制。具体操作可以见博客:https://kexue.fm/archives/4765

AA-Net的实现代码如下:


# https://github.com/leaderj1001/Attention-Augmented-Conv2d/blob/master/in_paper_attention_augmented_conv/attention_augmented_conv.py
class AugmentedConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, dk, dv, Nh, relative):
        super(AugmentedConv, self).__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.dk = dk
        self.dv = dv
        self.Nh = Nh
        self.relative = relative
        self.conv_out = nn.Conv2d(self.in_channels, self.out_channels - self.dv, self.kernel_size, padding=1)
        self.qkv_conv = nn.Conv2d(self.in_channels, 2 * self.dk + self.dv, kernel_size=1)
        self.attn_out = nn.Conv2d(self.dv, self.dv, 1)

    def forward(self, x):
        # Input x
        # (batch_size, channels, height, width)
        batch, _, height, width = x.size(
        # conv_out
        # (batch_size, out_channels, height, width)
        conv_out = self.conv_out(x)
        # flat_q, flat_k, flat_v
        # (batch_size, Nh, height * width, dvh or dkh)
        # dvh = dv / Nh, dkh = dk / Nh
        # q, k, v
        # (batch_size, Nh, height, width, dv or dk)
        flat_q, flat_k, flat_v, q, k, v = self.compute_flat_qkv(x, self.dk, self.dv, self.Nh)
        logits = torch.matmul(flat_q.transpose(2, 3), flat_k)
        if self.relative:
            h_rel_logits, w_rel_logits = self.relative_logits(q)
            logits += h_rel_logits
            logits += w_rel_logits
        weights = F.softmax(logits, dim=-1)
        # attn_out
        # (batch, Nh, height * width, dvh)
        attn_out = torch.matmul(weights, flat_v.transpose(2, 3))
        attn_out = torch.reshape(attn_out, (batch, self.Nh, self.dv / self.Nh, height, width))
        # combine_heads_2d
        # (batch, out_channels, height, width)
        attn_out = self.combine_heads_2d(attn_out)
        attn_out = self.attn_out(attn_out)
        return torch.cat((conv_out, attn_out), dim=1)


  • ECA-Net

上述提到的所有方法致力于开发更复杂的注意力模块,以获得更好的性能,不可避免地增加了计算负担。为了克服性能与复杂度权衡的悖论,ECANet就是一种用于提高深度CNNs性能的超轻注意模块。ECA模块,它只涉及k (k=9)参数,但带来了明显的性能增益。ECA模块通过分析SEnet的结构了解到降维和跨通道交互的影响,作者通过实验证明了降维是没有作用的(讲道理和我之前想的一样,,),并通过自适应内核大小的一维卷积实现局部跨通道的信息交互。


注意力残差网络_2d_09


ECA-Net的实现代码如下:


class eca_layer(nn.Module):
    """Constructs a ECA module.
    Args:
        channel: Number of channels of the input feature map
        k_size: Adaptive selection of kernel size
    """
    def __init__(self, channel, k_size=3):
        super(eca_layer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.conv = nn.Conv1d(1, 1, kernel_size=k_size, padding=(k_size - 1) // 2, bias=False) 
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        # x: input features with shape [b, c, h, w]
        b, c, h, w = x.size()

        # feature descriptor on the global spatial information
        y = self.avg_pool(x)

        # Two different branches of ECA module
        y = self.conv(y.squeeze(-1).transpose(-1, -2))
        y = y.transpose(-1, -2).unsqueeze(-1)

        # Multi-scale information fusion
        y = self.sigmoid(y)

        return x * y.expand_as(x)
# note
# 1: pytorch 中的expand_as函数的用法
将tensor扩展为参数tensor的大小。 该操作等效与:self.expand(tensor.size())
>>> x = torch.tensor([[1], [2], [3]])
>>> x.size()
torch.Size([3, 1])
>>> x.expand(3, 4)
tensor([[ 1,  1,  1,  1],
        [ 2,  2,  2,  2],
        [ 3,  3,  3,  3]])
>>> x.expand(-1, 4)   # -1 means not changing the size of that dimension
tensor([[ 1,  1,  1,  1],
        [ 2,  2,  2,  2],
        [ 3,  3,  3,  3]])

# 2: pytorch中的squeeze函数的用法
torch.squeeze(input, dim=None, out=None) 将输入张量形状中的 1 去除并返回。
>>> x = torch.zeros(2,1,2,1,2)
>>> x.size()
(2L, 1L, 2L, 1L, 2L)
>>> y = torch.squeeze(x)
>>> y.size()
(2L, 2L, 2L)
>>> y = torch.squeeze(x, 0)
>>> y.size()
(2L, 1L, 2L, 1L, 2L)
>>> y = torch.squeeze(x, 1)
>>> y.size()
(2L, 2L, 1L, 2L)

# 3.pytorch中transpose函数的用法
#https://pytorch.org/docs/stable/torch.html?highlight=transpose#torch.transpose
在对应的维度上进行转置操作
>>> x = torch.randn(2, 3)
>>> x
tensor([[ 1.0028, -0.9893,  0.5809],
        [-0.1669,  0.7299,  0.4942]])
>>> torch.transpose(x, 0, 1)
tensor([[ 1.0028, -0.1669],
        [-0.9893,  0.7299],
        [ 0.5809,  0.4942]])

#4. pytorch中unsqueeze函数的用法
# https://pytorch.org/docs/stable/torch.html?highlight=unsqueeze#torch.unsqueeze
在对应的维度上增加一个1维的维度
>>> x = torch.tensor([1, 2, 3, 4])
>>> torch.unsqueeze(x, 0)
>>> list(x.size())
tensor([[ 1,  2,  3,  4]])
(1,4)
>>> torch.unsqueeze(x, 1)
>>> list(x.size())
tensor([[ 1],
        [ 2],
        [ 3],
        [ 4]])
(4,1)


下图展示了上述各个注意力算法在ImageNet上的性能。


注意力残差网络_git_10