☰
CBAM注意力机制:通道与空间注意力模块全面解析
2026/10/5 17:41:56 网站建设 项目流程

大概2018年那会儿,图像分类网络的性能拼到了一个阶段后,大家开始琢磨:除了把网络做得更深、更宽,还有没有别的路可以走?注意力机制就是在这个背景下被推上舞台的。CBAM,全称Convolutional Block Attention Module,卷积块注意力模块,就是其中一个不是靠"加深"而是靠"聚焦"取胜的经典方案。哪怕到今天,CBAM依然是目标检测、语义分割、图像分类里低成本提分的首选插件之一,相关热搜词像"cbam注意力机制""cbam通道、空间权重"也说明大家对它的关注度一直没降过。

这篇博文我打算把它彻底讲透:CBAM到底在做什么,两个子模块为什么这样设计,公式和PyTorch代码怎么一一对应,论文里的结论在实际项目里到底灵不灵,以及我踩过的坑。尽量不用术语砸人,用大白话把原理和实操都交代清楚。

CBAM做的事情用一句话概括就是:拿到一张中间特征图,先通过通道注意力模块告诉网络"看什么"(哪个通道重要),再通过空间注意力模块告诉网络"看哪里"(哪个位置重要),依次给特征重新分配权重。整个过程不改变特征图尺寸,只调整每个通道和每个位置的响应强度,所以可以非常方便地插入到现有CNN骨干网络的任意位置。

不管你是刚入门注意力机制的初学者,还是想在项目里通过加模块提点的工程师,又或者是想搞清楚注意力内部实现细节的研究生,这篇文章应该都能给你点参考。

1. 为什么CNN需要"注意力":从特征图的平等困境说起

1.1 卷积网络的两个天然短板

先想一个问题:一个普通的卷积层对输入特征图做了什么?它用一个共享权重的卷积核,在整张特征图上滑动计算。也就是说,对于一张H×W的特征图,卷积核默认把每个通道、每个位置的权重都一视同仁。人眼看东西不是这样,人眼扫到一张合影时,会不自觉地先盯住里面的人脸,再扫到其他区域。相比之下,原始卷积操作像是一个没有视觉重点的扫描仪,在有用和无用的信息上花一样的力气。

这就带来两个直接的短板。

第一个短板在通道维度。假设输入特征图有C个通道,不同通道往往对应着不同类型的语义模式:有的通道在响应边缘,有的通道在响应纹理,有的通道在响应某个目标部件。但是对后续层来说,它并不知道哪些通道更值得信任,也没有机制去放大那些信息量大的通道。特征图里有些通道可能基本就是噪声,但网络依然会把它们一视同仁地传给下一层。

第二个短板在空间维度。图像里的目标往往只占画面的一部分,背景、遮挡、无关物体占据了大量空间。普通卷积在计算时并不会特别关心"当前位置是不是目标区域",所以模型在做分类或者定位的时候,容易被大面积的无意义背景信息带偏。

说白了,CNN从设计之初就缺少一种"选择性重点关注"的机制。

1.2 从SE模块到CBAM:一条清晰的演进路径

说到注意力机制,肯定绕不开SE模块(Squeeze-and-Excitation Network)。SE的做法是从通道维度入手:对特征图做全局平均池化,压缩成一个通道描述向量,然后用两个全连接层学习每个通道的权重,最后把权重乘回特征图。SE注意力在2017年的ImageNet分类上刷了一波存在感,关键是用很小的计算量换来了可观的精度提升。但SE只做了通道维度的重标定,空间维度它完全不碰。

CBAM的思路比SE更进一步:既然特征图天然有"通道"和"空间"两个维度,那为什么只在一个维度上做注意力呢?两个维度都做,效果应该更好。而且CBAM的通道注意力子模块在前人的基础上也做了改进,后面细说。

从整体结构上看,CBAM是一个轻量级的即插即用模块,输入输出形状完全一致。它内部由两个子模块串联而成,先过通道注意力,再过空间注意力。论文还专门做了实验,验证了这个顺序确实比反着来效果好。这类消融结论挺重要,因为在工程中我们经常面临"模块该怎么排列"的选择,没有实验依据就只能瞎猜。

2. 通道注意力模块:先决定"看什么",再谈其他的

2.1 通道维度上到底藏着什么信息

在卷积神经网络中,每一层卷积的输出特征图可以理解成对输入图像的一组"解释视角"。有的通道负责提取水平边缘,有的通道负责提取颜色斑点,有的通道负责响应某个特定物体的部件。信息是以"通道"为单位组织的。

问题在于,并不是每个通道对当前任务都同样重要。比如在ImageNet预训练模型里,有些通道对纹理和背景高度敏感,但在你的数据集上,这些通道可能帮倒忙。通道注意力模块的作用,就是自适应地学习一组权重,每个通道乘一个系数,重要的通道放大,不重要的通道压低。这相当于给特征图加了一个"通道级别的音量调节旋钮"。

2.2 最大池化与平均池化为什么要同时用

SE模块做通道注意力时,用的压缩方式是全局平均池化。平均池化的特点是"均匀地考虑所有像素",它反映的是每个通道的总体统计信息。但问题在于,平均池化容易忽略那些只在小区域内激活、但语义很强的特征。

CBAM的改进点就在这里:它同时使用全局平均池化和全局最大池化来压缩空间信息。最大池化提取的是每个通道最强烈的那个响应值,这个值往往对应着最具辨识度的特征。两者结合,既能感知整体分布,又能抓住显著峰值。论文里的实验也证实了,同时使用两种池化比只用其中任何一种效果都好。

用一个生活化的类比来说:评价一篇文章的价值时,平均池化是看整体平均质量,最大池化是看最精彩那个段落。两个维度都有参考价值,加在一起会过滤掉单一视角的偏颇。

2.3 通道注意力的完整计算路径

具体流程可以拆成四步:

输入特征图 F,形状是 C×H×W。

  1. 对F在空间维度(H×W)做全局平均池化,得到一个长度为C的向量,记为 F_avg。
  2. 对F在空间维度做全局最大池化,得到另一个长度为C的向量,记为 F_max。
  3. 把 F_avg 和 F_max 分别送进同一个共享的MLP。这个MLP是"窄腰"结构:第一层把C维压缩到C/r维,中间接ReLU激活,第二层再恢复到C维。压缩比r论文中默认取16。MLP输出两个C维向量,然后逐元素相加。
  4. 相加结果过Sigmoid函数,得到通道注意力权重 Mc,形状为C×1×1。把 Mc 乘到原始特征图的每个通道上,得到通道重标定后的特征图 F'。

公式写出来是:

Mc(F) = σ(MLP(AvgPool(F)) + MLP(MaxPool(F)))

其中σ表示Sigmoid,MLP的两个层共享参数。这里"共享"的意思是两个池化结果走的是同一套权重,不是各自一套,这样既控制了参数量,也让两个分支在统一的标准下融合。

为什么用"窄腰"MLP而不直接学一个C维权重向量?一方面是为了控制参数量,C/r这个瓶颈结构大大减少了要学习的参数量;另一方面,这个瓶颈结构相当于把通道信息先压缩再展开,迫使网络学到通道之间更本质的关联模式。

3. 空间注意力模块:补齐"看哪里"这块关键拼图

3.1 空间注意力补上了通道注意力的盲区

通道注意力解决了"不同通道哪个更值得信"的问题,但完全没有触碰另一个维度:"特征图不同空间位置哪个更重要"。举个典型的场景:一副图像主体是一只猫,周围是草地。经过卷积后,猫的位置和草地的位置在不同空间位置上都有各自的特征响应。如果只做通道注意力,等于给"猫类通道"整体加了权重,但背景区域上其他通道的响应依然可能干扰后续分类器。这时就需要空间注意力登场:在H×W平面上生成一张权重图,目标是区域的权重高,背景区域的权重低,然后让网络把注意力花在目标区域上。

空间注意力的输入,是经过通道注意力重标定后的特征图 F'。它对F'在通道维度上做两次池化,一次取平均值,一次取最大值,得到两张 H×W 的二维特征图。这两张图,一张代表"所有通道在该位置的平均响应",一张代表"所有通道在该位置的最强响应"。然后把它们沿通道方向拼接起来,输入到一个卷积层中,输出一张 H×W 的空间权重图,经过Sigmoid归一化后乘回原特征图。

公式如下:

Ms(F) = σ(f_7×7([AvgPool(F'); MaxPool(F')]))

这里的 f_7×7 代表一个7×7的卷积层,输入是拼接后的2×H×W特征,输出是1×H×W。需要特别提醒的是,这里的平均池化和最大池化都是在通道维度上做的,和通道注意力里的空间维度池化刚好方向相反,别绕晕。

3.2 两个池化结果拼接后为什么用7×7卷积

空间注意力模块里有个容易被忽略但很关键的细节:为什么用7×7的大卷积核,而不是常见的1×1或3×3?

原因是空间注意力本质上是想判断"哪个区域重要",而区域重要性通常需要借助周围上下文信息来确认。一个孤零零的像素点往前看,很难判断它属于前景还是背景;但如果能看到这个像素周围一大片区域的模式,判断就会准确很多。7×7卷积的每个输出点都聚合了周边7×7范围内的信息,感受野更大,能利用更丰富的空间上下文。论文里做了对比实验,kernel size设为7时效果最优,这背后就是感受野的功劳。

为什么把平均池化和最大池化拼接起来?因为这两个信息是互补的。平均池化图反映每个位置的整体激活强度,边界模糊但稳定;最大池化图则保留每个位置最显著的那个通道响应,能捕捉到更锐利的目标边缘。两张图拼在一起,卷积层等于同时看到了"整体热度"和"最大热点"。信息越互补,学出来的空间权重越精准。

空间注意力输出一张和原特征图同高同宽的权重图,会让特征图在每个局部位置都乘一个系数。经过这一层,特征图的前景区域响应被增强,背景区域被压制,整个网络往后的计算就更有针对性了。

4. 从公式到张量:PyTorch实现CBAM的每一行代码都讲清楚

4.1 通道注意力子模块的代码实现

本节可以边读代码边对照上面提到的公式。我直接用PyTorch写一个简洁版。

import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction=16): super(ChannelAttention, self).__init__() hidden = max(in_channels // reduction, 1) self.mlp = nn.Sequential( nn.Linear(in_channels, hidden, bias=False), nn.ReLU(inplace=True), nn.Linear(hidden, in_channels, bias=False), ) self.sigmoid = nn.Sigmoid() def forward(self, x): # x: (B, C, H, W) b, c, h, w = x.size() avg_pool = x.mean(dim=(2, 3)) # (B, C) max_pool = x.max(dim=2).values.max(dim=2).values # (B, C) avg_out = self.mlp(avg_pool) # (B, C) max_out = self.mlp(max_pool) # (B, C) weight = self.sigmoid(avg_out + max_out) # (B, C) weight = weight.view(b, c, 1, 1) # (B, C, 1, 1) return x * weight

代码里有两个细节值得展开。

第一,x.max(dim=2).values.max(dim=2).values是为了在H和W两个维度上连续取最大值,得到 (B, C) 的向量。也可以用F.adaptive_max_pool2d(x, output_size=1)实现,后者更贴近论文的原版写法,但结果一样。

第二,MLP内部激活函数用的是ReLU,并且只在中间层加。这是有意为之,如果两头都加激活函数,会限制最后权重表达的连续范围。Sigmoid放在最后的目的是把权重映射到0到1之间,这样乘回去就是"软性"缩放,而不是硬性丢弃某个通道。

4.2 空间注意力子模块的代码实现

空间注意力模块写完也不长。

class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super(SpatialAttention, self).__init__() assert kernel_size in (3, 5, 7), "kernel size must be 3, 5, or 7" padding = kernel_size // 2 self.conv = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): # x: (B, C, H, W) avg_pool = torch.mean(x, dim=1, keepdim=True) # (B, 1, H, W) max_pool, _ = torch.max(x, dim=1, keepdim=True) # (B, 1, H, W) concat = torch.cat([avg_pool, max_pool], dim=1) # (B, 2, H, W) weight = self.sigmoid(self.conv(concat)) # (B, 1, H, W) return x * weight

这里最需要注意的是torch.max(x, dim=1, keepdim=True)返回的是一个tuple,第二个元素是最大值对应的索引,我们用不到,直接用占位符_丢掉。拼在一起的时刻是在通道维度上torch.cat([avg_pool, max_pool], dim=1),得到的是2个通道的二维特征图,然后喂给单层卷积。

空间注意力子模块里没有用ReLU,只有一个卷积加一个Sigmoid。因为这里只是一次空间模式的提取,不同于通道注意力那样的特征变换流程,不需要复杂的非线性映射。在测试时,7×7卷积的kernel size是可以调的,我用3×3和5×5试过,效果确实不如7×7,和论文结论一致。

4.3 把两个模块串起来,并嵌入ResNet

串起来更简单,定义一个CBAM类,forward里先过通道注意力再过空间注意力就完事了。

class CBAM(nn.Module): def __init__(self, in_channels, reduction=16, spatial_kernel_size=7): super(CBAM, self).__init__() self.channel_attention = ChannelAttention(in_channels, reduction) self.spatial_attention = SpatialAttention(spatial_kernel_size) def forward(self, x): x = self.channel_attention(x) x = self.spatial_attention(x) return x

使用这个模块的时候,我习惯在ResNet的BasicBlock或者Bottleneck的残差相加之前插入。举个例子,在BasicBlock里修改:

class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1, downsample=None): super(BasicBlock, self).__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.downsample = downsample self.cbam = CBAM(out_channels) 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) if self.downsample is not None: identity = self.downsample(x) out = self.cbam(out) out += identity out = self.relu(out) return out

为什么放在残差相加之前而不是之后?我的理解是,残差分支经过CBAM重标定后,特征质量更高,和恒等映射相加时能把有效信号带出去;如果放在相加之后,相当于对已经叠加了原始信号的结果再做注意力,此时直接路径的信息会被权重改变,反而可能破坏梯度的恒等性质。PyTorch官方视觉库的一些实现也采用这个位置,实践下来效果更稳定。

5. 论文里的结论和我在复现时观察到的现象

5.1 消融实验最有价值的信息

CBAM原论文(ECCV 2018)在ImageNet-1K上做了一系列消融实验,这些结论对我们自己选型很有参考价值。

先看单独使用通道注意力或空间注意力的情况。单独加通道注意力(相当于SE的改良版)能提升模型准确率,单独加空间注意力也能提升,但幅度略小。这说明两个维度各自都在提供有效信息,但又都不全面。当两个模块串联一起用时,提升最大,超过了任意单一模块。

再看子模块顺序。论文比较了"通道注意力在前、空间注意力在后"和"空间注意力在前、通道注意力在后"两种排列,结论是前者更优。我的理解是,通道注意力相当于先做一次全局性的通道选择,把有用的特征通道筛选出来,之后再在空间上精确定位,逻辑上更顺;反过来,先做空间定位时,信息还没有经过通道筛选,空间权重容易受到无效通道的干扰。

CBAM和SE的对比更有意思。CBAM的通道注意力本身就是在SE基础上的改进,因此公平对比应该是SE和CBAM整体对比。在相同骨干网络和训练设置下,CBAM的top-1准确率提升比SE高出不少。论文报告里ResNet-50加CBAM之后在ImageNet上的top-1错误率大约降低了1.5个百分点,这个幅度在图像分类任务里已经相当可观。

5.2 计算开销与性能提升的性价比

我复现CBAM时最关心的就是它到底增加了多少计算负担。直接说结论:几乎可以忽略不计。

在ResNet-50上,单个BasicBlock加一个CBAM,增加的参数量主要来自通道注意力的MLP和空间注意力的7×7卷积。假设输入是256个通道,reduction取16,MLP两个Linear层的参数量大约是:

  • 第一层:256×16 + 16 = 4112
  • 第二层:16×256 + 256 = 4352 空间注意力7×7卷积:2×7×7×1 + 1 = 99 合计每个模块增加的参数量大概在8500左右,相比原本一个BasicBlock动辄几十万的参数量,占比非常小。

FLOPs层面,两个子模块的池化和逐元素乘法计算量都很低,7×7卷积作用在2通道的特征上,计算量也完全可以忽略。实际在单张GPU上测试,CBAM的推理耗时增加大约在1%-3%之间,具体取决于特征图大小和通道数。

需要提醒的是,计算开销低不等于它不需要调参。论文里的reduction默认16,但在不同任务和数据规模上,我试过8和32,结果差异还挺明显,后面专门讲。

6. 接入真实项目前,你需要知道的调参细节和坑

6.1 CBAM该插在哪一层

这是我在实际项目中踩得最久的一个坑。CBAM虽然号称即插即用,但插的位置不同,效果天差地别。

早期我习惯把CBAM加在骨干网络每个stage的最后一个Block之后,认为这样可以让注意力作用于该stage的输出。后来对比实验发现,这样做提升有限,甚至在深层stage还有负优化。后来改成在每个BasicBlock的残差分支拼接前插入(就是上面代码里的位置),效果才稳定起来。原因是:残差分支经过CBAM重标定后再与恒等映射相加,既保留原始信息,又突出重点特征,梯度流动也更顺。

如果是检测或分割任务,我还会在FPN融合后的特征图上再接一层CBAM。FPN从不同层聚合了多尺度特征,但每个尺度的重要性并不一致,这时候加一个CBAM做特征重标定,通常能带来1-2个点的mAP提升。这一点在项目里屡试不爽。

6.2 训练细节与参数调整

reduction值的设置要结合网络宽度来定。对大网络(如ResNet-50及以上),reduction取16是安全选择;对轻量网络(如MobileNet、ShuffleNet),中间隐藏层本身就很窄,reduction还取16的话,MLP的隐藏层可能被压得太小,学不到有效的通道关系,我建议取8甚至4,保证瓶颈层至少有几个神经元。

学习率上也有一点需要注意。加CBAM之后,模型总收敛速度会比原来稍慢一些,这不是坏事,是注意力模块在训练早期还在摸索哪些通道和区域重要。但如果你发现loss下降明显变慢,可以考虑给CBAM模块单独设一个更高的学习率倍数,比如主干的1.2到1.5倍。我在一个检测项目里这么干过,收敛速度和不加模块时基本持平,最终精度还高了0.8个点。

另一个容易被忽略的是权重初始化。PyTorch的Linear和Conv2d默认初始化方式对CBAM基本够用,但如果你的任务数据量很小,建议把CBAM里的Sigmoid分支初始化为偏向"不改变特征"的状态,比如让MLP最后一层的权重和偏置初始化为0。这样模块在训练初期基本是恒等映射,再逐步学习注意力,可以避免小数据下训练不稳定。实现也不难,自定义初始化函数,对最后一层执行nn.init.zeros_()就行。

6.3 易踩的坑集合

整理几个我遇到过的实际问题,给大家省点时间。

第一,输入输出形状不匹配。CBAM要求输入是四维张量(B, C, H, W)。有些分类网络在global pooling之前接入的是序列特征或者已经拉平的特征,这时候要先reshape再进CBAM,否则形状就崩了。

第二,空间注意力里的max操作在C++部署和TensorRT转换时偶尔会出兼容问题。PyTorch转ONNX时,torch.max(x, dim=1, keepdim=True)是支持导出的,但某些版本的ONNX Runtime会报奇怪的错误。稳妥做法是部署时用torch.ops.aten.max.dim没转过的话,直接在导出前把这一层换成平均池化,或者在推理脚本里单独用传统实现替代。我在嵌入式设备上踩到过一次,最后是在导出模型时改用average pool替代max pool,精度损失很小,部署难题倒是彻底没了。

第三,数据增强策略变了以后,CBAM的效果可能被放大或缩小。CBAM对目标位置比较敏感,如果训练数据里目标经常在中心区域,空间注意力学到的权重图会偏向中心,测试时目标一旦偏到角落,这种偏好就会拖后腿。最简单的规避方式是训练时加随机裁剪和随机缩放,让目标位置分布更均匀。

第四,BatchNorm和CBAM协同的问题。有些复现代码在CBAM两个子模块里也加BN层,我觉得不是好主意。CBAM内部本身是做特征重标定的,加BN会把已经过Sigmoid压到0-1的权重再归一化一次,反而破坏了注意力权重的尺度,影响收敛。我实际对比过,内部不加BN的版本精度更高,训练更稳。

是以我在实际项目中的体会来说,CBAM是一个性价比很高的注意力插件,但它不是"加了就涨点"的万能药。真正好看的效果来自合理的插入位置、合适的reduction、稳妥的训练设置,以及最重要的——在你自己数据集上做几组消融实验,而不是照搬论文的参数。理解了它背后的"通道筛选+空间聚焦"的设计逻辑,你再去看网上各种CBAM的变体,也就一目了然了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询