这一期“不定期读一篇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 双注意力为什么是“1+1>2”
理论上,空间注意力和通道注意力分开用都有明显天花板。纯通道注意力忽略了位置差异,纯空间注意力只关心位置关系、不关心通道语义。双注意力结构把两条分支并行算出来,最后直接相加或者拼接,等于同时补上了两个短板。
我在阅读时心里记了一笔:两条分支的特征图大部分区域是互补的。通道分支给出的权重相对平滑,空间分支产生的注意力图则呈明显的块状分布,往往能圈出物体的大致轮廓。把两者叠加后,主干特征里“语义是什么”和“相关性在哪里”这两部分信息都被强化了。当然,这种加法也会带来更多的参数量和计算量,具体值不值,要看任务本身对全局信息的依赖程度。
3. 拆解双注意力核心模块:从自注意力矩阵到特征融合
有了前置理解,就可以直接进入论文的核心结构。下面按我自己的理解方式,把整个模块过一遍。如果你手头正好有代码在做对照,会更容易跟上。
3.1 位置注意力模块:构造“谁和谁像”的关系矩阵
位置注意力的输入是一张特征图A,假设尺寸是C×H×W。关键一步是把它reshape成C×(H×W),然后和自身做两次矩阵乘法。
公式上的思路是:先通过两个1x1卷积生成两个新的特征图B和D,形状不变;把B reshape成C×N(N=H×W),把D reshape成N×C(注意转置关系),两者相乘,得到N×N的关系矩阵。N×N里的第i行第j列表示第i个位置和第j个位置的相似程度。再用softmax做归一化,得到注意力权重S。
这里有一个容易被忽略的设计点:相似度计算前的1x1卷积相当于给特征做了投影,把维度压到原来的几分之一,既降了计算量,又强制模型学习一个更适合做匹配的向量空间。我第一次复现时没加投影,直接用原始特征算相似度,训练出来的注意力图特别脏,到处都是高响应点,加了投影之后才正常。
得到S之后,把特征图A再经过一个1x1卷积得到C_out,reshape成C×N,与S相乘,实现“每个位置收集全图其他位置的加权信息”。最后把这个聚合结果加上原始特征图,就是一个完整的位置注意力输出。
需要提醒的是,如果H和W比较大,N×N矩阵的占用会非常夸张。比如特征图是64×64,N就是4096,N×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或者512,C×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卷积,轻量即可 |
| 优化器 | SGD,momentum=0.9,weight decay=1e-4 | 比Adam在分割任务上更稳 |
| 学习率策略 | warmup 3个epoch,poly衰减 | 长训练时收益明显 |
| Batch size | 尽量大于4 | 如果显存受限,优先减小主干而不是减注意力 |
4.5 一段可参考的简化实现
下面是双注意力模块中位置注意力分支的简化PyTorch风格伪代码,我加上了关键注释。空间维度和通道维度都做了统一的下采样控制,适合直接在现有Segmentation模型里替换试用。
class PositionAttention(nn.Module): def __init__(self, in_channels, proj_dim=32, feat_size=64): super().__init__() self.proj_dim = proj_dim self.query_conv = nn.Conv2d(in_channels, proj_dim, kernel_size=1) self.key_conv = nn.Conv2d(in_channels, proj_dim, kernel_size=1) self.value_conv = nn.Conv2d(in_channels, in_channels, kernel_size=1) # 用池化控制注意力矩阵规模,防止显存爆炸 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), mode='bilinear', align_corners=False) 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真正读通了。