ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SE注意力机制全面解析:通道注意力原理、实现与训练调参

SE注意力机制全面解析:通道注意力原理、实现与训练调参 搞深度学习做视觉的人基本都绕不开一个词——注意力机制。而在这个家族里SE注意力机制算是祖师爷级别的存在。2017年那篇《Squeeze-and-Excitation Networks》出来之后几乎成了所有做图像分类、检测、分割的人都绕不过去的一块积木。它做的事情说起来简单让网络自己学会判断哪些通道的特征更重要然后按重要程度给它们加权。这种通道注意力的思路后来衍生出了CBAM、CA、ECA、SKNet等一整个系列也间接影响了自注意力、多头自注意力在视觉任务里的落地方式。这篇文章不打算复述论文翻译而是从一个真正把SE模块写进项目、调过参、踩过坑的工程视角把它的来龙去脉、数学细节、代码实现、参数取舍、和同类机制的对比、以及训练里各种莫名其妙的掉点问题一次性讲透。不管你是刚接触注意力机制的小白还是想把SE嵌进自己backbone的老手都能从里面找到能直接抄作业的部分。我会重点讲清楚SE为什么这么设计、每一步在干嘛、参数量怎么算、和CBAM/CA/自注意力到底差在哪、实际训练时该注意什么。1. SE注意力机制到底解决了什么问题1.1 卷积的一视同仁与通道间的贫富差距普通卷积操作有一个被长期忽略的特性它对输入特征图的所有通道是平等对待的。也就是说当一层卷积输出了256个通道这256个特征图在下一层卷积核眼里权重是混在一起、没有显式区分度的。卷积核只能通过它自己的空间权重去决定从每个通道里提取什么但它没有办法表达我现在更关心第37个通道第12个通道先放一放这种动态的、样本相关的偏好。这就带来一个问题。特征图的每个通道本质上是某种语义响应的载体。有的通道专门响应边缘有的响应纹理有的响应某个特定物体的部件还有大量通道其实是冗余的、响应很弱的。图像内容不同真正重要的通道也不同——一张猫的图和一张汽车的图激活强烈的通道集合大概率不一致。可标准卷积是静态的所有样本都走同一套权重它学不会这种因图而异的通道偏好。SE模块要解决的正是这个通道权重一刀切的问题。它的核心主张是给每个通道算一个0到1之间的重要性分数然后乘回原特征让重要的通道放大、不重要的通道衰减。这个分数不是人工设定的而是网络根据当前输入自己算出来的所以是动态的、样本相关的。1.2 通道注意力从隐性到显式的建模在SE之前其实也有不少工作在尝试做特征重标定比如一些并发注意力、门控机制但它们大多依赖空间维度上的操作或者需要额外的监督信号。SE真正的贡献在于它把通道之间的依赖关系这件事显式地、低成本地建模出来了。为什么强调显式因为卷积本身理论上也能隐式学到通道相关性但那是嵌套在空间卷积里的学习难度高、效率低。SE把它拆出来单独变成一个支路让它专注做一件事看全局信息判断哪些通道重要。这个支路非常轻量通常只增加总参数的百分之几却往往能换来1%到2%的top-1精度提升——在ImageNet这种已经卷到极致的数据集上这是很可观的。我个人的理解是SE本质上是一种特征再校准feature recalibration。它不改动主干卷积怎么提取特征只是在特征提取之后加一道质检和加权的工序。这种非侵入式的设计正是它能被到处嫁接的原因ResNet能加MobileNet能加甚至一些轻量网络也能加改动量极小。注意SE解决的是通道维度的注意力不是空间维度。如果你的问题主要是模型不知道看图像的哪个区域那SE可能帮不上太多得考虑CBAM这种带空间注意力的方案。2. 拆开看SE模块的三步走结构SE模块的整体流程可以概括成三个动作Squeeze压缩、Excitation激励、Scale重标定。名字听着唬人拆开看其实非常朴素。下面一步步讲。2.1 Squeeze把 H×W 压成一个数假设输入特征图是 C×H×WC个通道每个通道是H×W的二维图。Squeeze这一步对每个通道做一次全局平均池化Global Average Pooling把 H×W 个空间位置上的值平均成一个标量。做完之后特征图从 C×H×W 变成了 C×1×1也就是每个通道只剩下一个数。这个数代表什么它代表这个通道在整个空间范围内的平均响应强度。通道响应越强、越普遍这个平均值就越大通道几乎没被激活平均值就趋近于0。所以Squeeze干的事情是用一个数来概括这个通道整体上有多活跃。为什么用全局平均池化而不是全局最大池化论文里做过对比平均池化效果更好。我的理解是平均池化综合了整个空间的信息噪声更小、更稳定最大池化只取最强烈的那个点在特征本身比较稀疏的时候容易过于敏感。当然这不是绝对的后来有些工作比如在某些检测任务里用最大池化或者两者结合也有不错效果属于可以调的细节。# Squeeze: 全局平均池化 import torch import torch.nn as nn class Squeeze(nn.Module): def __init__(self): super().__init__() self.gap nn.AdaptiveAvgPool2d(1) # 输出 C x 1 x 1 def forward(self, x): return self.gap(x) # 也可用 x.mean(dim(2,3), keepdimTrue)提示AdaptiveAvgPool2d(1)和x.mean(dim(2,3), keepdimTrue)在数值上等价前者更通用后者更省一点显存开销。实际项目里我用mean更多因为能少建一个池化层对象。2.2 Excitation两层全连接构成的门控拿到 C×1×1 的通道描述后Excitation用它来生成每个通道的权重。这一步是一个小小的两层瓶颈结构第一层全连接把通道数从 C 降到 C/rr 是压缩比reduction ratio常见取值16或8接一个ReLU激活第二层全连接再把通道数从 C/r 升回 C最后过一个Sigmoid把输出压到 (0,1) 区间作为通道权重。为什么先降维再升维这么绕直接一层全连接从C到C不行吗当然行但参数量是 C²。用瓶颈结构后参数量变成 2C²/r当r16时参数量降到原来的1/8左右大幅减少了开销。SE的卖点就是轻量这个瓶颈设计是它控制成本的关键。那ReLU和Sigmoid的分工是什么ReLU提供非线性让这个门控网络能拟合通道之间的复杂关系Sigmoid负责把最终权重限制在0到1之间这样加权时才不会出现某些通道被放大到离谱、某些被反向翻转的情况。这两个激活是不可省的去掉ReLU会退化成线性映射去掉Sigmoid权重会失去门控的意义。# Excitation: 两层FC ReLU Sigmoid class Excitation(nn.Module): def __init__(self, channels, reduction16): super().__init__() hidden max(channels // reduction, 4) # 防止通道太小时降为0 self.fc nn.Sequential( nn.Linear(channels, hidden, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(hidden, channels, biasFalse), nn.Sigmoid() ) def forward(self, x): return self.fc(x) # 输出 C 个权重这里有个容易忽略的细节hidden不能直接用channels // reduction因为当通道数小到一定程度比如32r16时得到2隐藏层会过小门控表达能力严重不足。我一般会加个下限保证至少是4经验上这时次不会掉。2.3 Scale逐通道相乘与残差式融合最后一步把Sigmoid输出的C个权重逐通道乘回原始特征图。第i个通道的整体特征乘以第i个权重。这就是重标定。这一步有个隐含好处如果某个权重接近0那个通道基本被抹掉接近1则原样保留。网络理论上可以通过学习权重接近1让SE模块形同虚设退化成普通卷积网络——这给了模块一个安全下限不至于越加越差。实际上大多数通道权重散落在0.4到0.8之间说明网络确实在做有区分度的取舍。class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.squeeze Squeeze() self.excitation Excitation(channels, reduction) def forward(self, x): b, c, _, _ x.shape y self.squeeze(x).view(b, c) # Squeeze w self.excitation(y).view(b, c, 1, 1) # Excitation return x * w # Scale广播相乘注意Scale用的是广播相乘w的shape是(b, c, 1, 1)x是(b, c, h, w)需要保证维度对齐。我见过不少新手在这里忘了view或者用了expand_as导致内存多了一份拷贝其实广播就够了。3. 动手实现从零写一个SE模块前面把结构拆完了这一节讲落地。从最简实现到嵌进ResNet Bottleneck再到参数量的账怎么算一步步来。3.1 PyTorch版本的最小可用实现把上面三个部件合起来就是一个完整的SE模块。我再补一个带BN和偏置处理的工程版本因为实际项目里你往往会把它当成一个可插拔的组件class SEBlock(nn.Module): Squeeze-and-Excitation Block可直接插入任意卷积主干。 def __init__(self, channels, reduction16): super().__init__() hidden max(channels // reduction, 4) self.avg_pool nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Linear(channels, hidden, biasTrue), nn.ReLU(inplaceTrue), nn.Linear(hidden, channels, biasTrue), 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.expand_as(x) # expand比expand_as更省内存这里我用了expand而不是直接乘原因是expand返回的是一个视图不会真正复制数据。虽然PyTorch的广播机制本身也不复制但显式写出来意图更清楚。实战里两种写法性能差距可以忽略看团队代码规范即可。还有一个可调点全连接是否带bias。论文里用的是带bias的版本我在一些论文复现里试过去掉bias精度差别在0.1%以内基本属于噪声。所以默认带bias图个稳妥。3.2 把SE嵌进ResNet的Bottleneck里SE最经典的用法就是放进ResNet的残差块。原始论文把它插在Bottleneck的第三个卷积之后、残差相加之前。也就是说SE重标定的是残差分支的输出然后再跟shortcut相加。这个位置有讲究。如果放在残差相加之后就等于同时对主分支和shortcut一起加权会破坏恒等映射的干净性深网络里容易出问题。放在相加之前只调整主分支特征shortcut保持原样整个块的恒等映射能力不受影响这也是SE能安全堆到上百层的原因。class SEBottleneck(nn.Module): expansion 4 def __init__(self, inplanes, planes, stride1, downsampleNone, reduction16): super().__init__() width planes self.conv1 nn.Conv2d(inplanes, width, 1, biasFalse) self.bn1 nn.BatchNorm2d(width) self.conv2 nn.Conv2d(width, width, 3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(width) self.conv3 nn.Conv2d(width, planes * self.expansion, 1, biasFalse) self.bn3 nn.BatchNorm2d(planes * self.expansion) self.relu nn.ReLU(inplaceTrue) self.se SEBlock(planes * self.expansion, reduction) self.downsample downsample def forward(self, x): identity 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) # SE 在残差相加之前 if self.downsample is not None: identity self.downsample(x) out identity return self.relu(out)提示SE模块的通道数必须等于Bottleneck的输出通道数也就是planes * expansionResNet里expansion4而不是中间那层的宽度。这个点我第一次写的时候就搞错了导致维度对不上直接报错。顺带一提插入后建议把bn3的momentum稍微调小一点或者初始化时把SE最后一层的权重初始得小一些避免训练初期权重剧烈波动。3.3 reduction ratio 与参数量的账要算清楚SE模块新增的参数主要来自那两个全连接层。设通道数C压缩比r参数量约为Params_SE ≈ C × (C/r) (C/r) × C 2C² / r以ResNet-50为例它各个阶段最后一层的通道数分别是256、512、1024、2048。每个Bottleneck里SE作用于planes*4也就是256/512/1024/2048这几个值。取r16阶段输出通道SE隐藏层维度单块SE参数量256162×256×16 8192512322×512×32 327681024642×1024×64 13107220481282×2048×128 524288ResNet-50每个阶段分别有3、4、6、3个Bottleneck全加起来SE的参数量大约是 3×8192 4×32768 6×131072 3×524288 ≈ 2.5M而ResNet-50原始参数量约25M也就是增加了约10%。说多不多说少不少在移动端部署时要掂量一下。如果嫌多把r调大比如r32参数量直接减半精度通常只掉一点点。反过来通道数很少的轻量网络比如MobileNet某层只有32通道r16会让隐藏层变成2太小了这时应该用r8甚至r4或者干脆这一层不加SE。注意不是所有层加SE都划算。分类网络里普遍是全加但检测、分割这类密集预测任务高分辨率特征层的空间信息更宝贵全加SE收益未必高建议先在小数据集上做消融看哪些阶段真正有效再加。4. 横向对比SE、CBAM、CA、自注意力怎么选SE火了之后通道注意力这条线冒出了一大堆变体。做项目时最常纠结的就是到底用哪个。这一节把几个高频出现的机制拉出来对比讲清楚它们各自在解决什么新问题。4.1 SE vs CBAM通道注意力与空间注意力的协同CBAMConvolutional Block Attention Module可以理解成SE的加强版。它把注意力拆成两段先做通道注意力跟SE思路类似但用了平均池化和最大池化两条并行支路共享一个MLP再做空间注意力在通道维度上做池化得到一个H×W的空间权重图。区别很直观SE只告诉你哪个通道重要CBAM还告诉你每个空间位置重要到什么程度属于通道-空间协同注意力。在分类任务上两者差距不大CBAM略优但在需要精确空间定位的检测、分割任务里CBAM的空间支路往往更有优势因为它能压掉背景区域、突出目标位置。代价是CBAM更重多了一个空间分支和两条池化支路计算量比SE高一大截。我的经验是分类任务、追求极致轻量用SE检测分割、对定位敏感用CBAM。对比项SECBAM注意力维度仅通道通道 空间池化方式全局平均平均最大通道支路额外参数量小约10%较大分类收益稳定略优于SE定位任务收益一般明显部署友好度高中4.2 CA注意力机制把位置信息保下来CACoordinate Attention是另一条路。SE和CBAM在压缩空间信息时用的都是全局池化会把空间位置信息彻底丢掉。CA的做法是把池化拆成沿高度和沿宽度两个方向先分别沿H和W做一维池化得到两组带方向的位置特征再拼接、卷积、拆开最终得到同时编码了通道关系和位置关系的权重。它解决的问题很具体SE是位置无关的容易在多目标、需要精确坐标的任务里吃亏。CA把坐标信息嵌进注意力里在移动端模型上特别受欢迎因为它本身也很轻甚至比SE还省。如果你在做的是目标检测、关键点、或者任何对位置敏感的任务CA往往比SE更合适纯分类的话两者差距不大选实现更顺手、部署工具链支持更好的那个就行。4.3 自注意力与多头自注意力另一条技术路线前面几个都属于卷积网络里的注意力补丁而自注意力Self-Attention是另一套体系。它的核心是让每个位置跟所有其他位置计算相关性然后加权聚合。多头自注意力就是把这件事并行做多份每份关注不同的子空间最后拼起来。跟SE比自注意力的表达能力强得多但代价也大计算复杂度是序列长度的平方。图像里位置数很多一张224×224的图有5万多个位置直接算根本扛不住。所以视觉里要么把图像切成patch降低长度ViT的做法要么用窗口化、下采样等技巧控制成本。那么SE和自注意力有没有结合有而且不少。一种常见思路是通道注意力做粗筛、自注意力做精修先用SE快速找出重要通道减少后续自注意力的计算量另一种是把SE的门控思想塞进自注意力的FFN里作为通道维的补充调制。多头自注意力里的多头本质上也是一种把注意力分散到不同子空间的策略跟SE给不同通道分配不同权重在哲学上是相通的——都是在有限算力下做有选择的聚焦。提示别把SE当自注意力的替代品两者维度不同、成本不同、适用场景不同。做小模型、要部署到端侧优先SE/CA这类轻量通道注意力做大数据、算力充足、要长距离依赖再考虑自注意力。顺带说一句时序注意力机制这个方向。在序列建模里注意力权重是随时间步变化的本质上是给不同时间步分配重要性。它和SE有相似的思想内核——都是对某一维度的信息做加权筛选——但维度从通道换成了时间。如果你在做时序预测或者视频理解通道注意力SE和时间注意力是可以叠加使用的一个管哪些特征通道重要一个管哪些时刻重要。5. 训练调参与踩坑实录前面讲的都是怎么用这一节讲怎么用得稳。SE看起来简单但我在实际项目里确实踩过不少坑这里挑最典型的几个说。5.1 常见问题速查表现象可能原因排查/解决思路加SE后精度不升反降reduction设太大门控太弱把r从16降到8或4观察变化训练初期loss剧烈震荡SE末层权重初始化过大末层权重用小方差初始化或降低学习率通道数很小的层报错hidden维度算成0加下限max(C//r, 4)推理速度明显变慢逐通道乘法在端侧未优化融合进前一层卷积或换ECA等更轻方案不同batch间权重方差大用了BN统计不稳定的主干检查BN momentum小batch时慎用分类提升明显但检测无感空间信息被池化抹掉尝试CA或CBAM的空间支路这张表里的每一条我基本都真实遇到过。尤其是第一条加了反而掉点很多人的第一反应是SE没用其实多半是r选得不合适或者加错了位置。5.2 我踩过的坑和实操心得第一个坑位置加错。我最早把SE加在残差相加之后结果深层网络训练到一半loss开始飘。后来改成加在相加之前问题消失。这个前面讲过原理这里提醒一句位置不是随便放的主分支的干净性对深网络很重要。第二个坑reduction一味照抄16。有次在一个通道数较小的自定义主干上加SE每层通道大多在64以内r16直接把隐藏层压到4以下注意力完全学不动加了等于白加。后来按通道数分档设r通道大于512用16256到512用8小于256用4效果立刻正常了。第三个心得初始化很关键。SE最后一层是Sigmoid如果它的权重一开始就偏大输出权重会剧烈偏离0.5等于训练一开始就粗暴地放大或抑制通道容易引起震荡。我通常把这个线性层的权重做小方差初始化比如 std0.001让初始输出接近0.5相当于先不起作用再慢慢学训练稳定性提升明显。第四个心得别迷信全加。论文里确实所有块都加了SE但那是在ImageNet这种大数据上。换成你自己的小数据集、或者密集预测任务全加未必最优。我一般会先在最深层通道多、语义强加确认有提升后再往浅层推进一层层消融稳扎稳打。第五个心得和别的注意力别硬叠。有人喜欢SE、CBAM、CA一股脑全加上觉得多管齐下实际往往是参数量翻倍、精度原地踏步甚至下降。注意力模块之间功能重叠叠加收益递减得很快。选一个主攻方向通道 or 空间 or 位置把它调好比堆一堆要强。最后分享一个我常用的验证套路在正式训练前先跑一个权重可视化的小脚本把某个batch的SE输出权重拿出来看分布。如果权重全都挤在0.5附近说明门控几乎没学到东西得回头查r和初始化如果权重明显分层、有高有低说明SE在工作。这个检查五分钟就能做却能在早期就发现大部分加了没用的问题。至于多分支结构里的SE变体比如一些网络用多个SE并行处理不同分支再融合核心思想没变还是通道加权只是把一个门控扩展成一组门控用在分组卷积或者多尺度分支上会更细粒度。如果你在做轻量网络设计这块值得再挖一挖。
返回列表