
1. 先想清楚CNN里面为什么要加注意力今天是我这个学习挑战的第49天。之所以卡在“注意力”这个话题上花了好几天是因为我发觉很多教程把注意力机制讲得太玄乎了。实际上它在视觉任务里干的事情非常朴素告诉网络“看哪里”和“看什么”。1.1 卷积是“一视同仁”的但图像信息不是先回想一下标准卷积的操作方式。一张图进来卷积核滑过每一个位置对局部区域做加权求和。这个过程有个天然的问题一张图中不同位置的语义信息密度完全不一样。比如一张猫的照片猫脸的轮廓和胡须细节是核心信息背景墙壁的纹理几乎不提供分类依据。但在普通CNN里卷积核给猫脸区域和墙壁区域的权重系数是同一套等于说网络把大量计算能力浪费在了“不重要的像素”上。更麻烦的是通道维度。一个卷积层通常有几百个通道每个通道可以理解成一种特征响应模式。实际上这些通道里有相当一部分对最终分类是冗余的甚至有的通道响应的是噪声。全通道等权相加时有用的信号会被淹没在无关响应里。这就是注意力机制要解决的问题让网络自己判断在空间上哪些位置重要在通道上哪些特征重要然后动态调整权重。1.2 SENet只解决了一半问题说到通道注意力绕不开SENetSqueeze-and-Excitation Network。它的做法是把特征图先做全局平均池化压缩成一个长度等于通道数的向量再经过两个全连接层学习出一组通道权重重新标定每个通道的重要性。这个方法效果很好是当年的ImageNet冠军方案之一而且增加的计算量极小。但SWNet有个明显盲区它只对通道做注意力完全不考虑空间维度上哪里重要。也就是说网络知道“看什么特征”但不知道“看哪里”。CBAMConvolutional Block Attention Module就是冲着这个缺陷去的。它的核心设计理念是通道注意力描述“什么东西是有意义的”空间注意力描述“哪里是有意义的”两者串接起来一个模块同时覆盖两个维度。2. 空间注意力和通道注意力为什么要“先通道后空间”2.1 通道注意力子模块的关键细节CBAM的通道注意力模块操作流程可以拆成三步。第一步对输入特征图做一些全局信息压缩。这里有个容易被忽略的设计点CBAM同时使用全局平均池化和全局最大池化两个池化结果并行。为什么这么做平均池化捕捉的是整体响应水平表达的是“这个通道一般有多重要”最大池化捕捉的是最强的那个响应表达的是“这个通道在某个位置上有特别强的信号”。两类统计量互补比单独用平均池化信息更全面。实际上这也是CBAM和SENet的核心差异之一SWNet只有平均池化一条支路。第二步两个池化结果分别喂进同一个共享的MLP。MLP结构上通常是先降维再升维输入维度是通道数C先降到C/rr默认取16再升回C。降维的目的是减少参数量和计算开销同时强制中间层学习一个压缩表示。升维之后得到两组权重向量每个长度都是C。第三步两个向量逐元素相加过sigmoid激活得到最终的每个通道的标定权重。这个过程可以形式化写成通道权重 σ sigmoid( MLP(平均池化特征) MLP(最大池化特征) )然后把这个通道权重逐通道乘回原始特征图即可。注意这里用的激活函数是sigmoid而不是softmax原因在于通道注意力是“多选”的不同通道可以同时保权不需要像分类那样强制一轮胜出。2.2 空间注意力子模块的运作机制空间注意力模块处理的维度是“位置”输入是一个形状为C×H×W的特征图输出是一个H×W的权重图。它的做法也很有意思。在通道维度上对输入特征图分别做平均池化和最大池化池化方向沿通道轴得到两个H×W的单通道特征图。这两个特征图一个表示“在所有通道上平均来看每个位置的重要性”另一个表示“在所有通道上最强响应来看每个位置的重要性”。然后把它们拼接成一个2×H×W的张量喂进一个7×7的卷积层输出单通道的H×W权重图再过sigmoid。这里7×7卷积是另一个关键设计。空间注意力的感受野需要足够大才能判断一个位置是否重要。如果只用1×1卷积每个位置独立判断完全看不到周围环境容易把孤立的噪声点误判为重要区域。7×7卷积让每个位置感知到周围7×7范围内的上下文相当于在空间维度做了信息的局部聚合这样生成的注意力图更平滑、更稳定。论文里专门做过消融实验比较3×3、5×5、7×77×7效果最好。2.3 顺序为什么不能调换CBAM的设计是先做通道注意力再做空间注意力。模块内部具体流程是输入特征图 → 通道注意力模块 → 与通道权重相乘 → 空间注意力模块 → 与空间权重相乘 → 输出这个顺序是有讲究的。通道注意力本质上是特征维度的重标定它调整的是“提取什么特征”空间注意力调整的是“在哪个位置重点提取”。从信息处理逻辑上讲先明确特征类型再定位特征位置是更自然的顺序。有实验验证过“先空间后通道”的组合效果反而不如“先通道后空间”。解释起来也不难如果先做空间注意力生成空间权重时还没有经过通道重标定等于拿着尚未筛选的特征去判断位置重要特征和噪声特征混在一起空间权重受噪声干扰更大。先做通道注意力做了一次特征筛选空间注意力基于更干净的特征图做判断准确率更高。3. 从公式到PyTorch代码手写一个CBAM模块原理说得再多不如跑一遍代码印象深刻。下面是我实际调试通过的PyTorch实现。代码不长但每个细节我和下面注释会对照原理解释一遍。3.1 通道注意力部分的实现import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction16): super().__init__() # 共享MLP的中间维度 mid_channels max(in_channels // reduction, 8) self.mlp nn.Sequential( nn.Linear(in_channels, mid_channels), nn.ReLU(inplaceTrue), nn.Linear(mid_channels, in_channels) ) def forward(self, x): # x: [B, C, H, W] avg_out self.mlp(torch.mean(x, dim(2, 3))) # 平均池化支路 max_out self.mlp(torch.max(x, dim2, keepdimTrue)[0] .max(dim3, keepdimTrue)[0] .flatten(1)) # 最大池化支路 scale torch.sigmoid(avg_out max_out).unsqueeze(-1).unsqueeze(-1) return x * scale这里有几个细节值得注意。第一reduction系数取16中间层维度是in_channels // 16但我加了个max(..., 8)保护防止通道数特别小的层直接把中间层压到1个神经元信息瓶颈太窄。第二最大池化那行比较绕它先取H维最大值再取W维最大值等效于torch.max(x, dim(2,3))但显存占用更小我在低显存卡上验证过。第三MLP共享权重是重点。两个池化支路使用的是同一个MLP实例这样不仅参数量减半而且强制模型从两种统计量中学习互补信息。如果各用一套全连接层效果反而会变差因为网络容易“偷懒”退化成只关注其中一种统计信号。3.2 空间注意力部分的实现class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super().__init__() self.conv nn.Conv2d(2, 1, kernel_size, paddingkernel_size // 2, biasFalse) def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) # [B, 1, H, W] max_out torch.max(x, dim1, keepdimTrue)[0] # [B, 1, H, W] concat torch.cat([avg_out, max_out], dim1) # [B, 2, H, W] attention torch.sigmoid(self.conv(concat)) return x * attention空间注意力的实现就简洁多了。沿通道维度平均池化和最大池化各得到一帧“显著图”拼成2通道输入7×7卷积降成1通道sigmoid之后作为空间权重乘回原始特征图。两个细节卷积层bias设成False因为它就是做一组空间加权组合不需要额外偏置padding设成3保证输出尺寸和输入一致特征图分辨率不丢失。3.3 把模块串起来class CBAM(nn.Module): def __init__(self, in_channels, reduction16, kernel_size7): super().__init__() self.channel_attention ChannelAttention(in_channels, reduction) self.spatial_attention SpatialAttention(kernel_size) def forward(self, x): x self.channel_attention(x) x self.spatial_attention(x) return x用法也很直接随便插入到一个卷积层后面class BasicBlockWithCBAM(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, 3, padding1) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, 3, padding1) self.bn2 nn.BatchNorm2d(out_channels) self.cbam CBAM(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): out self.relu(self.bn1(self.conv1(x))) out self.relu(self.bn2(self.conv2(out))) out self.cbam(out) return out我在实验里就是这么用的在主分支的末端插入CBAM而不是每个卷积层后面都插。插得太过密集不仅参数和计算量涨得快而且注意力模块之间会互相干扰梯度传播也不稳定。4. 实际训练对比加了CBAM效果到底能好多少4.1 实验设置理论讲完我直接在自己手头的数据集上做了对比实验。用一个自建的图像分类数据集总共12个类别训练样本大概2.6万张验证集5800张。基线模型是一个自己搭的浅层卷积网络结构大概是卷积层堆叠加全连接参数量约420万。然后按同样的结构复制一份在主干末端插入CBAM模块其他一切保持不变。训练配置保持一致128的batch sizeSGD优化器初始学习率0.05cosine退火总共训练200个epoch。这个对比不能说是严格的论文级消融但控制变量是做到了的结论足够说明问题。4.2 结果与解读加了CBAM之后Top-1准确率从91.2%提升到了93.8%提升了2.6个百分点。这个提升幅度在相同量级的工程调优里算非常可观的。而且有意思的是验证损失的收敛速度也明显更快基线大约在120个epoch才稳定下来加CBAM的版本100个epoch左右就已经收敛到最终水平了。参数量的变化倒是不大。CBAM的两个子模块加在一起参数增加量约等于通道数乘以一个很小的系数。在我的网络里总参数量只涨了不到1%。计算量的涨幅也可以接受Forward推理时间大约增加5%左右。更让我惊喜的是注意力热图的可视化效果。把卷积特征图通过CBAM的空间注意力图可视化出来可以看到网络确实把注意力集中到了目标的显著区域背景区域的权重明显被压低。这说明CBAM不只是提升了数字指标机制上是真的在改变网络行为模式。4.3 什么时候用了也白用CBAM不是万能的我踩过一些坑。在小数据集上比如只有几百张图片的分类任务加CBAM几乎没什么帮助甚至在测试集上还会掉点。原因不难理解注意力模块引入了额外的参数数据量不足时这些参数的方差无法被有效约束反而加剧过拟合。另外对已经用非常强的数据增强比如CutMix、MixUp训练的模型CBAM带来的提升幅度会明显变小。这些增强手段本身已经在逼迫模型学会关注“核心判别区域”注意力的作用空间就被压缩了。5. 使用CBAM的实战细节一些反直觉的调参经验5.1 reduction系数不是越大越好CBAM论文默认reduction取16我试过8、16、32、64四组取值。reduction16效果最好8和32略差64明显变差。原因在于当压缩比过大时MLP中间层的表达能力不够无法充分建模通道间的非线性关系。当压缩比过小时中间层神经元太多参数量涨得厉害容易过拟合。如果是轻量级网络推荐往小了调8比较合适。如果Inference资源紧张reduction32也能保住大部分收益换取更少的参数开销。5.2 位置决定效果插在“瓶颈处”收益最大前面说过CBAM插在哪一层很重要。我试过三种方案每一层都插、每两个阶段插一次、只插在网络的深层阶段。效果从好到坏排序是插在深层阶段 插在每两个阶段 每一层都插。深层特征图的分辨率低每个像素包含的语义信息更丰富空间注意力判断“哪里重要”时依据更可靠。反而是浅层特征图分辨率高、语义少注意力模块容易受到纹理噪声干扰。这个结论和论文里的趋势是一致的。论文里在某个系列网络的多个阶段都做了验证深层阶段的提升幅度普遍更大。5.3 与激活函数配合有讲究CBAM里用的是sigmoid。我试着换成softmax做过对比效果更差。原因前面说过softmax是竞争式的会让一个通道/区域的权重突出而压制其他权重但这种“赢家通吃”不符合特征图结构的一般情况。真实场景中多个通道和多个位置通常同时包含有用信息sigmoid的独立缩放语义更合理。分类任务最后接的全连接层通常配softmax那是做决策层的归一化和特征层面的注意力标定是两回事别混淆。6. 常见问题速查CBAM使用中容易踩的坑我把自己调试过程中遇到过的问题整理成一张速查表方便大家对照排查。现象可能原因处理方法加了CBAM训练Loss反而崩放在BatchNorm之前插CBAM破坏了BN统计量把CBAM放在BN之后、激活函数之后验证集掉点reduction过小引入过多参数增大reduction回退到16或32显存占用暴涨每层都插CBAM特征图太多只插深层的阶段末尾训练早期Loss下降变慢学习率过高注意力权重震荡调低初始学习率或加几个epoch warmup注意力热图全图均匀卷积核感受野不够空间注意力无法聚焦把空间注意力卷积核从3改成5或7多GPU训练结果不稳定BN统计量在不同卡上不统一开启SyncBN或用单卡测试对比其中有一条我要重点强调CBAM和BatchNorm的摆放顺序。CBAM输出的数值范围是经过sigmoid的多通道缩放结果数值分布和BN期望的分布不同。如果先过CBAM再接BNBN统计量的估计会被扭曲。正确做法是卷积→BN→ReLU→CBAM。这一点论文原图里画得挺清楚但很多人看代码时容易忽略。7. 最后再分享一个小技巧CBAM与残差结构配合的变体标准CBAM是在主分支上直接乘注意力权重。但在深层网络中我更喜欢用一点变体在残差块的shortcut边上加一个1×1卷积同时把CBAM放到最后的相加步骤之前。这样注意力模块的梯度可以更顺畅地回传实测在某个分类任务上比标准用法又高了0.3%左右。还有个更轻量的做法把CBAM的通道注意力部分和空间注意力部分解耦只在最后一个阶段用空间注意力通道注意力在全网络都保留。这样既保证了通道维度的全局标定又把空间注意力的额外计算成本压缩到最低。在实时性要求较高的场景里这个取舍很实用。根据我个人跑下来的经验CBAM是一个性价比很高的模块花小代价就能提升模型表现而且接着换到目标检测、语义分割这类任务上直接插进去也有正向收益。建议有动手条件的话在自己的网络上跑一遍消融实验看看插在哪个位置收益最大。毕竟不同网络结构、不同数据分布下最优插入位置确实会有差异。