📚 本文是《Transformer 由浅入深》系列第 3 篇 · 阶段二「核心机制」
上一篇我们用"逛图书馆"把注意力讲成了一句话:拿 Query 和所有 Key 比相关性,再按相关性把所有 Value 加权汇总。 这一篇,我们把这句话写成数学。
最终你会读懂那个被反复引用的公式:
$$ \text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right) V $$别被它吓到。有了上一篇的直觉打底,我们会发现它只是把"比相关性 → 归一化 → 加权汇总"逐字翻译了一遍。我们一项一项来。
打个比方:这个公式就像一份菜谱。第一次看满纸都是陌生的术语,但当你知道每个符号对应厨房里的哪一步——切菜、调味、装盘——它就不再神秘了。本篇要做的,就是把这份"菜谱"里的每个动作翻译成你熟悉的日常操作。读到最后,你会发现真正的"新概念"其实只有一个(点积),其余全是中学就学过的加减乘除。
本篇目标
读完你应该能:
- 说清公式里每个符号是什么、形状(维度)是多少;
- 解释为什么要除以 \( \sqrt{d_k} \);
- 自己手算一个最小例子。
阅读前提
- 知道矩阵乘法怎么算(行乘列);
- 知道 softmax 是"把一串数变成总和为 1 的概率"(第 2 篇讲过直觉)。
如果矩阵乘法对你有点陌生,只需记住一句直觉:矩阵乘法 = 一堆"行乘列再求和"的批量打包。我们后面所有公式,本质上都是在做"一行数和另一行数对应相乘再加起来",矩阵只是帮我们把成百上千次这样的小运算一次写完,免得用一堆 for 循环。所以看到大写字母的 \( Q \)、\( K \)、\( V \) 时,别紧张,它们不过是"把很多小向量摞成一摞"的整理箱而已。
1. 从词到向量:输入矩阵 X
一句话有 \( n \) 个 token,每个 token 先被表示成一个 \( d \) 维向量。把它们摞起来,就是输入矩阵:
$$ X \in \mathbb{R}^{n \times d} $$也就是 \( n \) 行、\( d \) 列:一行是一个词。比如句子"它 是 猫"有 3 个词,若每个词用 4 维向量表示,那 \( X \) 就是一个 \( 3 \times 4 \) 的矩阵。
可以把 \( X \) 想象成一张 Excel 表格:每一行是一个词,每一列是这个词的某种属性打分。只不过这些"属性"不是人能直接命名的(比如不是"褒义/贬义"“名词/动词"这种),而是模型在训练里自己琢磨出来的一组数字坐标。你不需要知道每一列具体代表什么含义——就像你不需要知道地图上的经纬度各自的物理意义,也照样能用它们定位一样,模型用这 \( d \) 个数字就能"定位"一个词在语义空间里的位置。
为什么非要把词变成一串数字?因为计算机不会"读"字,它只会算数。把"猫"变成一个向量,意思相近的词(猫、猫咪、喵星人)在这个空间里就会离得近,意思无关的词(猫、银行、方程式)就会离得远。后面所有的"比相关性”,其实都是在这个数字空间里量距离、比方向。
2. 三个投影:得到 Q、K、V
上一篇说,Q/K/V 是"同一个词的三副面孔"。数学上,这三副面孔就是把 \( X \) 分别乘上三个可学习的权重矩阵:
$$ Q = X W_Q, \qquad K = X W_K, \qquad V = X W_V $$其中 \( W_Q, W_K \in \mathbb{R}^{d \times d_k} \),\( W_V \in \mathbb{R}^{d \times d_v} \)。它们是模型训练时学出来的参数——“怎样的变换才能让相关的词被匹配到”,答案就藏在这三个矩阵里。
这一步在做什么?用大白话说:同一个词,要在三种不同的"场合"里出场,就得换三套行头。 \( W_Q \) 是"提问装",\( W_K \) 是"应答装",\( W_V \) 是"内容装"。同一个原始向量,乘上不同的权重矩阵,就被"打扮"成了适合不同用途的三个新向量。
再换个比方:把 \( X \) 乘以 \( W_Q \),就像给原始信息戴上一副"提问视角"的眼镜——戴上它,这个词强调的是"我在找什么样的信息";换成 \( W_K \) 这副"标签视角"的眼镜,它强调的就是"我能提供什么信息";\( W_V \) 这副眼镜下,它展示的是"如果你选中了我,我真正要交给你的内容是什么"。三副眼镜(三个矩阵)看的是同一个词,但各自提炼出不同的侧面。
要特别强调:这三个矩阵是学出来的,不是人手工设计的。训练之初它们是一堆随机数,模型在见过海量文本、不断被纠错之后,慢慢把"什么样的词该和什么样的词配对"这条规律,压缩进了这三个矩阵的数值里。所以注意力机制之所以聪明,功劳一大半要记在这三块"行头"上。
算完之后形状是:
| 矩阵 | 形状 | 含义 |
|---|---|---|
| \( Q \) | \( n \times d_k \) | 每行=一个词的"提问" |
| \( K \) | \( n \times d_k \) | 每行=一个词的"标签" |
| \( V \) | \( n \times d_v \) | 每行=一个词的"内容" |
实践中通常令 \( d_k = d_v \),为简洁后文统一写 \( d_k \)。
一个小提醒:这里的 \( d_k \) 不一定等于原始维度 \( d \)。投影这一步顺便还能"换维度"——把每个词从 \( d \) 维压成或拉成 \( d_k \) 维。这在多头注意力里很有用(下一篇会讲),现在只需知道:投影既换了"视角",也可能换了"尺寸"。
3. 打分:QKᵀ
现在要算"每个词的提问,和每个词的标签有多匹配"。两个向量的匹配度,最简单的度量就是点积(对应位相乘再求和):点积越大,方向越一致,越"相关"。
点积为什么能当"相似度"?给一个最朴素的直觉:点积就是在数"两个人有多少共同点"。 把 \( q \) 和 \( k \) 想成两份打分问卷,每一维是对同一个问题的态度。如果两人在同一个问题上都打了高分(正乘正)、或都打了低分(负乘负),这一项就贡献一个正数,表示"在这点上意见一致";如果一个高一个低(正乘负),就贡献负数,表示"在这点上唱反调"。把所有维度的"一致程度"加起来,总和越大,说明两个向量整体越像、方向越接近。这正是点积越大越相关的来历。
再补一个几何版的比方:两个向量就像两支箭。点积大致衡量"两支箭指向是否一致"——指向几乎相同(夹角小),点积就大;互相垂直(毫不相干),点积约等于 0;指向相反(夹角大),点积为负。所以"算点积"约等于"量两个词的方向有多接近"。
把所有词的 Q 和所有词的 K 两两点积,一步到位的写法就是矩阵乘法:
$$ S = Q K^{\top} \in \mathbb{R}^{n \times n} $$为什么要给 \( K \) 加个转置符号 \( \top \)?别被这个上标吓到,它纯粹是"为了让矩阵能对齐相乘"的技术动作。回忆矩阵乘法的规则是"前一个的行 × 后一个的列"。我们想让 \( Q \) 的每一行(一个词的提问)去和 \( K \) 的每一行(一个词的标签)做点积,而矩阵乘法只会拿"列"来配对,所以先把 \( K \) 立起来(转置),让它原来的行变成列,这样 \( Q \) 的行就能挨个和 \( K \) 的每个词配对了。一句话:转置只是把 \( K \) “摆正"到能相乘的姿势,不改变任何数值含义。
\( S \) 是一个 \( n \times n \) 的分数矩阵。它的第 \( i \) 行第 \( j \) 列 \( S_{ij} \),就是”第 \( i \) 个词的提问 · 第 \( j \) 个词的标签"——即第 \( i \) 个词对第 \( j \) 个词的关注原始分。
体会一下:输入 \( n \) 个词,得到一张 \( n \times n \) 的"谁关注谁"的表。这张表就是注意力的核心,也是它 \( O(n^2) \) 开销的来源(第 8 节再说)。
形象点说,这张 \( n \times n \) 的表就像一份"班级互评打分表":行是打分的人,列是被打分的人,格子里的数字是"这一行的同学,对这一列的同学有多关注"。注意这张表通常不对称——“我很在意你"不代表"你也同样在意我”。比如句子里的"它"会强烈关注"猫"(找指代对象),但"猫"未必同样回头去关注"它"。这种"单向偏爱"恰恰是注意力灵活的地方。
4. 缩放:为什么除以 √dₖ
这是公式里最容易被忽略、却很关键的一步。为什么不直接对 \( S \) 做 softmax,非要先除以 \( \sqrt{d_k} \)?
问题出在维度变大时,点积会变得很大。做个简单估算:假设 \( q \) 和 \( k \) 的每个分量都是相互独立、均值 0、方差 1 的随机数。它们的点积是
$$ q \cdot k = \sum_{i=1}^{d_k} q_i k_i $$每一项 \( q_i k_i \) 均值为 0、方差为 1,而 \( d_k \) 个独立项相加,方差会累加,于是
$$ \text{Var}(q \cdot k) = d_k $$也就是说,维度 \( d_k \) 越大,点积的波动范围就越大(标准差约为 \( \sqrt{d_k} \))。
这里给个不带公式的直觉:维度越多,可累加的"共同点"就越多,总分自然容易冲得很高或很低。 想象两份问卷,如果只有 2 道题,两人的总分差距撑死也就那么大;可要是有 500 道题,哪怕每道题只差一点点,累加起来总分的悬殊就会非常夸张。点积也是同理——维度 \( d_k \) 一大,分数的量级就跟着水涨船高,把原本温和的差距放大成了悬殊的鸿沟。
这会带来麻烦:当输入给 softmax 的数值绝对值很大时,softmax 会变得极度"尖锐"——几乎把全部权重压到最大的那一项上,其余几乎为 0。一旦进入这种饱和区,梯度会变得非常小,模型难以训练。
打个比方:softmax 本该像个"温和的分配者",按相关性给每个词分一点注意力;但当输入分数被维度撑得过大时,它就变成了"赢家通吃"——稍微高一点的那个词直接拿走 99.9% 的注意力,别人颗粒无收。这就好比评委打分,如果允许打 1 到 1000 万分,那哪怕第一名只比第二名强一丢丢,巨大的分差也会让结果变成"第一名独占,其余全部归零"。这样的极端分配既丢失了细腻的上下文信息,又会让模型"学不动"(梯度趋近于 0,等于没有改进的方向)。
解决办法很直接:除以 \( \sqrt{d_k} \),把方差重新拉回 1:
$$ \text{Var}\!\left(\frac{q \cdot k}{\sqrt{d_k}}\right) = \frac{d_k}{(\sqrt{d_k})^2} = 1 $$这就是那个 \( \sqrt{d_k} \) 的来历——它让分数的尺度不随维度膨胀,softmax 始终工作在"听得进梯度"的健康区间。所以这套机制的全名叫 Scaled Dot-Product Attention(缩放点积注意力)。
用一句最通俗的话总结:除以 \( \sqrt{d_k} \) 就是给打分"统一换算到同一把尺子",好比把不同币种都折算成人民币再比较,免得因为面额单位不同(维度高低不同)而误判谁更值钱。换算之后,评委的打分区间永远稳定在一个适中的范围,既能拉开差距,又不至于走极端。
5. 归一化:softmax(按行)
缩放之后,对分数矩阵逐行做 softmax:
$$ A = \text{softmax}\!\left(\frac{S}{\sqrt{d_k}}\right) \in \mathbb{R}^{n \times n} $$“逐行"是重点:第 \( i \) 行经过 softmax 后,变成一组总和为 1 的权重,代表"第 \( i \) 个词该把注意力如何分配给全句各词”。这正是上一篇那张热力图的每一行。
为什么是 softmax,而不是简单地"谁分高谁拿全部"?因为我们想要的是柔和的分配,而不是非黑即白的选择。softmax 做两件事:第一,通过指数运算 \( e^x \),把分数高的进一步拉高、分数低的进一步压低,从而强者更强但弱者不至于完全消失;第二,除以总和,保证所有权重加起来正好是 1。你可以把它理解成"分蛋糕":每个词分到的注意力,就是它在这一行里应得的那块蛋糕,而整块蛋糕(总注意力)的大小是固定的 100%。
为什么强调"按行"而不是"按列"或"整张表一起"?因为每一行代表"一个特定的词,如何分配它自己的注意力预算"。这个预算必须是该词自己的 100%,和别的词无关。如果错按列做 softmax,算出来的就成了"所有词对某一个词的关注占比",含义完全跑偏。记住:一行 = 一个提问者的注意力分配,每行各自归一,互不干涉。
工程上还会做一个数值稳定处理:softmax 前先减去每行最大值。结果不变,只是避免指数运算溢出。
这个"减去最大值"的小技巧值得多说一句:因为 \( e^x \) 涨得极快,\( x \) 稍大(比如 1000)就会大到计算机存不下(溢出成无穷大)。而 softmax 有个好性质——给每个数同时减去一个常数,结果一分不差。于是工程上统一减去该行的最大值,让最大的那项变成 \( e^0 = 1 \),其余都是 0 到 1 之间的小数,既不溢出,结果又完全等价。这是个"不改变答案、只为防止计算机算崩"的纯实现技巧。
6. 加权:乘以 V
最后一步,用权重矩阵 \( A \) 去加权汇总所有词的 Value:
$$ \text{Attention} = A V \in \mathbb{R}^{n \times d_v} $$输出仍是 \( n \) 行——每个词一行新表示,而这一行,正是它按注意力权重把全句 Value 混合后的结果。第 \( i \) 行写开来就是:
$$ \text{output}_i = \sum_{j=1}^{n} A_{ij}\, v_j $$\( A_{ij} \) 是第 \( i \) 个词分给第 \( j \) 个词的权重,\( v_j \) 是第 \( j \) 个词的 Value。这与上一篇"新的『它』= 0.78×猫的V + …“完全是同一件事,只是写成了求和式。
这一步可以理解成**“按比例调鸡尾酒”**:前面 softmax 已经定好了配方(每种原料占多少比例),这一步就是真正动手,把各个词的 Value 按这个比例兑在一起,调出一杯属于当前词的"上下文鸡尾酒”。关系越紧密的词,在这杯酒里占的分量就越大。
注意一个关键转变:这里加权混合的是 Value,而不是 Q 或 K。Q 和 K 只负责"算配方"(谁该关注谁),真正被舀进碗里的内容来自 V。这就是为什么我们要把一个词拆成三副面孔——用来比相关性的部分,和用来贡献内容的部分,可以是不一样的。一个词在"该不该被关注"上的特征,未必等于它"被关注后该交出什么信息",分开来学,模型更灵活。
经过这一步,每个词都不再是孤零零的原始向量,而是"自己 + 一圈相关上下文"揉合后的新向量。这正是注意力的全部意义:让每个词都带上它该带的上下文。
7. 把公式串起来 + 一个手算小例子
四步合一,就是开头那个公式:
$$ \text{Attention}(Q,K,V) = \text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V $$回头看这四步,其实就是一条流水线:投影(换三副面孔)→ 打分(比相关)→ 缩放 + softmax(归一成配方)→ 乘 V(按配方调和)。公式里从内到外读,正好是这条流水线从头到尾的顺序。下面我们用最小的例子走一遍,把抽象的符号变成具体的数字。
设 \( d_k = 2 \),只有两个词,它们的 Q、K、V 已经算好:
$$ Q = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix},\quad K = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix},\quad V = \begin{bmatrix} 10 & 0 \\ 0 & 10 \end{bmatrix} $$第一步,打分 \( QK^{\top} \):
$$ QK^{\top} = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} $$对角线为 1(每个词和自己最像),非对角为 0。
第二步,缩放,除以 \( \sqrt{d_k}=\sqrt{2}\approx 1.41 \),得到对角约 0.71、其余 0。
第三步,逐行 softmax。第一行 \( [0.71, 0] \) 过 softmax:
$$ \frac{[e^{0.71}, e^{0}]}{e^{0.71}+e^{0}} = \frac{[2.03, 1]}{3.03} \approx [0.67,\ 0.33] $$第二行同理为 \( [0.33, 0.67] \)。于是
$$ A \approx \begin{bmatrix} 0.67 & 0.33 \\ 0.33 & 0.67 \end{bmatrix} $$第四步,乘以 \( V \):
$$ AV \approx \begin{bmatrix} 0.67 & 0.33 \\ 0.33 & 0.67 \end{bmatrix}\begin{bmatrix} 10 & 0 \\ 0 & 10 \end{bmatrix} = \begin{bmatrix} 6.7 & 3.3 \\ 3.3 & 6.7 \end{bmatrix} $$读一下结果:第一个词的新表示是 \( [6.7, 3.3] \)——大部分来自自己(权重 0.67),小部分混入了第二个词。注意力就这样把上下文揉了进来。
这里还藏着一个值得玩味的细节:打分时两个词明明是"自己和自己满分 1、和对方零分",可经过 softmax,对方依然分到了 0.33 而不是 0。这说明 softmax 从不会把谁彻底"清零"——哪怕相关性为 0,也会保留一丝注意力。这是好事:它给了模型"留点余地"的弹性,万一这个词其实有点用,权重还有机会在训练中被调大。你也能借此体会到第 4 节的警告——如果当初分数被维度撑成了 \( [100, 0] \),softmax 后就会变成几乎 \( [1, 0] \),对方被彻底抹掉,这正是我们要靠缩放去避免的极端情况。
同样的过程,用 NumPy 几行就能复现:
import numpy as np
def softmax(x):
x = x - x.max(axis=-1, keepdims=True) # 数值稳定
e = np.exp(x)
return e / e.sum(axis=-1, keepdims=True)
def attention(Q, K, V):
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k) # 打分 + 缩放
A = softmax(scores) # 逐行归一化
return A @ V, A # 加权求和
Q = np.array([[1., 0.], [0., 1.]])
K = np.array([[1., 0.], [0., 1.]])
V = np.array([[10., 0.], [0., 10.]])
out, A = attention(Q, K, V)
print(A.round(2)) # [[0.67 0.33] [0.33 0.67]]
print(out.round(1)) # [[6.7 3.3] [3.3 6.7]]
把这段跑一遍,你就真正"摸"到注意力了。建议你动手改几个数试试:把 \( V \) 改大、把某个词的 Q 改得和另一个词的 K 更接近,看看输出和权重怎么变。亲手拨动这几个旋钮,比读十遍公式都更有感觉。
8. 复杂度分析
最后留一个伏笔。整个过程最贵的两步是 \( QK^{\top} \) 和 \( AV \),都是 \( n \times n \) 规模的矩阵参与运算,因此时间和显存开销都是:
$$ O(n^2 \cdot d_k) $$那个 \( n^2 \) 来自"每个词都要和每个词比一遍"的分数矩阵。当 \( n \)(上下文长度)很大时,\( n^2 \) 增长得很快——这正是长文本场景下的主要瓶颈,也是本系列第 10 篇"效率与变体"要重点解决的问题。
直觉上感受一下 \( n^2 \) 有多可怕:10 个词要算 100 次比对,100 个词就要算 1 万次,1000 个词直接飙到 100 万次。词数翻 10 倍,计算量翻 100 倍。这就像一场必须"人人握手"的聚会——10 个人握手没几下,100 个人就得握近 5000 次。词数(上下文长度)越长,这种"人人都要和人人打招呼"的代价就越是吃不消,这也是为什么处理超长文档会又慢又费显存。
常见疑问与易错点
问:Q、K、V 既然都来自同一个输入 X,那它们到底有什么区别?直接用 X 自己和自己点积不行吗? 答:区别全在三个权重矩阵 \( W_Q/W_K/W_V \) 上。它们把同一个 X 投影成三个不同的空间,分别专精于"提问"“应答"“供料"三种角色。如果直接拿 X 自己点积,等于强迫"用来比相关性的特征"和"用来贡献内容的特征"必须是同一套,模型就失去了灵活性。拆成三副面孔,正是为了让"谁该被关注"和"被关注后交出什么"可以各自独立地学。
问:\( QK^{\top} \) 里那个转置 \( \top \) 是不是很重要?它改变了数值吗? 答:转置不改变任何数值,它纯粹是为了让矩阵"摆正"到能相乘的姿势。我们想做的是”\( Q \) 的每一行 × \( K \) 的每一行”,但矩阵乘法只认"行 × 列",所以先把 \( K \) 转置,让它的行变成列。常见误区是把它当成某种复杂运算——其实它只是个对齐用的技术动作。
问:为什么是除以 \( \sqrt{d_k} \),不是除以 \( d_k \),也不是别的数? 答:因为点积的方差随维度增长到 \( d_k \),而标准差(也就是数值的典型波动幅度)是方差的平方根,即 \( \sqrt{d_k} \)。我们要抵消的是"波动幅度"这个尺度,所以除以 \( \sqrt{d_k} \) 正好把方差拉回 1。若除以 \( d_k \),反而会把分数压得过小,差距被抹平,注意力变得过于"平均",同样不利。
问:softmax 为什么必须"按行"做?整张表一起做或者按列做不行吗? 答:每一行代表"一个词如何分配它自己的注意力预算",这份预算必须是该词独有的 100%。按行做,才能保证每个词的权重各自加起来等于 1、互不干涉。按列做算出来的是"大家对某个词的关注占比",含义完全不同;整张表一起做则把不同词的预算混在了一起,都是错的。
问:既然两个词不相关、点积为 0,为什么 softmax 之后还会分到 0.33 的权重,而不是 0? 答:这是 softmax 的固有特性——它用指数函数 \( e^x \) 处理分数,而 \( e^0 = 1 \) 并不为 0,所以任何词都会保留一份非零的注意力。这其实是优点:它给模型留了"回旋余地",不会武断地把某个词彻底排除。只有当分数差距大到夸张时(比如没做缩放、分数飙到几百),softmax 才会近似变成"赢家通吃",把弱者压到接近 0。
问:为什么说注意力的复杂度是 \( O(n^2) \)?这个 \( n^2 \) 到底卡在哪一步? 答:卡在分数矩阵 \( S = QK^{\top} \) 上——它的尺寸是 \( n \times n \),意味着每个词都要和包括自己在内的所有词比一遍,一共 \( n \times n \) 次比对;后面的 \( AV \) 同样要遍历这张 \( n \times n \) 的表。所以词数 \( n \) 一翻倍,计算量和显存都涨到约 4 倍。这正是长上下文场景下的核心瓶颈,第 10 篇会专门讲怎么优化它。
小结 & 下一篇预告
这一篇,我们把图书馆直觉翻译成了精确的数学:
- 输入 \( X \to \) 三个投影得到 \( Q, K, V \);
- \( QK^{\top} \) 打分 \( \to \) 除以 \( \sqrt{d_k} \) 缩放(防 softmax 饱和)\( \to \) 逐行 softmax \( \to \) 乘 \( V \) 加权汇总;
- 复杂度 \( O(n^2 d_k) \),为后文埋下伏笔。
但你可能注意到:一组 \( W_Q/W_K/W_V \) 只能学到一种“什么算相关”。可语言里的关系千千万——句法的、指代的、语义的……一种视角哪够?下一篇,我们看 Transformer 怎么用多头注意力让模型"分头并进",同时从多个角度审视同一句话。
📖 系列目录
见 第 1 篇 文末完整目录。