资讯动态

双层注意力网络SAN:视觉问答中的多步推理与PyTorch复现

发布时间:2026/10/13 2:09:55 来源:尧图企业网站定制
1. 从标题拆解这篇VQA工作到底在解决什么问题1.1 视觉问答任务的本质难点视觉问答Visual Question AnsweringVQA这个任务说白了就是给机器一张图再问它一个用自然语言描述的问题让它像人一样给出答案。比如给一张厨房的照片问“台面上有几个苹果”模型需要同时理解图像内容识别出苹果、数出数量和问题语义问的是数量而不是颜色最后输出“3”这个答案。听起来简单但真正做过这个方向的人都知道难点集中在两个层面。第一个层面是跨模态对齐图像是像素矩阵问题是词序列两者处于完全不同的表示空间怎么让模型知道问题里的“苹果”对应图像里哪块区域第二个层面是推理的层次性很多问题不是看一眼就能答的需要多步推理。比如“桌上那个红色的杯子旁边是什么颜色的盘子”模型得先定位红色杯子再找它旁边的盘子最后判断颜色。这种链式推理对2015年前后的模型来说相当吃力。T. Do等人在ArXiv上挂出的这篇SANStacked Attention Networks堆叠注意力网络工作正是冲着第二个难点去的。标题里“双层注意力”这个说法指的就是它用两层注意力机制来逐步聚焦图像区域模拟人类“先大致看再仔细看”的认知过程。1.2 为什么是“堆叠”而不是“单层”在SAN之前VQA领域已经有人用单层注意力做跨模态对齐了。单层注意力的逻辑是用问题向量去查询图像特征算出一组权重加权求和得到一个“问题相关的图像表示”然后拿这个表示去预测答案。这套流程能work但有个明显短板——一次查询只能聚焦一个区域或一种语义。我举个具体的例子你就明白了。假设问题是“图中的人手里拿的是什么”单层注意力可能会把权重分散到“人”和“手里的物体”两个区域上因为它只有一次查询机会没法区分“先找人再看手”这个顺序。结果就是图像表示里混入了人的身体、背景等无关信息答案预测的准确率上不去。SAN的解决思路很直接既然一次不够那就来两次。第一层注意力先根据问题定位到大致相关的区域比如人的位置第二层再在这个基础上进一步聚焦比如手的区域最终得到一个更精准的图像表示。这个“堆叠”的设计本质上是在用注意力的层数换取推理的深度。1.3 这篇工作的历史位置与影响2015年前后是VQA研究的爆发期各种注意力机制层出不穷。SAN的价值在于它把“多步推理”这个思路引入到了VQA的注意力设计中而且实现得足够简洁——没有复杂的记忆网络没有外部知识库就是两层注意力堆叠端到端训练。这种简洁性让它的代码很容易复现也方便后续研究在此基础上做改进。从影响范围看SAN之后出现了大量“多跳注意力”“迭代注意力”的变体虽然具体实现各有不同但核心思想都能追溯到SAN这里。对于想入门VQA注意力机制的人来说SAN是一个非常好的起点结构不复杂代码量适中效果在当时也拿得出手。2. 双层注意力的核心设计拆解2.1 整体架构从图像和问题到答案的完整链路SAN的整体流程可以拆成四个阶段图像特征提取、问题特征提取、双层注意力推理、答案预测。我用一个具体的配置来说这样你脑子里能有个清晰的画面。图像侧用预训练的CNN比如VGG16提取特征。假设输入图像是224x224VGG16的最后一个卷积层输出是14x14x512的特征图展平后就得到196个512维的区域特征向量。每个向量对应原图的一个14x14像素块感受野更大但大致可以这么理解。这196个向量就是后续注意力要“查询”的对象。问题侧用LSTM或GRU对问题词序列做编码。假设问题是“what color is the cup”分词后得到5个词每个词先过embedding层变成300维向量再送入LSTM。LSTM最后一个时间步的隐藏状态比如512维就作为整个问题的表示向量。接下来是核心的双层注意力。第一层注意力用问题向量去查询196个图像区域特征算出196个权重加权求和得到第一个图像上下文向量。然后把这个上下文向量和问题向量拼接或相加形成新的查询向量再去查询196个区域特征得到第二组权重和第二个上下文向量。最后把第二个上下文向量和问题向量一起送入分类器输出答案。2.2 第一层注意力粗定位的实现细节第一层注意力的计算过程用公式说就是给定图像特征矩阵V196x512和问题向量q512维先算注意力分数。具体怎么算SAN用的是加性注意力也叫Bahdanau注意力。先把V的每一行v_i和q分别过一个线性层映射到同一个维度比如512维然后相加再过tanh激活最后过一个线性层得到标量分数。公式写出来是score_i w^T * tanh(W_v * v_i W_q * q b)其中W_v、W_q是线性层的权重w是最后的投影向量。算完196个分数后过softmax归一化得到196个权重α_i。然后加权求和context_1 Σ α_i * v_i这个context_1就是第一层注意力的输出它代表了“根据问题粗筛出来的图像信息”。我实测下来第一层注意力的权重分布通常比较分散会覆盖多个相关区域。比如问“人在做什么”权重可能同时落在人的身体、手、周围物体上。这符合“粗定位”的预期。2.3 第二层注意力精定位的递进逻辑第二层注意力的输入变了。查询向量不再是原始的问题向量q而是q和context_1的融合。融合方式有两种常见选择相加或拼接后过线性层。SAN原文用的是相加即q_2 q context_1然后拿q_2去查询同一组图像特征V重复上面的加性注意力计算得到第二组权重β_i和第二个上下文向量context_2。这里有个细节值得注意第二层查询的仍然是原始的V而不是第一层加权后的特征。这意味着第二层可以“推翻”第一层的判断重新分配权重。实际效果是第二层的权重分布明显更集中通常会聚焦到一两个关键区域上。还是用“人在做什么”这个例子。第一层权重分散在人、手、物体上第二层可能就把权重集中到了手和物体的交互区域因为q_2里已经包含了“人”的信息模型知道“人”已经被考虑过了现在需要关注更细的线索。2.4 答案预测与损失函数拿到context_2后把它和问题向量q拼接送入一个多层感知机MLP最后接softmax输出答案概率分布。损失函数用的是标准的交叉熵训练时用Adam优化器学习率设0.001左右batch size根据显存来定常见的是64或128。这里有个工程上的小技巧答案空间通常很大VQA v1数据集有几千个答案但大部分答案出现频率极低。实际训练时一般会截断只保留出现次数超过某个阈值比如9次的答案把剩下的归为“其他”类。这样分类数能降到3000左右训练更稳定。3. 实操复现从数据准备到模型训练3.1 数据集的获取与预处理复现SAN最直接的数据集是VQA v1。这个数据集包含约20万张图像来自COCO数据集和约60万个问题-答案对。下载后你会得到三个主要文件图像压缩包、问题JSON文件、答案JSON文件。预处理的第一步是图像特征提取。因为SAN原文用的是VGG16我建议你也从VGG16开始方便对照结果。用PyTorch的话加载预训练的VGG16去掉最后的分类层保留到最后一个卷积层。对每张图像做标准化减均值除标准差然后前向传播得到14x14x512的特征图。展平后存成numpy数组shape是(196, 512)。这里有个坑要注意VGG16的输入是224x224但COCO图像尺寸不一需要先resize。直接拉伸会导致形变影响特征质量。我的做法是先按短边缩放到224然后中心裁剪224x224。这样能保留图像主体内容避免形变。问题侧的处理相对简单。用spaCy或nltk分词建立词表。词表大小控制在10000左右出现次数少于2次的词统一映射到 。每个问题截断或填充到最大长度比如14个词然后转成索引序列。3.2 模型代码的关键实现下面是我实际用过的PyTorch实现核心部分。先定义注意力层import torch import torch.nn as nn import torch.nn.functional as F class AttentionLayer(nn.Module): def __init__(self, v_dim, q_dim, hidden_dim): super().__init__() self.W_v nn.Linear(v_dim, hidden_dim) self.W_q nn.Linear(q_dim, hidden_dim) self.w nn.Linear(hidden_dim, 1) def forward(self, V, q): # V: (batch, num_regions, v_dim) # q: (batch, q_dim) v_proj self.W_v(V) # (batch, num_regions, hidden_dim) q_proj self.W_q(q).unsqueeze(1) # (batch, 1, hidden_dim) scores self.w(torch.tanh(v_proj q_proj)).squeeze(-1) # (batch, num_regions) weights F.softmax(scores, dim-1) context torch.bmm(weights.unsqueeze(1), V).squeeze(1) # (batch, v_dim) return context, weights然后是完整的SAN模型class SAN(nn.Module): def __init__(self, vocab_size, embed_dim, q_dim, v_dim, hidden_dim, num_answers): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, q_dim, batch_firstTrue) self.att1 AttentionLayer(v_dim, q_dim, hidden_dim) self.att2 AttentionLayer(v_dim, q_dim, hidden_dim) self.classifier nn.Sequential( nn.Linear(v_dim q_dim, 1024), nn.ReLU(), nn.Dropout(0.5), nn.Linear(1024, num_answers) ) def forward(self, V, question): # V: (batch, num_regions, v_dim) # question: (batch, seq_len) embed self.embedding(question) _, (h_n, _) self.lstm(embed) q h_n.squeeze(0) # (batch, q_dim) context1, _ self.att1(V, q) q2 q context1 context2, _ self.att2(V, q2) combined torch.cat([context2, q], dim-1) logits self.classifier(combined) return logits这段代码里有个细节LSTM的隐藏状态h_n取的是最后一个时间步的输出。如果用了双向LSTM需要把两个方向的最后状态拼接起来。SAN原文用的是单向LSTM我建议先按原文来复现结果后再尝试改进。3.3 训练配置与参数选择训练时的超参数设置我列一个实际跑过的配置供参考参数取值说明图像特征维度512VGG16最后卷积层通道数区域数量19614x14展平词嵌入维度300可用预训练GloVe初始化LSTM隐藏维度512问题向量维度注意力隐藏维度512加性注意力的中间层batch size64显存8G以上可设128学习率0.001Adam优化器dropout0.5分类器前训练轮数20-30早停法监控验证集损失函数用交叉熵标签平滑可以加一点比如0.1能缓解过拟合。学习率调度用ReduceLROnPlateau验证集准确率连续3轮不升就降一半。训练过程中要监控两个指标训练集准确率和验证集准确率。如果训练集涨到60%以上但验证集卡在55%左右说明过拟合了可以加大dropout或加权重衰减。如果两个都低可能是欠拟合需要增加模型容量或训练轮数。3.4 训练时间与硬件需求用单张1080Ti11G显存跑VQA v1batch size 64一个epoch大约需要40分钟。20个epoch就是13个小时左右。如果用2080Ti或3090时间能缩短到6-8小时。CPU训练基本不可行太慢了。图像特征提取是一次性的提前跑完存成npy文件训练时直接加载能省不少时间。196x512的float32数组20万张图像大约占15G磁盘空间建议用内存映射或分片加载。4. 常见问题与排查技巧实录4.1 注意力权重不收敛怎么办这是复现SAN时最容易遇到的问题。表现是训练loss正常下降但把注意力权重可视化出来一看196个权重几乎均匀分布没有聚焦效果。排查思路分三步。第一步检查问题向量的质量。如果LSTM没训练好问题向量本身就是噪声注意力自然学不到东西。可以先把LSTM单独拿出来做个简单的问题分类任务确认它能提取有意义的语义。第二步检查注意力层的初始化。加性注意力里的W_v、W_q如果初始化方差太大tanh会饱和梯度传不回去。建议用xavier初始化偏置设0。第三步检查学习率。注意力层的参数对学习率比较敏感0.001可能太大。我试过降到0.0005注意力权重明显更集中了。如果还不行可以给注意力层单独设更小的学习率。4.2 验证集准确率上不去的调参方向VQA v1上SAN的合理准确率范围是55%-58%开放域。如果卡在50%以下可以从这几个方向调图像特征质量换用ResNet或更深的VGG特征表达能力更强。但要注意维度对齐ResNet50最后卷积层是2048维注意力层的输入维度要相应调整。问题编码器把单向LSTM换成双向或者用GRU替代。双向LSTM能同时利用问题的前后文信息对“not”“except”这类词更敏感。注意力层数SAN原文是两层可以试试三层。但层数太多会过拟合而且计算量增加明显。我的经验是两层到三层之间收益最大再往上就饱和了。答案空间检查答案截断阈值。阈值太高会导致很多正确答案被归为“其他”模型学不到。阈值太低则分类数太多训练困难。9次是个比较平衡的值。4.3 显存不足的优化方案196个区域特征batch size 64光图像特征就占64x196x512x4字节≈25MB加上模型参数和中间激活8G显存勉强够用。如果显存更小可以减小batch size到32或16但要注意学习率相应调小。用梯度累积比如batch size设16累积4步再更新等效batch size 64。图像特征用float16存储训练时再转float32。能省一半显存但要注意数值稳定性。减少区域数量比如把14x14改成7x7区域数降到49。但这样会损失空间细节准确率可能降1-2个点。4.4 常见问题速查表问题现象可能原因解决方法loss不下降学习率太小或太大尝试0.0001-0.001范围注意力均匀分布问题向量质量差预训练LSTM或加大问题编码器容量验证集准确率波动大batch size太小增大batch size或加梯度累积训练集准确率远高于验证集过拟合加dropout、权重衰减、数据增强预测结果偏向高频答案类别不平衡对损失函数加权或重采样图像特征加载慢磁盘IO瓶颈转成内存映射格式或预加载到内存5. 从SAN出发的改进思路与扩展方向5.1 注意力机制的变体尝试SAN的双层注意力是最基础的堆叠形式。后续研究里出现了几种有意思的改进我挑两个实际试过的说说。第一种是残差注意力。第二层注意力的查询向量不只用qcontext_1还加上了原始问题向量q的残差连接。这样能防止第一层注意力跑偏后第二层无法纠正。实现上就是把q_2 q context_1改成q_2 q context_1 α*qα是个可学习的标量。我实测在VQA v1上能涨0.5个点左右。第二种是多头注意力。把单头注意力换成8头每个头学不同的关注模式。有的头关注颜色有的头关注空间关系。最后把8个头的上下文向量拼接起来。这个改动计算量增加不多但准确率能涨1-2个点。不过要注意头数太多会过拟合4-8头比较合适。5.2 与其他模态的融合SAN只用了图像和问题两个模态。如果想引入外部知识比如常识库可以在注意力层之后加一个知识检索模块。具体做法是用context_2去查询知识库检索出相关三元组编码后和context_2拼接再送分类器。这个思路在需要常识推理的问题上效果明显。比如“香蕉是什么颜色的”纯视觉模型可能因为光照原因判断成绿色但知识库能提供“香蕉通常是黄色”这个先验。不过知识库的构建和检索本身是个大工程适合有资源的研究团队做。5.3 代码复现的工程建议如果你打算把SAN作为baseline做后续研究我有几个工程上的建议。第一把图像特征提取和模型训练解耦。特征提取写成一个独立脚本输出npy文件。训练脚本只负责加载特征和训练模型。这样调模型时不用反复跑CNN能省大量时间。第二用TensorBoard或WandB记录训练过程。loss曲线、准确率曲线、注意力权重可视化都记下来。调参时回看这些记录比凭感觉试效率高得多。第三写单元测试。注意力层的输出shape、softmax后的权重和是否为1、分类器的输出维度这些都用小规模数据测一遍。能避免很多低级bug。第四版本控制。每次改超参数或模型结构都commit一次commit message写清楚改了什么、预期效果是什么。我踩过的坑是改了学习率但忘了记录结果一周后复现不出之前的好结果。5.4 实际项目中的取舍经验最后分享一个我在实际项目中的体会SAN这种双层注意力在问题长度较短10个词以内、图像内容相对简单物体数量少、背景干净的场景下效果很好。但如果问题很长、图像很复杂两层注意力就不太够了。我试过一个医疗影像的VQA任务问题是“左肺上叶的结节边缘是否光滑”这种问题需要定位到非常具体的解剖区域两层注意力经常聚焦到错误的肺叶。后来改成四层注意力并且每层都加上位置编码才把准确率提上来。所以注意力层数不是固定的要根据任务复杂度来调。另一个体会是SAN对问题中的否定词和比较词处理得不好。比如“图中不是红色的物体是什么”模型经常忽略“不是”直接找红色物体。这个问题在后续的注意力变体里也没有很好解决可能需要引入专门的逻辑推理模块。如果你做的任务里这类问题多建议在问题编码阶段就加入否定词检测把否定信息显式地编码进问题向量。代码复现这件事最怕的就是“看起来懂了但跑不起来”。SAN的代码量不大核心就是注意力层那几十行。建议你先把注意力层单独拿出来用随机数据跑通确认shape和梯度都正常再往里面加LSTM和CNN。这样出问题时容易定位不会一上来就被整个模型的复杂度淹没。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑