05. 读懂训练曲线:loss、验证集与常见故障
上一篇算出资料助手的 LoRA 微调在一张 H100 上大约一两个小时就能跑完。跑起来之后,能看到的只有训练日志里的几条曲线。训练 loss 从 2.0 降到了 0.5,这是好事吗?第 3 轮要不要跑完?某一步 loss 突然跳高、或者直接变成 nan,该先查什么?这一篇用资料助手的 SFT 过程,讲清楚每条曲线代表什么、正常的样子、几种典型的异常形状,以及怎样挑出真正该上线的检查点。读完你应该能看着一组曲线说出“这里开始过拟合”“这个尖峰不用管”“这条线好得可疑,先查数据”。
本篇的曲线全部是示意,按常见形状绘制,用于说明判断方法,不是某次真实训练的日志。
日志里的几条曲线
一次训练通常会记录下面几项,常见工具(TensorBoard、Weights & Biases,或 Transformers 的终端输出)都按“训练步数”作为横轴:
- 训练 loss:当前这一批样本上的平均损失。对语言模型来说,它是每个需要预测的 token 上的交叉熵(基础系列第 6 篇讲过损失函数)。每一步只看一小批样本,所以噪声很大,通常要平滑后看趋势。
- 验证 loss:在一份不参与训练的留出数据(验证集)上算的同一种损失,每隔若干步评估一次。它回答的是“模型对没见过的样本表现如何”。
- 学习率:每一步参数更新的步长,按调度表变化,常见的是先预热、再逐步衰减(第 2 篇)。
- 梯度范数:把所有参数的梯度拼成一个长向量,算它的长度。它反映这一步“模型想改多少”。很多训练配置会设一个裁剪阈值(常见是 1.0),超过就按比例缩小。
loss 的数值可以换算成一个直观的量。交叉熵是“正确 token 预测概率”的负对数,所以:
text
正确 token 的平均预测概率 ≈ e^(−loss)(按几何平均)
loss = 2.0 → e^(−2.0) ≈ 0.14
loss = 1.0 → e^(−1.0) ≈ 0.37
loss = 0.5 → e^(−0.5) ≈ 0.61loss 从 2.0 降到 0.5,意思是模型给标准答案里每个 token 的概率,从平均约 14% 提高到约 61%。
一次正常的 SFT 长什么样
资料助手的数据是 1 万条问答,每条都按 Llama 3.1 的对话模板排好,只对回答部分计算 loss(第 9 篇)。沿用第 2 篇的全局批大小 32,每轮约 313 步,训练 3 轮,学习率先预热 10 步再按余弦衰减,每 20 步在 500 条验证样本上评估一次:
第一轮开头几十步,训练 loss 和验证 loss 一起快速下降。这时模型学的是最容易学的东西:回答的格式、口吻、开头怎么写、条款编号放在哪里。之后两条线都进入缓慢下降的阶段。
到第 2 轮开头,训练 loss 出现一个明显的台阶,而验证 loss 没有。原因是数据开始重复:从这一步起,模型看到的每条样本都是上一轮见过的,它对这些具体句子已经有了记忆,loss 自然一下子低一截。第 3 轮开头又是一个台阶。多轮训练时训练 loss 在轮次边界阶梯式下降是正常现象,同时也说明从这里起,训练 loss 越来越多地反映“背下了多少”,而不是“学会了多少”。
验证 loss 在第 2 轮末降到最低,第 3 轮开始回升,这是过拟合开始的位置。右侧的梯度范数开头较大(模型刚接触新格式,改动多),很快稳定在一个窄带内;学习率按调度平滑变化,两者都没有异常。
过拟合:训练集和验证集分道扬镳
过拟合的曲线信号是:训练 loss 继续下降,验证 loss 停止下降甚至回升,两者的差距越拉越大(基础系列的泛化与评估讲过这个现象的一般形式)。
在资料助手上,它的表现很具体:小林问“病假怎么请”,模型的回答里出现了训练集中“年假怎么申请”那条样本的原句和条款编号。模型不是在理解制度,而是在复述它背过的答案。
处理方法按代价从小到大:选验证 loss 最低处的检查点、减少轮数、降低学习率、减小 LoRA 的秩、补充表述更多样的样本。要注意的是,验证 loss 只衡量“和参考答案逐 token 有多接近”,它略有回升而业务评测仍在变好的情况也会出现,所以最终仍要用任务评测确认(见后文选检查点一节)。
验证集本身也要可靠。它应该和真实提问同分布,并且不能和训练集重叠:同一条制度的不同问法如果一半进训练集、一半进验证集,验证 loss 会好得失真。按“所属制度条款”或“问题聚类”整组划分,比随机抽样更安全。
学习率太大和太小
学习率决定每一步走多远。下面六个小图是常见异常形状的示意,前两个就是学习率的问题:
学习率过大时,loss 先降一下,然后开始剧烈震荡,接着一路上升,严重时变成 nan。参数每一步都迈过了谷底,越走越偏。学习率过小时,曲线平滑但下降很慢,几轮之后仍然停在一个偏高的位置;它不会报错,所以更容易被忽略,直到评测时才发现“训练了好像没效果”。
学习率的合适范围与训练方式有关:LoRA 只更新少量新增参数,常见配置的学习率比全参微调高一个数量级左右。拿不准时,用三个相差 10 倍的学习率各跑一两百步,比较 loss 下降的速度和平稳程度,再选中间合适的那个继续。
预热的作用也在这里体现。训练刚开始时,Adam 优化器对梯度大小的统计还没积累起来,前几步的更新幅度可能偏大;先用很小的学习率走几十步,再升到目标值,可以避开开头的不稳定。
梯度范数与 loss 尖峰
loss 尖峰是指 loss 在某一步突然跳高,随后几十步内回落,有时回到原来的轨道,有时留下一个台阶。它前面往往能在梯度范数上看到征兆:某一步的梯度范数比平时大好几倍。
尖峰的常见来源有三类:这一批里混进了异常样本(超长文本、乱码、几乎全是重复字符);学习率偏大,模型处在不稳定的边缘;数值问题(见下一节)。梯度裁剪能限制单步更新的幅度,但不能完全消除尖峰。PaLM 论文(2022)记录过,540B 模型在开启梯度裁剪的情况下训练中仍出现了约 20 次 loss 尖峰,出现时间没有规律;团队的处理是从尖峰前约 100 步的检查点重新开始,跳过尖峰前后那 200 到 500 个数据批次,之后尖峰没有再出现。
对资料助手这种规模的微调,判断方法更简单:单个尖峰、几十步内恢复、验证 loss 不受影响,记下来继续训练即可;反复出现、或者尖峰后 loss 回不去,就先找出那一步用到的样本逐条查看,再考虑降低学习率。训练框架通常可以按步数和随机种子还原出某一步的批次内容。
NaN 与 Inf:先看数值格式
曲线在某一步之后消失,日志里 loss 显示 nan,这是最需要立即处理的情况:一旦某个参数变成 nan,后续所有计算都会被它传染,继续训练没有意义。
最常见的来源是数值溢出。FP16 能表示的最大数约为 65,504,激活值或梯度超过它就会变成 inf,而 inf − inf、0 × inf 之类的运算结果就是 nan。BF16 的指数位和 FP32 一样多,能表示到约 3.4×10³⁸,几乎不会溢出,代价是有效数字更少(数值精度系列第 2 篇对比了两种格式)。这是现在训练大模型普遍用 BF16 的主要原因。如果硬件只支持 FP16,就需要配合损失缩放:先把 loss 放大,避免小梯度变成零,发现溢出时跳过这一步并缩小缩放系数(混合精度怎样计算)。使用 FP16 时日志里偶尔出现“跳过一步”是正常的;连续大量跳过才说明有问题。
除了溢出,还有几种情况会直接产生 nan:学习率过大导致发散,最终数值失控;某一批里所有 token 的标签都被屏蔽了,按“平均每个 token 的 loss”计算时分母为 0,变成 0 除以 0;对 0 取对数。排查时先定位第一次出现 nan 的步数,取出那一批数据,在小规模上复现,再逐层检查是哪一个算子的输出先变成了非有限值。
看起来像模型问题的数据问题
有一类故障,曲线看起来要么完全正常,要么“好得不像话”,问题却出在数据处理上。它们最容易被误判成“模型能力不够”或“需要更多数据”。
标签掩码错误。 SFT 通常只对回答部分计算 loss,系统提示和用户问题的标签设为忽略。如果掩码写错,把问题也算进了 loss,模型就会学着生成用户的提问,上线后可能在回答末尾自己续写一句“用户:那病假呢?”。曲线上它往往表现为 loss 开头就偏低、下降更平滑,因为模板和系统提示这类重复内容很好预测。反过来,如果把回答也屏蔽掉了,模型什么都学不到,loss 可能直接是 0 或 nan。检查方法是取几条样本,把标签没有被屏蔽的 token 解码成文字打印出来,确认正好是回答部分加上结束符。
数据重复。 训练集内部大量重复时,训练 loss 会很快接近 0,验证 loss 早早停住(示意 6)。如果重复跨越了训练集和验证集,验证 loss 会紧贴着训练 loss 一起下降,看起来非常健康,上线后却没有改善(示意 5)。资料助手的数据常从问答日志整理而来,同一个问题被问过几百次很常见。训练前要对问题做规范化后去重,并按问题分组划分训练集和验证集。
对话模板不一致。 训练时用的对话模板和推理服务实际拼出的提示不一样,比如角色标记写法不同,或者回答末尾没有放结束符 <|eot_id|>(Llama 3.1 用它标记一轮回答结束)。这种问题在曲线上完全看不出来,loss 一切正常;上线后模型回答完不停,继续往下编,或者格式时好时坏。检查方法是用分词器的 apply_chat_template 渲染一条训练样本,和推理服务日志里的实际输入逐字符比对。
选检查点:看评估,不看最低训练 loss
训练 loss 最低的检查点,几乎总是最后一个,也往往是过拟合最严重的那个。选检查点的依据应该是验证 loss,再加上业务评测。
资料助手团队准备了 200 道评测题,覆盖常见制度问题,也包括一些制度里没有答案、应当回答“不知道”的问题。对每一轮末的检查点各跑一遍(下表数字为示意):
| 检查点 | 验证 loss | 条款编号正确率 | 无答案时正确拒答 |
|---|---|---|---|
| 第 1 轮末 | 1.03 | 78% | 70% |
| 第 2 轮末 | 0.97 | 86% | 81% |
| 第 3 轮末 | 1.11 | 85% | 64% |
第 3 轮的条款正确率和第 2 轮相差无几,但“不知道就说不知道”的能力明显下降,模型更倾向于套用背过的答案。这里应该选第 2 轮末的检查点。只看训练 loss 会选第 3 轮,只看条款正确率会觉得两者差不多,只有把拒答也测了才能看出区别。
loss 为什么不等于任务质量
loss 是所有回答 token 上的平均值,每个 token 的权重相同。资料助手一条回答有两三百个 token,条款编号“第 12 条”只占其中两三个。编号答错,对 loss 的影响微乎其微,对小林却是致命的。同一个意思可以有很多种正确的说法,模型换了一种措辞,loss 会变高,但回答并没有变差。拒答、编造、语气是否得体,这些都很难从逐 token 的 loss 里读出来。
不同模型、不同分词器、不同对话模板下的 loss 也不能直接比较:分词方式不同,同一句话被切成的 token 数和难度都不一样。loss 适合用来判断“同一个训练过程是否健康”,判断“模型是否更好”要靠评测(第 21 篇)。
排查对照表
| 现象 | 可能原因 | 先检查什么 |
|---|---|---|
| loss 先降后震荡上升 | 学习率过大;预热太短 | 学习率曲线是否按预期;把学习率降到 1/3 或 1/10 重跑一两百步 |
| loss 下降很慢、几轮后仍偏高 | 学习率过小;LoRA 目标模块太少;标签几乎全被屏蔽 | 学习率实际值;可训练参数量;解码未屏蔽的标签 token |
| 训练 loss 每轮开头掉一个台阶 | 多轮训练、数据重复出现,属正常现象 | 同时看验证 loss 是否仍在下降 |
| 训练 loss 降、验证 loss 升 | 过拟合 | 选验证 loss 最低的检查点;减少轮数或学习率;补充多样样本 |
| 单次 loss 尖峰,随后恢复 | 异常样本;学习率偏大 | 尖峰那一步的样本内容;梯度范数是否同时跳高 |
loss 变成 nan | FP16 溢出;发散;某批标签全被屏蔽 | 第一次出现 nan 的步数和那一批数据;数值格式是否为 BF16 |
| 训练 loss 很快接近 0 | 训练集大量重复;标签泄露了答案 | 规范化后的问题去重统计;标签与输入是否错位 |
| 验证 loss 紧贴训练 loss、好得可疑 | 验证集与训练集重复 | 两个集合的问题重叠率;是否按问题分组划分 |
| 曲线正常,上线后回答停不下来或格式乱 | 训练与推理的对话模板不一致;缺结束符 | 用 apply_chat_template 渲染样本,和服务实际输入逐字符比对 |
| 曲线正常,评测分数不涨 | loss 与任务目标不一致;数据没覆盖评测场景 | 按题型拆分评测结果;查看失败样例对应的训练数据 |
| 吞吐突然下降、曲线变稀 | 数据加载或通信瓶颈;有卡降频 | 每步耗时、GPU 利用率;多卡时看各卡是否一致 |
曲线是训练过程的体检报告:训练 loss 说明模型在已见数据上进展如何,验证 loss 说明它能否推广到没见过的问题,梯度范数和学习率反映这一步的更新是否稳定。轮次边界的台阶是正常的,验证 loss 回升意味着该停了,尖峰先看批次内容,nan 先看数值格式和第一次出错的那一步;曲线好得可疑或完全正常却效果不对时,回头查掩码、去重和对话模板。选检查点和判断质量,最终都要落到评测上。到这里,训练的共同底座就讲完了。下一部分进入大语言模型本身,先从它吃进去的东西开始:预训练数据与分词。
