ARTICLE DETAIL

资讯详情

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

卷积注意力机制选型指南:从SE到MSCA的演进与工程实践

卷积注意力机制选型指南:从SE到MSCA的演进与工程实践 前阵子一个朋友问我同样一个分类网络别人加上CBAM之后能稳定涨两三个点我照葫芦画瓢加进去训练完反而比baseline还低问题出在哪我问他模型是不是跑在ImageNet预训练权重上数据集多大注意力模块放在哪些层他一问三不知。这种场景我觉得特别典型因为很多人把“卷积注意力机制”当成了某种万能补丁觉得网络不够强就往上贴一块。但实际上SE、ECA、CBAM、坐标注意力、SimAM、Triplet Attention、DANet、轴向注意力、再加上近两年工程里更常出现的MSCA、EMA这类组件它们解决的问题、适用的位置、训练的敏感度都不一样不加区分地乱用翻车太正常了。这篇文章我想按我自己的理解把这9类卷积注意力机制的演进和用法重新梳理一遍。我不会把它们写成九个独立的“论文摘要”而是按它们解决问题的思路分成几条线通道维度怎么挑重点空间位置怎么补长距离依赖怎么建以及2024年前后更值得关注的多尺度和大核方向。适合正在做图像分类、目标检测、语义分割想在模型里加注意力模块但不知道怎么选、不知道怎么调的人看。看完你至少能回答一个问题我手里这个任务到底该用哪种注意力加在哪里。1. 先把“卷积注意力机制”拆开它到底在调什么注意力机制这个词听着玄落到卷积网络里其实就一件事给特征图的每个位置、每个通道或者每个感受野算出一个权重然后用这个权重去缩放原始特征。说人话就是网络学会了在推理时“把话筒递给重要的发言人”而不是给所有人一样的发言时间。这个权重可以是标量、向量也可以是一张跟特征图一样大的mask不同设计决定了它能关注到哪种信息。理解这一点很重要因为很多人以为“用了注意力模型能看得更远”。实际上SE这类模块只是告诉网络“哪些通道更值得放大”它对空间位置完全无感而CBAM的空间注意力分支虽然能告诉网络“图像中间这块更重要”但它本身也不具备跨区域的长期依赖建模能力。真正想看清整张图的关系就得落到DANet、轴向注意力这类结构上。所以注意力不是一种东西是一类结构它们共享的思路是“生成权重、重新校准特征”但每家的假设完全不同。我在实际选型时会把它们分成四条线后面几节也按这个顺序聊通道注意力线SE、ECA解决“哪些通道重要”。通道空间混合线CBAM在两种维度上都做调制。位置与免参线Coordinate Attention、SimAM、Triplet Attention尽量保留位置信息或者不引入额外参数。长距离依赖线DANet、Axial Attention试图建立全图或大范围关系。工程化多尺度线MSCA、EMA这类近两年更常用的做法与前几条不同它们往往不是“即插即用小模块”而是嵌进backbone或neck里进行结构级改造。后面你会发现线越往后越难用“加一个模块”这种思路去套这也是很多人看论文看得懂、落地用不好的原因之一。下面先从最经典的通道注意力线开始。2. 通道注意力演进的两次迭代从SE到ECA2.1 SE为什么能涨点又为什么会被嫌弃SESqueeze-and-Excitation是2018年左右火起来的一代经典。它的思路非常直接先对特征图做全局平均池化把空间信息压成一个1x1xC的向量然后经过两个全连接层用sigmoid输出一组0到1之间的通道权重最后和原特征逐通道相乘。这样一来网络可以根据全局信息决定哪些通道应该被保留和放大。为什么要用两个全连接层而不是直接一个全连接出权重这是SE设计里最值得琢磨的地方。一个全连接层虽然也能输出权重但建模能力有限两个全连接层中间加一个降维瓶颈既减少了参数又能让网络在“压缩-扩张”的过程中学到通道之间的非线性关系。我在自己项目里复现SE时有个很实在的体会reduction不能随便设。论文里常用r16但如果某层通道数本来就小比如输入只有64通道c//164信息压缩得太狠注意力会退化成“几乎把所有通道都打成同一个值”等于白算。import torch import torch.nn as nn class SEBlock(nn.Module): def __init__(self, in_channel, reduction16): super().__init__() self.squeeze nn.AdaptiveAvgPool2d(1) self.excitation nn.Sequential( nn.Linear(in_channel, in_channel // reduction), nn.ReLU(inplaceTrue), nn.Linear(in_channel // reduction, in_channel), nn.Sigmoid() ) def forward(self, x): w self.squeeze(x).flatten(1) w self.excitation(w) return x * w[:, :, None, None]代码本身不复杂但很多人踩过一个隐性的坑小数据集上加SE涨点不稳定。我在一个只有几千张图的分类任务里试过ResNet18加SE反而掉了一个多点。原因很可能是小数据集本身学不出靠谱的全局通道统计SE的全局平均池化把噪声也平均进去了。这时候先不加SE或者只在最后一个stage加通常比无脑每层都加要稳。2.2 ECA用一维卷积绕开降维瓶颈ECAEfficient Channel Attention是我在移动端项目里用得最多的通道注意力方案。它的动机很直接SE的降维并不是必须的甚至还可能带来信息丢失。ECA直接对全局平均池化后的特征做一维卷积通过一个kernel size可自适应调整的卷积核实现对邻近通道的交互。这里的kernel size不是随便定的它和通道数C有关。计算方式大致是k |log2(C) / gamma b / gamma|然后取最近的奇数论文里gamma通常取2b取1。通道数越多卷积核越大能覆盖的相邻通道数越多。这种设计很巧妙它假设通道之间的关系主要存在于相邻通道之间不需要像SE那样做全局全连接。import torch import torch.nn as nn import math class ECABlock(nn.Module): def __init__(self, in_channel, gamma2, b1): super().__init__() t int(abs((math.log2(in_channel) b) / gamma)) kernel_size max(t if t % 2 else t 1, 3) self.avg_pool nn.AdaptiveAvgPool2d(1) self.conv nn.Conv1d(1, 1, kernel_sizekernel_size, padding(kernel_size - 1) // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): w self.avg_pool(x).squeeze(-1).transpose(-1, -2) w self.sigmoid(self.conv(w)).transpose(-1, -2).unsqueeze(-1) return x * wECA在ImageNet上的表现和SE相当但参数量少了一个数量级这就是为什么它很适合移动端和轻量backbone。我自己在替换SE时还发现ECA训练起来比SE“钝”一些也就是说对学习率和初始化不那么敏感这在实际调参中是个很大的优势。如果任务比较简单、数据量不大我更推荐先用ECA而不是SE。对比项SEECA核心操作全局池化 两个全连接全局池化 一维卷积新增参数量2CC/r较大约等于K几乎可忽略通道关系建模全通道非线性交互相邻通道局部交互训练稳定性对超参和数据集较敏感相对更稳定典型场景中大模型、分类任务移动端、轻量模型3. CBAM之后才明白空间注意力不是“哪里都该放”3.1 通道加空间的双重调制CBAMConvolutional Block Attention Module在SE的基础上往前走了一步既然SE只处理通道维度那我能不能同时把空间维度也处理一下它的结构是先用一个通道注意力模块再用一个空间注意力模块两者串联。通道注意力部分和SE很像区别在于它把全局平均池化和全局最大池化并行使用两个结果分别过全连接再相加过sigmoid。空间注意力部分则是对特征图在通道维度上分别做平均和最大操作得到两个HxW的平面拼成2xHxW的输入过一个7x7卷积再sigmoid得到空间权重。空间注意力分支里的7x7卷积是很容易写错的地方。很多人跑代码报错问题大多出在输入通道数上拼接之后是2个通道卷积输入通道必须是2输出是1。这个很基础但确实是我见过频率最高的CBAM实现bug。import torch import torch.nn as nn class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super().__init__() self.conv nn.Conv2d(2, 1, kernel_sizekernel_size, paddingkernel_size // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) max_out, _ torch.max(x, dim1, keepdimTrue) att torch.cat([avg_out, max_out], dim1) return x * self.sigmoid(self.conv(att))CBAM为什么有效因为它的通道分支用最大池化补足了平均池化丢失的“显著目标”信息空间分支又能在每个点告诉网络“这里重不重要”。我在检测任务里测试过CBAM放在ResNet的最后一个stage对中等目标的AP提升是比较明显的。但问题也出在这个“空间”上请看下一节。3.2 空间注意力为什么会掉点CBAM加了反而掉点这是我在多个任务里都见过的事。原因我总结下来主要有三个。第一空间注意力本质上是基于当前特征图的局部计算它没有全局上下文模型可能会把某个局部噪声误判为“重要区域”然后把它放大。第二如果数据里目标尺度变化很大一张特征图上的大目标和小目标共用同一套空间权重必然顾此失彼。第三很多人把CBAM加在网络的每一层低层特征上有大量边缘、纹理信息空间注意力一旦过度激活会把噪声也一路传下去。所以我的经验是CBAM不要无脑全层加。分类任务里放在网络后几个stage检测分割任务里放在backbone靠后的输出层效果往往比每层都加要好。另外原文推荐的“先通道后空间”顺序也不是铁律。我在一些检测任务里试过“先空间后通道”小目标AP反而更稳。这种东西论文里不会写只能靠在自己数据集上试。4. 坐标注意力、免参注意力和跨维度交互4.1 Coordinate Attention保留位置不压扁空间SE有一个天然短板全局平均池化把空间位置信息全丢了。网络只知道“哪个通道重要”但完全不知道“哪个位置重要”。Coordinate Attention坐标注意力的思路是把空间维度拆成高度和宽度两个方向分别做平均池化得到1xCxH和1xCxW两个方向的特征然后拼接起来过一个1x1卷积降维再分成两个分支各自过sigmoid最后作为两个方向上的权重和原特征相乘。这样做的好处特别明显它在不引入太多计算的前提下把位置信息保留了下来。我在这类模块的实际使用中经常用一个“解耦”版本实现起来更好读效果和原文差距很小class CoordAtt(nn.Module): def __init__(self, in_channel, reduction32): super().__init__() self.conv_h nn.Sequential( nn.Conv2d(in_channel, in_channel // reduction, 1), nn.BatchNorm2d(in_channel // reduction), nn.ReLU(inplaceTrue) ) self.conv_w nn.Sequential( nn.Conv2d(in_channel, in_channel // reduction, 1), nn.BatchNorm2d(in_channel // reduction), nn.ReLU(inplaceTrue) ) self.fc_h nn.Conv2d(in_channel // reduction, in_channel, 1) self.fc_w nn.Conv2d(in_channel // reduction, in_channel, 1) def forward(self, x): b, c, h, w x.shape x_h x.mean(dim3, keepdimTrue).permute(0, 1, 3, 2) # B,C,1,H x_w x.mean(dim2, keepdimTrue) # B,C,1,W att_h self.fc_h(self.conv_h(x_h)).sigmoid().permute(0, 1, 3, 2) # B,C,H,1 att_w self.fc_w(self.conv_w(x_w)).sigmoid() # B,C,1,W return x * att_h * att_w这个模块在移动端友好参数不多而且它对分割、检测这类对位置敏感的任务比较友好。原文里用的是拼接再拆分的做法让两个方向先交互再分开我这边用解耦分支是为了代码简洁实测在大多数任务上与原文差距可以忽略。需要注意的坑是reduction也别设太小坐标注意力同样有压缩信息的问题。4.2 SimAM零参数的纯能量注意力SimAM是我见过最“反直觉”的注意力机制它不引入任何可学习参数只根据当前特征图内部每个神经元的能量函数来生成权重。思路来自神经科学里的神经元抑制现象一个神经元如果能抑制周围神经元那它本身的信息量就更大应该被放大。具体做法是计算每个位置的能量值然后对所有位置的能量求倒数再经过sigmoid得到每个位置的权重。这里的能量函数和方差、均值有关代码核心部分很短class SimAM(nn.Module): def forward(self, x): b, c, h, w x.shape n h * w - 1 mean x.mean(dim[2, 3], keepdimTrue) var ((x - mean) ** 2).mean(dim[2, 3], keepdimTrue) e (x - mean).pow(2) / (4 * var 1e-4) 0.5 / n return x * torch.sigmoid(1.0 / e)这个模块最大的优点是省心。不需要调参数不会破坏预训练权重的通道结构部署到端侧也不用额外算参数。我在一个小型分类网络里把SE换成SimAM精度基本持平但模型体积一点没变。如果你要做模型压缩又不想牺牲那点精度SimAM可以作为通道注意力的替代方案。它的缺点是上限不如SE和ECA高毕竟没有任何可学习参数表达能力有限。4.3 Triplet Attention旋转张量让三个维度互相看Triplet Attention三联注意力走的是另一个方向既然通道和空间都在处理那我干脆让C、H、W三个维度两两互相“看”。它的结构有三个分支每个分支把输入张量旋转到不同的排列然后用一个Z-pool把被压缩的维度变成2个通道平均池化加最大池化各一份再过7x7卷积得到注意力权重乘回原特征最后三个分支的结果取平均。为什么用Z-pool而不是全局池化因为Z-pool同时保留平均池化和最大池化在只压缩一个维度时信息丢失更少而且对旋转操作更稳定。Triplet Attention的优势是参数少、能捕捉跨维度的交互信息但我实际用下来的感受是它比CBAM更难调效果起伏比较大需要在具体任务上反复试。如果时间有限我不会把它作为首选更适合做实验对比时使用。把这一组放在一起对比大家的定位差异就清楚了模块参数规模保留位置信息训练难度最适合场景Coordinate Attention较低是低分割、检测、移动端SimAM零参数否极低模型压缩、轻量任务Triplet Attention较低部分中探索性实验、跨维度特征5. 长距离依赖的代价双注意力和轴向注意力5.1 双注意力怎么建立全局关系DANetDual Attention Network是语义分割领域很有名的工作它把自注意力引入了CNN。它的结构包含两个分支位置注意力分支和通道注意力分支。位置注意力分支把特征图从CxHxW展成CxNNHxW然后在N这个维度上计算NxN的相似度矩阵用softmax做归一化得到一个“每个位置和所有其他位置的关系”矩阵再乘回去。通道注意力分支类似只是把矩阵换成CxC。这个思路理论上很漂亮但工程上有个特别现实的问题显存。NxN矩阵的复杂度是O(N^2)输入尺寸一大就直接爆炸。我在一个分割任务里试过输入512x512特征图64x64时N4096NxN矩阵是4096x4096单张卡勉强能跑再大一点就OOM了。所以DANet这类方法在实际使用中基本都要配合降采样或者只在最后的低分辨率特征图上使用。它的价值更多在于证明了“非局部操作能提升密集预测任务”而不是作为一个稳定的即插即用模块。5.2 轴向注意力把二维自注意力拆成两次轴向注意力Axial Attention给长距离依赖提供了一个更省资源的解法。既然在整张图上做二维注意力太贵那就先对每一行做自注意力再对每一列做自注意力。这样每个点能看到的范围从局部感受野扩展到整行加整列叠加起来近似于全图交互但计算量从O(N^2)降到了O(N(HW))。在Transformer类视觉模型里这个思路也被大量采用。不过我想提醒的是在CNN里加长距离注意力并不是稳赚不赔。视觉信息有很强的局部性一个像素和隔壁像素的关系通常比和千里之外的像素关系更重要无差别地计算全局关系反而会引入噪声。我自己在分类任务里测试DANet那种全局注意力对ImageNet预训练模型的提升不如SE直接但在分割、检测这种需要上下文理解的任务里长距离依赖的价值会明显体现出来。所以如果显存有限又需要长距离建模我推荐先用轴向注意力或窗口注意力过渡效果接近但训练省心很多。6. 2024年前后更值得关注的方向多尺度卷积注意力与大核拆分到了这个部分我想先澄清一件事。很多人看标题里写着“2024最新”期待的是横空出世的新模块。但我观察到的实际情况是最近这两年真正在工程界和论文里频繁出现的不是某个全新的即插即用注意力名字而是“注意力作为结构组件被塞进backbone、neck、head”的玩法。换句话说大家不再满足于“给模型打个补丁”而是把注意力机制和卷积结构本身融合在一起。6.1 MSCA不叫注意力但到处都是注意力MSCAMulti-Scale Convolutional Attention来自语义分割backbone SegNeXt虽然名字里没有Att但本质上就是一个多尺度卷积注意力模块。它的做法很巧妙先用几条不同卷积核大小、不同空洞率的深度可分离卷积并行提取不同尺度的特征把多尺度信息相加融合再通过1x1卷积生成一个注意力图最后和原始特征相乘。我这里给一个简化版的MSCA核心思路方便理解class MSCA(nn.Module): def __init__(self, in_channel): super().__init__() self.conv1 nn.Conv2d(in_channel, in_channel, 5, padding2, groupsin_channel) self.conv2 nn.Conv2d(in_channel, in_channel, 7, padding9, groupsin_channel, dilation3) self.conv3 nn.Conv2d(in_channel, in_channel, 11, padding21, groupsin_channel, dilation5) self.proj nn.Conv2d(in_channel, in_channel, 1) self.gate nn.Conv2d(in_channel, in_channel, 1) def forward(self, x): out self.conv1(x) out self.conv2(out) out out self.conv3(out) out att self.proj(out) return x * self.gate(att).sigmoid()这个模块的核心不是生成一个简单的通道权重而是让注意力图本身带有不同尺度感受野的信息。我在分割任务里把backbone最后一个stage换成MSCAmIoU有明显提升而且训练过程比想象中稳定。这也是我2024年最推荐尝试的方向之一不要孤立地加注意力让注意力直接参与多尺度特征提取。6.2 EMA、LSKA这些名字为什么在社区里火EMAEfficient Multi-Scale Attention是2023年出现的一个高效多尺度注意力模块它在YOLO系社区里反响很大。EMA把坐标注意力里“两个方向池化”的思路升级成了“多个并行子分支”每个分支处理不同尺度再通过跨空间和跨通道的交互让信息流通。相比Coordinate AttentionEMA在轻量模型上的精度表现更稳代码也清晰很多人直接拿它替换backbone里原有注意力。LSKALarge Separable Kernel Attention则是大核注意力方向的代表。大卷积核能给注意力更大的感受野但直接上大核计算量太难看LSKA把一个大卷积核分解成几步小卷积的叠加既保留了大感受野优势又把参数量和计算量压了下来。2024年前后把LSKA嵌入backbone最后几层的做法在分割和高分辨率任务里越来越多。我的建议是如果你已经在用YOLO系列或者自己搭轻量backboneEMA可以优先试如果你的任务需要大范围上下文又不想换Transformer结构可以看看LSKA。6.3 注意力“下沉”从backbone到neck和head还有一个我2024年感受很深的变化注意力不再只放在backbone里越来越多被放到neck和head中。典型像RT-DETR的混合编码器就用单层自注意力AIFI结合跨尺度特征融合CCFF在neck阶段把不同尺度特征拉通。原因其实很好理解backbone越堆越深之后继续加注意力对性能的边际收益在下降而检测、分割真正吃紧的地方往往在特征融合和输出分支。如果让我给一个安全的实验路径我是这样建议的第一步先保住backbone不变把注意力模块插到neck的融合层第二步观察训练曲线和验证指标如果稳定提升再考虑往backbone的深stage加第三步如果精度瓶颈仍然存在再把注意力下沉到head。这样从“影响面最小”的位置开始验出问题时也容易定位。这是我踩过几次坑之后得到的经验。7. 工程落地的选型表和排错清单说了这么多最后落到实际操作上。我整理了一张表把我常用的模块按“参数规模、关注维度、推荐位置”做了个汇总方便直接对照选型方法主要关注维度新增参数推荐插入位置备注SE通道中任意stage推荐中深层数据量小时慎用ECA通道极低任意stage移动端首选CBAM通道空间低深层stage注意顺序问题Coordinate Attention通道位置低中深层stage分割检测友好SimAM空间能量零任意压缩场景首选Triplet Attention跨维度低探索性试验稳定性需要自己验DANet通道全局空间高低分辨率特征图显存敏感Axial Attention行列空间中深stage大图长距离依赖优选MSCA多尺度卷积空间低Backbone结构块推荐替换深stageEMA多尺度通道空间低Backbone或Neck轻量模型优选这张表只能作为起点。我见过太多人拿着论文里的模块往自己模型里塞然后跑了一轮发现没涨点就下结论说“方法没用”。实际上问题多半出在位置、超参、训练策略这些外围因素上。常见的几个坑我列一下排查思路涨点不稳定多次实验一会儿高一会儿低。先检查训练seed是否固定顺便把baseline也跑3个seed看看方差有多大。小模块带来的精度波动有时本来就小于随机噪声。加了注意力之后loss震荡变大。检查BN层位置注意力模块不要在BN前插入sigmoid输出范围在0到1之间如果BN统计被干扰训练会很难收敛。显存暴涨。优先怀疑长距离注意力把DANet换成轴向注意力或者把输入分辨率降下来很多时候就解决了。加载预训练权重时报key不匹配。这是因为新模块的权重是随机初始化的尤其SE这种全连接层初始化对前期训练影响很大。可以先固定backbone部分参数让注意力模块先训练几十轮再解冻整体微调。最后说说我现在的习惯。拿到一个新模块我第一件事不是看论文里的图表而是先做一个“小模型快跑”实验在现有baseline上选一个位置插入固定总训练轮数跑三个seed看均值有没有提升。如果提升超过baseline自身的标准差我才会继续深入调参。这个流程能省下大量时间也帮我筛掉了很多“看起来很美”的模块。卷积注意力这条线发展了好几年工具本身已经非常成熟真正的差距往往不在一篇论文的代码里而在怎么判断它适不适合你的任务、放在哪里、怎么训。
返回列表