大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(二)
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer 大模型处理文本的全流程解析(一)-CSDN博客
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(三)-CSDN博客
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(四)-CSDN博客
7. 位置编码:告诉模型 Token 的顺序
Transformer 的注意力计算本身不包含位置信息。
更准确地说:
如果没有位置编码,
注意力机制主要看 token 的内容,
而不知道 token 的先后顺序。
所以模型需要额外的位置信息。
这就是位置编码,英文叫 positional encoding。
7.1 为什么需要位置编码
更准确地说,Self-Attention 本身主要依赖 token 的内容向量来计算相关性。
如果没有位置信息,模型很难区分 token 的先后顺序。
例如:
我吃鱼
鱼吃我
这两个句子包含的 token 一样:
我、吃、鱼
但顺序不同,语义完全不同。
在“我吃鱼”中:
“我”是动作发出者。
“鱼”是被吃的对象。
在“鱼吃我”中:
角色完全反过来了。
如果模型不知道顺序,就无法正确区分主语、谓语、宾语之间的关系。
所以必须引入位置信息:
“我”是第 1 个 token。
“吃”是第 2 个 token。
“鱼”是第 3 个 token。
位置编码要解决两个核心问题:
1. 绝对顺序:
当前 token 在序列中的位置是什么。
2. 相对距离:
两个 token 之间相隔多远。
现代大模型更重视相对位置信息,因为语言中很多关系取决于 token 之间的相对距离,而不是某个 token 的绝对位置本身。
7.2 常见位置编码方式
|
类型 |
说明 |
代表 |
|---|---|---|
|
正弦余弦位置编码 |
用 sin/cos 函数生成固定向量 |
原始 Transformer |
|
可学习位置 embedding |
给每个位置学一个向量 |
BERT |
|
RoPE 旋转位置编码 |
在 Q、K 上施加旋转,编码相对位置 |
LLaMA、Qwen、Mistral 等常见 |
|
ALiBi |
通过注意力偏置引入位置信息 |
BLOOM |
需要说明:
RoPE、RMSNorm、SwiGLU、GQA 是当前主流开源 LLM 的常见组合,
但并非所有模型都使用。
具体以模型的 config.json 和实现为准。
7.3 位置信息如何加入
方式一:直接相加
适用于原始 Transformer、BERT 等。
X = Embedding(token) + Position(position)
例如:
“我”的输入向量 = embedding_2513 + position_1
“吃”的输入向量 = embedding_1892 + position_2
“鱼”的输入向量 = embedding_3765 + position_3
方式二:RoPE 旋转
现代大模型常用 RoPE。
RoPE 不是把一个位置向量加到 token embedding 上,而是在注意力计算时对 Q、K 向量施加旋转:
q_i = rotate(q_i, position=i)
k_j = rotate(k_j, position=j)
然后 Q 和 K 再做点积。
这样注意力分数会自然包含相对位置信息:
i - j
所以对于 RoPE 模型来说:
“embedding → 位置编码”这个流程图只是教学简化。
RoPE 的位置信息实际上是在注意力计算阶段注入的。
7.4 RoPE 的长上下文外推问题
RoPE 虽然很适合表达相对位置,但它也有一个工程上非常重要的问题:
模型训练时见过的长度是有限的。
例如:
训练长度可能是 4096。
也可能是 8192。
也可能是 32768。
如果训练时只见过 8192 长度,推理时突然输入 32768 长度,RoPE 的旋转角度可能进入模型没有充分学习过的范围。
这时可能出现:
注意力分布异常
loss 升高
生成质量下降
长文本理解能力明显退化
所以现代长上下文模型通常会使用 RoPE scaling 技术。
常见方式包括:
Position Interpolation,简称 PI
NTK-aware scaling
YaRN
dynamic scaling
这些方法的目标都是:
让模型在超过原始训练长度时,
仍然能保持较稳定的位置感知能力。
MLOps 视角:
部署长上下文模型时,不能只看:
max_position_embeddings
还要看 config.json 中是否有:
rope_theta
rope_scaling
以及具体实现是否支持:
linear
dynamic
yarn
ntk
如果模型没有配置合适的长文本外推策略,即使显存够,长文本推理效果也可能不稳定。
8. Q、K、V 向量:注意力机制的核心
8.1 符号约定
为了避免混淆,本文使用以下符号:
q^(i) 表示第 i 个 token 的 Query 向量(Q向量:查询条件/检索意图,查询向量)
k^(i) 表示第 i 个 token 的 Key 向量(K向量:打标签,特征向量)
v^(i) 表示第 i 个 token 的 Value 向量(V向量:Q 和 K 交互,算出一组注意力权重;这组权重对所有 V 做加权求和,得到最终输出)
q_j 表示某个 Q 向量的第 j 个分量
也就是说:
上标表示 token 序号。
下标表示向量维度序号。
8.2 先搞清楚 X 是什么
句子:
我 吃 鱼
经过 embedding 后,每个 token 变成一个向量。
如果是 RoPE 模型,此时位置信息还没有直接加到 embedding 上。
如果是绝对位置编码模型,此时可能已经完成 embedding + position embedding。
为了方便观察,假设模型维度是 4。
实际模型通常是 4096、5120、8192 等。
X =
[
[ 0.2, -0.1, 0.5, 0.3 ], ← 我,第 1 个 token
[ 0.8, 0.1, -0.2, 0.4 ], ← 吃,第 2 个 token
[-0.3, 0.6, 0.1, 0.2 ] ← 鱼,第 3 个 token
]
每一行是一个 token 的向量。
8.3 为什么不直接用 X?为什么要变成 Q、K、V?
这是最关键的问题。
我们用一个场景讲透。
场景:你走进一个派对
你走进一个 100 人的派对,想找一个人聊天。
这个过程中有三件完全不同的事。
第一件事:你脑子里有一个“需求”
你心里想:
我想找一个喜欢户外运动的人。
这是你的 Query。
它描述的是:
你在找什么。
第二件事:每个人胸前贴着一个“标签”
派对上每个人胸前贴了一张纸条:
张三:喜欢爬山、跑步
李四:喜欢做饭、烘焙
王五:喜欢编程、数学
这是每个人的 Key。
它描述的是:
我能被什么找到。
第三件事:找到人之后,对方跟你聊的内容
你找到张三,张三跟你聊了半小时爬山经历,推荐了几条路线。
这是张三的 Value。
它描述的是:
我实际能提供什么信息。
关键:这三件事不能是同一个东西
看张三:
|
角色 |
内容 |
|---|---|
|
他的 Query |
“我想找一个会做饭的人,因为我太懒了。” |
|
他的 Key |
“喜欢爬山、跑步。” |
|
他的 Value |
“爬过珠峰、去过尼泊尔、推荐三条入门路线。” |
同一个人,三个角色,三套不同的信息。
他在找什么 ≠ 他能被什么找到 ≠ 他实际能聊什么。
换回模型
“吃”这个 token 也是一样。
|
角色 |
“吃”的信息 |
|---|---|
|
它的 Query,它在找什么 |
“谁在吃?吃的是什么?” |
|
它的 Key,它能被什么找到 |
“我是一个动作,需要动作相关的查询来找我。” |
|
它的 Value,它实际提供什么 |
“吃”这个动作的语义:进食、需要食物、可能有主语和宾语…… |
这三套信息都藏在同一个 embedding 向量里。
所以需要三组不同的投影矩阵,把它们提取出来:
q = x · W_Q
k = x · W_K
v = x · W_V
再换一个更日常的类比:图书馆找书
|
步骤 |
你做的事 |
对应 |
|---|---|---|
|
1 |
你在搜索框输入“Python 入门” |
Query |
|
2 |
每本书书脊上印着关键词 |
Key |
|
3 |
找到书后翻开读到具体内容 |
Value |
你的搜索词不等于书脊标签,书脊标签也不等于书的内容。
但它们必须配合:
Q 和 K 匹配上了,你才能拿到 V。
模型里的注意力机制,就是这个过程。
一句话:
Q:我在找什么。
K:我能被什么找到。
V:我实际提供什么。
这里需要特别强调一点:
Q、K、V 并不是 token 天然自带的三个字段。
每个 token 进入注意力层时,通常只有一个输入向量 x。
这个 x 可能来自最初的 embedding,
也可能来自上一层 Transformer 的输出。
模型会通过三组可训练的权重矩阵,
把同一个 x 投影成三种不同用途的向量:
q = x · W_Q
k = x · W_K
v = x · W_V
其中:
W_Q 是 Query 投影权重,决定“从 x 中提取什么查询信息”。
W_K 是 Key 投影权重,决定“从 x 中提取什么标签信息”。
W_V 是 Value 投影权重,决定“从 x 中提取什么内容信息”。
也就是说:
同一个 x,
经过不同的权重矩阵,
会得到三种不同的表示。
这些权重矩阵一开始通常是随机初始化的。
在训练过程中,模型会不断根据预测误差调整它们。
训练结束后,W_Q、W_K、W_V 才逐渐学会:
应该从 x 中提取哪些对语言建模有用的信息。
8.4 矩阵乘法到底在做什么
公式:
q = x · W_Q
看起来吓人,其实可以类比成加权打分。
第零步:你其实已经会了
假设你是老师,要给一个学生打“理科潜力分”。
学生四门成绩:
数学 90
语文 60
物理 85
英语 50
你心里有一个标准:理科潜力主要看数学和物理,语文英语不太重要。
于是你定权重:
数学 0.5
语文 0.1
物理 0.3
英语 0.1
计算:
理科潜力分
= 90 × 0.5 + 60 × 0.1 + 85 × 0.3 + 50 × 0.1
= 45 + 6 + 25.5 + 5
= 81.5
这就是一个加权求和。
第一步:再来一个“文科潜力分”
同一个学生,你还想打一个“文科潜力分”。
这次标准不一样:文科主要看语文和英语。
权重变成:
数学 0.1
语文 0.4
物理 0.1
英语 0.4
计算:
文科潜力分
= 90 × 0.1 + 60 × 0.4 + 85 × 0.1 + 50 × 0.4
= 9 + 24 + 8.5 + 20
= 61.5
第二步:把两组权重放在一起看
理科权重 文科权重
数学 0.5 0.1
语文 0.1 0.4
物理 0.3 0.1
英语 0.1 0.4
这张小表格,就是一个矩阵。
把名字换掉:
学生四门成绩 → x
理科潜力分 → q 的第 1 个分量
文科潜力分 → q 的第 2 个分量
那张权重表 → W_Q
所以:
q = x · W_Q
本质上就是:
用一组权重,对 x 做加权求和,提取出一种信息。
8.5 用模型的数字走一遍
假设“吃”的 embedding 是 4 维:
x = [0.8, 0.1, -0.2, 0.4]
假设 W_Q 是 4 行 2 列,把 4 维变成 2 维:
W_Q =
[
[ 0.5, 0.1],
[ 0.2, -0.3],
[-0.4, 0.8],
[ 0.1, 0.2]
]
计算 q 的第 1 个分量:
q_1
= 0.8 × 0.5 + 0.1 × 0.2 + (-0.2) × (-0.4) + 0.4 × 0.1
= 0.40 + 0.02 + 0.08 + 0.04
= 0.54
计算 q 的第 2 个分量:
q_2
= 0.8 × 0.1 + 0.1 × (-0.3) + (-0.2) × 0.8 + 0.4 × 0.2
= 0.08 - 0.03 - 0.16 + 0.08
= -0.03
结果:
q = [0.54, -0.03]
这就是“吃”的 Query 向量。
K 和 V 的操作完全一样,只是换不同的权重矩阵:
k = x · W_K
v = x · W_V
8.6 多个 Token:矩阵形式
三个 token 各自计算 q、k、v。
写在一起就是:
Q = X · W_Q
K = X · W_K
V = X · W_V
这不是新操作。
它只是:
每个 token 分别乘同一张权重表,
然后写成矩阵形式一次算完。
GPU 非常擅长这种矩阵乘法。
8.7 W_Q、W_K、W_V 是怎么来的
它们不是人工设计出来的,而是训练出来的。
还是用老师的例子
你第一天当老师,完全不知道什么权重好。
你随便猜:
数学 × 0.9 + 语文 × 0.9 + 物理 × 0.9 + 英语 × 0.9
结果预测不准。
于是你开始调:
第 2 天:数学 × 0.7 + 语文 × 0.3 + 物理 × 0.7 + 英语 × 0.3
第 3 天:数学 × 0.6 + 语文 × 0.2 + 物理 × 0.5 + 英语 × 0.2
...
第 1000 天:数学 × 0.5 + 语文 × 0.1 + 物理 × 0.3 + 英语 × 0.1
这组权重不是谁告诉你的,是你看了很多学生、对了多次答案后调出来的。
模型也是这样。
一次训练循环长什么样
- 给模型看:
今天天气很___
- 模型用当前参数预测:
汽车
- 正确答案是:
好
- 模型发现自己猜错了。
- 反向传播会告诉模型:
-
哪些参数往哪个方向调一点点, 下次遇到类似情况,猜对的概率会更大。 - 这个过程重复几十亿次、几万亿次。
训练前:
W_Q、W_K、W_V 都是随机数。
训练后:
这些矩阵被海量数据“逼”出有意义的投影方向。
例如:
“吃”的 Q 可能自然地去寻找动作发出者和动作对象。
“鱼”的 K 可能更容易被“吃”这类动作查询命中。
8.8 本节小结
1. X 是 token embedding 或上一层 hidden state 组成的矩阵。
2. Q、K、V 不是 token 自带的字段,而是通过权重矩阵投影出来的。
3. W_Q、W_K、W_V 是可训练的投影权重。
4. Q 表示“我在找什么”。
5. K 表示“我能被什么找到”。
6. V 表示“找到我后,我提供什么信息”。
7. 投影权重 W_Q/W_K/W_V 是模型参数;后面 softmax 得到的注意力权重是动态计算结果,两者不是同一个东西。
8.9 形状速查表:MLOps 必须熟悉的维度关系
假设:
batch_size = B
序列长度 = L
模型主维度 hidden_size = d_model
Query 头数 num_attention_heads = n_q
KV 头数 num_key_value_heads = n_kv
每个头维度 head_dim = d_head
通常有:
d_model = n_q × d_head
例如:
hidden_size = 4096
num_attention_heads = 32
head_dim = 128
那么:
4096 = 32 × 128
输入 X 的形状通常是:
X: [B, L, d_model]
如果使用标准 MHA:
W_Q: [d_model, n_q × d_head]
W_K: [d_model, n_q × d_head]
W_V: [d_model, n_q × d_head]
如果使用 GQA:
W_Q: [d_model, n_q × d_head]
W_K: [d_model, n_kv × d_head]
W_V: [d_model, n_kv × d_head]
其中:
n_kv < n_q
例如:
num_attention_heads = 32
num_key_value_heads = 8
则:
Q 头数 = 32
K/V 头数 = 8
每 4 个 Q 头共享一组 KV 头。
投影后:
Q: [B, L, n_q, d_head]
K: [B, L, n_kv, d_head]
V: [B, L, n_kv, d_head]
在推理框架中,通常会转成:
Q: [B, n_q, L, d_head]
K: [B, n_kv, L, d_head]
V: [B, n_kv, L, d_head]
KV Cache 显存大小主要和下面几个字段有关:
num_hidden_layers
num_key_value_heads
head_dim
缓存的 token 数量
精度类型 bytes
所以看模型配置时,不要只看:
num_attention_heads
一定要看:
num_key_value_heads
因为它直接决定 KV Cache 的显存占用。
工程注记:
在 PyTorch 中,nn.Linear的权重形状通常是:[out_features, in_features]实际计算是:
x @ W.T本文为了教学直观,把投影矩阵写成:
[d_model, output_dim]本质相同。
9. 注意力计算:从分数到输出
有了 Q、K、V 之后,注意力机制做三步:
1. Q 和 K 算相关性分数
2. softmax 把分数变成权重
3. 权重加权 V,得到输出
完整公式:
Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V
9.1 第一步:Q 和 K 做点积,算相关性
点积就是对应位置相乘再求和:
q · k = q_1 k_1 + q_2 k_2 + q_3 k_3 + ...
假设正在处理“吃”,也就是第 2 个 token。
“吃”的 Query:
q^(2)
去和所有可见 token 的 Key 做点积:
|
计算 |
含义 |
假设结果 |
|---|---|---|
|
|
“吃”和“我”有多相关 |
5.1 |
|
|
“吃”和“吃”自己有多相关 |
1.0 |
|
|
“吃”和“鱼”有多相关 |
4.3 |
模型认为:
吃应该多关注“我”和“鱼”,少关注自己。
这很合理:
谁吃?我。
吃什么?鱼。
矩阵形式:
Q K^T =
[
[q^(1)·k^(1), q^(1)·k^(2), q^(1)·k^(3)],
[q^(2)·k^(1), q^(2)·k^(2), q^(2)·k^(3)],
[q^(3)·k^(1), q^(3)·k^(2), q^(3)·k^(3)]
]
Q K^T 一次性算出所有 token 对之间的相关性分数。
9.2 为什么用点积?
也可以用其他打分方式,例如:
MLP 打分
加性注意力
余弦相似度
但点积有一个决定性优势:
Q K^T 是一次矩阵乘法,GPU 可以高度并行。
在大模型中,序列长度可能是几千、几万,效率是硬约束。
9.3 第二步:缩放 + softmax
为什么要除以 sqrt(d_k)?
d_k 是 K 向量的维度,也就是每个注意力头的维度。
如果 Q 和 K 的每个元素近似满足:
均值为 0
方差为 1
那么点积会累加 d_k 项。
结果近似:
点积方差 ≈ d_k
点积标准差 ≈ sqrt(d_k)
维度越大,点积值可能越大。
如果点积太大,进入 softmax 后会变得非常尖锐:
几乎全部权重集中在一个 token 上。
这可能导致:
梯度变小
训练不稳定
注意力分布过早坍缩
所以要除以:
sqrt(d_k)
让数值更稳定。
9.4 softmax 把分数变成权重
softmax 公式:
softmax(x_i) = exp(x_i) / sum_j exp(x_j)
效果是:
把一组任意实数变成一组和为 1 的权重。
例如:
scores = [5.1, 1.0, 4.3]
softmax 后近似:
weights = [0.68, 0.01, 0.31]
这些权重加起来等于 1。
9.5 第三步:权重加权 V
假设“吃”对所有 token 的注意力权重是:
对“我”:0.68
对“吃”:0.01
对“鱼”:0.31
那么“吃”的输出向量是:
output^(2)
= 0.68 · v^(1)
+ 0.01 · v^(2)
+ 0.31 · v^(3)
也就是说:
“吃”的新表示,
融合了“我”和“鱼”的信息。
此时“吃”不再是孤立的“吃”,而是:
谁在吃?我。
吃什么?鱼。
这个上下文融合后的“吃”。
一句话:
Q 和 K 决定“关注谁”。
V 决定“从被关注的对象那里拿到什么”。
9.6 举例:指代消解
句子:
这本书很有趣,它让我学到了很多。
当模型处理“它”时:
- “它”的 Q 去和可见位置的 K 匹配。
- 发现“书”的 K 和“它”的 Q 很相关。
- 模型给“书”较高注意力权重。
- 从“书”的 V 中取出信息。
- “它”的新表示就融合了“书”的信息。
于是模型更容易理解:
“它”指的是“这本书”。
9.7 注意力计算步骤汇总
|
步骤 |
公式 |
作用 |
|---|---|---|
|
1 |
|
生成查询向量 |
|
2 |
|
生成键向量 |
|
3 |
|
生成值向量 |
|
4 |
|
算所有 token 对的相关性分数 |
|
5 |
|
缩放,防止 softmax 过尖锐 |
|
6 |
|
把分数变成权重 |
|
7 |
权重 × V |
融合其他 token 的信息 |
9.8 带 mask 的注意力计算伪代码
实际工程中,注意力计算通常写成如下形式:
scores = Q @ K.transpose(-2, -1)
scores = scores / sqrt(d_k)
scores = scores.masked_fill(mask == 0, float("-inf"))
attn_weights = softmax(scores, dim=-1)
output = attn_weights @ V
解释:
Q @ K.transpose(-2, -1)
计算每个 Query 对每个 Key 的相关性分数。
除以 sqrt(d_k)
防止点积过大导致 softmax 过于尖锐。
masked_fill(mask == 0, -inf)
把不能看的位置 mask 掉。
softmax(scores, dim=-1)
对每一个 Query 自己的所有 Key 分数做归一化。
也就是说,softmax 是按行做的。
每一行权重加起来等于 1。
attn_weights @ V
用注意力权重对 Value 做加权求和。
得到融合上下文后的新表示。
在 decoder-only 自回归模型中,mask 通常同时包含:
causal mask:不能看未来
padding mask:不能看 padding
可以粗略理解为:
final_mask = causal_mask & padding_mask
不同框架的实现细节可能不同,但核心思想一致:
先算分数,再 mask,再 softmax,最后加权 V。
工程注记:
实际实现中要避免某一行全部被 mask 成-inf,否则softmax([-inf, -inf, ...])可能产生 NaN。
因此 padding、空输入、异常 batch 都需要在数据侧或框架侧处理。
注意区分两种“权重”:
W_Q、W_K、W_V、W_O 是模型参数,也叫投影权重。
它们在训练过程中被更新。
softmax(scores) 得到的 attn_weights 是注意力权重。
它不是固定参数,而是每次输入都会动态计算出来的结果。
10. 因果掩码:自回归模型不能偷看后文
很多现代大模型是 decoder-only 自回归模型。
它们生成文本时是从左到右逐 token 生成的。
例如:
我 → 今天 → 很 → 开心
在训练和生成时,第 t 个位置只能看到:
1, 2, ..., t
不能看到:
t + 1, t + 2, ...
这就是因果掩码,英文叫 causal mask。
10.1 因果掩码怎么实现
在:
Q K^T
算出分数后、softmax 之前,把不能看的位置设成负无穷:
scores =
[
[s11, -inf, -inf],
[s21, s22, -inf],
[s31, s32, s33]
]
经过 softmax 后:
-inf 位置的权重变成 0。
等价地,也可以理解成下面这种可见性掩码:
[
[1, 0, 0],
[1, 1, 0],
[1, 1, 1]
]
对应句子:
|
当前位置 |
能看到 |
|---|---|
|
我 |
我 |
|
吃 |
我、吃 |
|
鱼 |
我、吃、鱼 |
10.2 为什么因果掩码很重要
如果没有因果掩码,模型训练时就能直接看到后面的答案。
例如预测:
我 吃 ___
如果模型能直接看到后面的“鱼”,任务就太简单了。
这样学不到真正的语言建模能力。
10.3 双向模型不用因果掩码
BERT 这类双向编码器可以看到整个句子。
它更适合:
分类
实体抽取
填空
语义理解
GPT 类生成模型使用因果掩码,因为要逐 token 生成。
10.4 训练并行,生成串行
这是大模型工程中非常重要的一个概念。
生成时:
模型是一个 token 一个 token 往后生成的。
例如:
今天
今天天气
今天天气很
今天天气很好
每一步都需要依赖上一步生成的结果。
所以 Decode 阶段本质上是串行的。
但训练时不一样。
训练时,模型不需要真的先生成再学习。
训练数据里已经有完整句子。
例如:
我 吃 鱼
借助因果掩码,模型可以在一次前向传播中同时学习:
位置 1:
输入:我
目标:吃
位置 2:
输入:我 吃
目标:鱼
位置 3:
输入:我 吃 鱼
目标:EOS 或下一个 token
也就是说:
训练时,多个位置的 loss 可以并行计算。
生成时,下一个 token 必须逐步生成。
这就是为什么:
训练可以高度并行,效率很高。
推理生成阶段则更容易受显存带宽和 KV Cache 影响。
MLOps 视角:
训练关注:吞吐 loss 梯度 优化器 显存峰值推理关注:
TTFT TPOT tokens/s KV Cache batch 吞吐 显存带宽这两个阶段的瓶颈完全不同。
更多推荐
所有评论(0)