
很多刚接触注意力机制的读者第一次看到 CBAM 论文里的示意图都会觉得有点绕又是通道注意力又是空间注意力两个模块还串在一起到底先做哪个、后做哪个每个模块内部又是在“注意”什么我最早看这篇论文的时候也卡了好几天后来自己动手复现了一遍又把模块拆开单独做了消融实验才算彻底搞明白。这篇就用最直白的话把 CBAM 从原理到代码从头到尾捋一遍。CBAMConvolutional Block Attention Module是 2018 年发表在 ECCV 上的一篇注意力机制论文全称是 Convolutional Block Attention Module翻译过来就是“卷积模块的注意力机制”。它解决的问题很朴素卷积神经网络在提取特征时对每个通道、每个位置都是“一视同仁”的但实际任务里有的通道更重要有的空间位置更关键。CBAM 做的事情就是让网络自己学会“什么地方该多看两眼”。这篇内容适合谁看呢一是刚接触注意力机制、想搞懂 SE Net、CBAM、ECA 这些模块到底有什么区别的初学者二是想把 CBAM 接进自己的分类、检测或者分割网络里但不确定该怎么改结构、怎么调参的实践者。我会把模块内部的每一步计算都拆开讲附上完整的 PyTorch 代码再把我自己在实际项目中踩过的坑和总结的经验一起放出来。1. 整体设计思路CBAM 到底在“注意”什么先聊一个最核心的问题CBAM 和之前那些注意力机制相比到底多了什么在 CBAM 之前最出名的是 SENetSqueeze-and-Excitation Networks它只做通道注意力。SENet 的思路是:先用全局平均池化把每个通道压缩成一个数值然后用两个全连接层学习每个通道的重要程度最后把学到的权重乘回原来的特征图。这种做法有效但它只关注了“哪些通道重要”完全没有考虑“哪些位置重要”。CBAM 的论文作者指出了一个很直观的问题:通道注意力告诉网络该“看什么”但没告诉网络该“往哪儿看”。比如一张图里有一只鸟通道注意力可能让网络更关注“羽毛纹理”这种通道但羽毛在图像的左上角还是右下角通道注意力是区分不出来的。这时候就需要空间注意力来补充位置信息。所以 CBAM 的设计思路非常清晰通道注意力在前空间注意力在后两个模块串行连接。特征图先经过通道注意力模块得到加权后的特征图再把这个结果送进空间注意力模块再一次加权。两个模块各管一件事互不干扰最后串起来形成一个完整的“通道空间”双管齐下的注意力机制。这里有一个细节值得注意为什么要串行而不是并行论文里有消融实验对比过。如果把两个模块并行然后相加或者拼接效果都不如串行好。原因也很好理解串行时通道注意力先对特征图做了一次“筛选”把不重要的通道压低了空间注意力在这个已经被筛选过的特征图上去找关键位置任务更纯粹学起来更容易而并行时两个模块各自在原始特征图上操作信息没有先后依赖反而容易产生冗余。再补一个设计上的细节CBAM 里的通道注意力用的是“平均池化最大池化”双路结构这一点也是从 SENet 升级来的。SENet 只用平均池化作者认为平均池化能反映通道的整体响应但最大池化能捕获最显著的特征。两条路各学各的权重最后相加融合效果比只用一路要好。后面我会在代码部分具体展示这个双路结构怎么实现。2. 模块内部机制详解通道注意力和空间注意力分别怎么算2.1 通道注意力先“压缩”再“学习权重”通道注意力的输入是一张特征图形状是 (C, H, W)分别代表通道数、高度、宽度。我们要做的是给每个通道算出一个权重值然后把这个权重乘到对应通道的所有像素上。具体分三步走。第一步压缩。把特征图从 (C, H, W) 压缩成 (C, 1, 1)。怎么压缩呢用两种池化全局平均池化和全局最大池化。平均池化把 H 和 W 两个维度上的所有值取平均最大池化取最大值。这样每个通道就得到了两个数值一个是平均响应一个是最强响应。这一步的目的是把空间信息丢掉只保留通道维度的统计信息。第二步学习权重。把刚才得到的两个 (C, 1, 1) 向量分别送进一个共享的多层感知机MLP。这个 MLP 的结构是先降维再升维。假设输入通道数是 C中间隐藏层的通道数设为 C / r其中 r 是缩减比reduction ratio论文里默认取 16。也就是说先全连接把 C 降到 C/16经过 ReLU 激活再全连接升回 C最后经过 Sigmoid 函数把输出归一化到 0 到 1 之间。这个 MLP 的权重是两个通道共享的也就是说平均池化那条路和最大池化那条路用同一个 MLP只是输入不同。第三步融合。把两条路的输出逐元素相加得到最终的通道注意力向量形状还是 (C, 1, 1)。然后把这个向量和原始特征图逐通道相乘第 c 个通道的所有像素都乘上这个通道对应的权重。这一步就把“不重要的通道”压暗了“重要的通道”保留甚至增强了。你会发现整个通道注意力的计算其实不复杂核心就是“池化压缩MLP学习加权”。这也是它能作为即插即用模块被广泛使用的原因。2.2 空间注意力在压缩后的通道上找关键位置通道注意力做完之后特征图的形状还是 (C, H, W)只是每个通道的数值被重新标定了。接下来把这个结果送进空间注意力模块。空间注意力的思路和通道注意力是对称的这次我们沿着通道维度做压缩然后在空间维度上学习权重。同样是三步。第一步压缩。这次是沿通道维度操作把 shape (C, H, W) 变成 (1, H, W)。怎么变呢对每个空间位置 (i, j)把这个位置上所有通道的像素取平均得到一个值同时取最大值又得到一个值。这样我们就得到了两张“单通道特征图”一张是平均池化结果一张是最大池化结果形状都是 (1, H, W)。这里注意通道注意力是对每个通道做全局池化得到 (C, 1, 1)空间注意力是对每个像素位置做跨通道池化得到 (1, H, W)后者不再需要全连接层因为空间维度的尺寸在每个输入里都可能不同没法直接接全连接。第二步拼接。把平均池化和最大池化得到的两张 (1, H, W) 的特征图沿通道维度拼接起来得到 (2, H, W)。这一步的目的是把两种统计信息合并既保留“整体响应强度”也保留“局部最强响应”。第三步卷积学习。用一层 7x7 的卷积对这个 (2, H, W) 的输入做卷积输出通道数设为 1得到 (1, H, W) 的空间注意力图。为什么要用 7x7论文作者试过 3x3、5x5、7x7发现 7x7 效果最好。因为空间注意力要关注“哪些区域重要”这个判断需要一定的感受野7x7 能覆盖更大的局部范围捕捉更丰富的上下文信息。最后经过 Sigmoid 激活每个空间位置得到一个 0 到 1 之间的权重。把这个权重乘到通道注意力输出的特征图上每个位置的所有通道都乘上同一个空间权重。到这里CBAM 的一次完整前向计算就结束了输入 (C, H, W) 的特征图经过通道注意力加权再经过空间注意力加权输出和输入形状完全一致。这个特性保证了 CBAM 可以插入到任何卷积层之后不需要改变网络整体结构。2.3 为什么两个模块的顺序不能换这个问题我在前面提到过但值得再展开一下。从直觉看先做通道注意力再做空间注意力相当于先回答“看什么”再回答“往哪看”。如果顺序反过来先找位置再选通道会有什么问题空间注意力在计算时是对所有通道取平均和最大值如果这时候通道维度里混着大量噪声通道这些噪声会干扰空间注意力的判断——本来不该关注的背景区域可能因为某个噪声通道的异常大值而被选中。而先做通道注意力噪声通道的权重已经被压低后续空间注意力的输入更“干净”决策更可靠。论文里也给过一组实验数据串行通道在前空间在后的效果优于串行空间在前通道在后约 0.5% 到 1%更优于并行结构。虽然差距不算大但对于追求极致精度的任务来说这 1% 可能就是决定模型胜负的关键。我自己也在实际项目中验证过这个结论。有一段时间我在做细粒度图像分类把 SE 模块换成了 CBAM准确率提升了 1.8%后来试过把顺序对调准确率反而降了 0.6%。所以这个顺序不是一个随意的设计而是有实验依据的最优解。3. CBAM 的 PyTorch 实现与参数解析3.1 完整代码实现先直接给出可以跑的代码。我习惯把 CBAM 写成一个独立的 PyTorch 模块插入到网络里非常方便。import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction16): super(ChannelAttention, self).__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.mlp nn.Sequential( nn.Conv2d(in_channels, in_channels // reduction, kernel_size1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels // reduction, in_channels, kernel_size1, biasFalse) ) def forward(self, x): avg_out self.mlp(self.avg_pool(x)) max_out self.mlp(self.max_pool(x)) return torch.sigmoid(avg_out max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super(SpatialAttention, self).__init__() self.conv nn.Conv2d(2, 1, kernel_sizekernel_size, paddingkernel_size // 2, biasFalse) def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) max_out, _ torch.max(x, dim1, keepdimTrue) concat torch.cat([avg_out, max_out], dim1) return torch.sigmoid(self.conv(concat)) class CBAM(nn.Module): def __init__(self, in_channels, reduction16, kernel_size7): super(CBAM, self).__init__() self.channel_attention ChannelAttention(in_channels, reduction) self.spatial_attention SpatialAttention(kernel_size) def forward(self, x): x x * self.channel_attention(x) x x * self.spatial_attention(x) return x这段代码和论文伪代码是对应的。有几个实现细节值得说明。第一MLP 我用的是两个 1x1 卷积而不是线性层。原因很简单1x1 卷积本质上就是全连接但可以接受任意尺寸的输入而且输入特征是 (C, 1, 1) 的形状用 Conv2d 可以直接处理不需要先做 flatten 再 reshape代码上更干净。如果你更习惯用 nn.Linear需要先对池化结果做 flatten经过 MLP 之后再 reshape 成 (C, 1, 1)。第二池化用 AdaptiveAvgPool2d(1) 和 AdaptiveMaxPool2d(1)这样不管输入特征图是 7x7 还是 28x28输出都固定是 1x1。这个设计很实用保证了 CBAM 能接在各种尺寸的特征图后面。第三空间注意力里的卷积 padding 要等于 kernel_size // 2这样才能保证输出特征图的空间尺寸和输入一致。kernel_size 默认取 7但如果在分辨率很低的特征图上比如 7x7 的特征图7x7 卷积的感受野可能已经覆盖了整个特征图效果就接近全局注意力了。这种情况要不要换小核我在后面的“参数调优”部分会细聊。3.2 论文配置和代码配置对比我把论文里的关键参数和代码里的默认值整理了一下方便对照参数论文默认值代码默认值说明通道缩减比 reduction1616隐藏层通道数 输入通道数 / 16空间卷积核大小77空间注意力模块的卷积核尺寸激活函数ReLU SigmoidReLU SigmoidMLP内部用ReLU输出用Sigmoid池化方式avg maxavg max两个模块都用了双池化是否需要 bias无无卷积层不启用bias有一个小地方需要注意论文里通道注意力的 MLP 隐藏层维度是 C // reduction如果输入通道数比较小比如 64那么隐藏层就只有 4 个通道信息瓶颈可能太窄。在 CIFAR-10 这种小数据集上reduction 取 16 还是很常见的但如果你的骨干网络本身通道数就很少可以适当把 reduction 调小到 8 或者 4我试过这种情况确实有提升。3.3 怎么把 CBAM 接入现有网络CBAM 是即插即用模块理论上可以放在任何卷积层之后。最常见的用法有两种。第一种是插入到残差块的内部。以 ResNet 的 BasicBlock 为例通常在第一个卷积和第二个卷积之后都加一个 CBAM。具体来说原始 BasicBlock 的结构是:输入 - 卷积 - BN - ReLU - 卷积 - BN - 残差相加 - ReLU。加 CBAM 的做法一般是在第二个卷积的 BN 之后、残差相加之前或者在第一个卷积之后加。我自己常用的做法是把两个 CBAM 都加上一个放在第一个卷积之后对中间特征做注意力加权一个放在残差相加之后对输出特征做注意力加权。多加一个模块带来的计算开销很小但精度提升更明显。第二种是直接接在骨干网络最后一个 stage 的输出后面也就是在进入分类头之前。这种用法适合那些不想大改现有模型的场景。因为只加了一个 CBAM而注意力机制的参数量主要来自通道注意力的 MLP对于 ResNet-50 来说最后一个 stage 输出是 2048 通道MLP 隐藏层是 128增加的参数量可以忽略不计。我有一个印象很深的实验把 CBAM 加在 YOLOv5 的 CSP 结构的输出后面mAP 提升了大约 1.2%而推理时间只增加了 3% 左右。对于工业应用来说这个性价比是非常高的。如果你也在做检测或者分割任务CBAM 是非常值得尝试的涨点模块。4. 实际应用中的参数选择与训练技巧4.1 reduction 参数到底该怎么调我见过不少人不改任何参数直接上 CBAM然后发现效果不明显就下结论说“CBAM 不行”。其实很多时候是 reduction 设置得不合适。reduction 的意义在于控制通道注意力的“信息瓶颈”宽度。reduction 越大隐藏层越窄参数越少但信息损失也越大reduction 越小隐藏层越宽表达能力越强但参数也越多。论文里是用 ImageNet 上的实验验证后选了 16但你在自己的数据集上未必是最优。我给一个比较保守的调参思路先固定其他条件把 reduction 分别设成 4、8、16、32 跑四组实验对比验证集精度和参数量变化。如果精度随着 reduction 减小而持续上升说明模型还没到过拟合状态可以用更小的 reduction如果 reduction 从 16 降到 8 精度没有明显变化就保持 16因为小的 reduction 带来更多参数训练成本和过拟合风险都会增加。通道数很少的网络要特别注意。例如 MobileNet 的最后一层通道只有 1280如果按 reduction16 算隐藏层是 80问题不大但某些轻量级网络第一层只有 32 个通道隐藏层就只剩 2 个这个瓶颈太窄了基本学不到什么东西建议这种情况下把 reduction 调成 4 甚至不用缩减。4.2 空间注意力卷积核大小的选择空间注意力模块中间的卷积核大小也是可以调的。7x7 是默认配置但不是所有场景都合适。一个判断标准是特征图的分辨率。在图像分类任务里骨干网络最后一个 stage 的特征图通常是 7x7此时 7x7 卷积的感受野已经覆盖了整个特征图空间注意力退化为“全局权重”每个位置能被区分开吗能但区分度有限。而在检测任务里neck 部分的特征图往往是 40x40 或者 80x807x7 卷积的感受野只占局部注意力更关注局部显著区域。我做过一个对比实验在目标检测任务里把 7x7 换成 3x3mAP 下降了 0.4%但在图像分类任务里3x3 反而比 7x7 高了 0.2%。所以结论是对高分辨率特征图大的卷积核更好对低分辨率特征图过大的卷积核不一定有优势。你要是有时间可以把这个参数也纳入调参范围代价很低就是一次卷积替换。4.3 位置插入的策略与经验CBAM 不是加的越多越好。我见过有同学在每个卷积后面都加 CBAM结果模型训练得很慢精度反而没有提升。分享我踩过坑之后的经验。第一浅层特征图分辨率大加 CBAM 的计算开销也大。以 ResNet-50 为例第一个 stage 的特征图是 56x56如果在这里加 CBAM空间注意力模块的 7x7 卷积要在 56x56 的图上计算计算量远大于在 7x7 的最后一层上加。但浅层注意力对最终精度的贡献往往没有深层大所以我一般只在 stage 3 之后的特征图上加 CBAM前面的 stage 不动。第二残差结构里加 CBAM 的位置很讲究。如果放在残差分支内部即卷积和激活之间它只对残差分支做加权如果放在残差相加之后它会同时影响主分支和残差分支的融合结果。我的使用习惯是在残差相加之后加一个 CBAM相当于对“融合后的特征”做了一次重标定效果更稳定。第三如果模型用于小目标检测尽量在浅层特征图上也加 CBAM。小目标的信息主要存在于浅层高分辨率特征图里只在深层加注意力相当于对小目标区域完全不关心。我做过一个无人机视角的小目标检测项目FPN 三路输出都加了 CBAM小目标的 AP 提升了接近 2%。4.4 训练技巧初始化、学习率和正则化CBAM 对训练策略比一般模块更敏感这一点论文里没提但实践中很关键。首先Sigmoid 输出接近 0.5 时注意力权重对所有输入都差不多相当于模块在初始阶段接近恒等映射不会干扰原网络的训练。这个特性很好也就是说你加 CBAM 之后不需要重新设计训练策略可以沿用原网络的超参数。但如果你的任务数据量大、训练轮次长可以考虑在前 5 个 epoch 用一个较大的学习率预热让注意力模块更快进入状态。其次要注意过拟合。CBAM 的参数量本身不大但它对通道和空间位置做了显式的重标定容易让模型过度依赖某些特征在小数据集上表现尤为明显。我处理这种问题的经验是给 CBAM 后面的全连接层适当增加 dropout或者把 reduction 调大一点增加信息瓶颈宽度让模型学到的注意力分布更平滑。再提一个很多人不知道的技巧推理时可以把注意力权重可视化出来。做法很简单在 forward 里把 channel_attention 和 spatial_attention 的返回值保存下来用热力图的方式叠加到输入图像上能非常直观地看到模型在关注什么。它不只是涨点工具也是一个很好的诊断工具。5. 常见问题与排查技巧实录5.1 加了 CBAM 后精度不升反降怎么办这是最常见的困惑。我的排查顺序是这样的。第一步先确认模型是否过拟合。在小数据集上加了额外模块即使参数量不大也可能引起更严重的过拟合。解决方法是加正则化、数据增强或者把 reduction 调大。第二步检查是否所有分支都加了 CBAM。如果你用的是残差网络只在残差分支加 CBAM而不在 short-cut 上加那反向传播时梯度经过乘法门时可能会被放大或抑制。正确的做法是确保残差相加后再做注意力。也可以用论文里的做法在残差分支的输出上加 CBAM这样梯度路径不会被频繁缩放。第三步考虑 CBAM 插入位置是否合理。我之前遇到过一个问题把 CBAM 加在了 Downsample 之后、BN 之前结果精度下降了 1%。后来发现是 BN 的均值方差统计被注意力改变了而 BN 层还没有适应这种变化。建议把 CBAM 加在 BN 之后这样 BN 先做了归一化注意力权重可以直接作用在分布稳定的特征上。5.2 训练显存不足怎么办CBAM 增加的计算量相对可控但如果你在网络里加了多个 CBAM显存还是会明显上涨。原因在于通道注意力的 MLP 需要保存中间层的梯度。一个有效做法是把池化后的特征图 detach 一下再进 MLP。这样 MLP 的梯度不会回传到主特征图能省不少显存。缺点是注意力权重的梯度不再影响主特征图的特征提取理论上有损精度。实际操作里detach 后的精度损失一般在 0.1% 以内但如果显存很紧张这是一个非常实用的折中方案。另外空间注意力的 7x7 卷积在分辨率大的特征图上开销不小。如果显存不足可以先用 1x1 卷积降维再用 3x3 卷积或者直接换一个更轻量的注意力模块比如 ECA。ECA 只保留通道注意力用一维卷积代替 MLP参数更少在许多任务上效果接近 CBAM。5.3 推理速度慢怎么优化CBAM 在 GPU 上的推理速度损失主要体现在两个地方一是空间注意力的 7x7 卷积二是通道注意力的两路池化。针对 7x7 卷积可以用一个 7x1 再加一个 1x7 的卷积分解来代替效果接近但计算量更小。这个技巧在 MobileNet 系列的深度可分离卷积中也是这么用的。针对两路池化如果对速度极度敏感可以只保留平均池化这一路。这样通道注意力的计算量减少一半精度损失一般在 0.2% 以内。这也是后来很多改进版注意力机制的常见配置比如很多的轻量化注意力就是去掉 max-pooling 单路计算。还有个容易被忽略的点如果在 ONNX 导出或者 TensorRT 部署时遇到算子不支持的问题检查一下你的实现里是否有 torch.max 配合 keepdim 的写法。在某些版本里这类算子会导出失败可以把 max pooling 替换成 AdaptiveMaxPool2d兼容性更好。5.4 CBAM 和 SE、ECA 到底怎么选这个问题经常有人问。我的经验是可以总结成一张简单的决策表场景推荐模块原因精度优先算力充足CBAM同时考虑通道和空间信息涨点最多移动端/嵌入式部署ECA无全连接层参数极少推理快已有 SE想小幅升级CBAM加一个空间分支即可改动小大规模分类任务CBAM (reduction16)论文验证过的最优配置小数据集/轻量网络SE 或 CBAM(reduction4)空间注意力在小数据集上容易过拟合具体到 CBAM 和 SE 的差别可以说得非常直白SE 只告诉你“哪些通道值得关注”CBAM 在“哪些通道值得关注”之外还告诉你“这些值得关注的通道里哪些空间位置更重要”。如果你的任务对空间位置敏感比如检测、分割、细粒度识别CBAM 显然更合适如果任务相对粗粒度比如普通图像分类SE 已经足够CBAM 带来的提升可能不明显。写在最后的个人经验CBAM 是我在实际项目里用得最多的注意力模块不是因为它的论文写得最华丽而是因为它“干活”确实稳。几乎在所有视觉任务里把 CBAM 加到合适的位置都能带来稳定的涨点而且改动成本极低不需要重新训练骨干网络不需要修改损失函数。后来我逐渐习惯了用“通道注意力空间注意力”双管齐下的思路去拆解新的网络结构不管是最新的 Transformer 类模型还是各种轻量化 CNN本质上都能看到类似的分工一部分注意力在管通道选择一部分在管位置选择。CBAM 作为这个思路的经典代表至今仍然是很多后续工作的 baseline理解了它再看其他注意力模块会轻松很多。如果你手里有现成的分类模型、检测模型不妨花一个晚上把 CBAM 接进去跑几个对比实验。我赌你会回来感谢我的。