传统注意力机制
一、卷积注意力机制
通过引入卷积注意力机制,神经网络能够在同样的计算资源下获得更强的表征能力和更优的性能表现。
1. 注意力认知
AM: Attention Mechanism,注意力机制。
注意力机制 是一种让模型根据任务需求动态地关注输入数据中重要部分的机制。
通过注意力机制,模型可以做到对图像中不同区域、句子中的不同部分给予不同的权重,从而增强感兴趣特征,并抑制不感兴趣区域。
2. 注意力应用
注意力机制最初应用于机器翻译(如Transformer),后逐渐被广泛应用于各类任务,包括:
-
NLP:如机器翻译、文本生成、摘要、问答系统等。


机器翻译 关键词提取,摘要生成:输入句和参考摘要之间的重叠关键词(红色)涵盖了输入句的重要信息,可根据这些关键字生成摘要 -
计算机视觉:如图像分类(细粒度识别)、目标检测(显著目标检测)、图像分割(图像修复)等。


细粒度识别 图像修复:监控摄像头去除雨线、雨滴等 -
跨模态任务:如图文生成、视频描述等。
这里我们学习下视觉处理中常见的典型注意力机制,如特征注意力、空间注意力以及混合注意力。
二、通道注意力
对不同的特征通道进行增强或抑制,也就是赋予不同的权重参数。
|
|
|---|
| 不同卷积核卷之后会得到不同的96个特征:边缘、形状、颜色等,不同的任务关注不同的特征 |
1. SENet
Squeeze-and-Excitation Networks
挤压 - 和 - 激活、激发
大胆猜测,小心求证
SENet模型论文: https://arxiv.org/pdf/1709.01507
1.0 基本认知
Filter
SENet采用具有全局感受野的池化操作进行特征压缩,并使用全连接层学习不同特征图的权重,模型流程图如下:

1.1 流程详解
-
Squeeze阶段:
该阶段通过全局平均池化完成全局信息提取,公式如下:
$$
z_c=\mathbf{F}_{sq}(\mathbf{u}_c)=\frac1{H\times W}\sum_{i=1}^H\sum_{j=1}^Wu_c(i,j)
$$示意图如下:

# Squeeze:压缩、降维、挤压 self.sq = nn.AdaptiveAvgPool2d(1)
-
Excitation阶段:
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的权重向量。
-
权重归一化:使用sigmoid确保权重在0~1之间。
-
该向量代表每个通道的重要性,也就是注意力的权重。
# Excitation:激活 self.ex = nn.Sequential( nn.Linear(inplanes, inplanes // r), nn.ReLU(), nn.Linear(inplanes // r, inplanes), nn.Sigmoid(), )
-
-
输出阶段:
特征 \mathbf{u}_c 和 Excitation阶段产出的 \mathbf{s}_c 进行相乘操作,用于对不同的通道添加权重:
$$
\widetilde {x}_{c} = F_{scale}(u_{c},s_{c}) =s_{c}u_{c}
$$

def forward(self, x): # 缓存x intifi = x x = self.sq(x) x = x.view(x.size(0), -1) x = self.ex(x).unsqueeze(2).unsqueeze(3) return intifi * x
1.2 融入模型
作为一种即插即用模块,可以添加到任意的层后,只要保证输出通道不变即可,如把SE融入到Inception和ResNet模型,如下:

给ResNet-50加入SE注意力:

注解:表格中的 fc 后面的值,如 (16, 256) 或 (32, 512),表示在SE模块中的两个全连接层的维度变化。
1.3 性能对比
加入SE后的性能对比表:re-implementation是SE作者复现效果

1.4 缩放因子
太小参数量大,容易过拟合。太大的话特征丢失严重,整体看8或16是比较不错的选择,具体的还是根据业务来定。

1.5 有无Squeeze
我们可以考虑不要Squeeze做平均池化,直接在Excitation阶段进行卷积操作。从下标看的出来,这个Squeeze阶段还是很有必要的。

1.6 池化方式
我们也可以考虑采用最大池化,不过效果不如平均池化。因为对注意力来讲更多的是维持原始信息,而不是强化特征。

1.7 激活函数
这里是针对第二个全连接层,我们想要的是一个概率向量,无疑返回值在(0 ~ 1)之间的Sigmoid是最好的选择。

1.8 网络位置
SE模块灵活度较高,如下:

性能对比如下:POST模式最差

1.9 不同阶段添加
比如ResNet是分很多个阶段的,不同的阶段添加SE模块效果是不一样的。

看的出来,越靠后的位置效果越好,因为越靠后特征学习的越好,此时加入效果就越好。当然全加SE的效果最好,不过参数量也不。
2. SKNet
Selective Kernel Networks
可选择的 卷积核尺寸
SKNet论文地址:https://arxiv.org/pdf/1903.06586
2.0 基本认知
SK是对SE的改进版,可以动态调整感受野大小,分为Split-Fuse-Select共3个阶段,模型流程图如下:

2.1 Split阶段

-
在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}
$$
2.2 Fuse阶段

该阶段会整合分支信息,具体步骤如下:
-
通过element-wise summation得到 U
$$
\mathbf{U}=\widetilde{\mathbf{U}}+\widehat{\mathbf{U}}
$$ -
通过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)
$$就是一个平均池化操作。
-
通过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。
-
通过两个不同的FC层(即矩阵A、B)分别得到 a 和 b,这里将通道从 d 又映射回原始通道数 C。
-
对 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}} 的注意力系数。
2.3 Select阶段

具体步骤如下:
-
\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}
2.4 融入模型
ResNeXt加入SE和SK:

其中M表示SK中的分支数,M=2表示5×5和3×3两个分支。
2.5 注意力权重分析
|
|
|
|---|
图标注解:前景图越大,5×5和3×3的注意力权重差值越小
-
SK_X_Y 中的 X 代表网络的不同层级(Stage),数字越大表示层越深。
-
Y 代表该层级中的第几个SK模块。
-
不同的SK模块在不同的层级负责提取不同尺度、不同语义的特征。
-
从第2层到第5层,特征从低级(如边缘、纹理)逐渐过渡到高级语义信息(如物体、场景等)。
-
channel index(32、64、96等) 表示不同通道编号。
-
activation表示每个通道上的注意力权重值。这个值越高,表明网络对该通道上的特征越重视。
通过对比5×5和3×3卷积核的注意力值,可以表明较大卷积核在捕捉大目标时更有优势。
3. SENet搭建
SE模块灵活度较高,可以在任意位置添加SE模块。
3.1 构建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
3.2 融入ResNet
有4种融入模式
3.2.1 Standard SE block
模型代码实现:
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()
3.2.2 SE-pre block
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)
3.2.3 SE-POST block
模型代码实现:
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()
3.2.4 SE-Identity block
模型代码实现:
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,它在空间维度上选择性地关注输入特征图的不同位置,从而提升模型对关键区域的感知能力。其实现原理是基于不同像素位置,生成对应概率掩码,是比较低层的注意力机制。

1. Learn to Pay Attention
论文地址:https://arxiv.org/pdf/1804.02391
源代码地址:https://github.com/SaoYan/LearnToPayAttention
1.0 基本认知
结合全局特征和局部特征获得注意力机制,使用加权的局部特征来识别目标。

-
Local features:局部特征
如头部、轮子、尾翼、发动机、机身标志或窗户等,包含丰富的细节,对于识别飞机的具体种类、型号等非常有帮助。
-
Global features:全局特征
如整体形状、轮廓、大小、相对背景中的位置等;对于识别是什么飞机很重要,如战斗机、客机还是直升机。
-
特征融合:
在生成注意力权重前会对输入的局部和全局特征进行融合。通过全局池化(Global Average Pooling)来获得全局上下文信息。
-
Attention Estimator:
对输入特征图进行多层卷积、池化、激活等操作,用来挖掘特征之间的关系,从而生成注意力权重图。权重图的每个位置对应特征图中的一个空间位置,表示该位置的重要性。
-
Att. Weighted Combination:
将生成的注意力图与原始特征图逐点相乘,得到加权后的特征图。
1.1 融入模型
基于VGG16网络的多层注意力融合:是为了适配不同大小的目标

通过多层注意力估计器,模型能够学会在不同的特征层次上关注有用的信息,提升分类性能。
1.1.1 流程概述
-
局部特征向量,s表示特征图层数:\mathcal{L}^s=\{\boldsymbol{\ell}_1^s,\boldsymbol{\ell}_2^s,\cdots,\boldsymbol{\ell}_n^s\},s\in\{1,\cdots,S\}
-
L_n(L_1、L_2、L_3)为VGG不同层级的局部特征向量,将
FC-1, 512的输出 G 视作全局特征,同时移除FC-2, 10层。 -
Attention Estimator 接收 L_n 和 G 作为输入,计算出注意力权重图(Attention map),挖掘特征之间的关系。
-
Attention map作用于 L_n 的每个channel得到 Weighted local feature \mathscr{G}_{\mathrm{a}}^n。
-
把各个层级下的\mathscr{G}_{\mathrm{a}}^n进行连接操作后得到\mathscr{G}_{\mathrm{a}}:[\mathscr{G}_{\mathrm{a}}^1, \mathscr{G}_{\mathrm{a}}^2, \mathscr{G}_{\mathrm{a}}^3]
-
最后将 \mathscr{G}_{\mathrm{a}}送入全连接层
FC-2, 10进行分类。
1.1.2 \mathscr{G}_{\mathrm{a}}计算过程
计算过程及关联数学公式如下:
$$
\begin{aligned} &a_{i}^{s}=\frac{\operatorname{exp}(c_{i}^{s})}{\sum_{j=1}^{n}\operatorname{exp}(c_{j}^{s})}, i\in\{1\cdots n\}. \\ &\boldsymbol{g}_{a}^{s}=\sum_{i=1}^{n}a_{i}^{s}\cdot\boldsymbol{\ell}_{i}^{s} \\ &\boldsymbol{g}_{a} = [\boldsymbol{g}_{a}^{1},\boldsymbol{g}_{a}^{2},\cdots\boldsymbol{g}_{a}^{S}] \end{aligned}
$$
公式注解:
-
c_{i}^{s}:第 s 层特征图在位置 i 处的兼容性分数(compatibility score)。
-
a_i^s:通过 softmax 计算得到的第 s 层特征图在位置 i 处的注意力权重。
-
g_a^s:经过注意力加权后的第 s 层特征图的全局加权特征向量。
-
\ell_i^s:第 s 层特征图在位置 i 处的局部特征向量。
-
a_i^s \cdot \ell_i^s:注意力权重 a_i^s 和局部特征 l_i^s 相乘,表示该位置在注意力机制中的贡献。
-
g_a:最终得到的全局加权特征向量,它是不同层的加权特征向量 g_a^s 的拼接结果。
-
g_a^s:是第 s 层卷积特征图的加权特征向量。
整个过程最关键的是a_{i}^{s}的计算,而 a_{i}^{s} 的计算依赖于 c_{i}^{s},那么c_{i}^{s}是如何计算的呢?
1.1.3 兼容性得分计算
兼容性得分,compatibility score,论文给出了两种方式:
-
内积法:两个特征直接做点乘得到
$$
c_i^s=\langle\ell_i^s,\boldsymbol{g}\rangle, i\in\{1\cdots n\}.
$$
-
有参法:将两个张量逐元素相加后,再经过一个全连接层进行学习, 下式中 \boldsymbol{u} 就是学习到的线性映射
$$
c_i^s=\langle\boldsymbol{u},\boldsymbol{\ell}_i^s+\boldsymbol{g}\rangle, i\in\{1\cdots n\}
$$
1.2 实验效果
从可视化和数据化两个方面进行观察。
1.2.1 效果可视化

图阅读注解:
proposed:表示加入LTPA注意力机制
existing:表示加入传统的注意力机制
1.2.2 效果数据化

表阅读注解:
注意力获取方法:pc表示有参法,dp表示内积法
最终预测策略:concat表示特征拼接后预测,indep表示多尺度独立预测结果相加
2. 仿射变换
Affine Transformation,是一种线性空间变换,可以保持图形的平直性和共线性,但不一定保持角度和长度。
2.1 数学表达
在二维空间中,仿射变换可以用以下形式的矩阵运算来表示:
$$
\begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} a & b & t_x \\ c & d & t_y \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix}
$$
其中:
-
(x, y) 是原始图像中的点坐标。
-
(x', y') 是经过仿射变换后的点坐标。
-
矩阵中的参数 a, b, c, d 负责控制旋转、缩放、剪切等变换,也就是坐标系,默认是单位正交坐标系。
-
t_x, t_y 是平移参数。
2.2 常见类型
-
平移 (Translation): 只改变位置,不改变形状,即 t_x 和 t_y 是非零值:
$$
\begin{pmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \end{pmatrix}
$$ -
旋转 (Rotation): 绕某一点旋转,通常绕原点:
$$
\begin{pmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \end{pmatrix}
$$其中 \theta 是旋转角度。
-
缩放 (Scaling): 改变大小,横向或纵向进行比例缩放:
$$
\begin{pmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \end{pmatrix}
$$其中 s_x 和 s_y 分别控制水平和垂直方向的缩放比例。
-
倾斜 (Shear): Shear变换使得图像沿着 x 或 y 方向进行倾斜:
$$
\begin{pmatrix} 1 & k_x & 0 \\ k_y & 1 & 0 \end{pmatrix}
$$其中 k_x 和 k_y 是剪切系数。
-
复合变换 (Composite Transformation): 仿射变换可以叠加多个基本变换,如旋转加缩放、旋转加平移等。通过矩阵乘法可以将多个变换结合成一个统一的仿射变换。
2.3 具体应用
-
图像增强和几何校正: 仿射变换常用于图像增强任务,比如在照片失真时,仿射变换可以纠正视角失真。
-
目标检测和跟踪: 仿射变换用于将不规则形状的图像转换为标准形式,便于检测和跟踪。
-
数据扩增: 在机器学习中,仿射变换可以生成多样化的数据集,用于增强模型的泛化能力。
Tip:
仿射变换完成的是图形的像素点在整个图像中的坐标位置变换
像素点本身的值并没有进行改变
3. Spatial Transformer Network
论文地址:https://arxiv.org/pdf/1506.02025
3.0 基本认知
Spatial Transformer Network,STN,空间变换网络。它会学习空间特征图的形变,完成适合任务的预处理操作。STN 主要解决的问题是自动校正输入数据中的几何变换(如旋转、缩放、平移等)。

3.1 Localisation net
Localisation net是两个全连接层,用于生成仿射变换系数:
-
输入:C×H×W维的图像
-
输出:仿射变换系数 \theta 矩阵
$$
\left[\begin{array}{ccc}\theta_{11}&\theta_{12}&\theta_{13}\\\theta_{21}&\theta_{22}&\theta_{23}\end{array}\right]
$$
这个仿射变换系数就是STN中新增的要学习的参数,很明显这个值并不总是整数。
3.2 Grid generator
Grid generator完成仿射变换

3.2.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 层上对坐标进行的缩放、旋转、平移等操作。
3.2.2 实例助解
$$
\begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} + \begin{pmatrix} -1 \\ -1 \end{pmatrix}
$$
$$
f_l(x, y) = f_{l-1}(y - 1, x - 1)
$$

3.3 Sampler
采样器根据Grid Generator生成的网格坐标,从输入图像中提取像素值。
由于 Grid Generator 生成的坐标通常不是整数,需要使用插值技术(双线性插值)来获得精确的输出值 。
3.3.1 采样过程
-
输入图像和网格坐标
-
设输入图像大小为 H × W,每个像素值是对应的特征值。
-
采样器接收 Grid Generator 生成的一组浮点数坐标网格,例如网格中的坐标 (x', y'),它表示输出图像中的某个点需要从输入图像的对应位置采样像素值。此时,坐标 (x', y') 很可能不落在输入图像的整数像素位置。
-
-
计算邻近像素
-
假设 (x', y') 是浮点数,那么需要找到它所在的输入图像中最近的四个整数像素点。设它在输入图像中的位置是:
$$
(x', y') = (x + \delta_x, y + \delta_y)
$$其中, x 和 y 是输入图像的整数坐标, \delta_x 和 \delta_y 是小数部分。
例如,如果 x' = 5.3,那么 x = 5,且 \delta_x = 0.3;如果 y' = 7.6,那么 y = 7,且 \delta_y = 0.6。
这时,需要找到 (x, y), (x+1, y), (x, y+1), (x+1, y+1) 这四个整数坐标点。
-
-
双线性插值 双线性插值根据浮点坐标点周围的四个像素值进行加权计算。这四个像素点的权重取决于浮点坐标与这些整数坐标的距离。
假设周围四个像素点的值为:
-
I(x, y): 左上角像素
-
I(x+1, y): 右上角像素
-
I(x, y+1): 左下角像素
-
I(x+1, y+1): 右下角像素
使用双线性插值公式,浮点坐标 (x', y') 处的像素值可以计算为:
$$
I(x', y') = (1 - \delta_x)(1 - \delta_y) I(x, y) + \delta_x (1 - \delta_y) I(x+1, y) + (1 - \delta_x) \delta_y I(x, y+1) + \delta_x \delta_y I(x+1, y+1)
$$其中, \delta_x 和 \delta_y 分别表示浮点坐标 x' 和 y' 与最近整数坐标的距离,权重值根据距离线性变化。离浮点坐标越近的像素点,权重越大,贡献越多。
-
-
重复插值过程 采样器会为输出图像的每一个点重复上述插值过程,通过 Grid Generator 提供的坐标来从输入图像中采样出所有需要的像素值。这些采样结果将组成经过变换的输出图像。
3.3.2 插值方法
在 STN 中,双线性插值是最常用的插值方法,原因在于它简单、快速且可微分的。当然,STN 也可以选择其他插值方法,如最近邻插值或双三次插值。
3.3.3 输出
经过仿射变换,采样器生成了新的特征图,也就是输入图像经过几何变换后的版本。
3.4 实验效果
不同的数据集、不同的尺度和不同的任务上体现如何?
3.4.1 MNIST数据集
在MNIST数据集上的表现:目标更大且在核心位置

3.4.2 SVHN数据集
在SVHN数据集上的表现:多尺度应用,把STN插入到不同位置

不同位置的好处:有的关注旋转、有的关注平移、有的关注缩放,这种多尺度的应用效果还是有的。
3.4.3 细粒度分类
模型结构简图:

实验效果展示:整体效果还是正向的,就很好了

3.5 STN实战
先搭建一个即插即用的STN模块,再融入到模型中。
3.5.1 构建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
3.5.2 融入模型
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()
输出:
torch.Size([4, 1, 32, 32])
四、混合注意力
混合注意力机制(Hybrid Attention Mechanism)是一种结合空间和通道注意力的策略,旨在提高神经网络的特征提取能力。
1. CBAM
Convolution Block Attention Module ,卷积块注意力模块
论文地址:https://arxiv.org/pdf/1807.06521
1.0 基本认知
CBAM是一种轻量级的注意力模块,它通过增加空间和通道两个维度的注意力,来提高模型的性能。

一维的通道注意力图:\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}
$$
1.1 通道注意力模块
通道注意力模块的目的是为每个通道生成一个注意力权重,整体流程如下图:

通道注意力模块机制公式如下:
$$
\begin{gathered} \mathrm{M_{c}(F)} =\sigma(MLP(AvgPool(\mathbf{F}))+MLP(MaxPool(\mathbf{F}))) \\ =\sigma(\mathbf{W_1}(\mathbf{W_0}(\mathbf{F_{avg}^c}))+\mathbf{W_1}(\mathbf{W_0}(\mathbf{F_{max}^c}))), \end{gathered}
$$
其中:r是缩放因子,用以减少参数量
$$
\mathrm{W}_0\in\mathbb{R}^{C/r\times C} \\ \mathrm{W}_1\in\mathbb{R}^{C\times C/r}
$$
通道注意力模块机制详情如下:
-
输入特征:输入特征图 F 的尺寸为 H × W × C。
-
全局池化:
-
首先对 F 进行全局的MaxPool和AvgPool,得到两个特征图,尺寸为 1×1×C。
-
MaxPool提取了局部强响应特征,AvgPool提取了全局视角。
-
-
共享多层感知器(MLP):
-
池化后的2个特征向量分别送入一个共享MLP,它包含两个全连接层,用来处理和生成通道注意力。
-
MLP的共享权重减少了参数量,同时确保两个特征向量的变换方式是一致的。
-
MLP首先会降维为 C/r,然后升维为 C。
-
-
加法与激活:
MLP输出的两个特征向量逐元素相加后经Sigmoid后,生成维度为 1 × 1 × C的通道注意力图 M_c,表示每个通道的重要性。
-
输出:
通道注意力图 M_c 与输入特征图 F 逐通道相乘,生成经过通道注意力增强的特征图,维度不变的。
1.2 空间注意力模块
空间注意力模块通过卷积操作为特征图的每个空间位置生成权重,聚焦在图像中的关键区域。

空间注意力模块机制公式如下:
$$
\begin{aligned} \mathbf{M_s}(\mathbf{F})& \begin{aligned}&=\sigma(f^{7\times7}([AvgPool(\mathbf{F});MaxPool(\mathbf{F})]))\end{aligned} \\ &=\sigma(f^{7\times7}([\mathbf{F_{avg}^s};\mathbf{F_{max}^s}])), \end{aligned}
$$
其中:
$$
\mathbf{F_{avg}^s}\in\mathbb{R}^{1\times H\times W} \\ \mathbf{F_{max}^s}\in\mathbb{R}^{1\times H\times W}
$$
空间注意力模块机制详情如下:
-
输入特征:通道注意力模块的输出 F' 就是空间注意力模块的输入。
-
池化操作:
-
首先在 F' 的通道维度上进行全局的MaxPool和AvgPool,生成2个二维特征图,维度为 H × W × 1。
-
这样可以分别提取空间上最重要的局部和全局信息。
-
-
卷积层:
将池化得到的两个特征图按通道维度进行连接,形成一个 H × W × 2 的特征图,并通过大小为 7 × 7 的卷积层处理。
-
激活与输出:
-
卷积层的输出经Sigmoid激活后,生成单通道的空间注意力图 M_S,维度为 H \times W \times 1。
-
空间注意力图与经过通道注意力增强后的特征图 F' 逐元素相乘,输出最终的增强特征图。
-
1.3 不同策略效果对比
1.3.1 通道注意力
加入通道注意力:可以看的出来都比不用(baseline)效果要好

1.3.2 叠加空间注意力
在通道注意力的基础之上加入空间注意力,就是混合注意力:效果最好的就是CBAM,并且池化不需要参数

1.3.3 叠加顺序
空间注意力和通道注意力位置调整效果对比:还是CBAM的效果好

1.3.4 不同模型
不同模型对比:主打一个CBAM就是好

1.3.5 轻量级模型
在一些轻量级模型上的效果还是很明显的

1.3.6 注意力可视化
可视化的方式对比:


2. BAM
Bottleneck Attention Module,瓶颈注意力模块。
论文地址:https://arxiv.org/pdf/1807.06514
2.0 基本认知
BAM是通过在空间和通道两个维度上分别构建注意力模块,它们是并行处理的。

其中:形状不同的张量会自动进行广播机制
$$
\mathbf{M(F)=\sigma(M_c(F)+M_s(F)),} \\ \mathbf{F^{\prime}=F+F\otimes M(F),}
$$
2.1 通道注意力模块
通道注意力公式表达如下:
$$
\begin{aligned} \mathbf{M_c(F)}& =BN(MLP(A\nu gPool(\mathbf{F}))) \\ &=BN(\mathbf{W_1}(\mathbf{W_0}A\nu gPool(\mathbf{F})+\mathbf{b_0})+\mathbf{b_1}), \\ \text{其中:} \mathbf{W_0}\in\mathbb{R}^{C/r\times C},\mathbf{b_0}\in\mathbb{R}^{C}& ^{C/r},\mathbf{W_1}\in\mathbb{R}^{C\times C/r},\mathbf{b_1}\in\mathbb{R}^C. \end{aligned}
$$

通道注意力流程如下:
-
全局平均池化:对输入特征 F 进行GlobalAvgPooling,保留通道的重要全局信息。
-
全连接层:池化后的特征通过两个FC,第一个FC降维,第二个FC则恢复到原通道数 C。这一过程可以学习通道间的依赖关系。
-
通道注意力:通过激活函数 Sigmoid 生成通道注意力图 M_c(F),用于对原始通道进行加权,强调重要通道,抑制不重要通道。
2.2 空间注意力模块
空间注意力公式表达如下:
$$
\mathbf{M_s}(\mathbf{F})=BN(f_3^{1\times1}(f_2^{3\times3}(f_1^{3\times3}(f_0^{1\times1}(\mathbf{F})))))
$$

空间注意力流程如下:
-
1×1卷积:对输入特征 F 进行一次卷积操作,用于压缩通道维度并保持原始的空间信息,压缩因子是 r。
-
膨胀卷积:使用两层膨胀卷积(Dilated Convolution),膨胀率为 d。这样既扩大了感受野,又不增加参数量,帮助模型在空间维度上捕捉更广的上下文信息。
-
空间注意力生成:卷积操作生成一个空间注意力图 M_s(F),用于标识出空间维度上哪些位置更重要。
2.3 注意力融合
通道和空间注意力融合:M_c(F) 和 M_s(F)相加后,通过Sigmoid处理,生成最终的注意力图 M(F)。
2.4 注意力应用
-
BAM注意力图 M(F) 应用到 F 上,从而对特征图进行重新加权。
-
残差连接:将加权后的特征图与输入特征 F 进行相加,形成残差连接。
这样不仅保留了原始特征信息,还让网络学习到重要的注意力区域。
2.5 实验结果
对比不同情况下的模型效果。
2.5.1 超参数配置

超参数:膨胀卷积的膨胀系数、FC的缩放因子
2.5.2 融合方式

融合方式不同,效果也不同,最总就是两个注意力并行后相加效果最好。
2.5.3 模型横向对比

可以看的出来,加入BAM之后,都有明显的效果提升,说明这种方式是有效的且通用的。
3. CBAM 搭建

3.1 通道注意力搭建

参考代码:
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)
3.2 空间注意力搭建

参考代码:
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)
3.3 模型融入CBAM
这里我们融入到Resnet50
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()
更多推荐



所有评论(0)