第 3 章:概率、Softmax、交叉熵与 next-token
模型不是在分类图片,而是在继续写一句话。它到底怎么训练?
第 3 章:概率、Softmax、交叉熵与 next-token
本章核心困惑
模型不是在分类图片,而是在继续写一句话。它到底怎么训练?
很多人第一次学语言模型时,会以为模型是在“理解一句话后直接生成答案”。但从训练目标看,语言模型做的是一件非常朴素的事:
给定前面的 token,预测下一个 token 的概率分布。
比如输入:
我 喜欢 学 大模
模型不是一次性“想出”完整回答,而是在当前上下文后面预测:
型: 0.72
块: 0.08
...
然后选出或采样一个 token,接到上下文后面,再预测下一个。
这就是大模型看起来一个字一个字往外蹦的根本原因。
本章要回答一串关键问题:
logits 是什么?
Softmax 为什么能把分数变成概率?
交叉熵为什么是 next-token training 的 loss?
labels 为什么要右移?
temperature、top-k、top-p 又在改什么?
如果这章学透,后面你会更容易理解 SFT、蒸馏、困惑度、采样失控、格式训练和无依据幻觉。
前置知识
- 会看
[B,T,V]logits 和[B,T]labels。 - 知道概率分布表示“每个候选答案的可能性”。
- 已能跑最小 PyTorch 训练循环。
- 理解 token id 只是编号,还不是语义向量。
本章新增能力
学完本章,你应该能做到:
- 解释语言模型为什么是条件概率模型。
- 手算一个小词表上的 Softmax。
- 手算正确答案概率对应的交叉熵。
- 解释 label shift 为什么不能错。
- 区分训练时 teacher forcing 和推理时 autoregressive generation。
- 解释 greedy、temperature、top-k、top-p 和 perplexity。
一句话记忆:
语言模型训练是在最大化“正确下一个 token”的概率;生成是在不断从“下一个 token 概率分布”里选 token。
本章的问题演化链是:
语言有不确定性
-> 模型需要输出概率分布
-> logits 先给未归一化分数
-> Softmax 把分数变概率
-> 交叉熵惩罚正确 token 概率低
-> 最大似然解释为什么要这样训练
-> label shift 决定每个位置预测谁
-> 采样策略决定推理时如何选下一个 token
1. 概率先解决什么问题:不确定性
真实语言不是一条确定规则。
输入:
请分析这段合同的
后面可能是:
风险
条款
违约责任
法律后果
这些候选并不是非黑即白,而是都有可能。模型需要表达这种不确定性,所以它输出的不是一个答案,而是一整个概率分布。
概率分布至少满足两个条件:
每个概率 >= 0
所有概率加起来 = 1
例如小词表:
["风险", "条款", "猫"]
模型可能输出:
风险: 0.70
条款: 0.25
猫: 0.05
这不是说模型真的“知道法律”,只是说在当前上下文下,它认为“风险”是更可能的下一个 token。
2. 条件概率:语言模型预测的是 P(next | context)
语言模型的核心形式可以写成:
P(token_t | token_1, token_2, ..., token_{t-1})
读作:
在已经看到前面这些 token 的条件下,第 t 个 token 是什么的概率。
一句话的概率可以拆成很多个条件概率相乘:
P("我 喜欢 学 模型")
= P(我)
* P(喜欢 | 我)
* P(学 | 我, 喜欢)
* P(模型 | 我, 喜欢, 学)
这叫链式分解。
语言模型训练并不是直接给整句话打一个玄学分数,而是在每个位置都训练:
看到前文 -> 预测下一个 token
这就是 next-token prediction。
3. logits:模型先输出分数,不直接输出概率
模型最后一层通常输出 logits:
logits: [B,T,V]
对某个位置来说,logits 是长度为 V 的一排分数:
风险: 2.0
条款: 0.5
猫: -1.0
这些分数还不是概率,因为:
- 可以是负数。
- 不一定小于 1。
- 加起来不等于 1。
为什么模型不直接输出概率?因为分数空间更自由,训练更稳定。我们用 Softmax 把 logits 转成概率:
prob_i = exp(logit_i) / sum(exp(logit_j))
4. Softmax 手算:把分数变成概率
假设 logits 是:
风险: 2.0
条款: 1.0
猫: 0.0
先取指数:
exp(2.0) ≈ 7.39
exp(1.0) ≈ 2.72
exp(0.0) = 1.00
总和:
7.39 + 2.72 + 1.00 = 11.11
概率:
风险: 7.39 / 11.11 ≈ 0.665
条款: 2.72 / 11.11 ≈ 0.245
猫: 1.00 / 11.11 ≈ 0.090
Softmax 的直观效果:
- 分数大的 token 概率更高。
- 所有 token 都保留非零概率。
- 分数差距会被指数函数放大。
数值稳定技巧:真实代码不会直接 exp(logits),因为 logits 很大时会溢出。通常会先减去最大值:
shifted = logits - logits.max(dim=-1, keepdim=True).values
probs = shifted.exp() / shifted.exp().sum(dim=-1, keepdim=True)
减去同一个常数不会改变 Softmax 结果,但能避免 exp(1000) 这种溢出。
5. 交叉熵:惩罚模型不相信正确答案
训练时我们知道正确答案是什么。比如当前上下文后面的正确 token 是:
风险
模型给出的概率是:
风险: 0.665
条款: 0.245
猫: 0.090
交叉熵对单个样本可以理解成:
loss = -log(正确答案的概率)
所以:
loss = -log(0.665) ≈ 0.408
如果模型只给正确答案 0.1:
loss = -log(0.1) ≈ 2.303
如果模型给正确答案 0.01:
loss = -log(0.01) ≈ 4.605
直观感受:
- 正确答案概率越高,loss 越小。
- 正确答案概率越低,loss 增长很快。
- 交叉熵不关心模型说得像不像人,只关心正确 token 的概率。
这点非常重要。一个模型可以把训练集 token 概率学得很好,但在法律/医学任务中仍然可能没有证据、没有边界、没有安全意识。所以后面必须引入 RAG、SFT、评测和安全治理。
6. 最大似然:训练到底在最大化什么
如果一句话有很多 token,模型希望每个正确 next token 的概率都尽量高。
最大似然可以写成:
maximize P(token_1, token_2, ..., token_T)
用链式分解:
maximize ∏ P(token_t | tokens_<t)
乘很多小概率很容易数值下溢,所以实际会取 log:
maximize Σ log P(token_t | tokens_<t)
训练框架通常最小化 loss,所以变成:
minimize -Σ log P(token_t | tokens_<t)
这就是交叉熵 loss 的来源。
一句话记忆:
最大似然是“让训练文本中真实出现的下一个 token 变得更可能”;交叉熵是它的可优化 loss 形式。
7. label shift:为什么 labels 要右移
语言模型不是预测当前 token,而是预测下一个 token。
给定序列:
[A, B, C, D]
输入和标签应该对齐成:
input_ids = [A, B, C]
labels = [B, C, D]
如果保留同样长度,也可以写成:
input_ids = [A, B, C, D]
labels = [B, C, D, EOS]
意思是:
看到 A,预测 B
看到 A B,预测 C
看到 A B C,预测 D
看到 A B C D,预测 EOS
本课程在第 3、4 章的 tiny 代码里采用一个简单约定:dataset 预先右移 labels,模型和 language_modeling_loss 不再二次 shift。
dataset 输出:
input_ids = [A, B, C, D]
labels = [B, C, D, EOS]
loss 计算:
logits: [B,T,V] 直接对齐 labels: [B,T]
很多 Hugging Face causal LM API 采用另一种常见约定:dataset 输出同长度 input_ids 和 labels,labels 复制 input_ids 并把不训练位置设为 -100,模型或 loss 内部再使用 logits[:, :-1] 对齐 labels[:, 1:]。两种约定都对,但一个项目里只能选一种。最危险的是 dataset 已经右移,模型里又右移一次,或者两边都没右移。
最常见错误是 labels 没有右移:
input_ids = [A, B, C, D]
labels = [A, B, C, D]
这样模型学到的是复制当前 token,而不是预测下一个 token。训练 loss 可能看起来下降,但生成时会出怪现象。
8. Teacher forcing:训练时并行,推理时循环
训练时,模型可以一次性看到完整输入序列,并在每个位置预测下一个 token:
logits: [B,T,V]
labels: [B,T]
这叫 teacher forcing:训练时把真实历史 token 喂给模型。
并行训练不等于看未来。decoder-only LM 必须配合 causal mask,保证第 t 个位置只能使用 t 及之前的 token 表示;如果没有 causal mask,模型可能偷看未来 token,loss 会虚低。
推理时不一样。模型不知道未来 token,只能循环:
prompt
-> predict next token
-> append token
-> predict next token
-> append token
这就是 autoregressive generation。
训练和推理的差异解释了很多现象:
- 训练可以并行,推理通常按 token 逐步生成。
- 训练 loss 低,不代表长文本生成稳定。
- 一旦前面采样错了,后续上下文也会被污染。
9. temperature:改变概率分布的尖锐程度
生成时常用 temperature 调整 logits:
logits = logits / temperature
这个公式要求 temperature > 0。很多推理 API 里的 temperature=0 是工程特殊约定,通常表示 greedy 或 deterministic decoding,不是实际执行 logits / 0。
如果 temperature 小于 1:
- 大 logits 更突出。
- 分布更尖。
- 输出更稳定,但可能更保守。
如果 temperature 大于 1:
- 分布更平。
- 更多 token 有机会被采样。
- 输出更多样,但也更容易跑偏。
极端情况:
temperature -> 0:接近总选最高分
temperature 很大:接近随机
法律和医学场景通常更偏向低 temperature,因为安全、可追溯和稳定比“有创意”更重要。
10. top-k 与 top-p:限制采样候选
如果直接从整个词表采样,尾部低概率 token 也可能被抽中。top-k 和 top-p 是两种常见限制。
top-k:
只保留概率最高的 k 个 token
例如 k=3,只从前三名里采样。
top-p,也叫 nucleus sampling:
按概率从高到低排序,保留累计概率达到 p 的最小集合
例如 p=0.9,保留能覆盖 90% 概率质量的候选。
两者差异:
| 方法 | 固定什么 | 适合直觉 |
|---|---|---|
| top-k | 候选数量 | 每次只看前 k 个 |
| top-p | 概率质量 | 根据分布尖锐程度动态调整候选 |
高风险领域里,采样策略不能替代安全机制。即使 temperature 很低,模型仍然可能在无证据时编造。采样只是生成控制,不是事实保证。
11. perplexity:平均有多困惑
困惑度 perplexity 通常定义为:
perplexity = exp(average_cross_entropy)
如果平均交叉熵是 0.69:
perplexity ≈ exp(0.69) ≈ 2
可以粗略理解为:模型平均每一步像是在两个差不多的候选里犹豫。
如果平均交叉熵是 2.30:
perplexity ≈ exp(2.30) ≈ 10
模型平均更困惑。
但 perplexity 也有边界:
- 它适合衡量语言建模拟合程度。
- 它只适合在相同 tokenizer、相同 eval set、相同 loss mask 规则下比较。
- 不直接等于任务正确率。
- 不保证引用真实。
- 不保证安全拒答。
一个医学助手 perplexity 低,不代表它不会给危险建议。后面评测章节会把格式、事实、引用、拒答、安全切片分开测。
12. 最小 PyTorch 代码
训练 loss 的核心形式:
import torch
import torch.nn.functional as F
B, T, V = 2, 4, 10
sequence = torch.randint(0, V, (B, T + 1))
input_ids = sequence[:, :-1]
labels = sequence[:, 1:] # dataset 预右移:每个位置预测下一个 token
logits = torch.randn(B, T, V)
loss = F.cross_entropy(
logits.reshape(-1, V),
labels.reshape(-1),
ignore_index=-100,
)
为什么要 reshape?
F.cross_entropy 常见输入是:
logits: [N,V]
labels: [N]
语言模型的每个 batch、每个位置都是一个分类样本,所以:
[B,T,V] -> [B*T,V]
[B,T] -> [B*T]
这不是改变语义,而是把所有位置摊平成一批分类任务。
如果采用 Hugging Face 风格的“同长度 labels + 内部 shift”,核心会变成:
shift_logits = logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()
loss = F.cross_entropy(
shift_logits.view(-1, V),
shift_labels.view(-1),
ignore_index=-100,
)
这会跳过对应位置的 loss。无论采用哪种约定,ignore_index=-100 都只属于 labels,不是 tokenizer 的合法 token id。
13. 最小梯度推导:为什么 prob - one_hot 会出现
很多教材会直接说“交叉熵配合 Softmax 很方便”,但不解释方便在哪里。看一个单位置、三分类的最小推导。
设 logits 为:
z = [2.0, 1.0, 0.0]
Softmax 后概率约为:
p = [0.665, 0.245, 0.090]
如果正确类别是第 0 个,one-hot 标签是:
y = [1, 0, 0]
交叉熵:
L = -log(p0)
对每个 logit 的梯度可以化简为:
dL / dz_i = p_i - y_i
所以:
第 0 类: 0.665 - 1 = -0.335
第 1 类: 0.245 - 0 = 0.245
第 2 类: 0.090 - 0 = 0.090
梯度下降更新 logits 背后的参数时,会推动正确类别 logit 变大,错误类别 logit 变小。这个结果非常直观:
- 模型给正确 token 的概率还不够高,所以正确类梯度是负的,更新会抬高它。
- 模型给错误 token 分了一些概率,所以错误类梯度是正的,更新会压低它。
- 错误 token 概率越高,被压低得越多。
这也是为什么语言模型可以从海量 next-token 位置中学习:每个位置都给出一个“把概率从错误 token 挪向正确 token”的训练信号。
14. 两个例子、一个反例:概率目标的能力和边界
例子 1:法律回答的下一个 token。
上下文:
该条款可能存在
合理后续可能是:
风险、争议、无效、问题
训练数据如果总是写“可能存在风险,需要人工复核”,模型会提高这些 token 在类似上下文下的概率。SFT 本质上仍然是在塑造 next-token 分布,让边界表达更可能出现。
例子 2:医学红旗症状分流。
上下文:
胸痛伴呼吸困难持续 30 分钟,建议
安全数据应该让“立即就医”“联系急救”等 token 概率升高,而不是让“先观察”“自行服药”等 token 概率升高。这里的安全性来自数据和评测约束,不来自 Softmax 本身。
反例:流畅概率不等于事实正确。
如果训练语料里经常出现某种错误说法,模型可能给错误说法很高概率。交叉熵不会自动判断事实真伪,它只会让训练文本中出现过的 next token 更可能。法律和医学场景必须额外引入证据检索、引用支持、拒答边界和人工复核。
边界:perplexity 衡量的是模型对文本分布的拟合,不是“这个回答是否有依据”“是否合规”“是否安全”。一个低 perplexity 模型仍可能在缺少证据时编造法条或医学建议。
15. 常见错误
| 常见错误 | 正确认识 |
|---|---|
| labels 不右移 | 模型会学成复制当前 token |
对 logits 先 Softmax 再传给 cross_entropy | PyTorch 的 F.cross_entropy 内部已包含 log-softmax |
| 忽略 padding token | pad 进入 loss 会污染训练 |
| 以为训练时逐 token 生成 | 训练时可并行预测所有位置 |
| 以为 perplexity 低就能发布 | 还要任务评测、安全和证据 |
| temperature 能解决幻觉 | 它只改采样分布,不提供事实依据 |
| top-p 越高越好 | 高风险场景通常需要更稳、更可控 |
16. 测试验收
本章学完,至少应该能通过这些验收:
- 本章最低产物:跑通 next-token loss 的最小代码,并能说清 dataset 预右移与 Hugging Face 内部 shift 的区别。
- 能手算一个 3 token 词表的 Softmax。
- 能从正确答案概率手算交叉熵。
- 能把
[A,B,C,D]写成正确的 input/label 对。 - 能写测试证明错误右移会改变训练目标。
- 能解释
logits.reshape(-1,V)和labels.reshape(-1)。 - 能比较 greedy、temperature、top-k、top-p 的输出差异。
- 能计算 perplexity,并说明它不等于安全可靠。
FAQ
1. 为什么不用准确率训练语言模型?
因为语言模型要输出整个词表上的概率分布。准确率只关心最高分是不是正确,丢掉了“第二可能是什么”“模型有多不确定”等信息。交叉熵能更细致地惩罚概率分布。
2. 为什么 F.cross_entropy 接收 logits,而不是概率?
为了数值稳定和计算效率。它内部会做 log-softmax。如果你先手动 Softmax,再传进去,可能造成数值和语义错误。
3. SFT 和 next-token training 是不是两套东西?
底层仍然是 next-token training。区别是 SFT 把数据组织成 system/user/assistant 格式,并常常只对 assistant 部分算 loss,让模型学习回答格式和行为边界。
4. 为什么生成会越写越偏?
因为推理时模型把自己生成的 token 再喂回上下文。如果早期采样了不合适的 token,后续预测就建立在污染后的上下文上。
自测题
- logits
[2.0, 1.0, 0.0]经过 Softmax 后,最大概率大约是多少? - 正确答案概率从 0.9 降到 0.1,交叉熵会变大还是变小?
[A,B,C,D]的 next-token labels 应该是什么?- temperature 变大时,输出通常更稳定还是更多样?
- perplexity 低能否证明法律模型可以上线?
答案要点:
- 最大概率约 0.665。
- 交叉熵变大。
- 可以是
[B,C,D,EOS]。 - 更多样,也更可能跑偏。
- 不能,还需要任务指标、引用、安全、人工复核和发布门禁。
想继续深挖
继续深挖 next-token,可以把训练目标写成整段序列的负对数似然:
P(x_1, x_2, ..., x_T) = Π_t P(x_t | x_<t)
loss = - Σ_t log P(x_t | x_<t)
交叉熵不是额外发明的损失,它就是让真实下一个 token 的概率变大。若正确 token 概率是 0.8,loss 是 -log(0.8);若正确 token 概率是 0.01,loss 是 -log(0.01),惩罚会大很多。
采样也可以继续公式化:
p_i = softmax(logits_i / temperature)
temperature < 1 会让分布更尖,temperature > 1 会让分布更平。top-k/top-p 不是改变模型知识,而是改变从分布里取样的候选集合。领域项目里,这解释了为什么“生成更稳定”不等于“事实更可靠”:采样只控制输出分布形状,不提供证据。
和领域项目的关系
法律和医学回答最终也是一个个 token 生成出来的。理解 next-token 目标,才能解释为什么模型会流畅地编造、为什么 SFT 学的是格式和行为而不是事实来源、为什么 RAG 要给证据、为什么高风险场景需要拒答和人工复核。
本章的数学目标是后面所有训练方法的共同底层:SFT、LoRA、QLoRA、蒸馏都还在优化 token loss,只是训练数据、可训练参数和约束条件不同。