缩放点积注意力

给注意力装上可学习的参数,让模型自己决定「该关注谁」。

知识课 · 约 30 分钟 · 更新于 2026-08-14

问题:注意力还「不会学习」

在「注意力的直觉」中,我们用「点积打分 → softmax 归一化 → 加权求和」三步,让每个词的向量混合了上下文。但那套计算有一个致命缺陷: 没有任何可学习的参数 。

「苹果」和「手机」的相关度,永远等于它们静态词向量的点积。无论训练什么任务、 无论模型多想调整「该关注谁」,这个分数都纹丝不动。这节课就来给注意力装上参数, 解决这个问题。

先统一记号

后面要频繁谈论向量的维度,先给两个量起好名字,之后一直沿用:

三种身份:Query、Key、Value

解决方案的核心想法: 在算点积之前,先让每个词向量经过可学习的线性变换 。 变换矩阵是参数,训练时可以更新,「该关注谁」就变成可学习的了。

同一个词在注意力计算中还扮演着 三种不同的身份 , 我们给每种身份单独配一个变换矩阵:

其中 是把句子里 L 个词向量按行堆叠成的 矩阵。 和之前见过的线性层 是同一种变换,只是按行整批作用、不带偏置; 是三个可学习的变换矩阵,随训练更新,代码里就是三个 nn.Linear (调用 W_Q(X) 即做 )。于是打分变成「我的 Query 点积你的 Key」,混合的内容变成「你的 Value」, 关注谁、取什么信息,全部可学 。

这里 Q、K、V 都由同一个 变换而来,于是每个词都在和 同一句话里的其他词 互相打分、取信息,这种序列内部、各词彼此关注的注意力,就叫 自注意力(self-attention) 。

《注意力的直觉》里点积不除模长带来的偏差,现在也交给这些可学习的变换在训练中自行调节,不必再手动除以模长。

query / key / value 这几个名字来自数据库检索的术语,按 key 查询、取出 value。

一次算完所有词:矩阵形式

逐词做三步太啰嗦。把整个句子的 Q、K、V 各排成矩阵,三步可以浓缩成一行矩阵运算:

为什么相乘的结果恰好是 ?把三个矩阵的形状摆在一起就一目了然,下面以 L = 4(句子「苹果 发布 新 手机」)、 为例:

得到这张 分数表后,再对每一行做 softmax 得到权重,最后乘上 完成加权求和。全程没有循环,天然适合 GPU 并行。

一个隐患:维度越大,softmax 越「极端」

在把公式写完整之前,先做个实验。点积是 个乘积之和,维度越大,求和的项越多, 分数之间的差距也被拉得越开 (有正有负,差距越来越悬殊)。 softmax 看的正是分数之间的差距,差距一大它就会出问题。下面这段代码在 三个维度下各跑一次,跟着步进看终端打出的分数范围与最大权重:

一大,分数动辄几十,softmax 的输出变得 极端尖锐 :几乎全部权重压到一个词上,其余词的权重趋近 0。这种现象叫 softmax 饱和 。

饱和的 softmax 对输入分数的微小变化几乎不敏感,反向传播时回传的梯度非常小,训练初期注意力很难调整。

光看三行打印不够直观。下面把这组实验做成可操作的:拖动滑块改变维度 ,下方柱状图实时画出同一批 8 个 Key 经 softmax 后的权重。往右拖,看着权重如何从「接近平均」收紧成「集中在一个词上」。

这 8 个 Key 与 Query 固定不变,拖动滑块只是改变它们参与点积的维度 。维度越大、求和项越多,分数差距被拉得越开,softmax 就越尖锐,这正是「维度大 → 分数波动大 → 分布尖锐」三者连起来的样子。打开「除以 」开关,同一维度下分布立刻平缓回来,这就是下一节的修正方案。

修正:除以 √dₖ

既然分数的波动随维度增长(统计上,方差大约正比于 ),那就把分数除以 ,把波动拉回正常范围。补上这一步,就得到了 Transformer 论文中的核心公式, 缩放点积注意力(scaled dot-product attention) :

别看它紧凑,每个部分你都已经亲手搭过: 是可学习的打分, 是防饱和的缩放,softmax 是归一化,最后乘 是加权求和。

把公式写成代码

实现一个 scaled_dot_product_attention 函数,逐行对照公式,再在一个玩具句子上跑通它。下面的 是 Value 向量的维度,本课和 一样取 。跟着步进一行行看,函数体里每一步正好对应公式的一部分:

输入 4 个词、输出 4 个新向量,结构与「注意力的直觉」中的手算版完全一致, 区别在于,现在打分和取信息都经过了 , 训练可以塑造它们 。 注意力权重矩阵的细致观察(热力图等)放到「多头注意力」中,与多个头对比着看更有意思。

一只眼睛够用吗?

现在注意力可以学习了。但一组 训练完后是固定的,它只能学出 一种 关注模式。

答案是:多配几套。这就是「多头注意力」。