神经网络能够在同样的计算资源下获得更强的表征能力和更优的性能表现。
AM: Attention Mechanism,注意力机制。
注意力机制 是一种让模型根据任务需求动态地关注输入数据中重要部分的机制。
通过注意力机制,模型可以做到对图像中不同区域、句子中的不同部分给予不同的权重,从而增强感兴趣特征,并抑制不感兴趣区域。
注意力机制最初应用于机器翻译(如Transformer),后逐渐被广泛应用于各类任务,包括:
NLP:如机器翻译、文本生成、摘要、问答系统等。
计算机视觉:如图像分类(细粒度识别)、目标检测(显著目标检测)、图像分割(图像修复)等。
跨模态任务:如图文生成、视频描述等。
对不同的特征通道进行增强或抑制,也就是赋予不同的权重参数。
不同卷积核卷之后会得到不同的96个特征:边缘、形状、颜色等,不同的任务关注不同的特征。
https://arxiv.org/pdf/1709.01507
Squeeze-and-Excitation Networks
挤压 - 和 - 激活、激发
即插即用
SENet采用具有全局感受野的池化操作进行特征压缩,并使用全连接层学习不同特征图的权重
通道数在加入注意力机制之后前后不发生改变
该阶段通过全局平均池化完成全局信息提取,
$$
z_c=\mathbf{F}_{sq}(\mathbf{u}_c)=\frac1{H\times W}\sum_{i=1}^H\sum_{j=1}^Wu_c(i,j)
$$
Squeeze的输出作为Excitation阶段的输入,经过两个全连接层,动态地为每个通道生成权重,
$$
\mathbf{s}=\mathbf{F}_{ex}(\mathbf{z},\mathbf{W})=\sigma(g(\mathbf{z},\mathbf{W}))=\sigma(\mathbf{W}_2\delta(\mathbf{W}_1\mathbf{z}))
$$
全连接层加入激活函数,用于引入非线性变化:
第一个全连接层(ReLU),将通道数从C降维为C/r。
r是缩放因子,Ratio,比例的意思,用以减少运算量和防止过拟合。
$$
通过第二个全连接层(si4)将维度恢复为C,输出一个1 \times 1 \times C的权重向量。
$$
4.权重归一化:使用sigmoid确保权重在0~1之间。
5.该向量代表每个通道的重要性,也就是注意力的权重。
$$
特征 \mathbf{u}_c 和 Excitation阶段产出的 \mathbf{s}_c 进行相乘操作,用于对不同的通道添加权重:\widetilde {x}_{c} = F_{scale}(u_{c},s_{c}) =s_{c}u_{c}
$$
作为一种即插即用模块,可以添加到任意的层后,只要保证输出通道不变即可
作为一种即插即用模块,可以添加到任意的层后,只要保证输出通道不变即可
我们可以考虑不要Squeeze做平均池化,直接在Excitation阶段进行卷积操作。从下标看的出来,这个Squeeze阶段还是很有必要的。
我们也可以考虑采用最大池化,不过效果不如平均池化。因为对注意力来讲更多的是维持原始信息,而不是强化特征。
这里是针对第二个全连接层,我们想要的是一个概率向量,无疑返回值在(0 ~ 1)之间的Sigmoid是最好的选择。
SE模块灵活度较高
性能对比如下:POST模式最差
比如ResNet是分很多个阶段的,不同的阶段添加SE模块效果是不一样的。
看的出来,越靠后的位置效果越好,因为越靠后特征学习的越好,此时加入效果就越好。当然全加SE的效果最好,不过参数量也不少。
Selective Kernel Networks可选择的 卷积核尺寸
https://arxiv.org/pdf/1903.06586
SK是对SE的改进版,可以动态调整感受野大小,分为Split-Fuse-Select共3个阶段
在Split阶段会分出多个分支,每个分支实现不同大小的感受野,从而捕获不同的特征。
为提高效率,传统的5×5卷积被替换为带有3×3卷积核和膨胀大小为2的膨胀卷积。
$$
\widetilde{\mathcal{F}}:\mathbf{X}\to\widetilde{\mathbf{U}}\in\mathbb{R}^{H\times W\times C} \\ \widehat{\mathcal{F}}:\mathbf{X}\to\widehat{\mathbf{U}}\in\mathbb{R}^{H\times W\times C}
$$
整合分支信息,具体步骤如下:
1.通过element-wise summation得到 U
$$
\mathbf{U}=\widetilde{\mathbf{U}}+\widehat{\mathbf{U}}
$$
2.通过global average pooling得到特征 s
$$
s_c=\mathcal{F}_{gp}(\mathbf{U}_c)=\frac1{H\times W}\sum_{i=1}^H\sum_{j=1}^W\mathbf{U}_c(i,j)
$$
平均池化操作
3.
$$
通过FC全连接层得到 \mathbf{z}\in\mathbb{R}^{d\times1}
$$
$$
\mathbf{z}=\mathcal{F}_{fc}(\mathbf{s})=\delta(\mathcal{B}(\mathbf{W}\mathbf{s}))
$$
$$
其中 \mathcal{B} 是batch normalization,\delta 是ReLU,\mathbf{W}\in\mathbb{R}^{d\times{C}}。注意这里通过reduction ratio r 和阈值 L 两个参数控制 z 的输出通道 d:
$$
$$
d=\max(C/r,L),L默认值是32
$$
4.通过两个不同的FC层(即矩阵A、B)分别得到 a 和 b,这里将通道从 d 又映射回原始通道数 C。
5.对 a,b 对应通道 c 处的值进行 softmax 处理。
$$
a_{c}=\frac{e^{ {\mathbf{A}_{c}\mathbf{z}}}}{e^{ {\mathbf{A}_{c}\mathbf{z}}}+e^{ {\mathbf{B}_{c}\mathbf{z}}}} \\ \\ b_{c}=\frac{e^{ {\mathbf{B}_{c}\mathbf{z}}}}{e^{ {\mathbf{A}_{c}\mathbf{z}}}+e^{ {\mathbf{B}_{c}\mathbf{z}}}}
$$
$$
在公式中,A,B\in\mathbb{R}^{d\times C},A_c z 和 B_c z 分别代表不同(3×3、5×5)的卷积核经过全局池化(F_{gp})和全连接层(F_{fc})后得到的特征。
$$
$$
a,b分别表示 \widetilde{\mathbf{U}} 和 \widehat{\mathbf{U}} 的注意力系数。
$$
softmax分别对不同卷积核的结果做抑制和增强
$$
\widetilde{\mathbf{U}} 和 \widehat{\mathbf{U}} 分别与 sofmax 处理后的 a,b 相乘,再相加,得到最终输出的 V 和原始输入 X的维度一致。
$$
$$
\mathbf{V}_c=a_c\cdot\widetilde{\mathbf{U}}_c+b_c\cdot\widehat{\mathbf{U}}_c \\ \quad a_c+b_c=1
$$
$$
其中 \mathbf{V} = [\mathbf{V}_1,\mathbf{V}_2,...,\mathbf{V}_c], \mathbf{V}_c \in \mathbb{R}^{H\times W}
$$
图标注解:前景图越大,5×5和3×3的注意力权重差值越小
SK_X_Y 中的 X 代表网络的不同层级(Stage),数字越大表示层越深。
Y 代表该层级中的第几个SK模块。
不同的SK模块在不同的层级负责提取不同尺度、不同语义的特征。
从第2层到第5层,特征从低级(如边缘、纹理)逐渐过渡到高级语义信息(如物体、场景等)。
channel index(32、64、96等) 表示不同通道编号。
activation表示每个通道上的注意力权重值。这个值越高,表明网络对该通道上的特征越重视。
通过对比5×5和3×3卷积核的注意力值,可以表明较大卷积核在捕捉大目标时更有优势。
可以在任意位置添加SE模块。
import torch.nn as nn class SeNet(nn.Module): def __init__(self, inchannel, reduction=16): super(SeNet, self).__init__() # global average pooling:目标是池化为一个固定大小的输出 1 * 1 self.gavgpool = nn.AdaptiveAvgPool2d(1) # 两个全连接层 self.fc1 = nn.Sequential( nn.Linear(inchannel, inchannel // reduction, bias=False), nn.ReLU(inplace=True), ) self.fc2 = nn.Sequential( nn.Linear(inchannel // reduction, inchannel, bias=False), # 1 nn.Sigmoid(), # 2 ) def forward(self, x): identity = x # 获取形状的值 N, C, _, _ = x.size() # 进行SENet的Squeeze操作 x = self.gavgpool(x) # NCHW --> N*C*1*1 # 进行SENet的Excitation操作 --> N*C x = x.view(N, C) x = self.fc1(x) x = self.fc2(x) # 输出的是通道的注意力值(不是参数) x = x.view(N, C, 1, 1) return x * identity
import torch import torch.nn as nn # 导入注意力模块 from SENet import SeNet from torchvision.models.resnet import resnet18, ResNet18_Weights, _resnet, BasicBlock # 类:封装、继承、重写 class SEBasicBlock(BasicBlock): expansion: int = 1 def __init__( self, inplanes: int, planes: int, stride: int = 1, downsample=None, groups: int = 1, base_width: int = 64, dilation: int = 1, norm_layer=None, ): super(SEBasicBlock, self).__init__( inplanes, planes, stride, downsample, groups, base_width, dilation, norm_layer, ) # 加入注意力机制:上一层的输入是下一层的输入 self.se = SeNet(planes * self.expansion, 16) def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) # 加入注意力机制:即插即用 Standard SE out = self.se(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out def SEResnet18(*, weights=None, progress=True, **kwargs): weights = ResNet18_Weights.verify(weights) return _resnet(SEBasicBlock, [2, 2, 2, 2], weights, progress, **kwargs) def test001(): model = SEResnet18() # 导出为onnx input = torch.randn(1, 3, 224, 224) # 导出onnx torch.onnx.export(model, input, "SEResnet18.onnx", opset_version=11) print(model) if __name__ == "__main__": test001()
模型使用
import torch from StandardSEResnet18 import SEResnet18 def test001(): model = SEResnet18() # 导出为onnx:预热数据 input = torch.randn(1, 3, 224, 224) # 导出onnx:路径没有特别在意 torch.onnx.export( model, # 1 input, # 2 "SEResnet18-STANDARD.onnx", opset_version=11 ) print('模型导出success......') if __name__ == "__main__": test001()
import torch import torch.nn as nn # 导入注意力模块 from SENet import SeNet from torchvision.models.resnet import resnet18, ResNet18_Weights, _resnet, BasicBlock # 类:封装、继承、重写 class SEBasicBlock(BasicBlock): expansion: int = 1 def __init__( self, inplanes: int, planes: int, stride: int = 1, downsample=None, groups: int = 1, base_width: int = 64, dilation: int = 1, norm_layer=None, ): super(SEBasicBlock, self).__init__( inplanes, planes, stride, downsample, groups, base_width, dilation, norm_layer, ) # 加入注意力机制:上一层的输入是下一层的输入 self.se = SeNet(inplanes, 16) def forward(self, x): identity = x # 加入注意力机制:即插即用 SE-PRE x = self.se(x) out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) # 加入注意力机制:即插即用 Standard SE # out = self.se(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out def SEResnet18(*, weights=None, progress=True, **kwargs): weights = ResNet18_Weights.verify(weights) return _resnet(SEBasicBlock, [2, 2, 2, 2], weights, progress, **kwargs)
import torch import torch.nn as nn # 导入注意力模块 from SENet import SeNet from torchvision.models.resnet import ( ResNet18_Weights, BasicBlock, _ovewrite_named_param, ResNet, ) class SEResNet(ResNet): def __init__( self, block, layers, num_classes: int = 1000, zero_init_residual: bool = False, groups: int = 1, width_per_group: int = 64, replace_stride_with_dilation=None, norm_layer=None, ): # 执行父类的构造函数 super(SEResNet, self).__init__( block, layers, num_classes, zero_init_residual, groups, width_per_group, replace_stride_with_dilation, norm_layer, ) # self.layer1 - self.layer4 已经有了,这里是加入SE模块 self.layer1 = self._modify_layer(self.layer1) self.layer2 = self._modify_layer(self.layer2) self.layer3 = self._modify_layer(self.layer3) self.layer4 = self._modify_layer(self.layer4) def _modify_layer(self, layer): modify_layer = [] for block in layer: # 获取每个 BasicBlock 的输出通道数 out_channels = block.conv2.out_channels # 创建SE模块 se_block = SeNet(out_channels, 16) # 加入到原始的 BasicBlock 后面 modified_block = nn.Sequential(block, se_block) modify_layer.append(modified_block) # 构建新的顺序容器 return nn.Sequential(*modify_layer) def SEResnet18(*, weights=None, progress=True, **kwargs): weights = ResNet18_Weights.verify(weights) return _resnet(BasicBlock, [2, 2, 2, 2], weights, progress, **kwargs) def _resnet(block, layers, weights, progress, **kwargs): if weights is not None: _ovewrite_named_param(kwargs, "num_classes", len(weights.meta["categories"])) model = SEResNet(block, layers, **kwargs) if weights is not None: model.load_state_dict( weights.get_state_dict(progress=progress, check_hash=True) ) return model def test001(): model = SEResnet18() print(model) if __name__ == "__main__": test001()
import torch import torch.nn as nn # 导入注意力模块 from SENet import SeNet from torchvision.models.resnet import resnet18, ResNet18_Weights, _resnet, BasicBlock # 类:封装、继承、重写 class SEBasicBlock(BasicBlock): expansion: int = 1 def __init__( self, inplanes: int, planes: int, stride: int = 1, downsample=None, groups: int = 1, base_width: int = 64, dilation: int = 1, norm_layer=None, ): super(SEBasicBlock, self).__init__( inplanes, planes, stride, downsample, groups, base_width, dilation, norm_layer, ) # 加入注意力机制:上一层的输入是下一层的输入 self.se = SeNet(inplanes, 16) def forward(self, x): identity = x # 加入注意力机制:即插即用 SE-Identity se_identity = self.se(x) out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) # 加入注意力机制:即插即用 Standard SE # out = self.se(out) if self.downsample is not None: identity = self.downsample(x) # 形状可能不一致 if self.downsample is not None: se_identity = self.downsample(se_identity) # out += identity out = self.relu(out) return out + se_identity def SEResnet18(*, weights=None, progress=True, **kwargs): weights = ResNet18_Weights.verify(weights) return _resnet(SEBasicBlock, [2, 2, 2, 2], weights, progress, **kwargs)
空间注意力(Spatial Attention)主要用于CV,它在空间维度上选择性地关注输入特征图的不同位置,从而提升模型对关键区域的感知能力。其实现原理是基于不同像素位置,生成对应概率掩码,是比较低层的注意力机制。
论文地址:https://arxiv.org/pdf/1804.02391
源代码地址:https://github.com/SaoYan/LearnToPayAttention
结合全局特征和局部特征获得注意力机制,使用加权的局部特征来识别目标。
Local features:局部特征
如头部、轮子、尾翼、发动机、机身标志或窗户等,包含丰富的细节,对于识别飞机的具体种类、型号等非常有帮助。
Global features:全局特征
如整体形状、轮廓、大小、相对背景中的位置等;对于识别是什么飞机很重要,如战斗机、客机还是直升机。
特征融合:
在生成注意力权重前会对输入的局部和全局特征进行融合。通过全局池化(Global Average Pooling)来获得全局上下文信息。
Attention Estimator:
对输入特征图进行多层卷积、池化、激活等操作,用来挖掘特征之间的关系,从而生成注意力权重图。权重图的每个位置对应特征图中的一个空间位置,表示该位置的重要性。
Att. Weighted Combination:
将生成的注意力图与原始特征图逐点相乘,得到加权后的特征图。
基于VGG16网络的多层注意力融合:是为了适配不同大小的目标
Affine Transformation,是一种线性空间变换,可以保持图形的平直性和共线性,但不一定保持角度和长度。
$$
只改变位置,不改变形状,即 t_x 和 t_y是非零值:\begin{pmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \end{pmatrix}
$$
绕某一点旋转,通常绕原点:
$$
\begin{pmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \end{pmatrix}其中 \theta 是旋转角度。
$$
改变大小,横向或纵向进行比例缩放:
$$
\begin{pmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \end{pmatrix}其中 s_x 和 s_y 分别控制水平和垂直方向的缩放比例。
$$
$$
Shear变换使得图像沿着 x或 y 方向进行倾斜:\begin{pmatrix} 1 & k_x & 0 \\ k_y & 1 & 0 \end{pmatrix}其中 k_x 和 k_y是剪切系数。
$$
仿射变换可以叠加多个基本变换,如旋转加缩放、旋转加平移等。通过矩阵乘法可以将多个变换结合成一个统一的仿射变换。
图像增强和几何校正: 仿射变换常用于图像增强任务,比如在照片失真时,仿射变换可以纠正视角失真。
目标检测和跟踪: 仿射变换用于将不规则形状的图像转换为标准形式,便于检测和跟踪。
数据扩增: 在机器学习中,仿射变换可以生成多样化的数据集,用于增强模型的泛化能力。
仿射变换完成的是图形的像素点在整个图像中的坐标位置变换
像素点本身的值并没有进行改变
https://arxiv.org/pdf/1506.02025
Spatial Transformer Network,STN,空间变换网络。它会学习空间特征图的形变,完成适合任务的预处理操作。STN 主要解决的问题是自动校正输入数据中的几何变换(如旋转、缩放、平移等)。
两个全连接层,用于生成仿射变换系数
输入:C×H×W维的图像
输出:仿射变换系数 矩阵
$$
\left[\begin{array}{ccc}\theta_{11}&\theta_{12}&\theta_{13}\\\theta_{21}&\theta_{22}&\theta_{23}\end{array}\right]
$$
这个值并不总是整数。
Grid generator完成仿射变换
做几次仿射变换:目标字符数+1
$$
\begin{bmatrix}x^{\prime}\\y^{\prime}\end{bmatrix}=\begin{bmatrix}a&b\\c&d\end{bmatrix}\begin{bmatrix}x\\y\end{bmatrix}+\begin{bmatrix}e\\f\end{bmatrix}\quad f_l(x,y)=f_{l-1}(ax+by+e,cx+dy+f)
$$
$$
(x, y) :第 l 层的坐标。\\ (x', y') :通过仿射变换后映射到第 l-1 层的坐标。\\ 仿射变换由矩阵 \begin{pmatrix} a & b \\ c & d \end{pmatrix}和偏移向量 \begin{pmatrix} e \\ f \end{pmatrix}控制,表示在第 l-1 层上对坐标进行的缩放、旋转、平移等操作。
$$
采样器根据Grid Generator生成的网格坐标,从输入图像中提取像素值。
由于 Grid Generator 生成的坐标通常不是整数,需要使用插值技术(双线性插值)来获得精确的输出值 。
在MNIST数据集上的表现:目标更大且在核心位置3.4.2 SVHN数据集
在SVHN数据集上的表现:多尺度应用,把STN插入到不同位置
搭建一个即插即用的STN模块,再融入到模型中
import torch import torch.nn as nn import torch.nn.functional as F class STN(nn.Module): def __init__(self, c, h, w): super(STN, self).__init__() self.fc = nn.Sequential( nn.Linear(in_features=c * h * w, out_features=32), nn.Tanh(), nn.Linear(in_features=32, out_features=6), nn.Tanh(), ) def forward(self, x): batch_size, c, h, w = x.size() # 2行3列的矩阵 theta = self.fc(x.view(batch_size, -1)).view(batch_size, 2, 3) # 仿射变换矩阵 grid = F.affine_grid( theta, torch.Size((batch_size, c, h, w)), align_corners=False ) # 开始采样 sample = F.grid_sample(x, grid, align_corners=False) return sample
import torch import torch.nn as nn from STN import STN class LeNet5s(nn.Module): def __init__(self, c, h, w): super(LeNet5s, self).__init__() # 继承父类 self.stn = STN(c=1, h=32, w=32) # 新增STN模块 # 第一个卷积层 self.C1 = nn.Sequential( nn.Conv2d( in_channels=1, # 输入通道 out_channels=6, # 输出通道 kernel_size=5, # 卷积核大小 ), nn.ReLU(), ) # 池化:平均池化 self.S2 = nn.AvgPool2d(kernel_size=2) # C3:3通道特征融合对应的 卷积层 代码风格1 self.C3_unit_6x3 = nn.ModuleList([nn.Conv2d(3, 1, 5) for i in range(6)]) # C3:4通道特征融合单元 代码风格2 ,和上面完全一样 self.C3_unit_6x4 = nn.ModuleList( [ nn.Conv2d( in_channels=4, out_channels=1, kernel_size=5, ) for i in range(6) ] ) # C3:4通道特征融合单元,剔除中间的1通道 self.C3_unit_3x4_pop1 = nn.ModuleList( [ nn.Conv2d( in_channels=4, out_channels=1, kernel_size=5, ) for i in range(3) ] ) # C3:6通道特征融合单元 self.C3_unit_1x6 = nn.Conv2d( in_channels=6, out_channels=1, kernel_size=5, ) # S4:池化 self.S4 = nn.AvgPool2d(kernel_size=2) # 全连接层 self.fc1 = nn.Sequential( nn.Linear(in_features=16 * 5 * 5, out_features=120), nn.ReLU() ) self.fc2 = nn.Sequential(nn.Linear(in_features=120, out_features=84), nn.ReLU()) self.fc3 = nn.Linear(in_features=84, out_features=10) def forward(self, x): # 使用STN模块进行变换 stnimg = self.stn(x) # 加入STN模块 # 训练数据批次大小batch_size num = stnimg.shape[0] x = self.C1(stnimg) x = self.S2(x) # 生成一个empty张量 outchannel = torch.empty((num, 0, 10, 10)) # 6个3通道的单元 for i in range(6): # 定义一个元组:存储要提取的通道特征的下标 channel_idx = tuple([j % 6 for j in range(i, i + 3)]) x1 = self.C3_unit_6x3[i](x[:, channel_idx, :, :]) outchannel = torch.cat([outchannel, x1], dim=1) # 6个4通道的单元 for i in range(6): # 定义一个元组:存储要提取的通道特征的下标 channel_idx = tuple([j % 6 for j in range(i, i + 4)]) x1 = self.C3_unit_6x4[i](x[:, channel_idx, :, :]) outchannel = torch.cat([outchannel, x1], dim=1) # 3个4通道的单元,先拿五个,干掉中那一个 for i in range(3): # 定义一个元组:存储要提取的通道特征的下标 channel_idx = tuple([j % 6 for j in range(i, i + 5)]) # 删除第三个元素 channel_idx = channel_idx[:2] + channel_idx[3:] x1 = self.C3_unit_3x4_pop1[i](x[:, channel_idx, :, :]) outchannel = torch.cat([outchannel, x1], dim=1) x1 = self.C3_unit_1x6(x) # 平均池化 outchannel = torch.cat([outchannel, x1], dim=1) outchannel = nn.ReLU()(outchannel) x = self.S4(outchannel) # 对数据进行变形 x = x.view(x.size(0), -1) # 全连接层 x = self.fc1(x) x = self.fc2(x) # TODO:SOFTMAX output = self.fc3(x) return stnimg, output def test001(): print(torch.__version__) torch.random.manual_seed(1) # 输入数据 batch-size = 4 x = torch.randn(4, 1, 32, 32) model = LeNet5s(1, 32, 32) stnimg, out = model(x) print(stnimg.shape) # 导出模型onnx torch.onnx.export(model, x, "LeNet5s-STN.onnx", opset_version=20) if __name__ == "__main__": test001()
混合注意力机制(Hybrid Attention Mechanism)是一种结合空间和通道注意力的策略,旨在提高神经网络的特征提取能力。
Convolution Block Attention Module ,卷积块注意力模块
https://arxiv.org/pdf/1807.06521
轻量级的注意力模块,它通过增加空间和通道两个维度的注意力,来提高模型的性能。
$$
一维的通道注意力图:\mathcal{M}_{\mathbf{c}}\in\mathbb{R}^{C\times1\times1}\\ 二维的空间注意力图:\mathbf{M_s}\in\mathbb{R}^{1\times H\times W}
$$
$$
\begin{aligned}\mathbf{F^{\prime}}&=\mathbf{M_{c}}(\mathbf{F})\otimes\mathbf{F},\\\mathbf{F^{\prime\prime}}&=\mathbf{M_{s}}(\mathbf{F^{\prime}})\otimes\mathbf{F^{\prime}}\end{aligned}
$$
通道注意力模块的目的是为每个通道生成一个注意力权重
空间注意力模块通过卷积操作为特征图的每个空间位置生成权重,聚焦在图像中的关键区域。
效果最好的就是CBAM,并且池化不需要参数
Bottleneck Attention Module,瓶颈注意力模块。
https://arxiv.org/pdf/1807.06514
BAM是通过在空间和通道两个维度上分别构建注意力模块,它们是并行处理的。
形状不同的张量会自动进行广播机制
import torch import torch.nn as nn import torch.nn.functional as F class ChannelAttention(nn.Module): def __init__(self, in_planes, ratio=16): super(ChannelAttention, self).__init__() self.max_pool = nn.AdaptiveMaxPool2d(1) self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc1 = nn.Sequential( nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False), nn.ReLU(), nn.Conv2d(in_planes // ratio, in_planes, 1, bias=False), ) self.sigmoid = nn.Sigmoid() def forward(self, x): max_out = self.fc1(self.max_pool(x)) avg_out = self.fc1(self.avg_pool(x)) out = max_out + avg_out return self.sigmoid(out)
import torch import torch.nn as nn import torch.nn.functional as F class ChannelAttention(nn.Module): # 和前面一致,略...... 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" self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, 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.conv(x) return self.sigmoid(x)
import torch import torch.nn as nn import torch.nn.functional as F from torchvision.models import resnet50 from torchvision.models.resnet import ResNet50_Weights, ResNet, _resnet, Bottleneck from CBAMAttention import ChannelAttention, SpatialAttention # 重定义Resnet50:resnet50CBAM def resnet50CBAM(*, weights=None, progress=True, **kwargs) -> ResNet: weights = ResNet50_Weights.verify(weights) return _resnet(BottleneckCBAM, [3, 4, 6, 3], weights, progress, **kwargs) # 重新定义瓶颈结构:加入CBAM模块 class BottleneckCBAM(Bottleneck): expansion: int = 4 # 通道数在 conv3 后会扩大到 planes * expansion def __init__( self, inplanes: int, planes: int, stride: int = 1, downsample=None, groups=1, base_width: int = 64, dilation: int = 1, norm_layer=None, ): super(BottleneckCBAM, self).__init__( inplanes, planes, stride, downsample, groups, base_width, dilation, norm_layer, ) # 新增的CBAM模块:注意通道数有个倍增系数 self.ca = ChannelAttention(planes * self.expansion) self.sa = SpatialAttention() def forward(self, x): identity = 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) # 加入CBAM模块:即插即用 out = self.ca(out) * out out = self.sa(out) * out if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out def testCBAM(): model = resnet50CBAM(weights=None, num_classes=10) # 导出模型为onnx x = torch.randn(1, 3, 224, 224) torch.onnx.export(model, x, "resnet50CBAM.onnx") print("模型注意力添加成功......") if __name__ == "__main__": testCBAM()