Attention机制用于图像

4,201 阅读4分钟

2022 年什么会火?什么该学?本文正在参与“聊聊 2022 技术趋势”征文活动

Attention

一、在图像处理中,注意力机制分为空间、通道注意力。

  • 空间注意力机制:相对于一个层HWH*W而言,关注其中重要(权重高)的特征点
  • 通道注意力机制:可以将CHWC*H*W 通过平均池化将HWH*W浓缩为111*1,最终形成11C1*1*C线性特征向量,进而关注其中重要的通道(权重高)

二、常用的注意力机制模块

SENet模块

1.介绍:这里是单独使用通道注意力机制。2017年提出的SENet是最后一届ImageNet竞赛的冠军,其实现示意图如下所示。对于SENet模块,其重点是获得输入进来的特征层的每一个通道C的权值。利用SENet,我们可以让网络关注它最需要关注的通道。

SENet模块展示 2.代码实现(pytorch)

import torch
from torch import nn

class senet(nn.Module):
    def __init__(self, channel, ratio=16):
        super(senet, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1) # 全局平均池化操作,将H*W降维为1*1
        self.fc = nn.Sequential( # 两个线性层
            nn.Linear(channel, channel // ratio, False),
            nn.ReLU(),
            nn.Linear(channel // ratio, channel, False),
            nn.Sigmoid(),
        )

    def forward(self, x):
        b, c, h, w = x.size()
        # b, c, h, w -> b, c, 1, 1 -> b, c
        avg = self.avg_pool(x).view(b, c)

        # b, c -> b, c //ratio -> b, c -> b, c, 1, 1
        fc = self.fc(avg).view(b, c, 1, 1)
        print(fc)
        # 注意,这里的张量相乘是必须要两个张量的维度是对应相同才行,不同部分只能是1。
        # 例如此处的是:2*512*26*26和2*512*1*1张量之间的乘积
		# torch.Size([4, 4, 2])可以和torch.Size([4, 1, 1])乘积
        return x * fc

model = senet(512)
print(model)
inputs = torch.ones([2, 512, 26, 26])
outputs = model(inputs)
# print(outputs)

CBAM模块

1.概要介绍:这里是将通道注意力机制和空间注意力机制结合使用。效果比SENet好,其实现示意图如下所示。CBAM模块展示

2.具体介绍:将通道注意力机制和空间注意力机制分别展开。 通道注意力机制:示意图如下,首先将输入的特征图CHWC*H*W分别对每个通道的HWH*W进行最大池化和平均池化处理,生成一维张量C11C*1*1;之后分别通过线性层Shared MLP(和SENet一样,第一层对C降维,第二层恢复成输入时的C维);最后将通过线性层的两者相加并通过SigmoidSigmoid函数得到通道注意力输出结果McM_c通道注意力机制 空间注意力机制:示意图如下,首先对经过通道注意力机制优化后的特征图进行通道的平均池化和最大值池化操作,让通道转化为1,即将CHWC*H*W转化为1HW1*H*W;之后将最大池化和平均池化的结果进行堆叠,得到2HW2*H*W;最后利用通道数为1的卷积层调整通道数为1(H和W不变)并取SigmoidSigmoid函数得到每个特征点的权重,得到空间注意力机制输出结果MsM_s空间注意力机制

3.代码实现(pytorch)

import torch
from torch import nn

class channel_attention(nn.Module):
    def __init__(self, channel, ratio = 16):
        super(channel_attention, self).__init__()
        self.max_pool = nn.AdaptiveAvgPool2d(1)
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // ratio, False),
            nn.ReLU(),
            nn.Linear(channel // ratio, channel, False)
        )
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        b, c, h, w = x.size()
        max_pool_out = self.max_pool(x).view([b, c])
        avg_pool_out = self.avg_pool(x).view([b, c])
        max_fc_out = self.fc(max_pool_out).view([b, c, 1, 1])
        avg_fc_out = self.fc(avg_pool_out).view([b, c, 1, 1])
        out = max_fc_out + avg_fc_out
        out = self.sigmoid(out)
        print(x.size())
        return x * out

class spatial_attention(nn.Module):
    def __init__(self, kernel_size = 7):
        super(spatial_attention, self).__init__()
        padding = 7 // 2
        self.conv = nn.Conv2d(2, 1, kernel_size, 1, padding, bias = False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        # torch.max()有两个输出,第一个是最大值,第二个是最大值的索引
        max_pool_out, _ = torch.max(x, dim = 1, keepdim=True)
        mean_pool_out = torch.mean(x, dim = 1, keepdim=True)
        pool_out = torch.cat([max_pool_out, mean_pool_out], dim = 1)
        out = self.conv(pool_out)
        out = self.sigmoid(out)
        return x * out

class CBAM(nn.Module):
    def __init__(self, channel, ratio = 16, kernel_size = 7):
        super(CBAM, self).__init__()
        self.channel_attention = channel_attention(channel, ratio)
        self.spatial_attention = spatial_attention(kernel_size)

    def forward(self, x):
        x = self.channel_attention(x)
        x = self.spatial_attention(x)
        return x

model = CBAM(512)
print(model)
inputs = torch.ones(2, 512, 26, 26)
outputs = model(inputs)
print(outputs)


ECANet模块

1.介绍:ECANet可以看成是SENet的改进版,也是基于通道注意力机制的。ECANet的作者认为通过两个全连接层捕获所有通道的依赖关系是低效并且是不必要的,而使用卷积可以跨通道捕获信息。模块展示如下图所示,图左边是SENet模块,图右是改进之后的ECANet模块。ECANet模块展示

2.代码实现(pytorch)

import math

import torch
from torch import nn

class ECANet(nn.Module):
    def __init__(self, channel, gamma = 2, b = 1):
        super(ECANet, self).__init__()
        # 通过输入的channel来自适应调节卷积核的大小
        # math.log(channel, 2),此处是对channel取对数,底数为2
        # abs是取绝对值
        kernel_size = int(abs((math.log(channel, 2) + b) / gamma))
        # 将kernel_size取奇数
        kernel_size = kernel_size if kernel_size % 2 else kernel_size + 1
        padding = kernel_size // 2
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        # 输入和输出都是1维,此处的kernel_size是控制输入和输出的大小为1*1*C
        self.conv = nn.Conv1d(1, 1, kernel_size, padding = padding, bias = False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        b, c, h, w = x.size()
        # 对池化后的特征进行调整适合一维卷积的输入
        # b为batch_size,1为每一个step的特征长度,c代表每一个时序
        avg = self.avg_pool(x).view([b, 1, c])
        print(avg.size())
        out = self.conv(avg)
        print(out.size())
        out = self.sigmoid(out).view([b, c, 1, 1])
        print(out.size())
        return x * out

model = ECANet(512)
print(model)
inputs = torch.ones([2, 512, 26, 26])
outputs = model(inputs)
#print(outputs.size())


3.关于代码的注意事项:使用padding填充来保证输入和输出的特征条数量是相同的,代码中经过计算可知卷积核为151*5