6. 训练目标:预测下一个词元
模型输出的是分数,不是直接的答案。要训练它,必须把“分数”和“正确词元”比较,得到一个可以反向传播的数值。这个数值就是损失。
这一页讲清三件事:
- 模型在每个位置输出什么。
- 交叉熵怎样衡量预测好坏。
- 为什么评估时还要看每字节比特数(BPB)。
一个位置的一次预测
假设词表里只有四个候选词元:
A: 0.1
B: 0.7
C: 0.1
D: 0.12
3
4
这些原始分数叫 logits。它们可以是任意实数,还不是概率。
经过 softmax 后,变成总和为 1 的概率:
A: 0.15
B: 0.55
C: 0.15
D: 0.152
3
4
如果正确答案是 B,模型给 B 的概率是 0.55。损失用负对数似然计算:
loss = -ln(0.55) ≈ 0.60如果模型给正确答案的概率是 0.99,损失约为 0.01;如果只给 0.01,损失约为 4.61。预测越差,损失越大。
一句话的损失怎样算
一段长度为 T 的序列,每个位置都有一个目标词元。模型会输出 T 个损失,训练时通常取平均:
loss = (loss_0 + loss_1 + ... + loss_{T-1}) / T在代码中,这一步由 F.cross_entropy 一次完成:
nanochat/gpt.py第 510–521 行↗logits = self.lm_head(x)
logits = logits[..., :self.config.vocab_size]
logits = logits.float()
logits = softcap * torch.tanh(logits / softcap)
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)),
targets.view(-1),
ignore_index=-1,
reduction=loss_reduction,
)2
3
4
5
6
7
8
9
10
11
这里发生了四件事:
- 输出层把隐藏向量投影成词表大小的分数。
- 切掉为保证计算效率而填充的词表位置。
- 用
tanh对 logits 做平滑限制,避免数值过大。 - 把
(B, T, V)展平成(B×T, V),与目标逐一计算交叉熵。
ignore_index=-1 表示目标是 -1 的位置不计算损失。SFT 中用户消息和填充位置就用这个值屏蔽。
为什么目标要右移一位
输入和目标来自同一行词元,但错开一格:
row: [BOS, 法国, 的, 首都, 是, 巴黎]
inputs: [BOS, 法国, 的, 首都, 是]
targets: [法国, 的, 首都, 是, 巴黎]2
3
模型在位置 0 看到 BOS,目标是 法国;在位置 4 看到 是,目标是 巴黎。
这看起来像“模型提前看到了答案”,其实不是。训练时整行都在输入里,但因果掩码保证位置 4 不能关注位置 5 的 巴黎。它只能根据 BOS 法国 的 首都 是 预测下一个词元。
softcap 是什么
nanochat 在输出层后加了一步:
logits = 15 * torch.tanh(logits / 15)tanh 会把输入平滑地压到 -1 到 1,再乘以 15,就把 logits 限制在 -15 到 15 之间。
这不是为了让概率更准,而是为了数值稳定。没有 softcap 时,个别 logits 可能变得非常大,softmax 后几乎变成 one-hot,训练容易变得不稳定。
训练损失和验证损失
- 训练损失:在当前参与训练的数据上计算。
- 验证损失:在模型没有训练过的数据上计算。
训练损失持续下降,不一定说明模型变强了。它可能只是记住了训练数据。验证损失更能反映模型对新文本的预测能力。
nanochat 在预训练过程中定期在验证分片上计算损失。你会在日志里看到 Validation bpb,而不是单纯的 Validation loss。原因是按词元平均的损失受分词器影响,不同词表之间不可直接比较。
从损失到每字节比特数
假设分词器把一个词元表示成多个字节。不同分词器的词元长度不同,所以“每个词元的平均损失”不能公平比较。
nanochat 的做法是:
- 为每个普通词元记录它对应多少个字节,保存在
token_bytes.pt。 - 对每个目标词元,取出它的损失和字节数。
- 把损失换算成比特,再除以总字节数。
公式是:
BPB = 总损失(自然对数单位) / (ln(2) × 总字节数)ln(2) 把自然对数单位换算成以 2 为底的对数,也就是比特。
nanochat/loss_eval.py第 8–64 行↗特殊词元和被掩码的位置不计算字节数。这样 BPB 只衡量真实的文本内容,不会因为对话格式或填充而偏移。
BPB 越低越好。它和困惑度(perplexity)相关,但单位是“每个字节需要多少比特”,更适合比较不同分词器的模型。
为什么不能只看 BPB
BPB 衡量的是“预测文本的能力”,不完全等于“回答问题的能力”。
一个模型可能在验证文本上 BPB 很低,但不会做数学题,也不会遵循对话格式。因此 nanochat 还使用:
- CORE:基础模型在多项选择、语言理解等任务上的综合评分。
- ChatCORE:对话模型在 ARC、MMLU、GSM8K、HumanEval 等任务上的综合评分。
这三个指标分别回答不同问题:
| 指标 | 回答的问题 |
|---|---|
| 训练损失 | 当前训练批次拟合得怎样? |
| 验证 BPB | 对未见文本的下一个词元预测得怎样? |
| CORE / ChatCORE | 在具体任务上表现怎样? |
评估细节在评估。
损失不下降时先检查什么
- 数据是否正确:
inputs和targets是否错开一位? - 掩码是否正确:不该计算损失的位置是否被设成
-1? - 学习率是否合理:太大可能震荡,太小可能几乎不动。
- 模型是否在训练模式:有没有误调用
model.eval()? - 梯度是否正常:有没有出现
nan或梯度爆炸? - 分词器和模型词表是否一致:不一致会在加载检查点时直接报错。
不要一看到损失不降就改模型结构。大多数问题出在数据、学习率或检查点加载上。
本章要记住的三件事
- 模型输出 logits,交叉熵把“正确词元的概率”变成损失。
- 因果掩码防止模型读取未来词元,目标右移一位不是数据泄漏。
- 验证 BPB 是按字节归一化的指标,比按词元平均的损失更适合比较不同分词器。
下一章进入训练循环:训练循环。