共计 2840 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点:为什么需要注意力机制?
传统 CNN 通过堆叠卷积层提取特征,但存在两个明显缺陷:

-
平等对待所有通道:每个卷积核输出的特征通道被同等处理,但实际不同通道的重要性不同。例如在猫狗分类中,耳朵形状的特征通道比背景纹理更重要。
-
忽略空间关系:标准卷积操作平等处理特征图所有位置,无法聚焦关键区域。比如识别鸟类时,头部区域比天空背景更具判别性。
这就像用相同音量播放交响乐所有乐器声部,既无法突出小提琴主旋律,也难捕捉低音鼓的节奏变化。
主流注意力模块对比
| 模块类型 | FLOPs (ResNet50) | 参数量 | 核心机制 |
|---|---|---|---|
| SE | 0.004G | 2.5M | 仅通道注意力 |
| CBAM | 0.005G | 3.3M | 通道 + 空间双注意力 |
| ECA | 0.003G | 1.8M | 局部通道交互 |
测试环境:PyTorch 1.10 + NVIDIA V100,输入尺寸 224×224
CBAM 核心实现详解
通道注意力子模块
数学原理:
M_c(F) = \sigma(MLP(AvgPool(F)) + MLP(MaxPool(F)))
其中 $\sigma$ 表示 sigmoid 激活,MLP 是共享权重的两层全连接网络。通过同时使用平均池化 (GAP) 和最大池化(GMP),既能捕捉整体特征分布,又保留显著局部特征。
PyTorch 实现:
class ChannelAttention(nn.Module):
def __init__(self, in_planes, ratio=16):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1) # [B,C,H,W] -> [B,C,1,1]
self.max_pool = nn.AdaptiveMaxPool2d(1)
# 注意第一个全连接层输出维度是 C /r,减少计算量
self.fc1 = nn.Conv2d(in_planes, in_planes//ratio, 1, bias=False)
self.relu = nn.ReLU()
self.fc2 = nn.Conv2d(in_planes//ratio, in_planes, 1, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avg_out = self.fc2(self.relu(self.fc1(self.avg_pool(x))))
max_out = self.fc2(self.relu(self.fc1(self.max_pool(x))))
return self.sigmoid(avg_out + max_out) # [B,C,1,1]
空间注意力子模块
数学原理:
M_s(F) = \sigma(f^{7×7}([AvgPool(F); MaxPool(F)]))
其中 $f^{7×7}$ 表示 7×7 卷积核,通过沿通道维度拼接两种池化结果,捕获跨通道的空间信息。
代码实现要点:
class SpatialAttention(nn.Module):
def __init__(self, kernel_size=7):
super().__init__()
# 使用 padding 保持特征图尺寸不变
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) # [B,1,H,W]
max_out, _ = torch.max(x, dim=1, keepdim=True)
concat = torch.cat([avg_out, max_out], dim=1) # [B,2,H,W]
return self.sigmoid(self.conv(concat))
实战:ResNet18+CBAM 改造
在残差块的 shortcut 连接前插入 CBAM 模块:
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(inplanes, planes, 3, stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes, planes, 3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
# 新增 CBAM 模块
self.ca = ChannelAttention(planes)
self.sa = SpatialAttention()
self.downsample = downsample
self.stride = stride
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.ca(out) * out # 通道注意力
out = self.sa(out) * out # 空间注意力
if self.downsample is not None:
identity = self.downsample(x)
out += identity
return self.relu(out)
在 CIFAR-10 上的测试结果(训练 50 个 epoch):
| 模型 | 准确率(%) | 参数量(M) |
|---|---|---|
| ResNet18 | 93.2 | 11.2 |
| ResNet18+CBAM | 94.7 | 11.4 |
三大避坑指南
- 维度不匹配问题:
- 错误现象:当输入图像尺寸非正方形时,空间注意力可能输出错误维度
-
解决:确保 SpatialAttention 中的卷积 padding 设置正确,推荐使用
padding=kernel_size//2 -
池化方式混淆:
- 错误:在通道注意力中误用全局最大池化代替平均池化
-
正确做法:同时使用 GAP 和 GMP,经验表明二者互补能提升效果
-
注意力应用顺序:
- 错误:先空间注意力后通道注意力
- 原理:通道注意力能先强化重要特征通道,为空间注意力提供更干净的输入
延伸思考方向
-
动态注意力机制:当前 CBAM 的注意力权重在推理时固定,能否根据输入内容动态调整 ratio 等超参数?
-
跨任务泛化性:在目标检测任务中,CBAM 模块更适合放在 Backbone 还是 FPN 部分?实验表明检测头前的空间注意力能显著提升小目标召回率。
通过本指南,你应该已经掌握 CBAM 的核心原理和实战技巧。建议尝试在自定义数据集上验证效果,并思考如何结合其他注意力机制(如 Transformer)进行改进。
