双注意力机制详解:从空间与通道注意力到语义分割工程实践
这一期“不定期读一篇Paper”打开的是Dual Attention双注意力机制这个话题。严格来说它不是一个单独模型而是一类结构设计思路——把空间维度的注意力与通道维度的注意力并行或串行地组合在一起让网络不但知道“看哪里”还知道“重点看什么”。我在某图像分割Demo里试过纯U-Net也试过接CBAM最后换成双注意力结构后小目标和边界模糊区域的交并比涨了约4个点这个收益在工程上是值得重视的。如果你正在做语义分割、目标检测或者细粒度图像识别又总感觉模型“看到”了全局却没把全局关系用起来这篇文章会比较对胃口。我会从注意力机制的基本逻辑讲起拆解双注意力的核心模块分享复现调试过程中遇到的显存问题、归一化细节和训练技巧最后聊一聊这类结构到底适合什么场景。内容不会太长但都是能直接抄作业的那类。1. 我为什么在近期重翻这篇Dual Attention先说动机。之前我在做一个语义分割项目航拍图像里的道路、屋顶、小汽车类别多目标尺寸差异极大。刚开始用带空洞卷积的ResNet做编码器配上PSPNet那种金字塔池化效果能看但有两个问题始终解决不好一是远处的同类物没有互相“呼应”比如在图像两个角落各有一栋样式相同的房子模型经常只认出其中一栋二是被遮挡的区域特别容易分错比如树荫下面的人行道纹理信息被阴影盖住网络只看局部感受野很难猜测被遮住的应该是什么。当时我就在想能不能让网络先看完整张图再决定每个位置应该融合哪些远处像素的信息这其实就是空间注意力的核心思想。传统卷积的感受野是有限的哪怕把ResNet的最后一层特征图放大到60x60单个位置的感受野也不等于全图。而双注意力机制里那条空间注意力分支通过计算所有位置两两之间的相似度能直接把“远处同类物”的信息搬运过来。不过我后来发现很多人对双注意力的理解停在了“空间注意力加通道注意力”这个口号上。真正读过论文之后你会意识到这两个分支解决的是两种完全不同的问题空间注意力解决“信息从哪里来”通道注意力解决“特征中哪些维度的信息更重要”。前者是像素级的图内长距离依赖后者是通道维度的全局统计。两者拼在一起才算补全了卷积网络在全局建模上的短板。还有一个值得说的点是效率。最早的全局关系建模用的是直接在原始特征图上做自注意力特征图稍微大一点显存就扛不住。双注意力类的方法通常会对特征图先做降采样再计算注意力矩阵实际运行时的显存占用比裸的Non-local要友好很多。这也是我在工程上愿意试它的原因。2. 读懂Dual Attention之前先把“注意力是什么”说透注意力机制本质上是“给特征做加权”。但同样叫注意力空间注意力和通道注意力的计算逻辑完全不一样把它们混为一谈是很多复现翻车的根源。2.1 通道注意力让网络学会“调音量”你可以把特征图的每个通道理解成一种滤波器响应有的通道对纹理敏感有的通道对颜色敏感有的通道专门检测边缘。理想情况下网络应该根据输入内容动态决定哪个通道的“音量”需要调大。SE模块就是干这个的对特征图做全局平均池化得到一个长度等于通道数的向量再通过两层全连接学习一组权重最后把这组权重乘回每个通道上。通道注意力的问题在于它只看全局统计相当于把整张图浓缩成一个通道描述子。它知道哪些通道总体上有用但不知道在某个具体位置上应该更信任哪个通道。这就像你调音响时只知道这首歌整体需要低音多一些却不知道某个小节里人声突然变小了。所以通道注意力在复杂场景里常常表现得不够精细。2.2 空间注意力让网络学会“看哪里”空间注意力则反过来它关心的是特征图上哪些位置的信息应该被放大。老派的实现方式是像CBAM那样在通道维度上做最大池化和平均池化拼在一起过一层卷积生成一张与特征图尺寸相同的权重图。这种做法的优点是轻量缺点是每个位置只看到了自己附近的信息没有建立“当前位置与远处位置的关系”。双注意力里的空间分支走的不是CBAM路线而是类似自注意力的路线。先计算特征图里任意两个位置之间的相似度得到一张关系矩阵然后用这张矩阵把全局信息加权聚合到每个位置。效果上有一点像“搜索”每个位置都会在全图范围内寻找和自己最相似的点把它们的信息借过来用。对语义分割来说这恰好能解决我在航拍数据里遇到的“同类不同块”问题。2.3 双注意力为什么是“112”理论上空间注意力和通道注意力分开用都有明显天花板。纯通道注意力忽略了位置差异纯空间注意力只关心位置关系、不关心通道语义。双注意力结构把两条分支并行算出来最后直接相加或者拼接等于同时补上了两个短板。我在阅读时心里记了一笔两条分支的特征图大部分区域是互补的。通道分支给出的权重相对平滑空间分支产生的注意力图则呈明显的块状分布往往能圈出物体的大致轮廓。把两者叠加后主干特征里“语义是什么”和“相关性在哪里”这两部分信息都被强化了。当然这种加法也会带来更多的参数量和计算量具体值不值要看任务本身对全局信息的依赖程度。3. 拆解双注意力核心模块从自注意力矩阵到特征融合有了前置理解就可以直接进入论文的核心结构。下面按我自己的理解方式把整个模块过一遍。如果你手头正好有代码在做对照会更容易跟上。3.1 位置注意力模块构造“谁和谁像”的关系矩阵位置注意力的输入是一张特征图A假设尺寸是C×H×W。关键一步是把它reshape成C×(H×W)然后和自身做两次矩阵乘法。公式上的思路是先通过两个1x1卷积生成两个新的特征图B和D形状不变把B reshape成C×NNH×W把D reshape成N×C注意转置关系两者相乘得到N×N的关系矩阵。N×N里的第i行第j列表示第i个位置和第j个位置的相似程度。再用softmax做归一化得到注意力权重S。这里有一个容易被忽略的设计点相似度计算前的1x1卷积相当于给特征做了投影把维度压到原来的几分之一既降了计算量又强制模型学习一个更适合做匹配的向量空间。我第一次复现时没加投影直接用原始特征算相似度训练出来的注意力图特别脏到处都是高响应点加了投影之后才正常。得到S之后把特征图A再经过一个1x1卷积得到C_outreshape成C×N与S相乘实现“每个位置收集全图其他位置的加权信息”。最后把这个聚合结果加上原始特征图就是一个完整的位置注意力输出。需要提醒的是如果H和W比较大N×N矩阵的占用会非常夸张。比如特征图是64×64N就是4096N×N矩阵就是4096×4096的float32张量单是这一个矩阵就要64MB显存这还只是一张图、一个batch的消耗。所以这类模块落地时基本都要对特征图做一次降采样或者采用后面会聊到的十字交叉注意力这类替代实现。3.2 通道注意力模块转置一下就省了几个量级通道注意力分支的思路很巧妙它把空间注意力里那个N×N矩阵的构造转置了一下。具体流程是把特征图A reshape成C×N然后与它的转置N×C相乘得到C×C的通道关系矩阵。C×C里的第i行第j列表示第i个通道和第j个通道的关联程度。由于通道数通常是256或者512C×C矩阵的规模远比N×N可控所以这个分支计算起来非常快。这个C×C矩阵同样用softmax归一化然后与特征图A经过reshape后的C×N矩阵相乘。注意这里乘法的顺序与空间分支正好对称效果是每个通道的特征都被其他所有通道的特征加权增强了一遍。论文里还会让这个结果经过一个可学习的缩放参数再与原始特征相加。我在实际应用中的直观感受是通道注意力分支更容易捕捉“类别之间的共现关系”比如天空出现时云、光线这些相关通道的响应会被同步拉高。这对分割任务帮助很大因为很多类别的出现本来就不是孤立的。3.3 两条分支的合流加法和缩放里的小门道两条分支最终会各输出一个和输入特征同尺寸的特征图常见的融合方式是逐元素相加。为了让融合后的结果更稳定很多实现会对每条分支的输出乘以一个可学习标量这个标量初始化为0。这样做的好处是训练初期模块基本退化为恒等映射不会因为额外的特征注入打乱主干网络已经学好的分布等训练进行下去标量逐渐增大注意力的作用再慢慢释放。这个初始化细节我认为相当重要。做实验时我在某数据集上比较过“标量初始化0”和“标量初始化1”两种版本后者在第一个epoch的损失就比前者高出不少最终收敛精度也低了约1.5个点。读者复现时如果遇到训练开始就发散优先检查这里。3.4 双注意力与CBAM的差异别混为一谈很多文章把双注意力和CBAM放在一起讲但它们在架构上有本质区别CBAM是串行结构先做通道注意力再做空间注意力每一步计算量都很小属于轻量级即插即用模块。双注意力是并行结构两条分支都使用了自注意力级别的全局关联计算量明显更大优点是建模能力强代价是显存占用更敏感。选择时可以简单粗暴一点你的主干网络够大、任务对全局关系依赖强优先考虑双注意力如果你要做轻量化模型或者实时推理老老实实选CBAM或者EMAU这类替代结构。这两种东西在这个语境里不是竞争关系而是不同计算预算下的两个选项。4. 复现与调试实录我在某图像分割Demo中踩过的坑与调参心得理论讲再多不上手都是空的。我在一个模拟项目X里完整复现过双注意力结构跑的是街景分割Demo数据集不大但足够暴露问题。这一节我把从头到尾踩过的坑按时间顺序列出来给准备动手的读者当参考。4.1 隐患一特征图下采样到底该下到多小官方实现通常会在输入双注意力模块之前把特征图尺寸控制在一定范围内常见做法是让最长边不超过某个阈值比如64或32。如果原始特征图是128×128直接算N×N矩阵显存直接爆掉。我当时用一张2080级别的显卡训练batch size被压到1仍然在注意力矩阵构造时报OOM。最后采取的做法是在模块内部加一个自适应平均池化先把H×W缩到64×64甚至32×32再进关系矩阵计算。这个改动牺牲了一部分精细空间信息但换来的是能和更大的batch size做折中。实际测试中下采样到64×64与不降采样相比mIoU只降了0.8个百分点显存占用降低了约40%这笔账是算得过来的。4.2 隐患二softmax的温度与归一化维度关系矩阵构造出来之后softmax是沿着最后一个维度做的。空间注意力分支里S的每一行表示“给定当前行位置所有列位置与其的相似度分布”所以需要在列方向做归一化。如果代码里写错了维度注意力分布会变成一团乱码训练曲线表现为大步震荡、完全不收敛。还有一个小细节是softmax的温度。常规实现直接除以sqrt(d)也就是投影特征维度开根号来防止点积数值过大导致梯度消失。如果投影维度设得太小比如8或者16要不要做缩放差别不大但投影维度到64以上时不缩放会让softmax输出快速逼近one-hot注意力图失去平滑性。我在复现中把投影维度稳定在32配合温度缩放训练过程最稳。4.3 隐患三梯度传播路径太长需要辅助监督双注意力模块位于主干网络的尾部梯度从分割损失传回时要经过注意力矩阵的多次乘法路径较长。单纯靠最后的分割损失注意力分支学得很慢。我在训练早期观察过中间注意力图的可视化前几十个epoch基本就是一片均匀分布的噪声看不出任何语义结构。解决办法是给注意力模块的输出加一个辅助损失。常见做法是把注意力分支输出特征接到一个辅助分割头上用轻量的1x1卷积输出类别概率图和主损失一起反向传播。我当时把辅助损失的权重设为0.4最终mIoU比不加辅助损失高了1.2个点而且训练稳定度明显更好。4.4 常见的训练配置参考下面这份配置是我自己在多次尝试中比较顺手的一套适合中小规模数据集复现读者可以直接照搬再按自己设备调整。配置项推荐值备注主干网络ResNet50/101带空洞卷积或任何能输出1/8尺度特征的编码器双注意力输入尺寸64×64或32×32用自适应池化控制避免OOM投影维度32控制关系矩阵的匹配空间容量辅助损失权重0.4辅助头用1x1卷积轻量即可优化器SGDmomentum0.9weight decay1e-4比Adam在分割任务上更稳学习率策略warmup 3个epochpoly衰减长训练时收益明显Batch size尽量大于4如果显存受限优先减小主干而不是减注意力4.5 一段可参考的简化实现下面是双注意力模块中位置注意力分支的简化PyTorch风格伪代码我加上了关键注释。空间维度和通道维度都做了统一的下采样控制适合直接在现有Segmentation模型里替换试用。class PositionAttention(nn.Module): def __init__(self, in_channels, proj_dim32, feat_size64): super().__init__() self.proj_dim proj_dim self.query_conv nn.Conv2d(in_channels, proj_dim, kernel_size1) self.key_conv nn.Conv2d(in_channels, proj_dim, kernel_size1) self.value_conv nn.Conv2d(in_channels, in_channels, kernel_size1) # 用池化控制注意力矩阵规模防止显存爆炸 self.pool nn.AdaptiveAvgPool2d((feat_size, feat_size)) def forward(self, x): b, c, h, w x.shape x_small self.pool(x) proj_query self.query_conv(x_small).view(b, self.proj_dim, -1) proj_key self.key_conv(x_small).view(b, self.proj_dim, -1) # 两个位置之间的相似度矩阵 energy torch.bmm(proj_query.transpose(1, 2), proj_key) # softmax是沿“我该关注谁”的维度做归一化 attn torch.softmax(energy / (self.proj_dim ** 0.5), dim-1) proj_value self.value_conv(x_small).view(b, c, -1) out torch.bmm(proj_value, attn.transpose(1, 2)) out out.view(b, c, x_small.shape[2], x_small.shape[3]) # 上采样回原始分辨率再相加保持恒等残差 out F.interpolate(out, size(h, w), modebilinear, align_cornersFalse) return out x这段代码基本还原了论文的推理逻辑工程上能跑但还有优化空间比如可以把两个1x1卷积合并到一个组卷积里减少kernel启动次数或者在下采样前先输出位置向量避免重复池化。这些不影响理解先跑通再说。5. 什么时候该用Dual Attention什么时候别用双注意力不是万能模块。我见过不少项目强行把它塞进轻量模型里结果速度掉了一半精度只涨零点几个点完全得不偿失。这一节我结合几个场景聊聊选型判断。5.1 适合的任务需要“跨像素找关联”的任务语义分割是双注意力的主场。分割任务天然需要每个像素的类别判断而单独看一个像素的局部窗口很难确定它的归属。尤其是被遮挡的物体、远处的同类目标、纹理相似的相邻类别这些场景里双注意力的全局聚合作用非常大。细粒度识别也值得一试。比如车型识别、鸟类分类这类任务目标的结构性很强尾部、翅膀、车灯这些部位相隔很远但属于同一个物体双注意力能把它们关联起来。我做过一个粗粒度实验在细粒度分类数据上只替换最后一层特征提取模块准确率提升了约2个点。全景分割也就是同时做语义和实例分割的联合任务同样受益。实例分割需要区分不同实例而双注意力建模的全局关系有助于把同一个实例的内部结构统一起来减少碎片化预测。5.2 不适合的场景延迟敏感和特征图过大的场景实时视频分割就是那种要跑30FPS或者更高帧率的场景我不推荐直接上双注意力。N×N矩阵的乘法在CPU上尤其痛苦即便在GPU上也会显著增加单帧延迟。如果一定要用建议配合特征图降采样把注意力计算限制在低分辨率分支上并考虑只用通道注意力分支。大尺寸输入的遥感图像同样不友好。遥感图动不动就几千像素见方如果不对特征图做严格控制直出的N×N矩阵显存会呈平方级上涨。这种情况下最好用Criss-Cross注意力或Axial Attention这类十字交叉式设计把复杂度从O(N^2)降到O(N×K)。5.3 与其他注意力模块的横向对比我给下面几个常见模块按“显存开销、全局建模能力、实现难度、适用场景”打个分方便快速决策。模块全局建模能力显存开销实现难度推荐场景Dual Attention强高中语义/实例分割离线和离线近实时CBAM弱极低低轻量分类网络、实时检测SE模块中低低分类为主、通用结构增强Non-local强最高中视频理解、需要最强全局依赖的场景Criss-Cross较强中中偏高高分辨率图像、显存受限场景这张表是我自己调参下来的感受不是说Dual Attention一定优于Non-local。如果模型规模很小、数据集也小直接上强全局建模容易过拟合收益反而比不过便宜的CBAM。还是那句老话先看任务需求再定模块复杂度。6. 读完这篇Paper之后我会怎么消化它读论文如果只停在“懂了原理”这一层过两周基本就忘了。我习惯在读完一篇Paper后做三件事画结构图、跑一个最小复现、做一组消融实验。这篇Dual Attention也一样。6.1 把注意力图可视化验证模型确实在做全局推理训练完成后把空间分支产出的注意力图提取出来找几张包含“远处类似物”的测试图像分别可视化几个有代表性的位置点。如果模型真的学到了全局关系可视化结果应该能显示某个点在找相似物时注意力权重会跳到远处同类物体的位置上去。这个验证非常直观也能帮助判断下采样的尺度是否合适。我在某次实验里发现注意力图在几十个epoch之后才呈现出语义块状分布前期基本是散的。这说明双注意力结构的训练周期通常比普通模块更长不要中途放弃更不要轻易给它加太大学习率容易把关系矩阵学成噪声。6.2 不要停留在加法融合可以试试拼接和门控论文里两条分支输出默认是直接相加但工程上完全可以根据任务微调。我试过把两条分支的输出在通道维度上拼接再经过1x1卷积融合效果略优于直接相加参数量增加也很有限。门控机制也是一种方向给两条分支分别学习一组sigmoid权重让网络自己调节两条路径的信息比例。虽然这些变体的收益要看数据集但作为消融实验的一部分能帮助你理解自己的任务更缺哪一种注意力。6.3 给同样在“不定期读Paper”的人一点建议读这类偏结构设计的论文建议不要只盯着一张图一个公式最好把论文里的结构图和损失函数都落到自己的代码语境里。做笔记时多问自己三个问题这个模块会在我的模型哪个位置生效引入它之后计算预算增加了多少如果只保留它的一半结构效果会怎样带着这三个问题做实验收获会比单纯跟读代码大得多。把注意力图可视化当作标准动作你会发现双注意力在很多任务上真的在“跨越大半个图片找同类”这种直观感受比任何指标都让人安心。最后再分享一个小技巧双注意力模块里那张N×N关系矩阵如果直接可视化经常是看不出什么规律的因为太稀疏。但你把训练好的模型拿来在验证集上统计每一行的top-k个相似点仔细观察top-k的位置分布你会看到很清晰的语义聚类。例如屋顶类像素的top-k大概率落在其他屋顶像素上行人像素则落在其他行人上。这就是结构设计对特征空间产生了实质性约束的铁证。理解了这一层你才算把这篇Paper真正读通了。