微调里那些会骗你的数字
/ 6 min read
Table of Contents
微调一个 14B 模型做内容合规质检的时候,我先后被两个漂亮的数字骗过:训练 loss 低到 0.0137,评估准确率高到 93.6%。两个都是假的。这篇记录怎么发现它们是假的,以及它们共同指向的那个真问题。
假象一:loss=0.0137
第一版训练(exp_005)收敛后,train_loss 停在 0.0137。
这个数字低得反常。一个 LoRA 微调,正常收敛到零点几是常态,0.0137 这种量级要么是严重过拟合,要么是哪里不对。我没有直接拿它当好消息,而是去看了训练配置——cutoff 设的是 1024。
然后就对上了:数据集里有些样本(尤其是规则长的那几类)的助手输出很长,超过 1024 会被截断。被截掉的 token 不参与损失计算。 这些长样本恰恰是难的,结果它们的难 token 大量没进 loss,平均下来 loss 自然被拉得很低。
0.0137 不是模型学得好,是大量 token 压根没算进去。
把 cutoff 放开,重训一版(exp_006),train_loss 变成 0.245。所有 token 都参与计算后,这才是真实水平。
结论很简单但容易忽略:loss 的绝对值依赖于你在多少 token 上计算它。截断设置不同、loss mask 不同,loss 就不可比。 看到一个异常漂亮的 loss,第一反应不该是高兴,而是去查它到底算的是哪些 token。
假象二:93.6% 的准确率
换个角度,看评估。全量评估准确率 93.6%,看起来很能打。
但我去拆了评估时模型的实际输入。批量推理为了对齐用了 padding_side="left",输入超长时的截断方向是”去头保尾”。这个设置有个隐蔽后果:
- 尾部保住了——所以那段要求”输出 JSON”的指令在,模型还能正常吐合法 JSON,看起来一切正常;
- 但头部被砍了——而规则上下文都在前面。模型实际只看到了最后 1024 个 token,前面大量规则根本没进去。
也就是说,这 93.6% 是模型在”几乎没看到规则”的情况下蒙出来的。可能它学到了某种先验(多数内容合规),靠瞎猜也能到九成。这个准确率衡量的不是”模型按规则判断的能力”,而是”在没有规则时的基线猜测”。
一个评估数字,如果你不去核对模型实际看到了什么,就可能把流程里的截断 bug 误读成模型的能力。
两个假象指向同一个真问题
把线索拼起来:训练时因为 cutoff=1024,模型每条样本其实只稳定见到 1-3 条规则;而推理时,WebUI 一次给它 6 条规则,全量评估一次给它 53 条。
模型从没在训练里见过”一次几十条规则”这种输入格式。 推理时给它这种没见过的长上下文,它表现变差,是必然的。
这就是训练/推理分布错位(train-serve skew)。它的麻烦在于隐蔽:
- 它不在 loss 上——loss 是在训练分布上算的;
- 它不在同分布评估上——如果评估也按训练的格式来,照样很好看;
- 它只在真实使用时暴露——而真实使用往往没有自动化指标盯着。
找到它,靠的不是盯指标,而是把”训练样本长什么样”和”线上请求长什么样”摆在一起对比。一对比,错位就很明显。
几条我留下的习惯
- 异常好的 loss 要查 mask 和截断。低 loss 可能只是因为算的 token 少。不同截断/掩码下的 loss 不可直接比较。
- 高指标要核对模型的真实输入。截断方向、padding、上下文有没有被砍,决定了这个数字衡量的到底是什么。
- 线上差、指标好,先比分布。把训练分布和推理分布并排看,train-serve skew 通常一眼可见。
微调里最危险的不是会报错的地方——那些你迟早会修。危险的是这些看起来一切正常、却在悄悄骗你的数字。