首页 / 资讯中心 / 文章详情

少样本SFT过拟合防治:从症状到LoRA、标签平滑与KL约束的实战指南

少样本SFT过拟合防治:从症状到LoRA、标签平滑与KL约束的实战指南 ★ FEATURED ARTICLE
做 few shot SFT 最怕的不是模型学不会而是“学得太快”。我有一次拿六十来条人工清洗过的对话样本去微调一个开源底座模型第一轮 epoch 还没跑完训练 loss 就掉到了零点零几看起来验证集也还行。等我把模型接到真实对话环境里一测才意识到它把大部分输入都往那几十条样本的答案模板上靠换个说法问同一个问题它背诵原文问个完全不相关的话题它也能强行拐回训练集主题。后来我把这种状态叫作“训练集满分真实场景零分”。这篇文章会把我在 few shot SFT 上跟过拟合斗智斗勇的经验梳理一遍从症状、机制到数据侧、训练侧、损失函数层的对策最后给一套可量化的检测流程。无论你是刚开始接触少样本微调还是已经在用 LoRA 跑业务模型应该都能从里面找到可以直接抄走的配置和经验。1. 先认清症状训练曲线越漂亮越要警惕过拟合1.1 训练loss快速归零的“满分幻觉”少样本场景下最常见的误判就是把训练 loss 当成了模型是否学好的核心指标。几十条样本、两三个 epoch训练 loss 一路俯冲到 0.02甚至接近 0很多人的第一反应是“成了”。但这里藏着一个大问题一个参数量动辄几十亿的模型想要背下几十条输入输出映射几乎是零成本的事。我常用的判断标准是看训练集 loss 和验证集 loss 之间的 gap。如果训练 loss 还在降验证 loss 却开始反弹或者震荡走高这就不是“模型不够聪明”而是模型开始把训练样本当成标准答案死记硬背了。可 few shot 场景往往没有足够的验证集很多人到这一步根本察觉不到异常等到部署上线才翻车。所以少样本微调里的第一个原则是不要迷信训练 loss。它降到极低只说明模型有足够的容量去记忆不代表它学到了可泛化的规则。1.2 真实场景里的复读机与“什么问题都往领域答”过拟合模型在真实场景里的表现非常有辨识度。最典型的是复读机行为用户把问题换一种措辞表达模型依然输出训练集里的原答案好像完全没听懂这句话。再严重一点模型会形成“领域路径依赖”——不管用户问什么它都要把话题拽回自己训练过的那个领域里。我印象很深的一个例子是用 50 条电商客服对话做 few shot SFT训练集里全是“退款”“退货”“物流”相关的问题和话术。模型训练完之后你问它“今天天气怎么样”它回答的开头竟然是“您好关于您的退款申请我们已经收到……”这不是模型变蠢了而是它在少量样本里发现了一个“捷径”只要输出这类话术loss 就低。至于输入是不是匹配它已经顾不上了。这种行为的本质是模型学到的是“输入到输出的固定映射”而不是任务背后的规则。规模越大的模型越容易把这种固定映射学得又快又好这也是 few shot 过拟合比大样本微调更难处理的原因。1.3 更隐蔽的代价预训练通用能力被覆盖除了任务本身跑偏过拟合还会带来一个更隐蔽的代价模型原本的通用能力被覆盖。少样本数据分布非常窄当模型把参数往窄分布方向猛拉时预训练阶段积累的开放式对话能力、知识问答能力、多样化的语言风格都会受损。一个很典型的迹象是微调前底座模型写出来的句子自然多变微调后只剩一种腔调。哪怕是跟领域完全无关的闲聊模型也会自动收敛成训练集里的表达风格。有人把这个叫“灾难性遗忘”但在 few shot SFT 里它往往不是彻底遗忘而是被过拟合的强信号覆盖住了。这里有个我一直在用的土办法微调之前先拿 5 到 10 个开放域问题问一遍底座模型把回答存档微调完成后再拿同样的问题去问微调后模型做一个并排对比。如果通用回答质量肉眼可见地下降那不管领域评测分数多高我都默认这次微调是有问题的。2. 原理层面为什么越少样本越容易过拟合2.1 模型容量与数据量严重失衡过拟合的根本原因可以从容量和数据量的角度看。一个拥有几十亿参数的模型本身就是一个巨大的记忆体。你给它几十条样本它完全可以把这些样本逐个存入参数分布里而不需要提炼任何抽象规则。打个比方一个记忆力极好的学生考前只背了三道题。这三个题目考多少分他都能默写出来但换一道同类型的新题他就不会了。模型也是一样预训练阶段已经让它具备了强大的模式存储能力few shot 数据量太少根本不足以约束它往“泛化”的方向走反而给了它走捷径的空间。理解了这一点就会明白为什么 rank、学习率、冻结层这些在常规微调里不太敏感的参数在 few shot 场景里会变得特别敏感。因为我们需要主动限制模型“背题”的能力逼它只能学个大概的规则。2.2 有限样本里的表面模式会被梯度反复强化少样本数据往往带有很强的人工挑选痕迹覆盖面有限。如果这些样本里频繁出现某些关键词或句式模型就会把这些表面相关性当成规律来学。举个例子训练集里如果很多样本都包含“订单号”这个词模型可能学到“只要输入里有订单号就输出某个模板”如果几条样本都要求“先道歉再处理”模型会把“道歉前置”当成所有回复的铁律。这是一种伪规律但它确实能让训练 loss 降得很快因为数据量太少没有其他样本去冲抵这种偏置。从优化角度看小 batch 下如果每条样本的高频模式一致梯度的方向会高度趋同。模型每更新一步就会往同一个方向再推一点。几轮 epoch 下来参数就被推到一个很极端的区域。这也是为什么我会在数据侧花大量时间做多样性审计而不是事后再指望用正则去救。2.3 交叉熵目标天然鼓励“过度自信”交叉熵损失的作用机制是不断提高正确 token 的概率、压低其他 token 的概率。样本量充足时这种压力会被分布广泛的训练数据摊薄模型学到的是一种概率分布但样本量极少时交叉熵会把模型快速推向“对训练样本过于确定”的状态。说白了交叉熵不关心模型是否“合理”地完成任务它只看最后输出的分布能不能贴合训练标签。当几十条样本反复出现时模型很容易把某个答案的概率推到接近 1形成尖锐的 one-hot 分布。这种过度自信恰恰就是过拟合的特征。所以后面要讲的标签平滑、KL 散度惩罚本质上都是在跟交叉熵的这种“自信”较劲让模型不要对训练集里的噪声和巧合过于认真。3. 数据侧防过拟合先让样本“经得起放大”3.1 多样性审计删掉冗余样本保留信息量数据是防过拟合的第一道防线而且优先级比调参高得多。我拿到 few shot 样本后的第一件事不是急着写训练脚本而是做一次多样性审计。具体操作我之前分享过这里再讲一遍步骤把每一条样本的输入文本用 embedding 模型编码成向量计算两两之间的余弦相似度设定一个阈值一般 0.85 左右把相似度超过阈值的样本归到一组每组里只保留信息量最大的一到两条其余删掉。删完之后再按主题粗分一下看分布是否均匀。比如 60 条样本里如果 40 条都在问“退款”那这个数据集即使有 60 条实际有效信息量也低得可怜。模型在训练时反复看到退款相关的话术自然会把“退款”当成最高优先级模式。这步做完样本数量可能从 60 掉到 40但相信我这 20 条的减少换来的往往是更低的过拟合风险。少样本阶段每一条样本都要有不可替代的信息量。3.2 合法扩充数据的两个有效姿势改写与格式扩展删完之后如果觉得样本量确实不够可以做受控的数据扩充。这里我推荐两种方式。第一种是同义改写。把每条样本的输入输出用大模型改写三到五个版本比如变换提问方式、调整句式结构、增加角色前缀等。但要注意风险很多模型批量改写出来的文本只是换了个词序语义相似度依然很高对多样性贡献不大。所以改写完之后我建议再用 embedding 相似度过滤一遍只保留确实“长得不一样”的版本。第二种是格式扩展。如果任务要求输出 JSON、Markdown 或某种结构化格式不要所有样本都用同一种格式。比如 JSON 的键顺序、嵌套层级、字段命名方式都适当变一变。这么做能让模型学到“格式背后的语义”而不是把某一套固定的格式写法背下来。数据扩充量控制在原来的三到五倍以内比较合适。少样本条件下盲目扩到几十倍很容易把噪声一起放大。3.3 负样本和边界样本同样承担正则化职责很多人做 few shot SFT 只收集正样本就是“用户问什么模型答什么”。但只靠正样本训练出来的模型特别容易出现“什么输入都接活”的过拟合问题。我建议在数据集里混入两类特殊样本。第一类是负样本用户的问题明显不在任务范围内模型需要礼貌拒绝或表示无法处理。比如客服模型里加一条“附近有什么好吃的”期望输出是“抱歉我暂时不掌握本地生活信息”。这类样本能让模型学会识别任务边界而不是看到任何输入都往领域模板上套。第二类是边界样本用户信息不完整时模型应该先追问而不是直接给结论。这类样本并不多但效果奇好它们能教会模型“不同输入状态应该走不同的输出路径”从根上减少把所有输入都汇聚到同一条生成路径上的倾向。你可以把这两类样本看作一种数据层面的正则化。它们的数量可以少但不能没有。3.4 输入输出长度配比一个容易被忽略的坑还有一个数据侧的坑是输入输出长度的配比。如果训练样本的输出都集中在三四十字模型会对生成长度形成很强的偏好正式使用时凡是需要长回答的问题它都答得又短又空反过来如果样本都是长输出模型又容易啰嗦。我处理少样本数据时会特意检查输出长度的分布确保既有短的、也有中等和偏长的样本。输入侧也是一样不要全是同一种长度的 query。训练阶段可以按样本实际长度做 batch 内的 padding但不要把所有样本都强行 pad 到同一个超长长度那样会让模型忽略长度变化这个信号。长度特征也是一种容易过拟合的模式只是它藏在文本表面之下不对比看根本注意不到。4. 训练侧防过拟合把学习速度和参数改动幅度同时压住4.1 学习率、epoch与有效更新次数数据准备好之后训练超参是下一个关键环节。少样本场景下我强烈建议把学习率压到比常规微调更低。这里给出我常用的起点配置不是唯一答案但可以作为参照参数全参微调起点LoRA/QLoRA起点优化器AdamWAdamW学习率1e-5 ~ 2e-51e-4 ~ 2e-4batch size8 ~ 168 ~ 16epoch1 ~ 21 ~ 3weight decay0.01 ~ 0.10.01 ~ 0.1梯度裁剪1.01.0warmup5% ~ 10% 步数5% ~ 10% 步数标签平滑0 ~ 0.10.05 ~ 0.1这里有个概念值得认真算一下有效更新次数。它的粗略公式是 数据量 × epoch ÷ batch size。比如 50 条样本、batch size 8、跑 1 个 epoch总共只有 7 步左右的有效更新。这么点步数模型连 LoRA 增量都未必充分训练所以不是“只要 epoch 少就不过拟合”。我的经验是few shot 场景下“低学习率、多重复几轮”比“高学习率、一两个 epoch”更稳。比如 LoRA 用 1e-4 的学习率跑两到三个 epoch配合标签平滑和 KL 约束效果通常比 2e-4 跑一个 epoch 要好。学习率低一点每次参数跳动的幅度就小一点模型就更难在某个错误方向上走远。4.2 LoRA低秩微调天然是正则但要选对rankLoRA 在少样本场景里几乎是我的首选方案。它的核心思路是冻结原始权重只训练低秩的增量矩阵。可训练参数占比很小这本身就是一种参数层面的正则大幅度限制了模型改写自身表示的能力。但 LoRA 不是随便配个 rank 就完事。rank 越大增量矩阵的表达能力越强过拟合风险也越高。few shot 场景下我通常从 rank 8 到 16 起步alpha 设置为 rank 的一到两倍也就是 16 到 32。不要一上来就用 rank 64完全没有必要反而更容易把几十条样本的细节全部记住。LoRA 的 dropout 也不要忽略0.05 到 0.1 都可以。它的作用是在训练时给增量路径加随机扰动让模型不至于过度依赖某几条样本产生的固定方向。还有个小经验LoRA 的目标模块不要只盯着 attention很多模型在 MLP 层上加 LoRA 效果更明显具体可以按模型架构和任务类型去试。4.3 冻结embedding与lm_head防止表示层漂移少样本微调时embedding 层和 lm_head 层是我建议优先冻结的模块。这两个层的参数量非常大而且直接关系到 token 的表示和词表分布。如果让它们在几十条样本上跟着梯度更新很容易把预训练阶段学到的语义表示拉偏。embedding 被拉偏的典型后果是模型用词变得单一某些 token 的概率被异常抬高。lm_head 被拉偏的典型后果是生成的文本倾向于从训练集的用词区间里选词导致整体风格趋同、缺乏多样性。冻结住它们相当于让模型只能在 transformer 内部的投影空间里做小幅调整不太容易伤及预训练知识。实际操作上配置 LoRA 的 target_modules 时只包含内部的 attention 和 MLP 投影层不让 embedding 和 lm_head 进入可训练名单。如果任务必须要模型学会输出新的特殊 token再考虑解冻 lm_head但这时一定要配合 KL 散度惩罚否则灾难性遗忘会来得特别快。4.4 weight decay、梯度裁剪、EMA这些细功夫除了上面几个大头还有一些看起来不起眼但实际很管用的训练细节。weight decay 建议设置在 0.01 到 0.1 之间。它会对权重做衰减抑制参数朝过大方向漂移。梯度裁剪也很重要max_grad_norm 设成 1.0 就好。少样本下 batch 内的样本方差大偶尔会出现很大的梯度如果不裁剪一步更新就能把参数推出很远。EMA 是我近半年越来越依赖的一个技巧维护一份权重移动平均推理时用平均权重而不是最后一步的权重。它相当于间接做了时间维度的正则对缓解少样本训练后期 loss 震荡有奇效。另外提醒一句训练时开 dropout评估和推理时关掉 dropout。这个基础细节在少样本场景下更关键因为数据少dropout 提供的随机性是模型避免死记硬背的重要助力。5. 损失函数层面的三招让模型“不敢”死记硬背5.1 标签平滑降低对训练答案的绝对信心训练数据少交叉熵又天生鼓励过度自信这时候给它手动降降温是个好办法。标签平滑的做法是把 one-hot 目标改成软标签比如 ε 设为 0.1正确 token 的概率不再是 1而是 0.9剩下的 0.1 平均分给其他 token。这样一来模型不需要在训练集上达到绝对的 one-hot 分布也就不会产生“非此答案不可”的极端偏置。少样本场景下我个人会从 ε0.1 起步如果任务对输出格式要求非常严格可以降到 0.05如果模型明显复读也可以试着调到 0.2。实现不算麻烦如果用 Transformers 的 Trainer直接设置 label_smoothing_factor 参数如果自己写训练循环在 CrossEntropyLoss 里也有对应的 label_smoothing 参数。成本几乎为零但对过拟合的抑制很直观。5.2 KL散度约束把输出分布拉回预训练状态比标签平滑更主动的做法是在损失函数里加一项 KL 散度惩罚。具体思路是冻结一份微调前的底座模型作为 reference训练时计算当前模型输出分布与 reference 输出分布之间的 KL 散度把它乘上一个权重系数加到原始交叉熵 loss 上。这样做的作用是模型在训练样本上可以把对应答案的概率拉高但不允许整个输出分布偏离预训练状态太远。形象点说交叉熵在往前拽KL 约束在往回拉两个力平衡的结果就是“学到了一些东西但没学过头”。代码示意大致长这样# 示意在标准交叉熵之外加一项 KL 散度约束 cur_log_probs model(input_ids).log_softmax(-1) base_log_probs ref_model(input_ids).log_softmax(-1) kl_div F.kl_div( cur_log_probs, base_log_probs, reductionbatchmean, log_targetTrue, ) loss ce_loss 0.3 * kl_div权重系数 λ 可以从 0.1 到 0.5 之间调。数据越少、样本越窄λ 就可以适当调大一点。实现时要注意 reference 模型不需要梯度它的前向结果可以缓存或者用同一个模型在冻结状态下发一份副本。显存稍微多点但换来的是模型稳定性的显著提升。5.3 混入通用数据成本和效果最稳的“伪正则”如果不想写 KL 散度的代码还有一个更朴素但同样有效的替代方案在训练集里混入一部分通用指令数据。比如领域样本 50 条就再混 10 到 20 条通用问答样本让模型一边学新任务一边复习原有的表达能力。这本质上也是一种分布约束。通用数据会把部分梯度引向“保持原模型能力”的方向抵消纯领域样本带来的单向拉扯。代价低、实现简单效果也非常稳。很多开源的 SFT 配方里都会混入通用数据背后就是这个道理。需要注意节奏通用数据比例不要太高否则领域任务学不到位也不要太低否则起不到约束效果。我个人通常控制在 20% 到 30% 左右先跑一轮看领域评测和通用能力评测的变化再动态调整比例。这个思路可以作为 KL 散度约束的简易入门替代两者叠加使用也可以。6. 怎么确认没有过拟合一套可执行的检测流程6.1 训练集/验证集的loss曲线怎么读才有意义防过拟合做得怎么样不能靠感觉要量化。第一个量化手段还是训练集和验证集的 loss 曲线但要读得聪明一点。如果样本量在 100 条以上可以考虑做 80/20 划分留一部分样本做验证。观察的重点不是单次验证 loss 的绝对值而是它和训练 loss 之间的 gap 变化趋势。训练 loss 持续下降、验证 loss 开始反弹这是最直接的过拟合信号。少样本下的验证集通常很小单次 loss 波动很大所以建议多看几步的平滑趋势或者做几次不同划分交叉验证。如果样本量少于 50我不建议强行划分验证集因为分出去之后训练数据更少了反而容易欠拟合。这种极端少样本场景重心应该放在下面的对比生成和指标检测上。6.2 与基座模型做对比生成比看曲线更靠谱我觉得少样本过拟合最有效的检测方式是直接拿基座模型做 AB 对比。准备几组输入既包括领域内的典型问题也包括领域外的开放闲聊然后分别用微调前和微调后的模型生成排在一起看。人工看几个维度就行相关性、多样性、自然度。如果领域内题目答得不错但一遇到域外问题就往领域模板上拐那基本就是过拟合如果连领域内问题的回答都开始复读训练集原句说明已经严重过拟合了。这个测试成本很低效果却非常直接。我一般会固定准备 5 个问题每个实验迭代后都把三个版本的输出拿过来对比打分。与其去猜验证集 loss 有没有意义不如亲自看几段生成结果来得踏实。6.3 量化信号distinct值、输出长度方差与困惑度人工对比之外还可以上一些量化指标辅助判断。最简单的是 distinct-1 和 distinct-2分别表示生成结果中单字和二元词组不重复的比例。过拟合模型生成文本的 distinct 值会明显下降因为它翻来覆去就那么几个词、几个句式。输出长度的均值方差也是很好的信号。正常模型的回答长度会有一定波动过拟合模型会收敛到训练集里的固定长度区间附近方差急剧变小。此外可以算生成结果的困惑度在训练集上困惑度异常低在域外文本上困惑度异常高这往往意味着模型把训练样本背得太牢了。需要提醒的是这些指标只能辅助判断不能单独作为证据。比如任务本身要求固定格式输出distinct 值天然就会偏低。所以我的习惯是把它当作一个快速筛查工具发现可疑信号后再用人工对比确认。6.4 最后一个工程师技巧检查参数更新幅度最后一个指标可能很多人没试过检查参数更新幅度。在训练前保存一份模型权重训练后对比每个模块参数的 L2 范数差异或者直接看 LoRA 增量矩阵的范数大小。如果某个模块的参数变化幅度明显大于其他模块同时验证集表现又没跟上那这个模块大概率就是过拟合的重灾区。这有助于反向定位问题是 attention 层学过头了还是 MLP 层没有约束住还是 lm_head 被拉偏了。比如我发现某次微调里 attention 层的增量矩阵范数异常大验证集上生成又开始复读就会先把学习率降下来或者把 KL 散度权重调大再重新跑一轮。这种排查方式比盲目调参有目标得多也算是我自己比较依赖的内部技巧。最后分享一个我每次做 few shot SFT 之前都会过的检查清单算是个人习惯先做数据多样性审计删掉高相似样本补上负样本和边界样本确认输出长度分布不是单一峰值必要时做同义改写和格式扩展LoRA 从 rank 8 到 16 起步冻结 embedding 和 lm_head学习率用正常微调的十分之一左右epoch 控制在 1 到 3加上标签平滑条件允许时加 KL 散度约束或者至少混入少量通用数据训练前后保存 base 模型输出跑对比生成顺带看一眼参数更新幅度。这一套组合拳打完基本上能把“训练集满分、真实场景零分”的概率压到很低。少样本微调本身就是一个在欠拟合和过拟合之间找平衡的活儿不用指望一步到位先跑通、再量化、再微调。过拟合这东西就是这样你堵住了它的一条去路它还会换一条路冒出来但只要检测手段跟得上它就跑不远。
阅读完成 · 觉得有帮助?
咨询建站