微调模型输出坍缩成单一模板时,先用 grep 统计训练集而非直接重训练,5/1610 的污染率即可导致 36% 的输出问题。
我在 Amazon Bedrock 上跑了一个微调过的 Llama 3.3 70B。它生成简短的第一人称叙事帖子:先是一个铺垫,然后几行故事,最后一句点题的收尾。
上周我注意到收尾句全部塌缩成了一个模板。不是精神上相似,而是字面上相同的语法结构,反复出现:
and that's how [someone] [learns/teaches] [something].
大约三分之一产出的内容都以这种方式结尾。显而易见的诊断是过拟合:训练数据肯定被这个模式渗透了,模型把它学成了讲故事的固定结尾。显而易见的修复方案同样清晰。清洗训练集,重新训练,重新部署。
在我的流程里,这大概要花 30 美元的训练算力加上五小时的训练任务,再加上后续的评估环节才能知道是否有效。不至于破产,但也不是免费的,而且这已经是第三个重新训练的循环了。
在花这笔钱之前,我做了件本该一开始就做的事:数了一下。
我的训练文件是 JSONL,每行一条样本。检查大约花了十秒:
grep -o -i "that's how" train_v6_2.jsonl | wc -l
# 5
wc -l train_v6_2.jsonl
# 1610
1610 条训练样本中出现了 5 次。0.3%。
然后我数了一下模型实际生成内容中同样模式的出现频率。每条生成的内容都有持久化存储,所以就是查一下最近 25 条的 SQL:
select right(content, 200) as ending
from generated_stories
order by created_at desc
limit 25;
25 条中有 9 条。36%。
这两个数字根本对不上。训练数据 0.3% 的出现率不可能产生 36% 的输出。微调会移动模型的分布,但不会把一个模式放大一百倍。无论是什么在驱动这个现象,都不是权重的问题。
这意味着重新训练什么也改变不了,而我之前会得出微调坏了的结论。
生成 Prompt 里包含一行我几个月前写的,之后再也没重新看过的内容。结构上长这样:
THE CLOSER: lead with a thesis line that names what the story proves, e.g. "and that's how [X] teaches [Y] to expect [Z]."
问题就在这儿。我给模型提供了一个完整的、格式良好的目标收尾示例,而它做了最合理的事:把示例完完整整地复制了过去,包括模板。
这不是模型在作妖。当你在 Prompt 里给语言模型一个具体模式的一个实例,然后让它产生这个模式,这个示例就成为了上下文窗口里最强的信号。它每次都会压倒在权重中 0.3% 的倾向。我实际上把我的输出硬编码了,然后怪到训练数据头上。
修复方案零成本,当天下午就上线了。
我没有用一个硬编码示例,而是建了一个包含七条收尾句的小池子。关键属性是它们结构上不相似:一句平铺直叙的判断、两句式的揭露、一句后果陈述、一句规范性建议、一句反讽回调。重要的是,它们没有一条使用那个被复制的句式。
我的生产用收尾句来自私人存档,所以这里用一个中立领域的示例池来说明同样的多样性:
_CLOSER_EXEMPLARS = [
"The process was the problem.",
"Nobody had tested it. Everybody had approved it.",
"We saved an hour that afternoon and spent the next year paying for it.",
"Write the runbook before you need it. You will not be calm enough later.",
"So much for the quick fix.",
"We called it a deadline for four months before anyone admitted it was a guess.",
"What would you have checked first?",
]
examples = "\n".join(
f"- {c}" for c in random.sample(_CLOSER_EXEMPLARS, 3)
)
每次调用随机采样三条,外加一条明确的指令:研究这些句子所实现的功能,然后为这个故事写一条新的,不要复用它们的措辞。
推理是:提供一个示例教的是模板,而提供多个不相似的示例教的是功能。如果每个示例看起来都不同,但它们都达到了同样的效果,那么唯一可以模仿的就只有效果本身。
在相同的已部署模型、相同主题、不重新训练的前提下,用线上推理来测量:
样本量较小,在有更多数据之前我不想把 8% 当作精确数字。但方向是明确的,而且这次修复只改了一个 Prompt,而不是重新训练一整轮。
剩下的 8% 我认为是真正的权重问题。那个 0.3% 依然在那里,Prompt 修复触及不到它。这部分残差是下一次训练数据清洗的合理目标。区别在于现在它是一个小的、充分理解的清理工作,而不是一次基于错误诊断的重新训练。
当微调模型产生了重复内容时,有三种可能的原因,它们的代价差异巨大:
Prompt 给了它这个模式。修复零成本。优先检查这一项。
训练数据被这个模式渗透了。需要清洗数据并重新训练。
基模型有强烈的先验。需要专门针对它反制的样本。
区分这三种情况的诊断方法是:一行 grep 加一条 SQL 查询。把训练数据中的频率和输出中的频率做对比。如果输出频率显著更高,原因在 Prompt 或检索层,而不是权重。如果两者频率相近,那确实是数据问题,重新训练是合理的。
从这次经历中我养成了两个习惯,建议所有在 Bedrock 上跑微调的人效仿:
持久化你的生成内容。我之所以能做"之后"的对比,是因为每条输出都已经在带时间戳的表里了。没有这段历史就没有衡量,只有印象,而印象正是让我一开始走向重新训练的原因。
把 Prompt 示例当作训练数据来看待。它们本质上就是。任何你放在 Prompt 里作为例子的具体内容都会被复制,所以要么不给,要么给足够多的多样性,让只有底层功能可以被模仿。一个例子是最坏的两全其美。
这个教训让人不舒服的版本:模型没问题。我差点花钱花时间用hard way证明它没问题,而证据一直就躺在我随时可以数一下的那个文件里。