Q、K、V 从何而来:Attention 的演化与直觉

7 minute read

Published:

为什么每个 token 已经有一个向量,Attention 还要把它变成 Query、Key、Value 三份?答案藏在 Transformer 出现之前。

QKV 不是数学定理要求的唯一结构,而是围绕一个问题逐渐形成的设计:怎样让神经网络根据当前需要,从一组信息里读出有用的内容? 沿着机器翻译这条线,三个字母就不再显得突然。

从“记住整句话”,到“随时回看原文”

早期一种典型的机器翻译方案是 encoder–decoder:encoder 用 RNN 或 LSTM 读完源句,把它压成固定维度的编码 $c$;decoder 以此为条件,逐词生成译文。2014 年的 Seq2Seq 是其中的重要代表。[1]

这像读完材料后把原文收起来,只带着一张固定大小的笔记完成翻译。句子越长,细节越难保留。一个自然的改进是:把原文每个位置的表示都留下,让 decoder 需要时回头读取。

从固定句子编码,到按需读取原文上半图:源句经过编码器变成一个固定大小的向量 c,供所有解码步骤使用。下半图:保存每个源位置的表示,当前解码状态决定如何加权读取它们。 固定编码:每一步读同一份摘要 源句 → 编码器 c 生成译文 全部源句信息经过固定大小的 c Attention:每一步重新决定读哪里 h₁ · 小猫 h₂ · 坐在 h₃ · 垫子上 当前步骤的 cₜ
图 1 从共用一个句子编码,改成每一步按需读取。h₁、h₂、h₃ 是包含上下文的源位置表示,cₜ 是当前步骤读出的信息。

Attention:每一步,重新决定读哪里

把“小猫坐在垫子上”翻译成“The cat sits on the mat”,生成 cat 时应该更多参考“小猫”,生成 sits 时则更多参考“坐在”。我们希望模型学会类似这样的权重:

当前生成小猫坐在垫子上
cat0.850.100.05
sits0.100.800.10
mat0.050.100.85

这些数值仅作示意。实现时,用 decoder 当前状态 $s$ 与每个源位置的表示 $h_j$ 打分,再用 softmax 转为权重:

\[e_j=\operatorname{score}(s,h_j),\qquad \alpha_j=\frac{\exp(e_j)}{\sum_r\exp(e_r)}.\]

然后读取一份适合当前步骤的上下文:

\[c=\sum_j\alpha_jh_j.\]

decoder 用它来帮助预测目标词。下一步状态变了,读取重点也可以改变。Bahdanau 等人在 2014 年发布、发表于 ICLR 2015 的工作,让模型把翻译与这种“软对齐”一起学出来。[2]

“软”是指不必只选一个位置,而是连续地混合读取多个位置。整条计算链可微,翻译损失可以反向传播,通常不需要人工标注每个词该对齐哪里。

最初的打分可以由一个小神经网络完成;2015 年 Luong 等人又研究了点积等形式。[3] Attention 的核心是按需加权读取,点积只是打分的一种实现。

Q、K、V:把“找到”与“读出”分开

回看刚才的过程,三个角色已经出现了:

角色在早期翻译 attention 中
Query:发起读取Decoder 当前状态 $s$
Key:供查询匹配源位置表示 $h_j$
Value:被加权读取还是同一个 $h_j$

早期的 $h_j$ 同时承担了 Key 和 Value:既决定“读哪里”,又充当“读出的内容”。 但这两件事不一定要用同一套表示。

想象查询“财务部的张三”:定位时需要姓名、部门,找到后想取回的却是电话号码。神经网络也可以分别学习适合匹配和适合传递的表示。2015 年的 End-to-End Memory Networks、2016 年的 Key-Value Memory Networks 已体现了这种分工。[4][5]

一次 attention 读取:Query 和 Key 决定权重,权重作用于 Value查询 q 与候选 k1、k2、k3 匹配后,假设得到权重 0.1、0.8、0.1。使用同样的权重汇总 v1、v2、v3,得到输出 o。Key 与 Value 按位置一一对应。 Query · 当前需求 q Key · k₁Key · k₂Key · k₃ 匹配分数 → softmax → 读取权重 0.10.80.1 Value · v₁Value · v₂Value · v₃ 输出 o = 0.1v₁ + 0.8v₂ + 0.1v₃
图 2 Q 与 K 决定权重,再用这些权重汇总对应的 V。权重为人为构造的示意值。

于是,QKV 可以理解为一次“软查询”:Q 提供需求,K 用于定位,V 提供内容。 它不像普通字典只返回精确匹配的一项,而是对多个候选做加权汇总。

为什么还要学习三组投影?

下面是设计上的解释,不是对历史的逐步复述。假设位置 $i$ 的表示是行向量 $x_i$,直接令 $q_i=k_i=v_i=x_i$,依然能做 attention。独立投影的意义在于给不同角色更多自由:

\[\begin{aligned} q_i&=x_iW_Q,\\ k_i&=x_iW_K,\\ v_i&=x_iW_V. \end{aligned}\]

首先,有关系不等于很相似。 在“小猫吃了鱼”中,“吃”可能需要读取“小猫”的信息,但动作与实体未必在原始表示空间里相近。分别变换查询方和候选方,可以学习更合适的兼容关系:

\[q_i k_j^\top=x_iW_QW_K^\top x_j^\top.\]

这种关系还可以有方向性:“动作寻找施事者”,不必与“实体寻找动作”采用同样的打分。$W_V$ 则另外学习选中一个位置后,应该传递什么内容。

三组矩阵通过任务损失共同训练;我们并没有人工规定哪一维代表“主语”,也没有保证某个 head 专门处理指代。

手算一次,看清匹配与内容的区别

设查询 $q=[1,0]$,有三个候选位置:

位置KeyValue
A$[2,0]$$[1,0]$
B$[0,2]$$[0,1]$
C$[1,1]$$[1,1]$

点积分数是 $[2,0,1]$。按 Transformer 的做法除以 $\sqrt{2}$,再做 softmax,权重约为 $[0.576,0.140,0.284]$。最后汇总的是 Value:

\[\begin{aligned} o&=0.576v_A+0.140v_B+0.284v_C\\ &\approx[0.860,0.424]. \end{aligned}\]

保持 Q、K 不变,只改 V,权重不变,读出的内容却会变;保持 V 不变,只改 Q 或 K,则会改变混合比例。这就是匹配与内容传递的分工。

Self-attention:让同一句话内部相互读取

早期翻译 attention 的查询来自译文 decoder,被读取的信息来自源句 encoder。再往前一步:同一句话的每个位置,也可以查询其他位置。

例如“小猫吃完鱼后,它睡着了”,“它”可以通过读取前文,形成更有用的表示。令上一层各位置的表示组成矩阵 $X$,同一份输入分别产生 Q、K、V:

Self-attention:同一份输入分成查询、匹配和内容三条路径上一层各位置的表示 X 分别乘以 WQ、WK、WV,形成 Q、K、V。Q 与 K 计算注意力矩阵 A,A 对 V 加权汇总得到本头输出 O。 上一层各位置的表示 X Q = XWQ K = XWK V = XWV QKᵀ / √dₖ → softmax A:谁从哪里读多少 O = AV · 汇总内容
图 3 “Self”指查询与被读取的信息来自同一组位置,不是只关注自己。图中画的是一个 head。

把所有位置的查询一起计算,就得到熟悉的公式:

$$ O=\operatorname{softmax}_{\mathrm{row}} \left(\frac{QK^\top}{\sqrt{d_k}}\right)V. $$

$QK^\top$ 是位置之间的打分表:第 $i$ 行表示查询 $i$ 对各候选位置的分数。逐行 softmax 后,乘以 V 完成读取。除以 $\sqrt{d_k}$ 用于控制点积尺度,避免 softmax 过于饱和;多头则让多组投影分别读取,再组合结果。[6]

2017 年的 Transformer 把 self-attention 放到跨位置交互的中心,结合位置编码、前馈网络等组件构成完整架构。[6] 它不是 attention 的起点。QKV 也不是凭空冒出的三个矩阵:它们把“当前需要什么”“怎样匹配”“传递什么”变成了可以分别学习的功能。

文末一笔:KV cache

因果 self-attention 生成新 token 时,用新位置的 Q 查询历史位置的 K、V。旧位置看不到未来,已有前缀不变时,它们在各层算出的 K、V 就可以缓存复用。旧 Q 通常不需要保留,因为下一次查询由新位置发起。这便是 KV cache:用存储换取少做历史位置的重复计算。

参考文献