【大模型应用开发】自注意力机制深讲 —— Q/K/V详解
10.1 先建立直觉:注意力 = 自动化的"查资料"
上一课说了 Transformer 的核心是注意力。这一课把它彻底讲透。
先做个类比:注意力就像图书馆查资料。
你要写一篇关于"北京"的论文(这是你的"查询")。
你跑到图书馆,看每本书的标题(这是"键"),
觉得哪本相关,就翻开内容记笔记(这是"值")。
最终你写出的论文,其实是:
每本书的相关度 × 每本书内容 的加权融合
对应到模型里:
| 图书馆类比 | 模型里的名字 | 含义 |
|---|---|---|
| 你要查的问题 | Q(Query 查询) | 当前词"想找什么" |
| 每本书的标题 | K(Key 键) | 其他词"能提供什么" |
| 书里的内容 | V(Value 值) | 其他词"实际的信息" |
一句话总结注意力:拿我的 Q,去和所有词的 K 做匹配,算出每个词的相关度,再按相关度加权取 V。
10.2 Q/K/V 是从哪来的?
不是模型里"天然就有"三个向量。它们是由同一个输入向量,乘以三个不同的权重矩阵得到的:
输入向量 x(第 3 个词 "首都")
x · W_Q = Q(这个"首都"想找什么信息)
x · W_K = K(这个"首都"能提供什么信息)
x · W_V = V(这个"首都"的实际内容)
import numpy as np
np.random.seed(42)
d_model = 64 # 向量维度
x = np.random.randn(d_model) # 某个词的输入向量
W_Q = np.random.randn(d_model, d_model) * 0.1
W_K = np.random.randn(d_model, d_model) * 0.1
W_V = np.random.randn(d_model, d_model) * 0.1
Q = x @ W_Q
K = x @ W_K
V = x @ W_V
print(f"输入 x: {x.shape}") # (64,)
print(f"Q: {Q.shape}") # (64,) —— 同一个词,三种身份
print(f"K: {K.shape}") # (64,)
print(f"V: {V.shape}") # (64,)
这三个矩阵 W_Q、W_K、W_V 就是模型的参数,是在训练中"学出来"的。 训练得越好,模型越知道"什么词该关注什么词"。
10.3 注意力分数的计算:一步步来
设一句话有 4 个词:["我", "爱", "北京", "天安门"],每个词向量 4 维(简化):
第 1 步:算 Q 和所有 K 的点积(相关性打分)
以"北京"的 Q 为例:
得分(北京, 我) = Q北京 · K我 = 0.6
得分(北京, 爱) = Q北京 · K爱 = 0.4
得分(北京, 北京) = Q北京 · K北京 = 0.9 (和自己最相关)
得分(北京, 天安门) = Q北京 · K天安门 = 0.8 (地理位置相关)
第 2 步:除以缩放因子 √d
d = 向量维度(这里 4,真实模型 64~128 不等)
每个分数 ÷ √d:
0.6/2 = 0.30
0.4/2 = 0.20
0.9/2 = 0.45
0.8/2 = 0.40
为什么除以 √d?如果向量维度很大,点积结果会很大,导致 softmax 之后"最相关的词拿走几乎全部权重",其他词啥也分不到。除一下,让分数分布更温和。(11.4 细讲)
第 3 步:softmax 归一化成权重
softmax([0.30, 0.20, 0.45, 0.40]) ≈ [0.25, 0.20, 0.30, 0.25]
现在这些数加起来 = 1,可以当"权重"用了
第 4 步:按权重加权求和 V
输出(北京) = 0.25×V我 + 0.20×V爱 + 0.30×V北京 + 0.25×V天安门
这个加权后的向量,就是"北京"在看过全句之后的新表示。 它既保留了"北京"自己的信息,又融入了"我爱北京天安门"这个上下文。
完整公式(给想记的读者)
Attention(Q, K, V) = softmax( QKᵀ / √d ) V
import numpy as np
def softmax(x):
e = np.exp(x - np.max(x))
return e / e.sum()
def attention(Q, K, V):
d = K.shape[-1]
scores = Q @ K.T / np.sqrt(d) # 1. 点积打分 2. 缩放
weights = softmax(scores) # 3. 归一化
return weights @ V # 4. 加权求和
# 4 个词,每个 4 维
X = np.random.randn(4, 4)
W_Q = np.random.randn(4, 4) * 0.1
W_K = np.random.randn(4, 4) * 0.1
W_V = np.random.randn(4, 4) * 0.1
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
output = attention(Q, K, V)
print(f"输出形状: {output.shape}") # (4, 4) 每个词都看过全句了
10.4 为什么要除以 √d?
这是大家最容易忽略的细节。原因:
- 点积的规模随维度增长:两个 d 维向量点积,期望值大约 √d
- 如果不缩放,softmax 输入数值会很大
- softmax 对很大数值敏感:会把最大的分数"放大到接近 1",其他归 0
- 结果:每个词只盯着最相关的 1 个词,梯度也容易消失,学不动
类比:两个队比赛,如果得分普遍是 5 分制,你除以 √d 就是"统一成 1 分制",让差距不会一下子把弱者打死。
10.5 Multi-Head:为什么要有"多头"?
单头的局限
一组 Q/K/V 只能捕捉一种关注模式。比如它学会了"关注地理位置相关的词",就没精力学"关注句法关系"。
多头 = 多组"眼镜"
把 64 维分成 8 个头,每个头管 8 维:
头 1:关注"代词指代谁"(它 → 小明)
头 2:关注"动词的宾语"(吃 → 苹果)
头 3:关注"数字与单位"(3 → 个)
头 4:关注"否定词的作用"(不 → 后面整句反转)
...
8 个头的结果拼接起来 → 一个"看得更全"的表示
# 多头注意力(简化示意)
def multi_head_attention(X, num_heads=8, d_model=64):
head_dim = d_model // num_heads
heads = []
for h in range(num_heads):
# 每个头有自己的 Q/K/V 权重(这里用同一个权重示意)
W_Qh = np.random.randn(d_model, head_dim) * 0.1
W_Kh = np.random.randn(d_model, head_dim) * 0.1
W_Vh = np.random.randn(d_model, head_dim) * 0.1
Q = X @ W_Qh
K = X @ W_Kh
V = X @ W_Vh
heads.append(attention(Q, K, V))
return np.concatenate(heads, axis=-1) # 拼回完整维度
类比:一个人看问题只有一个视角;8 个专家从 8 个角度(句法、指代、语义、数量……)分别看,最后汇总讨论,结论自然更全面。
10.6 注意力的代价:O(n²)
注意力很强大,但也有代价:计算量随句子长度平方增长。
句子长度 n = 100 → 计算量 ∝ 100² = 1 万
句子长度 n = 10000 → 计算量 ∝ 10000² = 1 亿
长度增加 100 倍,计算量增加 1 万倍!
这就是为什么"长上下文"很贵、很慢。围绕它有一堆优化技术:
- Flash Attention:优化显存访问,不改变算法但快得多
- 稀疏注意力:每个词只看附近词,不看全句
- GQA / MLA:减少 KV 缓存(第 14 课会讲)
记住:只要提到"模型上下文变长为什么难、为什么贵",答案里一定有 O(n²)。
10.7 注意力可视化:模型在"看"什么?
研究者经常把注意力权重画成热力图,非常直观:
我 爱 北京 天安门
我 [0.15, 0.45, 0.20, 0.20]
爱 [0.30, 0.15, 0.35, 0.20]
北京 [0.10, 0.20, 0.45, 0.25] ← "北京"最关注"北京"(自指)
天安门 [0.10, 0.15, 0.30, 0.45]
行 = 当前词,列 = 它关注的词。深色 = 权重高 = 关系强。 你会看到模型确实学出了合理的语法/语义关系。
更多推荐
所有评论(0)