几乎所有 Transformer 的科普都会先给出那个公式:
Attention(Q, K, V) = softmax(QKᵀ / √dk) · V
公式本身不复杂,但它掩盖了一个更直观的事实:注意力就是加权平均,权重由「谁跟谁更像」决定。
先从一个句子开始¶
假设输入是四个词:
tokens = ["我", "在", "写", "代码"]
每个词先变成一个向量。为了能打印出来看,我们只用 3 维:
import numpy as np
X = np.array([
[1.0, 0.0, 0.2], # 我
[0.3, 0.8, 0.1], # 在
[0.1, 0.2, 0.9], # 写
[0.0, 0.9, 0.7], # 代码
])
三步算出注意力¶
第一步:算相似度。 每个词都去和所有词(包括自己)做点积:
scores = X @ X.T # 4×4
得到的 scores[i][j] 就是第 i 个词「应该关注第 j 个词多少」的原始分数。
第二步:归一化成权重。 除以维度的平方根防止数值爆炸,再 softmax:
d = X.shape[1]
weights = np.exp(scores / np.sqrt(d))
weights = weights / weights.sum(axis=1, keepdims=True)
第三步:加权求和。 用权重去混合所有词的信息:
out = weights @ X # 4×3
到这里,self-attention 就结束了。out[0] 不再是「我」这个词的原始向量,而是混入了上下文之后的新表示。
为什么除以 √d¶
假设 d = 512,每个分量方差是 1,那么点积的方差会累积到 512,标准差大约是 22。softmax 对大数值极其敏感——输入差 22 的话,输出几乎会退化成 one-hot,梯度全部消失。
除以 $\sqrt{d}$ 之后,方差重新回到 1 附近,softmax 才有温和的输出分布。
一个可以自己验证的小实验
把上面代码里的 / np.sqrt(d) 删掉,打印 weights.max()。当维度升到 64 以上时,你会看到某一列迅速趋近于 1,其余趋近于 0。
多头注意力在解决什么¶
单头注意力只有一个「相似度标准」。但语言里的关系是多维的:
| 头 | 可能学到的关系 |
|---|---|
| 头 1 | 代词指向哪个名词 |
| 头 2 | 动词和它的宾语 |
| 头 3 | 相邻词的局部搭配 |
| 头 4 | 句首与句尾的长距离依赖 |
多头注意力就是把向量切成 8 份(或更多),每一份各算各的注意力,最后拼回来。代价是参数量不变,但表达能力的上限高了很多。
Q、K、V 到底是什么¶
回到公式里的三个字母,它们其实是同一个输入 X 经过三个不同线性变换的结果:
W_q = np.random.randn(3, 3)
W_k = np.random.randn(3, 3)
W_v = np.random.randn(3, 3)
Q, K, V = X @ W_q, X @ W_k, X @ W_v
weights = softmax(Q @ K.T / np.sqrt(3))
out = weights @ V
- Q(Query):我在找什么
- K(Key):我能提供什么标签
- V(Value):你选中我之后,真正拿走的内容
类比搜索引擎:Q 是你的搜索词,K 是网页标题,V 是网页正文。相似度决定排名,你最终读到的是正文。
理解了这层类比,剩下的编码器堆叠、位置编码、残差连接都是工程上的补丁,不再是认知负担。