ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SENet通道注意力机制解析:SE模块原理、消融与工程实践

SENet通道注意力机制解析:SE模块原理、消融与工程实践

很多人第一次翻到 SENet 的源码时都会愣一下:核心模块拢共三十来行,一个全局池化、两个全连接、一次逐通道相乘,连个像样的循环都没有。就这么点东西,拿了当年图像分类竞赛的冠军,还把 ResNet-50 的 top-5 错误率从 7.48% 压到 6.62%。这事儿搁谁身上第一反应都是"是不是有什么魔法没写出来"。真正的答案藏在设计动机里:卷积核在空间上是局部的,在通道上是"民主"的,它把所有输入通道一视同仁地加权求和,可实际上一张特征图里不同通道承载的信息价值天差地别。SE(Squeeze-and-Excitation)模块干的事,就是给每个通道打一个 0 到 1 的分,再按分数加权回去。这篇文章把论文里那些一笔带过的推导、消融表里那些容易被误读的数字、以及我自己复现时踩过的坑,一条条拆开讲。适合已经能跑通 ResNet、想搞清楚通道注意力到底怎么回事的读者,也适合准备把它塞进自己骨干网络但不确定怎么改才不掉点的同学。

1. 三十行代码拿下冠军:SE模块到底改动了什么

要理解 SE,先得看清它没有改什么。它没有增加网络深度,没有引入多尺度分支,没有改卷积核大小,甚至连一个卷积层都没动。它动的是一个更底层的东西:特征响应在通道维度上的重新分配。

标准卷积层的工作方式是,输出通道 c 的响应由所有输入通道的局部窗口加权求和得到,这个加权系数由卷积核学习,训练完就固定了。问题在于,这个"固定"是针对整张图、整个数据集平均而言最优的,但具体到某一张输入图片,不同通道的重要性是变化的。一张以狗为主体的图,纹理通道和边缘通道的贡献比例,跟一张风景图完全不同。传统卷积没有能力根据输入内容动态调整这个比例。

SE 模块的补法很直接,分三步走:

  • Squeeze:把每个通道的 H×W 空间响应压缩成一个标量,得到一个长度等于通道数的向量,论文里叫 channel descriptor。这一步的作用是把"空间上分散的响应"聚合成"通道级别的全局统计量"。
  • Excitation:用一个小的门控网络学习这 C 个标量之间的非线性关系,输出 C 个 0 到 1 之间的权重。
  • Scale:把权重逐通道乘回原始特征图。

写成公式就是,输入 X ∈ R^(H×W×C),先做空间压缩得到 z ∈ R^C,再经过门控:

s = F_ex(z, W) = σ(W₂ · δ(W₁ · z))

其中 δ 是 ReLU,σ 是 Sigmoid,W₁ ∈ R^(C/r × C),W₂ ∈ R^(C × C/r),r 是降维比例。最后输出 X̃ = s · X,这里的乘是逐通道广播。

我特别喜欢用团队协作来类比这三步。假设一个项目组有 C 个人,每人负责一部分工作(通道),最终成果是所有人产出的加权汇总。传统卷积相当于事先定死每个人的权重,不管这次任务是什么。SE 的做法是:先让每个人汇报一下自己这块的整体完成度(Squeeze),然后开个短会把所有汇报综合评估一下,给每个人重新定一个贡献系数(Excitation),最后按新系数重新分配成果(Scale)。短会本身很便宜,但它让最终分配从"静态"变成了"看菜下饭"。

值得注意的是,SE 和 ResNet、Inception 这类结构改进是正交的,不是替代关系。ResNet 解决的是深层网络的梯度通路问题,Inception 解决的是多尺度感受野问题,SE 解决的是通道信息筛选问题。论文里正是把它分别插进 ResNet、Inception、MobileNet 里验证,才会出现 SE-ResNet-50、SE-Inception-ResNet-v2 这些组合。这一点很关键,意味着你可以把它当作一个通用插件,往自己现有的骨干网络里塞,而不需要重构整个架构。

还有一个容易被忽略的点:SE 建立的是通道之间的依赖关系,而不是空间位置之间的关系。它对每个通道只产出一个标量,整张图共享同一个权重。这跟后来的空间注意力机制(对每个像素位置单独加权)是两条路。理解这个区别,就不会在看到"注意力"三个字时,把它和自注意力、Non-local 那些东西混为一谈。

2. Squeeze那一步:全局平均池化为什么不能被替换

Squeeze 做的事情一句话就能说完:对每个通道做全局平均池化,把 H×W 的特征图压成一个数。但为什么偏偏是平均池化,论文在这一步其实做了消融,结论是不能随便换。

先看为什么需要"全局"。卷积层的感受野是有限的,一个 3×3 卷积在深层的有效感受野虽然会扩大,但仍然局限于局部区域。如果 SE 模块只用一个局部区域的统计量来做门控,那它学到的就是"这个通道在这块小区域上重不重要",而不是"这个通道对整张图重不重要"。用全局池化能一次性拿到覆盖全图的统计量,这是最省事也最直接的获取全局信息的方式。对比之下,要想让卷积自己获得全图感受野,得堆很多层,成本和难度都高得多。

再看为什么是"平均"而不是"最大"。论文的消融实验里对比了平均池化和最大池化两种 squeeze 方式,平均池化在 ImageNet 上的表现更好。我的理解是这样的:平均池化保留的是通道的整体响应强度,它回答的是"这个通道在整张图上平均激活了多少";而最大池化保留的是最强响应点,回答的是"这个通道在某个位置激活得最猛"。判断一个通道是否重要,整体强度是更稳健的指标。最大池化对噪声和局部异常响应过于敏感——哪怕整张图只有一个小角落激活很强,也会被当成重要通道,这在真实数据里很容易误判。

不过这也不是绝对的。在一些目标很小、背景占比极大的检测任务里,最大池化反而可能更有优势,因为它对小目标的强响应更敏感。我个人的经验是,分类和通用骨干网络直接用平均池化,不要折腾;如果是密集预测类任务且目标占比很小,可以两种都试一下,差异一般在千分位。但这种微调属于收益很小、验证成本很高的操作,优先级应该排在很后面。

实际写代码的时候有个坑得提前说。早期很多复现代码里写的是nn.AvgPool2d(7)或者nn.AvgPool2d(kernel_size=7, stride=1),这是把输入尺寸硬编码成 224 了。一旦换成 320×320 或者做多尺度训练,就会直接报维度错误。正确的写法是nn.AdaptiveAvgPool2d(1),不管输入多大都输出 1×1,省心。

import torch import torch.nn as nn class SEModule(nn.Module): def __init__(self, channels, reduction=16): super().__init__() hidden = max(1, channels // reduction) self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channels, hidden, bias=False), nn.ReLU(inplace=True), nn.Linear(hidden, channels, bias=False), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y

这段代码不到二十行,但里面藏着后面要讲的三个坑:max(1, ...)的保护、bias=False的选择、以及view带来的部署开销。

3. Excitation的瓶颈设计:r=16、ReLU和Sigmoid各自的分工

Excitation 是整个模块唯一有参数的部分,也是设计上最值得琢磨的地方。它要完成的任务是:输入 C 个标量,输出 C 个 0 到 1 的门控值。最直接的做法是一个 C×C 的全连接层,但参数量是 C²,在通道数 2048 的层上就是四百多万参数,太贵了。论文的方案是加一个瓶颈:先降维到 C/r,过 ReLU,再升维回 C。

3.1 瓶颈的两个作用,不只是省参数

很多人以为加瓶颈纯粹是为了控制参数量,其实还有第二层考虑。降维把 C 维压到 C/r 维,相当于强迫网络用一个低维子空间来描述通道之间的关系,这本身是一种结构正则化。如果没有这个瓶颈,直接学一个稠密的 C×C 矩阵,在小数据集上非常容易过拟合——通道关系矩阵会记住训练集的特定模式,泛化性反而不如低秩近似。

参数量从 C² 降到 2C²/r,以 r=16、C=2048 计算,从 419 万降到 52 万,差了八倍。这个降幅是实打实的,而性能几乎不掉,说明通道关系矩阵本身就存在大量冗余,低秩假设是成立的。

3.2 r 取多少:论文消融给出的区间

论文对降维比例做了系统消融,测试了 r = 2、4、8、16、32 这几档。结论大致是:r 从 2 增大到 8 时性能上升,在 16 附近达到最好,再继续增大到 32 时性能开始回落。这个 U 形曲线的两侧原因是相反的。

r 太小的那一侧(比如 r=2,相当于几乎不降维),参数多、模型容量大,但增益反而不如 r=16。这说明问题不在于表达力不够,而在于过拟合。r=2 时门控网络有足够的能力去记忆训练集的噪声模式,结果在验证集上吃亏。

r 太大的那一侧(比如 r=32,压缩到原来三十二分之一),信息损失就明显了。原始 C 个通道的描述符被压到很低的维度,一些细微但有用的通道组合关系在压缩过程中丢掉了,门控的质量下降。

工程实践上,r=16 是个非常安全的默认值。我试过在不同规模的骨干网络上用 16,基本没有翻车过。如果通道数本来就很小(比如 64 通道),64 // 16 = 4,这个隐藏层维度就有点太小了,可以考虑把 r 调成 8 或者 4,让隐藏层保持在 8 到 16 维以上。这也是为什么代码里要写max(1, channels // reduction),避免通道数小于 r 时隐藏层变成 0 导致崩溃。

3.3 Sigmoid 而不是 Softmax,这个选择有明确理由

这是我认为整个模块设计里最容易被忽略、也最能体现作者功力的一个细节。为什么门控用的是 Sigmoid 而不是 Softmax?毕竟 Softmax 在注意力机制里更常见。

区别在于归一化的语义。Softmax 会强制所有 C 个权重之和等于 1,这隐含了一个前提:这些通道在竞争同一个资源,此消彼长。如果通道 A 的权重涨了,其他通道的权重就必然被压下去。可实际情况是,通道之间的关系并不总是竞争的,有些通道可以同时重要,也有些场景下整体响应都该被抑制。

Sigmoid 是逐元素独立的,每个通道的权重只取决于自己,不受其他通道的约束。这允许所有通道的权重同时接近 1(所有通道都重要),也允许同时接近 0(整体响应都该弱化)。对于"给每个通道独立评估重要性"这个任务,Sigmoid 的语义才是对的。

另外一个现实原因是,Sigmoid 的输出范围就是 (0,1),天然适合做乘法门控,不会出现负数翻转特征符号的问题。

3.4 为什么是 ReLU 而不是别的激活

论文也对比了 ReLU、Tanh、Sigmoid 作为中间激活函数的效果,ReLU 略优。差别其实不大,但 ReLU 在瓶颈层上有额外的好处:它把负的通道组合关系直接截断为零,相当于做了一次稀疏化,让低维表示更干净。Tanh 是零中心的、平滑的,表达能力更强,但在这么小的瓶颈层上,更强的表达力未必是好事,容易加剧过拟合。

至于为什么两个全连接层都用bias=False,这是个小优化但不是必须的。全局池化后的输入本身已经是零中心的统计量,BatchNorm 不在这个分支里,加 bias 的作用有限,去掉能省一点点参数。我见过的实现里有加有不加的,性能差异在噪声范围内,不要在这上面纠结。

4. 插入位置比模块本身更影响效果:残差分支内外的一次对照

模块设计好之后,接下来的问题是在网络里插在哪。这个问题的答案比很多人想的更重要,论文专门做了对照实验,结论是把 SE 放在残差分支内部、与 shortcut 相加之前,效果明显好于放在相加之后。

先明确两种插法。以标准的 ResNet bottleneck 块为例,主路径是 1×1 卷积降维 → 3×3 卷积 → 1×1 卷积升维 → BatchNorm,然后是残差相加。第一种插法是把 SE 接在残差相加之后,也就是整个块的输出再过一个 SE;第二种是接在最后一个 1×1 卷积和 BatchNorm 之后、残差相加之前,只调制主路径。

论文的实验结果显示第二种更好。原因我分析有两条。

第一条是恒等映射的纯净性。残差连接之所以能让极深的网络训练得动,核心在于它为梯度提供了一条畅通的直通路径。如果 SE 放在相加之后,那么恒等映射的信号也要经过门控乘法,权重小于 1 时会把这个直通信号一起衰减掉。在几十上百层的堆叠下,这种逐层的衰减累积起来会损害梯度传播,尤其是在训练初期门控还没学好的时候。放在分支内部,shortcut 完全不受影响,直通路径保持干净。

第二条是门控的对象更准确。放在相加之后,SE 看到的是"主路径输出 + 恒等映射"的混合特征,它学到的门控要同时照顾两个来源,语义就不够聚焦。放在分支内部,SE 只看主路径的输出,门控的含义清晰得多——它评估的是"这一层新提取的特征里,哪些通道值得保留"。

落到代码上,标准的 SE-ResNet bottleneck 长这样:

class SEBottleneck(nn.Module): expansion = 4 def __init__(self, inplanes, planes, stride=1, reduction=16): super().__init__() width = planes * self.expansion self.conv1 = nn.Conv2d(inplanes, planes, 1, bias=False) self.bn1 = nn.BatchNorm2d(planes) self.conv2 = nn.Conv2d(planes, planes, 3, stride, 1, bias=False) self.bn2 = nn.BatchNorm2d(planes) self.conv3 = nn.Conv2d(planes, width, 1, bias=False) self.bn3 = nn.BatchNorm2d(width) self.relu = nn.ReLU(inplace=True) self.se = SEModule(width, reduction) self.downsample = None if stride != 1 or inplanes != width: self.downsample = nn.Sequential( nn.Conv2d(inplanes, width, 1, stride, bias=False), nn.BatchNorm2d(width) ) def forward(self, x): identity = x if self.downsample is None else self.downsample(x) out = self.relu(self.bn1(self.conv1(x))) out = self.relu(self.bn2(self.conv2(out))) out = self.bn3(self.conv3(out)) out = self.se(out) # 关键:在相加之前 out = self.relu(out + identity) return out

注意最后一行self.relu(out + identity),ReLU 是在相加之后才做的,这是 ResNet v1 的原始顺序。有些实现用的是 pre-activation 版本(BN-ReLU-Conv 顺序),SE 的位置也要相应调整,但原则不变:紧贴残差分支的末端,在合并之前。

提示:如果把 SE 加到一个预训练好的模型上做微调,初始阶段门控值应该在 1 附近,否则会立刻破坏原有的特征分布。默认随机初始化下 Sigmoid 输出在 0.5 左右,相当于把所有通道的响应砍掉一半,虽然 BN 之后的层能适应,但训练初期的震荡会明显变大。把 W₂ 初始化为零可以让初始门控等于 0.5,如果想让初始门控更接近 1,可以把 W₂ 的偏置设成一个正数(比如 2 到 4),这样 Sigmoid 的输出会落在 0.88 到 0.98 之间,对预训练权重的扰动小得多。这个小技巧论文里没写,但在实际微调场景里很有用。

还有一点:SE 应该放在 BatchNorm 之后,而不是之前。BN 会重新归一化特征分布,如果 SE 在 BN 之前,它学到的门控要面对一个未归一化的、尺度随训练变化的输入,收敛会变慢。放在 BN 之后就稳定得多,和卷积-BN-激活的标准顺序是对齐的。

5. 把参数和FLOPs算清楚:SE几乎"免费"的代价从哪来

论文里那句"只增加少量参数和极小计算开销"经常被引用,但具体是多少,自己算一遍才算真的理解。这里我把账摊开。

单个 SE 模块的额外参数量是两个全连接层:

params = C × (C/r) + (C/r) × C = 2C² / r

以 C=2048、r=16 计算,结果是 2 × 2048² / 16 = 524,288,约 0.52M。

ResNet-50 有四个 stage,通道数分别是 256、512、1024、2048,block 数分别是 3、4、6、3。逐 stage 累加:

Stage输出通道 CBlock 数单块 SE 参数小计
stage225638,19224,576
stage3512432,768131,072
stage410246131,072786,432
stage520483524,2881,572,864
合计---2,514,944

约 2.51M,和论文里 SE-ResNet-50 相对 ResNet-50 的参数增量(25.56M → 28.09M,差 2.53M)基本吻合。相对增幅约 10%。

计算量方面,SE 分支的乘加操作主要来自两个全连接层,输入只有 1 个空间位置(池化之后),所以每个样本的额外 MACs 就是 2C²/r,相对于卷积层动辄 H×W×C×C' 的规模,可以忽略。全局池化本身的代价是 H×W×C 次加法,也很小。论文给出的 GFLOPs 从 3.86 增加到 3.87,增幅不到 0.3%。

这个性价比是 SE 最有说服力的地方。10% 的参数换来接近 1 个百分点的 top-5 准确率提升,在 ImageNet 这种已经卷到极致的任务上,属于相当划算的交易。对比一下,要把 ResNet-50 换到 ResNet-101,参数增加约 75%,top-5 提升也就 1 个百分点左右。

不过这个账有个前提:SE 的额外参数集中在 stage5,那里通道数最大。如果把它加在浅层(通道数 64、128 的 stage),参数增量小得可以忽略,但增益也相应有限。论文的消融里对比过"只在某些 stage 加"和"所有 stage 都加",结论是全加效果最好。这也合理,因为浅层的通道选择同样有价值,只是每个模块的绝对参数量小。

注意:如果你的部署环境对参数量卡得很死,一个折中方案是只在后两个 stage 加 SE,参数量增加能控制在 3% 以内,性能损失通常在 0.2 个百分点以内。这个取舍在移动端模型上比较常见。

6. 论文消融表的正确读法:哪些结论可以直接抄,哪些要打折扣

论文里的消融实验做得比较扎实,但表格里的数字如果不结合实验设置去读,很容易得出错误结论。我挑几个关键结论说说怎么用。

6.1 主干网络对比结果

先看主实验。论文在 ImageNet 上的分类结果大致是:

模型Top-1 错误率Top-5 错误率相对基线提升(top-5)
ResNet-5024.567.48-
SE-ResNet-5023.296.620.86
ResNet-10123.206.52-
SE-ResNet-10122.386.070.45
ResNet-15223.006.09-
SE-ResNet-15221.845.500.59

有个现象值得注意:SE-ResNet-50(top-5 6.62)已经超过了未加 SE 的 ResNet-101(6.52 接近,top-1 上 23.29 对 23.20 也基本持平),而参数量只有 28M 对 44M。这是 SE 最有说服力的一组对比,用小模型的代价拿到了更大模型的精度,对实际部署的启发比单纯的精度提升更大。

另外注意提升幅度随深度增加并不是单调的。ResNet-50 提升 0.86,ResNet-101 只提升 0.45,ResNet-152 回升到 0.59。这个波动说明基线越强,SE 的边际收益越小,但也不是简单的饱和关系。如果在小数据集上做实验,这种量级的波动很容易被随机种子带来的噪声淹没,所以验证 SE 是否有效时,一定要跑多次取平均,单次结果说明不了问题。

在轻量网络上 SE 的收益更明显。MobileNet 加 SE 后 top-1 提升超过 1.5 个百分点,这是因为轻量模型的通道数被压得很紧,通道之间的信息冗余和干扰问题更严重,显式的通道重标定能补回一部分损失。

6.2 消融实验的适用边界

论文的消融主要在 ImageNet 上做,而 ImageNet 是个类别均衡、分辨率固定、目标占比适中的数据集。把这些结论搬到其他场景时,需要留个心眼。

比如"平均池化优于最大池化"这个结论,在目标检测或分割任务上就未必成立,因为那些任务里小目标占比高,最大池化能保留的强响应信息可能更有价值。同样,"r=16 最优"是在 ResNet 这种通道数 256 到 2048 的骨干上得到的,如果你用的是通道数只有 32 或 64 的轻量网络,最优 r 很可能是 4 或 8。

还有一个容易忽略的点:论文所有实验都是从头训练或者基于相同训练策略对比的。如果你是在预训练模型上加 SE 做微调,增益模式和从头训练是不一样的。微调场景下 SE 的增益通常更小,因为预训练权重已经学到了一套不错的通道重要性隐式表示,SE 能补的有限。我在几个迁移学习任务上试过,加 SE 的收益在 0.3 到 0.8 个百分点之间波动,不如从头训练明显,偶尔还会因为训练不充分而掉点。

7. 手写一遍SE模块:五个真实会踩的坑

前面已经给过基础实现,这里把实际落地时真正会让人卡住的几个问题单独拎出来。

7.1 通道数不能被 r 整除

如果 channels=100,reduction=16,100 // 16 = 6,隐藏层是 6,升维时6 → 100没问题,信息损失会大一些但不会报错。真正的坑在 channels 小于 reduction 的情况,比如某个轻量网络的某个分支只有 8 个通道,8 // 16 = 0,全连接层输出 0 维,直接崩。所以max(1, channels // reduction)这个保护是必须的。

更细一点,如果希望隐藏层维度不要太小,可以用四舍五入而不是向下取整:max(1, round(channels / reduction))。不过这个改动会让参数量计算和论文不完全一致,做对比实验时要注意。

7.2 部署时的 reshape 开销

用nn.Linear实现的话,forward 里需要.view(b, c)和.view(b, c, 1, 1)两次形状变换。在训练时这几乎没影响,但在移动端或端侧推理引擎里,这类形状变换操作可能触发内存重排,成为隐藏的性能瓶颈,尤其是在小分辨率、高帧率的场景下。

一个常见做法是把 Linear 换成 1×1 卷积。数学上完全等价(全连接就是在 1×1 空间上的卷积),但省掉了 reshape,推理图更干净:

class SEModuleConv(nn.Module): def __init__(self, channels, reduction=16): super().__init__() hidden = max(1, channels // reduction) self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Conv2d(channels, hidden, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(hidden, channels, 1, bias=False), nn.Sigmoid() ) def forward(self, x): return x * self.fc(self.avg_pool(x))

注意权重形状会变。如果要从 Linear 版本迁移权重,记得做一次weight.view(C_out, C_in, 1, 1)的转换,否则加载会报尺寸不匹配。

7.3 初始化对预训练微调的冲击

前面提过一次,这里展开说。默认初始化下,第二个全连接层的输出在 0 附近,Sigmoid 之后门控值约为 0.5。这意味着刚加上 SE 时,整个网络的输出幅度会被砍到一半。虽然后面的 BN 层会逐渐适应,但训练初期的 loss 会有一个明显的跳变,学习率需要相应调小。

更稳妥的做法是把 W₂ 的偏置初始化成一个正值,让初始门控靠近 1。我一般设成 3,对应 Sigmoid 输出约 0.95。这样微调开始时网络行为几乎不变,SE 只是在此基础上做小幅调整,训练曲线会平滑很多。

反过来,如果是从头训练,初始门控靠近 1 反而可能让 SE 学得慢,因为它一开始就接近"什么都不改"的状态。这种时候用默认初始化甚至更好的做法是让门控初始接近 0.5,给网络更大的调整空间。所以这个技巧要看场景,不是无脑套用。

7.4 分布式训练里的一个误解

有人担心全局平均池化在分布式训练时会不会跨卡聚合。答案是不会,也不需要。AdaptiveAvgPool2d(1)是在单个样本的特征图上做空间维度的池化,跟批次维度和设备维度都无关,每张卡上的每个样本独立计算,天然正确。只有当你自己写了一个跨样本的统计量(比如某些对比学习里的 batch 级归一化)时,才需要考虑跨卡同步。

真正需要注意的是,如果骨干里有 SyncBN,SE 分支里没有 BN,那 SE 的输入分布会随 SyncBN 的行为变化,但它自身的参数更新不受影响。这点不用额外处理。

7.5 感受野和特征图尺寸的边界情况

SE 在特征图尺寸降到 1×1 时会退化成恒等变换。这种情况在最后几层比较常见,比如输入 224、经过 5 次下采样后只剩 7×7,池化后还是 1 个数,再池化还是那个数,没有任何额外的信息。这时候加 SE 纯粹浪费参数,而且门控值学不到有意义的东西。

比较常见的处理是:最后那个 stage 之后、全局池化之前的 SE 可以省掉,或者干脆让最深层不加。不过论文的消融显示全加效果最好,所以这点在实际中不必过度优化,除非你的模型极深、特征图降得极快。

8. SENet之后的变体在改什么

理解了 SE 的设计,再看后续的变体会清晰很多,因为它们大多是在动 SE 的三个环节之一。

ECA-Net动的是瓶颈结构。它指出降维这个操作本身就是有害的——把 C 维压到 C/r 维,通道关系信息已经损失了,再学出来的门控自然不准。于是它干脆去掉两个全连接,换成一个一维卷积直接作用在通道维度上,卷积核大小 k 通过通道数自适应计算。参数量比 SE 少了两个数量级,效果还更好。这个改动的核心洞察是:降维省参数,但省掉的信息可能比省掉的参数更值钱。

CBAM在通道注意力之外并联了一个空间注意力分支,先做通道再空间。它的思路是通道重要性和空间重要性是互补的,一个通道在整张图上重要,不代表它在所有位置都重要。

SKNet把选择机制从"给通道打权重"扩展到"在多尺度分支之间做选择",用类似 SE 的门控来决定不同卷积核尺寸的权重。思路和 SE 一脉相承,只是选择的对象从通道变成了分支。

Coordinate Attention意识到全局平均池化把空间信息压成一个标量,丢失了位置信息。它把池化拆成沿高度和宽度两个方向的一维池化,保留位置线索,对密集预测任务更有用。

这些变体共同的演进方向是:在保留轻量门控这个核心思想的前提下,减少信息损失。SE 的全局平均池化丢了位置,瓶颈降维丢了通道关系细节,后续工作就是一块块往回补。理解了这条主线,再看到新出的注意力模块,基本能一眼看出它是在补哪一块。

我自己的经验是,在自己项目里加注意力模块的优先级其实没那么高。SE 加上去几乎稳赚,成本极低,直接抄论文的写法就行。再往上的变体收益就变得很看任务,有些场景里差距在千分位,验证成本远超收益。把精力放在数据质量和训练策略上,回报通常比换一个更花哨的注意力模块高得多。

返回列表