一句话总结:Q(Query)是“我在找什么”,K(Key)是“我挂着什么标签”,V(Value)是“我能贡献什么内容”。Q 和 K 的点积给出学出来的匹配分,再用归一化权重混合允许读取的 V。
10.1 本章目标
上一章我们建立了 Attention 的几何直觉:点积同时看方向和长度,可以拿来做学出来的 Query-Key 匹配分。
但还有几个关键问题没回答:
- Q、K、V 是什么意思?
- 它们是怎么从输入生成的?
- 整个 Attention 的维度变化是怎样的?
这一章,我们来完整追踪 Attention 的计算过程,把每一步的维度变化都搞清楚。
10.2 输入数据的形状
10.2.1 理解输入维度
在实际训练中,我们会批量处理多个句子。输入数据的形状是:
X: [batch_size, seq_length, d_model]
以图中为例:
- batch_size = 4:同时处理 4 个句子(批次大小)
- seq_length = 16:这个批次把每个句子整理成 16 个 token 位置(序列长度)
- d_model = 512:每个 token 用 512 维向量表示(模型维度)
10.2.2 具体例子
图中展示了 4 个句子:
1. 小沈阳江西演唱会邀请了,
2. 明星刀郎的歌火遍大江南北
3. 2002年的第一场雪比2001年来得更
4. LLM张老师的粉丝全是正能量
在这个教学例子里,每个句子会通过截断或 padding 整理成 16 个 token 位置,每个位置用 512 维向量表示。
所以总的输入形状是 [4, 16, 512]:
- 4 个句子
- 16 个位置
- 512 维向量
10.2.3 三个维度的含义
| 维度 | 英文 | 中文 | 含义 |
|---|---|---|---|
| batch_size | 批次 | 批次大小 | 同时处理多少个句子 |
| seq_length | 序列长度 | 序列长度 | 这个批次里每个句子有多少个 token 位置 |
| d_model | 模型维度 | 学习维度 | 每个 token 的向量维度 |
10.3 生成 Q、K、V
10.3.1 核心思想
Q、K、V 都是从同一个输入 X 生成的,通过三个不同的权重矩阵:
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
这三个权重矩阵(W_Q、W_K、W_V)是可学习的参数,会在训练过程中不断调整。
10.3.2 维度计算
以生成 Q 为例:
X: [4, 16, 512] (batch_size, seq_length, d_model)
W_Q:[512, 512] (d_model, h × d_k)
Q: [4, 16, 512] (batch_size, seq_length, h × d_k)
矩阵乘法的维度规则:[..., A, B] @ [B, C] = [..., A, C]
所以:[4, 16, 512] @ [512, 512] = [4, 16, 512]
这个常见例子取 h × d_k = h × d_v = d_model = 512,所以合并后的 Q、K、V 恰好和 X 同形状。这不是硬性规则:同一个 head 里的 Q、K 必须共享 d_k 才能做点积,V 可以使用不同的 d_v。
10.3.3 为什么需要三个不同的矩阵?
你可能会问:既然形状都一样,为什么要用三个不同的矩阵?
答案是:Q、K、V 承担不同的角色。
- Q(Query,查询):代表"我在找什么信息"
- K(Key,键):代表"我有什么信息可以被找到"
- V(Value,值):代表"如果被找到,我提供什么内容"
通过学习不同的 W_Q、W_K、W_V,模型可以把同一个输入变成三种视角。Q 和 K 会进入彼此兼容的逐头空间来打分,V 则携带最后要混合进输出的特征。
10.3.4 一个直观的类比
想象一个图书馆检索系统:
| 角色 | 类比 | 作用 |
|---|---|---|
| Query (Q) | 读者的搜索词 | "我想找关于机器学习的书" |
| Key (K) | 每本书的索引标签 | "机器学习, Python, 入门" |
| Value (V) | 书的实际内容 | 整本书的内容 |
当你搜索时:
- 用你的 Query 和每本书的 Key 比较
- 匹配分更高的书得到更大的归一化权重
- 按这些权重混合各本书的 Value(内容)
10.4 第一次矩阵乘法:Q @ K^T
10.4.1 计算匹配分数矩阵
合并投影拆成多个 head 以后,下一步是在每个 head 内计算 Query-Key 匹配分:
Q @ K^T
注意:K 需要转置(K^T),因为我们要让 Q 的每一行和 K 的每一行做点积。
10.4.2 维度变化
Q: [4, 4, 16, 128] (batch, heads, seq, d_k)
K^T: [4, 4, 128, 16] (batch, heads, d_k, seq)
scores: [4, 4, 16, 16] (batch, heads, query_seq, key_seq)
这里取
h = 4,所以d_k = d_model / h = 512 / 4 = 128。head 到底怎么拆,下一章详细讲;这一章先把 head 这一维写出来,保证每一步形状都对得上。
10.4.3 结果的含义
结果是一个 [4, 4, 16, 16] 的张量:
- 4 个句子
- 每个句子有 4 个 head
- 每个 head 有一个 16×16 的原始分数矩阵
- 位置
(i,j)是第i个 Query 和第j个 Key 的点积
图中下方用“猫爱吃鱼”做了一个手工简化示例。它只演示形状和点积,不是从真实模型里测出来的:
猫 爱 吃 鱼
猫 [ ]
爱 [ ]
吃 [ ]
鱼 [ ]
每个位置的值 = 对应 Q 行向量和 K 列向量的点积。真实 head 不一定对角线更大;高分也不等于“语义相关性的真值”。还要经过缩放、mask 和逐行 Softmax,它才会变成允许位置上的混合权重。
10.5 Scale:为什么除以 √d_k
10.5.1 缩放操作
Q @ K^T 的结果需要缩放:
scaled_scores = (Q @ K^T) / √d_k
除以 √d_k 不是为了把数值硬塞进 [-1, 1],而是让不同 head 宽度下的典型分数尺度大致可比。
10.5.2 为什么要缩放?
问题:当 d_k 变大时,未缩放点积的典型幅度也会变大。
点积 = Σ(q_i × k_i) # 128 个数相乘再相加
如果 Q、K 各维彼此独立、均值为 0、方差为 1,那么点积的方差约为 d_k,标准差约为 √d_k。
后果:数值太大会让 Softmax 变得极端。
Softmax([100, 1, 2]) ≈ [1.0, 0.0, 0.0] # 极端分布
Softmax([1.0, 0.1, 0.2]) ≈ [0.539, 0.219, 0.242] # 没那么饱和
Softmax 饱和时,大多数导数会变得很小,优化更困难。
解决方案:除以 √d_k,让上面理想化条件下的分数标准差回到 1 左右。
点积 / √128 ≈ 点积 / 11.3
10.5.3 公式中的位置
这里的 √d_k 就是 Scale,M 代表在 Softmax 前加入的 causal、padding 等 mask。
10.6 Mask:防止"偷看"未来
10.6.1 为什么需要 Mask?
在 GPT 这样的自回归模型中,预测下一个词时不能看到未来的词。
比如预测"小沈阳江西演唱会邀请了____",模型不能看到答案。
但是 Q @ K 会先计算所有位置对的原始分数,包括未来的位置!其他架构还可能需要 padding 或结构性 mask。
10.6.2 Mask 的实现
解决方案:用一个三角形 Mask 把未来的位置"遮住"。
原始注意力矩阵: Mask 后:
[0.3, 0.2, 0.1, 0.4] [0.3, -∞, -∞, -∞ ]
[0.2, 0.5, 0.2, 0.1] → [0.2, 0.5, -∞, -∞ ]
[0.1, 0.3, 0.4, 0.2] [0.1, 0.3, 0.4, -∞ ]
[0.2, 0.1, 0.3, 0.4] [0.2, 0.1, 0.3, 0.4]
把右上角(未来的位置)设为负无穷(-inf)。
10.6.3 为什么用 -inf?
因为 Softmax 会把 -inf 变成 0:
Softmax([0.3, -∞, -∞, -∞]) = [1.0, 0.0, 0.0, 0.0]
这样,经过 Softmax 后,未来位置的注意力权重就变成 0 了——模型"看不到"未来。
10.6.4 图中的示例
图中沿着同一行展示了 Mask 前后的变化:Scale 后是 [0.32, 1.87, 0.94, 0.72],加入 causal mask 后,未来位置变成 -∞。下方三角矩阵再把规则画完整:第 i 个 Query 只能读取第 i 个位置和它左边的位置。
10.7 Softmax:把分数变成权重
10.7.1 转换过程
Mask 之后,对每一行应用 Softmax:
Softmax 前:[0.32, 1.87, 0.94, -inf]
Softmax 后:[0.132, 0.622, 0.246, 0.000]
10.7.2 Softmax 的作用
- 归一化:每一行的和变成 1
- 保持排序:分数越大,权重越大;权重比由指数决定
- 处理 -inf:-inf 变成 0
10.7.3 图中的观察
看图中的数据:
- 第 0 行("小"):[1.00000, 0.00000, 0.00000, ...]
- 只能看到自己,所以自己的权重是 100%
- 第 1 行("沈"):比如 [0.32, 0.68, 0.00, ...]
- 可以看到"小"和"沈",但具体权重由学出来的分数决定
- 后面的行虽然可选位置更多,某个 head 仍然可能把大部分权重集中到一两个位置
10.7.4 这就是"注意力权重"
Softmax 的输出就是注意力权重矩阵——它是混合允许读取的 Value 时用的逐行系数,不是某个 token “语义相关”的概率。
10.8 第二次矩阵乘法:Attention @ V
10.8.1 加权求和
有了注意力权重,下一步是用它对 V 进行加权求和:
Output = Attention_Weights @ V
10.8.2 维度变化
Attention_Weights: [4, 4, 16, 16] (batch, heads, ctx_len, ctx_len)
V: [4, 4, 16, 128] (batch, heads, key_seq, d_v)
head_output: [4, 4, 16, 128] (batch, heads, query_seq, d_v)
注意:这里显示的是 Multi-Head 的情况,有 4 个 head。
10.8.3 这一步在做什么?
每个位置的输出 = 所有允许读取的 V 行的加权和,权重来自逐行 Softmax。
output[i] = Σ(attention_weight[i,j] × V[j])
如果第 i 个 token 对第 j 个 token 的注意力是 0.7,对第 k 个 token 的注意力是 0.3,那么:
output[i] = 0.7 × V[j] + 0.3 × V[k]
输出是"根据注意力重新组合"的向量表示。
10.9 Attention 输出的含义
10.9.1 输出维度
经过一个 head 的 Attention_Weights @ V,每个位置得到一个逐头输出:
head_output: [batch_size, ctx_length, d_v] = [4, 16, 128]
4 个 head 的结果会拼回 [4, 16, 512],再经过 W_O 投影回 d_model。下一章会把这一步完全展开。
10.9.2 输出的语义
这个输出有什么特别之处?
它融合了上下文信息!
在第一层,X 从 token 和位置信息出发;到了更深的 Block,X 本身已经带着上下文。每个 Attention head 都会再加入一份来自允许读取的 V 行的加权混合。
10.9.3 图中的说明
更准确的路径是:多个 head 的输出拼接后经过 W_O,Attention 分支再和 residual stream 相加,然后才继续进入后面的子层。它更新的是当前层的隐藏状态,不会直接改写 embedding 参数表。
10.10 完整的 Attention 计算流程
10.10.1 流程图
让我们把整个流程串起来:
步骤1:生成合并后的 Q, K, V
Q = X @ W_Q [4, 16, 512]
K = X @ W_K [4, 16, 512]
V = X @ W_V [4, 16, 512]
↓
reshape 成 4 个 head;本例 d_k = d_v = 128
Q, K, V: [4, 4, 16, 128]
↓
步骤2:逐 head 计算匹配分
scores = Q @ K^T [4, 4, 16, 16]
↓
步骤3:缩放
scores = scores / √d_k [4, 4, 16, 16]
↓
步骤4:按需要加 mask M(causal / padding 等)
scores = scores + M [4, 4, 16, 16]
↓
步骤5:Softmax
weights = softmax(scores) [4, 4, 16, 16]
↓
步骤6:逐 head 加权求和
head_output = weights @ V [4, 4, 16, 128]
↓
拼接 heads: [4, 16, 512]
output = concat @ W_O: [4, 16, 512]
10.10.2 PyTorch 代码
# 代码示例
import torch
import torch.nn.functional as F
def attention(Q, K, V, allowed_mask=None):
"""
计算 Scaled Dot-Product Attention
Args:
Q: [..., query_len, d_k]
K: [..., key_len, d_k]
V: [..., key_len, d_v]
allowed_mask: 可广播到 [..., query_len, key_len] 的 bool 张量;
True 表示允许读取
Returns:
output: [..., query_len, d_v]
attention_weights: [..., query_len, key_len]
"""
d_k = Q.size(-1)
# 步骤2: Q @ K^T
scores = torch.matmul(Q, K.transpose(-2, -1))
# 步骤3: Scale
scores = scores / (d_k ** 0.5)
# 步骤4: Mask
if allowed_mask is not None:
# 每个 Query 行必须至少保留一个可读的 Key。
scores = scores.masked_fill(~allowed_mask, float('-inf'))
# 步骤5: Softmax
attention_weights = F.softmax(scores, dim=-1)
# 步骤6: 加权求和
output = torch.matmul(attention_weights, V)
return output, attention_weights
10.11 Q、K、V 的深层理解
10.11.1 三者的关系
| 角色 | 生成方式 | 作用 | 参与的计算 |
|---|---|---|---|
| Q | X @ W_Q | 表示“我在找什么” | Q @ K^T |
| K | X @ W_K | 表示“我挂着什么标签” | Q @ K^T |
| V | X @ W_V | 表示“我能贡献什么内容” | weights @ V |
10.11.2 为什么 K 和 V 要分开?
你可能会问:K 和 V 都来自同一个输入,为什么要用两个不同的矩阵?
答案:解耦"匹配"和"提取"。
- K 负责"被匹配":决定哪些位置应该被关注
- V 负责"被提取":决定关注后提取什么信息
这给了模型更大的灵活性:负责决定路线的特征,可以和沿着路线传递的特征不同。
10.11.3 一个例子
考虑这个手工例句:“皮影艺人收起了驴皮影人。”
当处理“收起”时:
- Q("收起") 可能在找“谁做了这个动作”
- K("皮影艺人") 的表示可能包含“我是动作的发出者”
- V("皮影艺人") 的表示可能携带这个人物在当前层里的特征
这是某个训练好的 head 可能学会的模式,不代表每个模型、每个 head 都按这套语法角色编码。
10.12 本章总结
10.12.1 核心概念
| 概念 | 形状 | 含义 |
|---|---|---|
| X | [batch, seq, d_model] | 输入向量 |
| W_Q / W_K | [d_model, h × d_k] | 可学习的 Query / Key 投影 |
| W_V | [d_model, h × d_v] | 可学习的 Value 投影 |
| Q、K | [batch, h, seq, d_k] | 逐 head 的 Query / Key |
| V | [batch, h, seq, d_v] | 逐 head 的 Value |
| Scores | [batch, h, query_seq, key_seq] | 原始匹配分数 |
| Weights | 与 Scores 同形状 | mask + Softmax 后的逐行混合权重 |
| Head output | [batch, h, query_seq, d_v] | 每个 head 对 V 的加权混合 |
| Block 输出 | [batch, seq, d_model] | 拼接各 head,再经过 W_O |
10.12.2 计算流程
X → [W_Q, W_K, W_V] → 合并 Q, K, V → 拆成 heads
↓
Q @ K^T (匹配分数)
↓
/ √d_k (缩放)
↓
+ M (按需要做 causal / padding mask)
↓
Softmax (归一化)
↓
@ V (逐 head 加权求和)
↓
拼接 heads → W_O → Output
10.12.3 核心认知
Q、K、V 是 Attention 的三个主角。Q 发问,K 挂标签,它们的点积给出学出来的匹配分;缩放、mask 和 Softmax 把每行变成路由权重,再用这些权重混合 V。最后把各个 head 合并,经
W_O回到 residual stream。
本章交付物
学完这一章,你应该能够:
- 说出 Q、K、V 各自的含义
- 解释它们是如何从输入 X 生成的
- 追踪 Attention 计算过程中的维度变化
- 理解 Mask 的作用(防止看到未来)
- 解释为什么要 Scale(除以 √d_k)
下一章预告
这一章我们把 QKV 的整条路径走完了,并且把 head 这一维提前写出来,保证形状从头到尾一致。
但实际的 Transformer 使用的是 Multi-Head Attention(多头注意力)——把 Attention 分成多个"头",每个头关注不同的信息。
为什么要这样做?多个头是怎么工作的?下一章,我们来揭开 Multi-Head Attention 的神秘面纱!