Pretrain 的 Eval 指标
这一章讨论的是 pretrain 阶段最核心的一类问题:我们到底该如何判断训练是不是在朝着正确的方向进行。
在前面的章节里,我们其实已经多次接触过一些和评估相关的现象。例如在第 2 节“优化器、学习率和数据设置”里,我们已经提到过 loss 曲线,以及不同的 batch size、learning rate 会怎样影响曲线形态。不过那一节的重点仍然是训练配置本身,还没有把“如何系统地观察和解释这些指标”单独展开。
但在实际实验中,评估并不是训练结束之后才考虑的附属环节,而是整个实验流程的一部分。一个训练实验如果没有合适的观测指标,就很难判断当前配置是否稳定,也很难验证自己的假设到底对不对。换句话说,如果不能有效评估,我们的实验就失去了意义。
因此,这一节会把 pretrain 中常见的评估指标单独整理出来。我们会先从最直接的训练 loss 和 loss 曲线开始,再进一步说明为什么仅看 training loss 还不够,以及为什么还需要 validation set、validation loss 乃至更完整的评估方式。
Q1: 训练时最直接的指标Loss怎么用?
Loss 本质上是有效 token 位置上的平均交叉熵:
其中,\(m_t\) 是 loss mask,表示第 \(t\) 个位置是否参与损失计算;\(y_t\) 是真实的下一个 token;\(q_\theta(y_t \mid x_{\le t})\) 是模型给真实 token 分配的概率。
loss 越低,说明模型平均来看给真实 token 分配了更高的概率。因此在 pretrain 中,loss 是最基础、最直接、也最容易持续监控的指标。
不过在实际训练里,我们通常并不是只盯着某一个孤立的 loss 数值,而是更关注 loss 曲线。因为真正有信息量的,往往不是“当前 step 的 loss 是多少”,而是“loss 随训练进展是如何变化的”。
Q1.1: 训练里常看的几种 loss 曲线分别是什么?
在实际实验里,常见的 loss 观察方式至少有下面几种:
- train step loss:每个 step 直接记录一次训练 loss。这是最原始的曲线,信息最丰富,但噪声也最大。
- smoothed train loss:对 step loss 做滑动平均或指数平滑,目的是更容易看出整体下降趋势。
- loss vs step:横轴是 optimizer step,纵轴是 loss。这是训练日志里最常见的一种画法。
- loss vs tokens seen:横轴换成训练过程中模型已经看过的 token 总数。这种画法在比较不同 batch size、不同梯度累积设置时更重要。
这里尤其要区分 loss vs step 和 loss vs tokens seen。
设:
- \(B_{\text{eff}}\) 表示 effective batch size;
- \(L\) 表示序列长度;
- \(T_{\text{update}}\) 表示每次参数更新对应的 token 数;
- \(S\) 表示已经执行的 optimizer step 数;
- \(D_{\text{seen}}\) 表示已经看过的 token 总数。
那么有:
\[ T_{\text{update}} = B_{\text{eff}} \cdot L \]
\[ D_{\text{seen}} \approx S \cdot T_{\text{update}} \]
这意味着:如果两个实验的 \(B_{\text{eff}}\) 不同,那么即使它们都训练了同样多的 step,它们实际看过的 token 数也可能完全不同。所以有时候 loss vs step 看起来某个实验更优,其实只是因为它每一步吃进了更多 token。
Q1.2: 不同 loss 曲线各自回答什么问题?
不同的 loss 曲线,回答的问题其实并不一样。
1. 看 train step loss:主要是在看局部稳定性。
这条曲线最容易暴露训练初期的不稳定现象,比如:
- loss 突然 spike;
- loss 长时间剧烈震荡;
- 出现
NaN或Inf; - warmup 结束附近突然不稳定。
这些现象往往提示 learning rate 过大、数值精度不稳定、梯度爆炸,或者 batch / warmup / optimizer 配置之间不匹配。
2. 看 smoothed train loss:主要是在看整体是否真的在下降。
单步 loss 抖动本身并不可怕。尤其在 batch 较小、梯度噪声较大时,step loss 很可能会明显上下波动。但如果平滑后的曲线仍然稳定向下,就说明模型整体还在学习。
所以很多时候我们会把两条曲线结合起来看:
- 原始 step loss 用来观察噪声和异常尖峰;
- smoothed loss 用来观察长期趋势。
3. 看 loss vs step:主要是在看“按更新次数计”的训练过程。
如果我们关心的是 warmup 有没有结束、learning rate schedule 在第几个 step 开始衰减、某个 checkpoint 前后训练是否稳定,那么 loss vs step 很直观。
但它有一个局限:当实验之间的 effective batch size 不同时,step 不再是公平的比较单位。
4. 看 loss vs tokens seen:主要是在看“按数据消耗计”的学习效率。
这一点和第 2 节最后讨论的 batch size 问题是连在一起的。effective batch size 变大之后,通常会看到两种现象:
- 曲线可能更平滑,因为每次更新用到了更多 token,梯度噪声更小;
- 但在固定
tokens seen下,模型未必学得更快,因为 batch 变大后参数更新次数会减少,而且 learning rate 也可能需要重新匹配。
所以如果我们只是看 loss vs step,很容易高估大 batch 实验;而如果改看 loss vs tokens seen,就更容易判断“模型在消耗同样多训练 token 的前提下,到底哪套配置学得更快”。
也正因为如此,在比较不同 effective batch size 的实验时,最好至少同时看两条曲线:
loss vs step:看训练过程是否稳定;loss vs tokens seen:看数据利用效率和收敛速度。
5. 不同曲线还可以帮助区分不同类型的问题。
例如:
- 如果 step loss 抖动很大,但 smoothed loss 仍然稳定下降,通常更像是 batch 偏小带来的噪声;
- 如果 loss 出现频繁尖峰、持续升高,甚至 NaN,通常更像是 learning rate 过大或训练失稳;
- 如果曲线非常平滑,但按
tokens seen看下降很慢,通常说明训练虽然稳定,但未必高效; - 如果某次改动后曲线整体形状明显变化,就说明这次改动很可能真的影响了训练动力学,而不只是让日志看起来更“好看”。
所以 loss 曲线最核心的作用,不只是告诉我们“当前 loss 是多少”,而是帮助我们回答下面这些更重要的问题:
- 当前训练是否稳定?
- 模型是否还在持续学习?
- 学习率是不是过大或过小?
- effective batch size 的设置是否合理?
- 不同实验之间的差异,究竟来自训练配置,还是只是比较方式不同?
Q2: loss 下降就代表训练收敛了吗?
loss 下降是好信号,但不能简单等同于训练已经收敛。
更准确地说,loss 下降说明模型正在更好地拟合训练数据中的 next token 分布。但它还不能回答这些问题:
- 模型是否过拟合了训练集?
- 模型在没见过的数据上表现如何?
- 模型是否真的具备可用的生成能力?
- 模型是否能遵循指令?
- 模型是否只是学会了某些高频模板?
所以训练 loss 更适合回答:“模型在当前训练数据上是否还在学习?”
它不能单独回答:“模型是否已经好用?”
换句话说,前面这些训练曲线更多是在回答 训练过程是否正常,而不是直接回答 模型能力是否足够好。这也是为什么我们接下来必须引入 validation set。
Q3: 为什么需要 validation loss?
如果只看 training loss,很难判断模型是不是过拟合。更理想的做法是准备一份验证集,训练过程中定期在验证集上计算 validation loss。
假设训练集 loss 持续下降,但 validation loss 开始上升,通常说明模型对训练数据拟合得越来越好,但泛化能力可能变差。
对于大模型 pretrain 来说,validation loss 很重要,因为它比人工看生成样例更稳定。生成样例会受到 prompt、采样参数、随机性影响,而 validation loss 是一个更可比较的数值指标。
更具体地说,training loss 和 validation loss 分工不同:
- training loss 更适合看训练是否稳定、是否还在继续学习;
- validation loss 更适合看模型在未参与训练的数据上表现如何;
- 如果两者一起看,还可以观察 train / val gap 是否在不断扩大,从而辅助判断过拟合风险。
Q3.1: validation set 需要多大?
validation set 不需要像训练集那样大,但也不能小到完全没有统计意义。它的核心目标不是“覆盖所有知识”,而是提供一份 固定、稳定、能够重复比较 的参考数据。
实践里更重要的往往不是绝对大小,而是下面几个原则:
- 和训练集分布尽量一致:如果验证集和训练数据分布差太远,那么 val loss 变化未必反映真实训练效果。
- 不参与训练:验证集必须和训练集严格隔离,不能一边训练一边混进去。
- 规模足够稳定:样本太少时,validation loss 会有较大随机波动,不利于比较实验。
- 固定不变:一旦开始做实验对比,最好始终使用同一份 validation set,否则不同实验之间就失去了可比性。
对于像 MiniMind 这样的学习项目,一个常见而实用的做法是:从原始语料里单独切出一小部分,规模不一定要很大,但最好保证包含足够多的 token,使得每次 eval 算出来的 loss 不会因为样本太少而大幅波动。
如果用符号表示,设:
- \(N_{\text{val}}\) 表示 validation set 的样本数;
- \(D_{\text{val}}\) 表示 validation set 的 token 总数;
- \(\hat{\mathcal{L}}_{\text{val}}\) 表示估计得到的 validation loss。
那么直觉上,\(D_{\text{val}}\) 越大,\(\hat{\mathcal{L}}_{\text{val}}\) 的统计波动通常越小;但代价是每次 eval 的计算开销也会更大。
所以 validation set 的大小本质上是在做一个平衡:
- 太小:波动大,不稳定;
- 太大:评估成本高,训练中频繁 eval 会拖慢整体吞吐;
- 适中:既能提供稳定趋势,又不会让 eval 成本过高。
Q3.2: 应该多久做一次 validation?
validation 也不是越频繁越好,因为每做一次 eval 都要消耗额外计算资源,还会打断训练节奏。
常见做法是每隔固定的若干个 step,或者每消耗固定数量的 tokens seen,就在同一份 validation set 上跑一次 eval。这样做的重点不是“评估得越勤越高级”,而是:
- 能及时发现训练已经失稳;
- 能观察 validation loss 是否仍在下降;
- 能在不同实验之间保留统一的评估节奏。
如果实验规模比较小、训练时间不长,可以把 eval 设得稍微频繁一点,方便观察曲线;如果训练规模较大,通常会把 eval 间隔拉长,避免评估开销过高。
MiniMind 当前这个学习项目里,训练脚本主要记录的是训练 loss。如果后续要继续完善实验记录,可以考虑加入一个固定的 validation set,并在每隔若干 step 后运行 eval。这样我们就不只是知道“训练 loss 在下降”,还能够进一步判断:“模型在没见过的数据上,是否也在同步变好。”
Q3.3: 如果 validation loss 和 training loss 都几乎不变,还有必要继续训练吗?
这个问题不能只凭一句“loss 不动了”就直接下结论,更准确的问法其实是:当前继续消耗更多训练 token,还能不能换来足够值得的收益?
如果在相当长的一段训练区间内,training loss 和 validation loss 都几乎没有继续下降,而且这种“几乎不变”已经明显超过了正常波动范围,那么通常说明这段训练的边际收益已经变小了。此时继续训练不一定完全没有收益,但性价比往往会越来越低。
更形式化一点地说,我们真正关心的其实是:每增加一部分训练 token,loss 还能下降多少。设:
- \(\mathcal{L}\) 表示 loss;
- \(D_{\text{seen}}\) 表示训练过程中已经看过的 token 总数。
那么可以关注这样一个量:
\[ \frac{\Delta \mathcal{L}}{\Delta D_{\text{seen}}} \]
如果 \(\Delta D_{\text{seen}}\) 已经很大,但 \(\Delta \mathcal{L}\) 仍然极小,那么就说明继续训练的边际收益在下降。
不过在实践里,是否停训通常还要结合下面几种情况一起判断:
- 如果 train loss 和 val loss 都进入平台期,而且按
tokens seen看也长期没有明显改善,通常说明继续训练的收益已经很有限。 - 如果 loss 变化很小,但下游能力还在提升,比如生成更稳定、某些 benchmark 还在缓慢上升,那么仍然可能值得继续训练。
- 如果当前正处在 learning rate schedule 的阶段切换附近,例如刚结束 warmup,或者刚进入更小 learning rate 的阶段,那么后面仍然可能出现一小段新的缓慢下降。
- 如果训练数据已经重复很多遍,而 validation loss 依然没有改善,那么继续训练更可能只是继续消耗 compute,而不一定能带来新的泛化收益。
所以更稳妥的说法不是“loss 不动了就一定该停”,而是:当 train loss、val loss 和下游能力都同时进入平台期时,停训才更有依据。
Q3.4: LLM 训练里会出现反直觉的 validation loss 曲线吗?
会,而且这恰恰是 LLM pretrain 和传统小数据监督学习很不一样的地方。
在很多经典深度学习任务里,我们常见的教科书式现象是:
- training loss 持续下降;
- validation loss 先下降,随后开始明显上升;
- 这个拐点通常被解释为比较明确的过拟合信号。
但在 LLM pretrain 里,这种曲线不一定总是出现。原因之一是:pretrain 往往面对的是更大的数据规模、更低的数据重复率,以及更长时间的 next-token 建模过程。很多时候,模型在很长一段训练里都还没有进入那种非常典型的“小数据过拟合”状态。
因此在 LLM 训练里,更常见的反而是下面几种情况:
- train loss 下降,validation loss 也继续下降,但下降得更慢。
- train loss 继续下降,但 validation loss 很早就进入平台期。
- validation loss 短期有波动,甚至偶尔反弹,但后面又继续下降。
- train loss 和 validation loss 几乎贴得很近,但模型实际生成能力仍然有限。
这些现象看起来有些“反直觉”,但并不奇怪。
第一种情况说明模型还在持续从大规模数据中学习,验证集也还没有明显表现出过拟合。
第二种情况说明模型虽然还在继续优化训练目标,但验证集上的平均 next-token 改善已经很有限。
第三种情况往往和 learning rate、评估噪声、eval 间隔、验证集规模有关,不一定意味着真正的过拟合开始。
第四种情况则说明:语言建模 loss 和“模型是否已经好用”并不是同一回事。
这也是为什么在 LLM 里,我们不能机械地套用传统监督学习里那种“只要 val loss 一上升,就说明该立刻停训”的经验。对于 pretrain,更常见的不是 validation loss 明显拐头向上,而是:
- 下降速度越来越慢;
- 长时间平台期;
- 和 training loss 一起缓慢下降,但幅度很小。
所以在 LLM pretrain 里,停训信号往往不是一个非常尖锐的拐点,而更像是一个综合判断:
- validation loss 是否还在明显改善;
- 在固定
tokens seen下,继续训练的收益是否已经非常边际; - 生成样例或下游评测是否还在变好;
- 当前 compute 是否值得继续投入在这次训练上。
Q4: Perplexity 是什么?
Perplexity 通常翻译成困惑度,可以理解成由语言模型的 cross entropy loss 派生出来的指标。
不过在正式定义 Perplexity 之前,最好先把这里的 loss 再统一一下。因为在前面的章节里,我们已经从不同角度写过语言模型的训练目标;如果这里不先把符号和视角对齐,后面直接写 PPL = exp(loss) 会显得有些跳。
更完整的推导可以参考 1.loss.md 中 Q4 的讨论。这里先把后面最需要用到的三种等价写法整理出来。
Q4.1: Loss的两种视角?
设:
- \(x_{1:T}\) 表示一段真实文本对应的 token 序列;
- \(x_{<t}\) 表示第 \(t\) 个位置之前的上下文;
- \(x_t\) 表示序列在第 \(t\) 个位置上的真实 token;
- \(y\) 表示“给定上下文后的真实 next token”这个随机变量;
- \(q_\theta(\cdot \mid x_{<t})\) 表示模型在上下文 \(x_{<t}\) 下预测的条件分布;
- \(p_{\text{data}}\) 表示真实数据分布。
从 最大似然 的角度看,自回归语言模型建模的是整条序列的联合概率:
于是训练目标可以写成最小化真实序列的负对数似然:
再利用 \(\log \prod = \sum \log\),就得到等价形式:
这表示:模型希望给真实序列中每一个位置上的真实 token 分配尽可能高的概率。
从 交叉熵 / 条件分布拟合 的角度看,同一个目标也可以写成:
这个式子的含义是:先从真实数据分布中采样上下文 \(x_{<t}\) 和对应的真实 next token \(y\),然后计算模型在这个位置上没有给真实 token 足够高概率时所付出的代价,也就是 \(-\log q_\theta(y \mid x_{<t})\)。
这三种写法本质上描述的是同一个训练目标,只是观察角度不同:
- 前两种更强调 最大化真实序列的联合概率;
- 第三种更强调 在每个条件分布上最小化交叉熵 / 负对数似然。
它们最终会落到同一种 token-level loss 上。所以在本章前面 Q1 里看到的:
其实就是上面这些理论目标在实际训练中的 batch 内、有效 token 上的平均形式。这里只是额外加入了 loss_mask,用来忽略 padding 等不参与训练的位置。
也可以这样理解符号:
- 在“整条序列”的视角里,通常写 \(x_t\);
- 在“当前位置标签”的视角里,通常写 \(y_t\) 或 \(y\);
- 对于自回归 next token prediction,这两者本质上指向的是同一个真实 token,只是书写视角不同。
Q4.2: Perplexity 是什么? 如何理解它?
在语言模型里,如果 loss 使用的是自然对数,那么 Perplexity 定义为:
\[ \text{PPL} = \exp(\mathcal{L}) \]
其中,\(\mathcal{L}\) 就是我们上文提到的平均 token-level cross entropy loss。
如果把平均 loss 写成:
\[ \mathcal{L} = - \frac{1}{N}\sum_{n=1}^{N}\log q_\theta(y^{(n)} \mid c^{(n)}) \]
其中:
- \(N\) 表示参与统计的有效 token 数;
- \(c^{(n)}\) 表示第 \(n\) 个位置对应的上下文;
- \(y^{(n)}\) 表示这个位置上的真实 next token;
那么:
1. Perplexity 的定义
Perplexity 本质上不是一个独立于 loss 的新目标,而是对平均负对数似然做了一次指数变换后的结果。
所以它和 loss 的关系非常直接:
- loss 在优化时更基础;
- Perplexity 更像是 loss 的另一种表达尺度。
2. Perplexity 隐含的含义是什么?
Perplexity 的关键在于:它不是先对概率做平均,而是先对 log 概率 做平均,再用 \(\exp\) 变换回来。
利用对数和指数的性质,可以把上面的式子改写成:
这说明 Perplexity 可以理解为:
真实 token 概率倒数的几何平均。
这句话很重要。它说明这里的“平均”不是算术平均,而是 几何平均。
之所以出现几何平均,本质上就是因为我们先在 log 空间求平均,再映射回普通概率空间。
因此,Perplexity 的直觉含义可以表述成:
- 它衡量的是模型平均需要在多大规模的不确定性中做选择;
- 或者说,它近似反映了模型对真实 token 平均还剩下多少“等效候选数”。
如果模型在每个位置都把真实 token 的概率大约压到 \(1/K\),那么:
\[ \text{PPL} \approx K \]
这也是为什么 Perplexity 常被直觉地解释成“模型平均困惑于多少个候选 token”。
3. Perplexity 的取值范围是什么?
Perplexity 的取值范围是:
\[ 1 \le \text{PPL} < \infty \]
原因是:
- 如果模型在每个位置都把真实 token 的概率预测为 1,那么
\[ \mathcal{L} = 0,\qquad \text{PPL} = \exp(0)=1 \]
- 如果模型给真实 token 的概率越来越小,那么 \(-\log q_\theta(y \mid x_{<t})\) 会越来越大,于是 loss 和 PPL 都会增大。
因此:
- PPL = 1 对应理论上的完美预测;
- PPL 越小越好;
- PPL 越大,说明模型平均给真实 token 分配的概率越低。
不过这里要注意,Perplexity 的绝对数值会受到 tokenizer、词表大小、数据分布和评估方式影响。因此它更适合同一套设置下做比较,而不适合跨任务、跨 tokenizer 直接横向比较。
4. Perplexity 更深层的数学含义是什么?
从信息论角度看,如果把平均 loss 记成交叉熵:
\[ \mathcal{L} = H(p_{\text{data}}, q_\theta) \]
那么:
\[ \text{PPL}=\exp\left(H(p_{\text{data}}, q_\theta)\right) \]
这说明 Perplexity 本质上是 交叉熵的指数形式。
如果一个离散分布在每次预测时都需要在 \(K\) 个等可能候选中做选择,那么它的不确定性大致满足:
\[ H \approx \log K \]
反过来就有:
\[ K \approx \exp(H) \]
这正是 Perplexity 的形式来源。
所以从更深一点的角度看:
- cross entropy / loss 衡量的是平均编码代价;
- Perplexity 衡量的是与这个编码代价等价的不确定性规模。
它把对数尺度下的平均误差,重新解释成了一个“等效分支数”或“等效选择空间”。
5. \(\text{PPL} = K\) 是否意味着模型什么都没学到?
不一定。这里最容易产生的误解是:一看到 \(\text{PPL} = K\),就把它理解成“模型只有 \(1/K\) 的概率猜对”,甚至进一步理解成“模型几乎什么都没学到”。这两个理解都过于粗糙。
更准确地说,\(\text{PPL} = K\) 表示的是:
\[ \left( \prod_{n=1}^{N} q_\theta(y^{(n)} \mid c^{(n)}) \right)^{1/N} \approx \frac{1}{K} \]
也就是说,模型给真实 token 的概率,其几何平均大约是 \(1/K\)。
因此,“\(K\) 个候选 token”只是一个等效解释。它的真正含义是:如果模型在每个位置都像是在 \(K\) 个等可能候选里做选择,那么它产生的平均不确定性,大致会和当前这个模型相当。
但这并不意味着:
- 每个位置都真的有
\(K\)个等可能候选; - 模型在每个位置都刚好给真实 token 分配
\(1/K\)的概率; - 模型的 top-1 准确率一定就是
\(1/K\)。
例如,模型可能在一些位置给真实 token \(0.8\) 的概率,在另一些位置只给 \(0.001\),最后综合起来,几何平均仍然可能对应某个固定的 Perplexity。
所以 \(\text{PPL} = 20\) 更准确的意思是:模型给真实 token 的几何平均概率大约是 \(0.05\),而不是它只有 \(5\%\) 的 top-1 正确率。
同样地,\(\text{PPL} = K\) 也不自动意味着“模型什么都没学到”。关键要看这个 \(K\) 相对于任务本身有多大:
- 如果词表大小是
\(V=50000\),而模型的 PPL 仍然接近\(50000\),那才比较接近“几乎和瞎猜差不多”; - 如果模型的 PPL 已经降到
\(20\)、\(10\)甚至更低,那通常说明它已经把原本巨大的不确定性压缩到了相对小得多的范围。
所以更合理的判断方式不是孤立地看 \(K\),而是看:
- 它相对于随机基线有多低;
- 它相对于之前的 checkpoint 有没有持续下降;
- 它相对于同设置下的其他模型有没有改善。
6. 为什么自然语言的 Perplexity 本来就不可能非常低?
这是理解 Perplexity 时非常关键的一点:自然语言本身就有不可消除的不确定性。
很多位置并不存在唯一必然的下一个 token。比如一句话:
“今天晚上我们去吃……”
后面可能接:
- 火锅
- 烧烤
- 日料
- 饭
- 什么
这些 continuation 都可能合理。也就是说,即使模型已经很好地理解了上下文,它也不可能总是把真实 token 的概率压到接近 1,因为真实文本本身就是在多个合理可能里选择了其中一个。
从信息论角度看,如果模型分布已经完全等于真实数据分布,那么此时的最优 loss 也不会一般性地等于 0,而是等于真实数据分布本身的熵。设:
- \(p_{\text{data}}\) 表示真实语言分布;
- \(H(p_{\text{data}})\) 表示这个分布本身的熵。
那么当模型已经达到最优时,有:
对应的理论下界则是:
因为真实语言分布的熵通常大于 0,所以:
这说明 \(\text{PPL} = 1\) 只会出现在一个极端理想化的情形里:每个位置都毫无不确定性,而且模型还始终预测完全正确。对于自然语言,这几乎是不可能的。
所以在实践中,Perplexity 包含了两部分来源:
- 语言本身的内在不确定性;
- 模型还没有学好的额外误差。
这也是为什么我们不应该把 “PPL 没有接近 1” 理解成模型很差。更合理的标准是:
- 在同样的 tokenizer、同样的数据分布、同样的评估方式下,PPL 是否持续下降;
- 相比基线模型或更小的 checkpoint,它是否显著更低。
7. 为什么模型“自信地犯错”会导致更高的 Perplexity?
这是 Perplexity 一个很值得强调的性质:它不只是惩罚“猜错”,还会更强烈地惩罚“高置信度地猜错”。
原因直接来自 token-level loss 的形式:
\[ \ell = -\log q_\theta(y \mid x_{<t}) \]
其中:
- \(y\) 是真实 next token;
- \(q_\theta(y \mid x_{<t})\) 是模型给真实 token 分配的概率。
如果模型给真实 token 的概率还不算太低,比如:
\[ q_\theta(y \mid x_{<t}) = 0.1 \]
那么:
\[ \ell = -\log 0.1 \approx 2.30 \]
但如果模型非常自信地把概率给错了,只给真实 token 极小的概率,比如:
\[ q_\theta(y \mid x_{<t}) = 10^{-4} \]
那么:
\[ \ell = -\log 10^{-4} \approx 9.21 \]
可以看到,后者的惩罚会大得多。
这说明 Perplexity 对模型的要求不只是“把正确答案排在前面”,而是还要求:不要对错误答案过度自信。
从 Perplexity 的定义看,这件事同样成立。因为:
如果某些位置上真实 token 的概率被压得特别低,那么这些位置对应的概率倒数就会特别大,从而显著拉高整体的几何平均。
所以在直觉上,可以把几种情况区分开:
- 不太确定,但分布还算温和:真实 token 的概率不高,但也没有低到离谱,PPL 会偏高,但不一定灾难性。
- 经常预测正确,而且概率分配合理:真实 token 的概率整体较高,PPL 会下降。
- 经常高置信度地预测错:真实 token 的概率被压得极低,loss 会非常大,PPL 也会明显恶化。
这也是为什么有时候两个模型的 top-1 行为看起来差不多,但 PPL 仍然可能差很多:
它们也许都“猜错了若干次”,但一个模型是谨慎地错,另一个模型是非常自信地错。对语言建模目标来说,后者会受到更重的惩罚。
Q4.3: 应该怎么看 Perplexity 曲线?
从定义上说,Perplexity 和 loss 是一一对应的:
\[ \text{PPL} = \exp(\mathcal{L}) \]
因此 PPL 曲线和 loss 曲线在趋势上是完全一致的:
- loss 下降,PPL 也下降;
- loss 进入平台期,PPL 也会进入平台期;
- loss spike,PPL 往往会出现更明显的 spike。
所以问题并不是“看 loss 更对,还是看 PPL 更对”,而是它们分别更适合什么用途。
1. 训练过程中,通常优先看 train loss。
因为 train loss 是优化器真正对应的目标,而且数值变化更线性,更方便观察训练稳定性、learning rate 是否过大、warmup 是否有问题。
train Perplexity 当然也可以画,但很多时候它只是 train loss 的指数变换,新增信息并不多。
2. 做模型评估时,更常看 validation Perplexity。
原因是 validation Perplexity 和 validation loss 一样,都是在固定验证集上统计出来的,更适合比较不同 checkpoint 或不同实验的泛化表现。
所以更常见的搭配往往是:
- 调训练时重点看
train loss; - 做评估或汇报时重点看
val loss或val perplexity。
3. 如果只想保留一条 PPL 曲线,通常优先保留 validation Perplexity 曲线。
因为 train PPL 更多反映模型对训练数据的拟合程度,而 val PPL 更接近我们真正关心的问题:模型在未参与训练的数据上平均还有多大的不确定性。
4. PPL 曲线更适合看整体趋势,不适合过度解读单点波动。
因为 PPL 是 loss 的指数变换,所以当 loss 在较高区间有小波动时,PPL 上的波动会被放大,看起来可能更夸张。
因此:
- 看短期训练稳定性时,loss 往往更稳妥;
- 看整体评估趋势和对外汇报时,PPL 往往更直观。
所以更实用的建议是:
- 调参时优先看 loss 曲线;
- 比较模型时重点看 validation loss / validation Perplexity;
- 如果是实验汇报或论文表达,Perplexity 往往比原始 loss 更容易让读者形成直觉。
Q5: 生成能力如何? 生成样例如何Eval?
前面几节讨论的 train loss、validation loss 和 Perplexity,本质上都是定量指标。它们非常重要,因为它们能帮助我们判断训练是否稳定、模型是否还在学习、不同实验之间谁更优。
但是这些指标也有边界:它们只能告诉我们模型在平均意义上是否更会预测下一个 token,却不能直接告诉我们“生成出来的文本读起来到底怎么样”。
因此在 pretrain 阶段,除了看 loss 和 PPL 这类数值指标,我们通常还会配合看一些生成样例。不过这里要先强调一点:生成样例更适合作为定性观察,而不是严格的、可重复的最终 eval 指标。
这是一个很大的问题。更系统的生成评测、指令跟随能力评测、偏好评测,我们会在后续的 SFT 章节里再展开。这里先讨论 pretrain 阶段最初步、最实用的几种观察方法。
Q5.1: 为什么还要看生成样例?
因为 loss 和 PPL 只能说明:模型平均来看是不是更擅长给真实 token 分配更高概率。
但它们不能直接回答下面这些更直观的问题:
- 生成出来的句子是否基本通顺?
- 会不会很快陷入重复?
- 会不会前面看起来正常,后面突然跑偏?
- 对常见格式,比如列表、问答、代码片段,是否已经有基本模式感?
这些现象很难只靠一个平均 loss 数字直接看出来,所以实践中常常会补几条固定 prompt 的生成样例,作为对训练状态的辅助观察。
Q5.2: 为什么生成样例不能单独当作严格 eval?
虽然生成样例很有价值,但它本身并不是特别稳定的指标。
原因是生成结果会受到很多因素影响,比如:
- prompt 的具体写法;
- temperature、top-k、top-p 等采样参数;
- 随机种子;
- 生成长度;
- 模型当时是否更擅长某一类局部模式。
这意味着:同一个 checkpoint,有时可以生成出一段看起来不错的文本;但换一个 prompt、换一个采样参数,结果可能就完全不同。
所以生成样例更适合用来回答下面这类问题:
- 模型大致学会了什么?
- 模型最明显的失败模式是什么?
- 和上一个 checkpoint 相比,它在局部连贯性、重复、格式感上有没有改善?
它不太适合单独回答:
- 模型是否已经“足够好”?
- 两个模型之间谁一定更强?
因此,更稳妥的做法通常是:
- 用
validation loss / validation PPL做主要的定量比较; - 用固定 prompt 的生成样例做辅助的定性分析。
Q5.3: Pretrain 阶段看生成样例时,主要看什么?
在 pretrain 之后,模型可能已经学到一些语言分布,比如能接上常见短语、能生成看似通顺的句子、能复述一些常见知识。但因为它还没有经过 SFT 和对齐训练,它不一定会稳定遵循聊天格式,也不一定真的理解用户指令。
所以 pretrain 阶段看样例时,重点通常不是“它能不能像聊天模型一样回答问题”,而是看它是否已经从接近随机输出,变成了一个初步可用的语言模型。
比较常见的观察点包括:
- 局部续写是否合理:能不能顺着 prompt 的局部语义往下接。
- 句法和语法是否基本通顺:有没有明显的乱码、词序崩坏、标点异常。
- 格式模式是否初步形成:比如列表、标题、缩进、问答格式能不能延续。
- 是否容易重复:会不会很快进入某个短语或句式的循环。
- 是否容易跑题:前两句还正常,后面突然和 prompt 毫无关系。
- 中长程连贯性是否开始出现:能不能在几句话范围内维持大致统一的主题。
Q5.4: 一些初步例子
下面给几个很初步的观察例子。
例 1:观察常见短语续写
如果 prompt 是:
今天晚上我们去吃
那么一个已经学到基本语言统计规律的 pretrain 模型,往往会续出:
火锅。
或者:
烧烤吧。
这说明它至少已经学会了一些高频 continuation。
但如果它续出完全无关的 token、乱码,或者很快陷入重复,那就说明基础语言建模还比较弱。
例 2:观察格式延续能力
如果 prompt 是:
1. 数据预处理
2. 模型训练
3.
那么如果模型倾向继续写出:
模型评估
或者:
结果分析
这通常说明它已经学到了一些局部格式模式,而不只是孤立地记住词和词之间的共现关系。
例 3:观察局部通顺但整体跑偏
有时 pretrain 模型会生成出这样的文本:
Transformer 是一种基于注意力机制的模型,它能够有效地建模上下文信息。
今天的天气很好,我们可以一起去公园散步。
前一句看起来很正常,但后一句突然跳到完全无关的话题。
这种现象说明模型已经学到了一部分局部流畅性,但中长程主题维持能力还比较弱。
例 4:观察重复和退化
还有一种常见失败模式是:
这个问题需要从多个方面进行分析,首先我们需要从多个方面进行分析,首先我们需要从多个方面进行分析……
这类重复退化通常说明模型虽然学到了一些局部高频结构,但生成时缺少更稳定的全局控制能力。
Q5.5: Pretrain 阶段该怎样看待这些样例?
这也是为什么实践中会看到一种很有意思的现象:只做 pretrain 的模型,有时已经能说出一些像样的话,但经常前言不搭后语。
这不是失败,而是 pretrain 的性质决定的。pretrain 教模型学习文本分布,SFT 才进一步教模型按照指令和对话格式输出。
所以在这一阶段,更合理的看法是:
- 不要因为模型偶尔说出几句通顺的话,就高估它的能力;
- 也不要因为它还不会稳定对话,就否定 pretrain 的价值。
Pretrain 阶段的生成样例,更像是在帮助我们回答:
- 模型是否已经摆脱了接近随机的输出?
- 模型是否开始具备局部语言流畅性?
- 模型的典型失败模式是什么?
- 不同 checkpoint 或不同训练配置之间,样例差异是否和 loss / PPL 的变化相互印证?
真正更系统的“生成能力评测”,尤其是指令跟随、问答质量、偏好对齐和人类主观体验评测,还需要放到后续的 SFT 和对齐章节里再展开。
Q5.6: 这些定性判断能不能做成自动化评测?
可以,但通常不能只靠单一指标完成。更实际的做法是:把原本比较主观的观察点,拆成若干可以重复执行的自动化检查项。
例如,前面提到的这些现象:
- 是否重复;
- 是否跑题;
- 是否能延续格式;
- 是否基本通顺;
- 是否已经摆脱接近随机的输出;
都可以部分转成自动化评测。
一种最直接的做法是:固定一组 prompt、固定采样参数、固定生成长度,然后在每个 checkpoint 上跑同一套生成脚本,再统计一些规则指标。
例如:
- 重复问题:可以统计
distinct-n、重复 n-gram 比例、最长重复子串长度; - 格式延续:可以写规则检查是否继续编号、是否保持 Markdown 列表、是否保持代码缩进;
- 乱码或退化:可以统计标点比例、中文字符比例、异常字符比例、句长分布;
- 主题偏移:可以用 prompt 和生成结果的 embedding 相似度,或者用更强模型判断主题是否一致;
- 局部通顺性:可以用更强语言模型或 judge model 做打分。
所以从方法上看,自动化通常可以分成三类:
1. 规则型指标
这类方法最便宜,也最容易复现。典型特点是:
- 不需要参考答案;
- 适合抓住明显退化现象;
- 但通常只能覆盖质量的一部分。
例如重复率、格式合法性、字符分布异常检测,都属于这一类。
2. 参考答案型指标
如果任务本身存在某种“标准输出”或至少有若干参考输出,就可以计算 BLEU、ROUGE、BERTScore、embedding 相似度之类的指标。
但在开放生成里,这类方法有明显局限:自然语言往往一题多解,所以“和参考答案像不像”不一定等于“生成质量高不高”。
3. Judge Model 评测
也就是让一个更强的模型按照固定 rubric 来打分,比如:
- 是否通顺;
- 是否重复;
- 是否跑题;
- 是否保持原格式;
- 是否存在明显事实或语法错误。
这类方法现在很常见,因为它比纯规则更灵活,比纯人工评测更便宜。但它本身也会带来评审模型偏差,所以更适合作为辅助指标,而不是唯一标准。
因此,更实用的经验通常是:
- 用规则指标做第一层自动筛查;
- 用 judge model 做第二层质量判断;
- 最后再做少量人工抽样复核。
Q5.7: 这些自动化方法也适合 SFT 或 function calling 的测评吗?
适合一部分,但不能直接把 pretrain 阶段的这些方法原封不动搬过去。更准确地说,这些方法更像是一套通用评测工具箱,而不同任务通常还需要自己的专门测试集和判分标准。
有一些评测方法是跨任务通用的,例如:
- 固定测试集;
- 固定推理参数;
- 自动脚本跑分;
- judge model 打分;
- 人工抽样复核;
- 成功率 / 失败率统计;
- 输出格式合法性检查。
这些方法不只 pretrain 可以用,SFT、function calling、RAG、agent 任务也都可以用。
但问题在于:不同任务的“正确”定义并不一样。
对于 pretrain,前面这些生成样例主要是在看:
- 是否重复;
- 是否通顺;
- 是否延续格式;
- 是否会突然跑题;
- 是否已经学到基本语言分布。
而到了 SFT,评测重点会变成:
- 是否遵循指令;
- 是否回答完整;
- 是否安全;
- 是否风格符合预期;
- 多轮对话是否一致。
到了 function calling,重点又会变成:
- 是否该调用工具;
- 是否调用了正确的工具;
- 参数是否齐全;
- 参数类型是否正确;
- 工具调用之后是否真正执行成功。
这时,单纯看“生成得通不通顺”已经远远不够了。很多时候,即使自然语言写得很好,只要工具名选错、参数传错、JSON 结构非法,这次 function calling 在任务上就是失败的。
所以更合理的理解是:
- 自动化评测的方法论有通用部分;
- 但每个任务的测试集合和专门指标通常需要单独设计。
可以把它概括成两层:
第一层:通用层
这部分几乎所有任务都能复用:
- 固定样本集;
- 自动跑分脚本;
- judge model;
- 人工 spot check;
- 稳定性、成本、延迟统计。
第二层:任务层
不同任务要补自己的核心指标:
- pretrain:loss、PPL、重复率、生成退化;
- SFT:指令跟随、帮助性、安全性、偏好胜率;
- function calling:tool selection accuracy、argument accuracy、execution success rate;
- RAG:retrieval recall、faithfulness、citation correctness;
- agent:task completion rate、步骤效率、工具使用正确性。
这里还有一个很重要的经验:
- 越开放的任务,越依赖 judge model 和人工评审;
- 越结构化的任务,越适合做精确自动评测。
例如:
- 开放式问答或自由写作,很难只靠一个规则精确判分;
- function calling 的 JSON 结构、参数字段、执行结果,则往往很适合自动精确比对。
所以如果放到后续章节里,更自然的衔接方式是:
- pretrain 阶段先学会如何结合
loss / PPL / 生成样例看一个基础语言模型; - 到 SFT 阶段,再引入更专门的 instruction-following 测试集和 judge 方式;
- 到 function calling 阶段,再引入结构正确性、工具调用正确率和执行成功率这类更任务化的指标。
Q6: 除了 loss / PPL,还有哪些训练中值得顺手盯着的曲线?
在实际训练里,loss 和 PPL 当然最核心,但它们不是唯一值得看的量。很多训练问题并不是先体现在最终指标上,而是先体现在优化过程、数值稳定性和吞吐监控上。
因此更完整一点的训练监控面板里,通常还会顺手看下面这些曲线或指标:
| 曲线 / 指标 | 主要看什么 | 常见异常含义 |
|---|---|---|
learning rate curve | warmup、decay 是否按预期执行 | schedule 配错,实际 LR 和预期不一致 |
gradient norm curve | 梯度是否整体稳定 | 梯度爆炸、LR 过大、训练失稳 |
parameter norm / update norm | 参数尺度和每次更新幅度是否异常 | 更新过猛、参数漂移异常、优化器状态异常 |
loss scale / NaN count | 混合精度训练是否数值稳定 | 溢出、下溢、AMP 配置问题 |
tokens/sec / step time | 吞吐和训练效率 | loss 虽然在降,但训练效率很差 |
train loss 和 eval loss 的 gap | 训练集和验证集是否开始分化 | 过拟合、数据污染、训练分布不匹配 |
GPU memory / utilization | 显存和算力利用率 | batch 太小、数据管道瓶颈、硬件没有吃满 |
这些指标不一定每次都要展开分析,但它们能帮助我们更早发现问题。很多时候,真正的异常并不是先出现在最终的 validation loss 上,而是先出现在:
- 学习率曲线没有按预期走;
- 梯度范数突然 spike;
- loss scale 频繁回退;
- 吞吐异常下降;
- train / eval gap 提前拉大。
所以如果把训练过程想成一个监控面板,那么:
loss / PPL更像是主指标;- 上面这些曲线更像是辅助诊断指标。
Q7: 更现实的测试和训练流程: 开放世界中的不确定性
到这里其实会自然出现一个更现实的问题:在真正的大模型系统里,训练和测试并不是一条简单的直线流程,而更像是一棵不断分叉、不断回环的能力树。
我们前面讨论了 pretrain 的 loss、validation loss、Perplexity,也讨论了生成样例、自动化评测和后续任务专用 benchmark。但当问题真的落到开放世界里的复杂任务上时,一个现象往往不再只有唯一原因。
例如,一个下游任务表现不好,表面上看只是“模型回答不行”,但真实原因可能完全不同:
- 基础模型的 pretrain 还不够强;
- post-training 没有把任务形式教出来;
- function calling 或 tool use 训练不够;
- system prompt、采样参数或工具 schema 设计得不好;
- 评测集本身没有测到真正的问题;
- 任务定义混合了多个目标,导致训练和评测都不清晰。
这也是为什么现实里的训练和测试流程,往往比教科书里更复杂。它不是单次训练、单次评测就能结束的,而是一个不断观察、定位、修正、再评估的循环。
Q7.1: 为什么现实里的模型训练更像一棵能力树?
如果把一个商业大模型系统粗略拆开,它通常至少包含下面几层能力:
1. 基础层:Pretrain Base Model
- 语言流畅性;
- 知识覆盖;
- 长上下文建模;
- 基础推理和模式归纳;
- 多语言、代码、数学等底座能力。
2. 对齐层:Post-training
- 指令跟随;
- 回复风格;
- 拒答策略;
- helpful / harmless / honest;
- 多轮对话稳定性。
3. 工具层:Task-Specific Training
- function calling;
- tool selection;
- 参数填写;
- RAG 引用;
- agent planning。
4. 产品层:Inference & System Design
- system prompt;
- routing;
- sampling;
- tool schema;
- memory;
- fallback 策略。
5. 评测层:Eval & Monitoring
- 离线 benchmark;
- judge model 和人工评测;
- 在线 A/B test;
- failure case 分类;
- 用户反馈闭环。
所以现实中的训练流程,很少是:
- 训完 pretrain。
- 再训一次 SFT。
- 结束。
更常见的实际流程反而是:
- 先得到一个 base model。
- 做通用 post-training。
- 再做任务专项训练。
- 用专项 eval 找失败模式。
- 再回到数据、训练策略、产品逻辑继续修正。
- 然后重新评测。
从这个角度看,训练流程更像是一棵树,甚至更像一张带回路的图:每做一次评测,都会把问题重新分流到不同层级。
Q7.2: 下游任务做不好时,怎么判断是 base model 不够强,还是后训练不够?
这是一个非常现实,也非常难的问题。因为很多下游问题并不是“base 不够强”和“后训练不够”二选一,而更像是两者共同作用的结果。
例如,function calling 做不好,可能有几种完全不同的原因:
- base model 根本没理解任务语义;
- base model 其实理解了,但没有被训练成稳定输出结构化 schema;
- 工具调用的数据不够;
- 工具 schema 设计得不清楚;
- 系统 prompt 没把调用规则写明白。
所以工业实践里通常不会直接问:“是不是基础模型太弱?”
而是先把问题拆成更细的定位问题。
一个常见的思路是:
先怀疑上层问题,再怀疑底层问题。
这是因为:
- 改 prompt、采样参数、tool schema、routing 策略最便宜;
- 改 SFT 数据或 task-specific post-training 次之;
- 继续大规模 pretrain 或重训 base model 最贵。
所以现实里更常见的排查顺序是:
- 先看评测定义是不是有问题;
- 再看推理配置和产品编排是否有问题;
- 再看 task-specific post-training 是否不够;
- 再看通用对齐是否损伤了能力;
- 最后才看是不是 base model 本身不够强。
Q7.3: 有什么更具体的判断思路?
如果一个任务失败了,常见的判断方法包括:
1. 看模型“会不会”,还是“会但不稳定”。
如果给一点更明确的示范,模型就明显能做出来,那么问题往往更像是:
- prompt 不够清楚;
- 输出格式没有教好;
- 后训练还不够稳定。
如果即使给了很强的引导,它仍然做不好,那么问题就更像底层能力不足。
2. 看把任务改成更结构化之后,模型会不会。
例如把自由生成问题改成:
- 选择题;
- 判断题;
- 填槽题;
- 固定 JSON 输出。
如果结构化之后模型明显变好,那么通常说明它并不完全是“不会”,而可能是:
- 自由生成阶段不稳定;
- 格式对齐不够;
- 输出空间太开放。
3. 看是不是某一类失败特别集中。
例如在 tool use 任务里,可以继续细分:
- 理解错任务;
- 选错工具;
- 参数填错;
- 应该调工具但没调;
- 工具调用成功了,但结果没整合进最终回答;
- 输出格式非法。
只有把失败继续切成这些更细的子类,才能知道下一步该补哪一层能力。
4. 看更便宜的修复能不能救回来。
如果换一个更清楚的 system prompt、改一下 schema、补一点更贴任务的 SFT 数据,性能就明显上升,那么通常没有必要立刻怀疑 pretrain 底座。
反过来,如果这些便宜修复几乎都无效,才更有理由怀疑 base model 的能力上限。
Q7.4: 这和我们现在做的小模型实验有什么关系?
MiniMind 这种规模的模型非常适合学习训练流程,但它的能力边界也很明显。
因此评估时要避免两个极端:
- 看到模型能说出几句通顺的话,就高估它的能力;
- 看到模型回答不稳定,就否定 pretrain 的意义。
更合理的看法是:小模型的 pretrain 结果主要用于观察训练机制,而不是追求最终可用的智能水平。
对于这一类实验,我会优先关注:
- loss 曲线是否正常下降。
- validation loss / validation PPL 是否和训练趋势基本一致。
- checkpoint 是否能稳定保存和恢复。
- 模型是否从完全随机输出变成能生成局部通顺文本。
- 相同设置下,不同实验之间的 loss 和样例是否有可解释差异。
这些指标不一定能证明模型“强”,但能证明训练流程在工作。
Q7.5: 一个更现实的总结
如果把这整章内容放在一起看,一个更现实的结论是:
- pretrain 阶段先用
loss / validation loss / PPL把训练过程看清楚; - 再用固定 prompt 的生成样例和一些自动化指标补充定性观察;
- 到 SFT、function calling、RAG、agent 等阶段,再分别引入更专门的测试集和任务指标;
- 当下游任务做不好时,不要急着把原因都归到“模型不够强”,而要先把问题拆到不同能力层级上去定位。
换句话说,真实世界里的大模型训练和测试,不是单条流水线,而是一个在开放世界不确定性中不断分叉、不断回收的闭环系统。