☰
少样本SFT防过拟合实战:数据扩展、LoRA与训练控制
2026/10/22 14:40:30 网站建设 项目流程

要是在 few shot SFT 上只记住一件事,那大概是:模型的过拟合方式和你想的往往不太一样。我见过不少同学以为,只要减小学习率、拉长训练轮数,就能让模型慢慢“学得更好”,结果恰好反过来——小样本下往往只需要几步,模型就开始背答案,训练损失跌得看似漂亮,验证集一测立刻暴露原型。few shot SFT 的核心矛盾就在这里:可用样本太少,而模型参数太多,训练过程容易走捷径。这篇文章不绕圈子,直接讲几个我在实际调参中反复验证过的防过拟合打法,包括数据侧怎么处理、训练过程怎么控制、模型侧哪些参数该动哪些不该动。适合正在做开源底座模型微调、对话增强、领域指令优化的同学参考,尤其是手里只有几十条到几百条标注样本的场合。

1. few shot 场景下过拟合为什么这么“凶”

1.1 模型背答案,不等于学会了

先把这个观点说透:小样本里的过拟合,不是传统意义上的“学死记硬背”,而是一种更隐蔽的参数捷径。一个七八十亿参数的开源底座,你只喂给它三十二条训练对话,反向传播更新不了几轮,就能把这三十二条答案“钉”在权重里。这时候你问训练集里出现过的问题,它回答得一字不差,语气都对;可一旦换成训练集里没出现过的问法,它就开始胡说八道。

关键是,训练损失和验证损失并不同步。我复现过不少项目,训练 loss 能从 1.2 降到 0.9,梯度下降得非常“健康”,验证 loss 却一路上扬。这说明模型并不是在“学会任务”,而是在“记住样本”。为什么会这样?因为监督微调本质上是在优化经验风险,也就是最小化“训练集上的平均损失”。小样本情况下,经验风险和真实风险之间的差距非常大,模型完全没有动力去学习问题背后的一般规律,它只需要把训练样本按字面碾压一遍就能拿到很低的损失。

这个过程没有任何玄学成分。优化器在参数空间中找了一条最容易让当前几个样本“全部命中”的路径,这条路径往往不是泛化路径。你要意识到这一点,才能在训练早期就果断干预,而不是等到模型彻底背熟样本之后才去调参。

1.2 小样本下的“捷径学习”倾向

可以做一个很简单的对照实验:你拿同样的问题,换成不同句式、不同语气去问微调后的模型,如果答案质量剧烈波动,基本可以断定模型走了捷径。所谓捷径学习,在我的理解里就是模型捕捉到了训练集中“过于突出的表面特征”,比如特定开场白、特定称谓、特定句式,然后把这些表面特征当作任务核心来响应。

举个常见的例子:训练集中有一批以“请帮我”开头的请求,模型学着学着就形成一种倾向——只要看到“请帮我”,就输出一种固定结论,不管后面请求的具体内容如何。这在数据增强时极其常见,因为人工标注的对话往往带有个人写作习惯,同样的过渡句、同样的标点符号、同样的口语词会高频出现。模型不需要理解整句语义,它只需要抓住一两个高频触发词就能蒙混过关。

所以我在做 few shot SFT 时,最优先考虑的不是怎么把训练 loss 压得更低,而是让模型不要抓住训练样本里的“统计巧合”。后面对数据做扩写、改写也好,对模型做冻结、正则化也好,本质上都是为了让模型别那么轻松地找到那条捷径。

2. 数据侧:先把少样本变成尽可能接近全貌的样本

2.1 构造验证集——判断过拟合的前提

你可能觉得验证集没什么好说的,不就是从样本里切一部分出来吗?但在 few shot 场景下,验证集的切法直接影响你对“过拟合是否发生”的判断。假设你手里只有四十条数据,最常见的做法是三七开,二十八条训练、十二条验证。这个小验证集也有价值,但噪声太大,经常出现训练 loss 在降、验证 loss 一会在 0.1 一会在 0.5 的情况,压根分不清是过拟合还是随机波动。

我个人的建议是,如果总数少于五十条,至少把验证集留足十条,而且验证集的内容分布要尽量贴近真实线上请求。不能为了“切均匀”就把请求类型生硬打散——比如十条验证里八条都是“帮我写邮件”,训练集里却只有两条邮件类样本,这等于人为制造了一个不稳定的估值器。另一个更稳的做法是 k 折交叉验证,例如五折,每折都让模型重复训练一遍,最后取验证分数的均值。样本很少的时候,这种方法花费并不高,得到的泛化估计却远比单次三七开可靠。

还有一种值得做但常被忽略的验证方案:把训练集内部切一份“语义近义改写版”作为验证集。什么意思?就是你拿训练集里的问题,换一种问法、换一个同义表达,然后在验证阶段去测。如果模型在两个表达下的输出差别很大,说明它对字面形式做了过拟合。这个验证集不代替真实分布,但作为“过拟合哨兵”非常灵敏。

2.2 数据扩展:改写、回译、同义替换的实操

少样本数据要扩展,但扩展不是让你随便想几个同义词替换一下完事。在 SFT 里,我比较常用且实测有效的扩展方式有三种。

第一种是同义改写。把用户问题改成完全等价但措辞不同的说法,改的时候要注意控制长度和语感,别把口语问题改成书面语问题,否则模型学会的其实是“语体转换”而不是“意图理解”。第二种是回译,也就是把中文样本翻译成英文,再翻译回中文。这个方案的好处是能引入自然的句式错位,过程中往往会冒出一些你没预料到的表达方式,对增加多样性很有帮助。代价是回译后的文本偶尔会丢细节,必须做一轮人工校对。

第三种是模板化扩展。人工从原始样本中提炼出“意图骨架”,比如把“我需要一份关于XX的周报”扩展成“帮我写一份关于XX的周报”“这周的XX周报怎么弄”“领导让我写XX周报,帮我搭个框架”。这类扩展会明显增加模型面对不同前缀输入时的稳定性。

这里要提醒一个我很早就踩过的坑:数据扩展时千万不要改变答案的语义结构。有一次我扩展某个项目的问答数据,为了提高多样性,把答句里的两个要点调换了顺序,顺手改了中间连接词。当时觉得无伤大雅,结果模型在真实请求里特别喜欢把后置条件提前,逻辑顺序完全乱了。原因很直白,模型同时看到了两种顺序的“标准答案”,它学到的是随机排列而非因果顺序。扩展的前提是保真,其次才是多样性。

2.3 让样本量倍增的另一种思路:混合增强

除了对单条样本做改头换面,我还会用“混合增强”的方式扩展上下文配对小样本。比如你有十条用户问题,每条对应一种标准行为,那你可以在不同问题之间做条件组合——把问题 A 的场景抽出来,和问题 B 的业务要求拼成一个新问题。这个方法在客服、法律问答、结构化信息抽取类任务里很管用,因为真实请求本来就经常组合多个子意图。

做混合增强时,验证逻辑要跟得上,不能自动生成一批“伪答案”就直接丢进训练集。我通常会让另一个模型先生成候选答案,再人工快速过一遍,保证每个新增样本的意图标签和答案结构无误。实际操作中我会先按字面抽一遍关键词,再做人工抽检,六十四条数据扩展到两百条左右,训练效果会有肉眼可见的提升。

注意一点,数据扩展不是越多越好。超过一定量之后,扩展带来的“多样性收益”会被“噪声污染”抵消。我更愿意把扩展量控制到原数据的 2 到 5 倍,并且每一条新增样本都经过抽检,而不是一次性生成几百条然后全盘接收。

3. 训练策略:少样本场景下的反过拟合控制

3.1 早停与验证集监控

虽然每个框架都有早停参数,但在 few shot SFT 里,早停不是一个简单开关,而是一套需要你主动设计的机制。首先,验证集要稳定,前面已经说过了。其次,早停的触发条件不能只看验证 loss 的绝对数值,要看它相对训练 loss 的偏离趋势。我常用的一个判断指标是“损失分离比”:当验证 loss 开始上升,或者连续两个 epoch 验证 loss 不再下降但训练 loss 还在大幅下降时,就已经到了必须停止的时刻。

很多教程建议用“验证 loss 上升”作为触发点,但我发现小样本训练里验证 loss 的波动太大,直接用上升触发会频繁误判。我的做法是加一个容错窗口:验证 loss 连续三到五次评估没有改善,或者出现单次大幅跳增,才真正触发早停。窗口太短会把正常波动当成过拟合信号,过早停止会让模型欠拟合;窗口太长又会错过最佳检查点。经验值是每 50~100 步做一次验证,窗口设 10 次验证以内。

还有一个小经验,训练过程中把“最佳验证 checkpoint”保存下来,训练结束后不要用最后一个 checkpoint,而是加载验证指标最佳的那个。这个逻辑在标准深度学习里很常见,但我在实际项目里发现很多人写着写着就忘了,最终发出去的模型其实是过拟合后几个 epoch 的权重。

3.2 学习率、批量大小与梯度裁剪

few shot 场景下,学习率的选择比大样本场景更挑剔。大样本数据量大,即使学习率偏大,模型也能在大量样本的“平均梯度”里慢慢校准;小样本数据相反,每一步梯度都是少数几条样本“共同推动”的,学习率稍微大一点,等于这几条样本有了一票决定权,很容易把权重推向特定样本的方向。

我倾向于在 1e-5 到 5e-5 这个区间起步。以底座模型的学习率参考值来看,如果用的是自然语言模型,通常比这个更低一些比较安全。具体可以先跑一个短步数实验,看验证 loss 的第一轮下降幅度;如果下降幅度巨大且在第二个 epoch 立刻反弹,那就说明学习率设高了,要往下降半个量级。相反,如果验证 loss 在两个 epoch 里纹丝不动,可能是学习率太低,也要适度调高。

批量大小方面,我建议小批量配合梯度累积。单卡能放的 batch size 通常不大,那就先用真实 batch size 4~8,然后用梯度累积到等效 batch size 16~32。这里有个反直觉的点:在小样本场景,过大的有效批会让模型在很少的样本上重复踏过,强化记忆,反而不好。有效批次宁可小一点,让优化器每次看到不同的样本组合,梯度方向会更“漂移”,这种漂移其实有助于避免死记硬背。

梯度裁剪一定要加。小样本下某些样本的偶发噪声会让梯度出现尖峰,如果不限制梯度范数,一次大更新就可能把好几轮微调的效果破坏掉。我一般把梯度范数裁到 0.5 到 1.0 之间,这个参数不需要经常调,但能帮你守住训练的稳定性。

3.3 训练轮数与“收敛即要小心”的节奏感

标准微调里三到五个 epoch 很常见,但 few shot SFT 的 epoch 策略完全不一样。数据量小意味着每个 epoch 里模型都会反复看到同一条样本,重复次数越多,背诵风险越高。我这里有个很实用的估算方法:把训练总步数除以可训练参数的有效更新次数。虽然这个数字很难精确算出来,但你心里要有个谱——三十二条数据,训练五个 epoch,相当于每条数据参与了五次完整梯度更新,参数空间里的移动路径会不断强化同一批样本的记忆。

我实际跑过的几个 few shot SFT 项目里,四个 epoch 以上出现过拟合的概率非常高。所以我的默认配置是 2 到 3 个 epoch,配合早停窗口。如果数据量在几十条这个量级,甚至只训 2 个 epoch 就能取得不错的验证效果。训练 loss 在第二个 epoch 还在下降并不代表该继续训,你要看验证集是否依然受益。当验证 loss 已经停止下降,训练 loss 还在缓慢下降时,这就是过拟合正在展开的信号,应该立刻考虑停止。

另外一个容易被忽略的细节是学习率调度。如果整体训练只有几百步,用带热启的余弦衰减会比较舒服。前 5%~10% 步数做 warmup,让优化器状态稳定下来,然后逐步衰减到很小的值。训练后期学习率越低,模型越不可能大规模重排权重,也就越不容易发生突然的灾难性泛化崩溃。我甚至在小数据集上用过“固定学习率 + 早停”的简单方案,效果也 OK,但带调度能让你更容易定位最佳 checkpoint。

4. 模型与正则化:让微调“软一点”

4.1 冻结骨干底座,只调浅层或适配器

如果条件允许,few shot SFT 里最稳的做法就是冻结底座大部分参数,只训练一小部分。直接把几十条样本输入到全参数微调流程里,哪怕学习率再低,训练出的模型也极大概率是“局部特化”的。全参数微调带来的参数变化范围太大,任何微小的权重偏移都可能破坏底座原本具备的通用能力。

谈到冻结,最实用的方式是 LoRA 这类低秩适配方案。LoRA 只训练注入到注意力层的小规模低秩矩阵,可训练参数量通常是底座总参数量的 1% 以下。这个设计天然就有正则化效果:因为可训练参数少,模型没有足够容量去逐字背下所有训练样本,它只能抓住数据里最共性的规律。这也是为什么我在 few shot SFT 中强烈建议优先考虑 LoRA,而不是挑战全参数微调。

如果你连 LoRA 都不想用,也有“只训练 embedding 加部分全连接层”的折中办法。但实际操作中,我几乎不会建议小样本项目去做全参数微调,收益太小、风险太大。冻结多少层取决于你的基座模型和算力,但对大多数 7B 级别的开源模型,直接用 LoRA 冻结一切底座参数,是最稳妥的起点。

4.2 LoRA 的秩与 alpha 怎么选

关于 LoRA 的超参数,我见过太多人直接照抄默认值,结果小样本下照样过拟合。LoRA 的秩 r 决定的是低秩矩阵的表达能力,r 越大,适配器能存储的信息越多,也就越容易记住训练样本;r 越小,表达受限,模型会更偏向“泛化”一些。这听起来很符合直觉,但实际还要看你的数据复杂度。

小样本场景下,我建议把 r 控制在 4 到 16 之间。如果是纯对话风格调整或格式统一类任务,r=8 通常够了;如果任务涉及结构化逻辑或领域知识,r=16 更稳。再往上加 r 并没有给我带来过显著收益,反而会让验证集开始波动。alpha 一般取 r 的两倍,比如 r=8,alpha=16 就是我的默认组合。

这里有一个细节值得展开:LoRA 的 dropout 参数。它在小样本下简直是救星。默认 LoRA dropout 通常设为 0.1,但当你发现微调后期模型开始高度复述训练集样式时,把 LoRA dropout 提高到 0.2~0.3,往往能有效削弱适配器对单一样本的过度响应。代价是收敛会变慢,但小样本场景下收敛本来就是次要目标,泛化才是第一要务。

另一个比较新的做法是给 LoRA 适配矩阵做正则化约束。比如对适配矩阵计算 L2 范数并加入 loss,或者直接用“低秩分解权重衰减”来控制更新量。这类方法不是所有框架都原生支持,需要手写,但在小样本任务里确实能进一步限制适配器对特定样本的偏向。我建议先尝试 LoRA dropout 和权重衰减,如果仍过拟合,再考虑更复杂的正则化。

4.3 权重衰减、标签平滑、dropout 的适用性

权重衰减在小样本 SFT 里是有用的,但要看作用在哪些参数上。全参数微调时,权重衰减会让所有参数往零收缩,这能压制一些过大权重,但也会让模型某些关键路径失去原有的表达能力。LoRA 训练时,权重衰减主要作用在低秩矩阵上,效果更干净。

我通常给 AdamW 设置 weight_decay=0.01 到 0.1,这是个经验区间。设太小几乎没有约束作用,设太大则有可能让 LoRA 秩矩阵过于稀疏,反而影响表达。如果你发现模型训练 loss 在降、验证 loss 却高位徘徊,优先把 weight_decay 从 0.01 提到 0.1 试试,这个改动既简单又经常有效。

标签平滑在普通分类任务里是防过拟合的经典手段,但在生成式 SFT 里我持保留态度。因为自回归语言模型的监督信号是“下一个 token 的真实标签”,对真实 token 做平滑,往轻了说会让模型对这些 token 的置信度下降,往重了说会让生成内容变得不果断、喜欢用一种模棱两可的语气输出。我在个别任务里试过 label_smoothing=0.1,输出变得软绵绵,后期还得额外加温度控制补救。所以我更建议把“抑制过度自信”的任务交给 LoRA dropout 和权重衰减,而不是把标签平滑直接压在生成头上。

dropout 方面,除了 LoRA 自带的 dropout,我还见过有人在小样本场景给注意力层额外加 attention dropout。这能增加训练时的随机性,抑制注意力对个别 token 的过度专注,但我提醒你,dropout 加太多会让训练集自身都拟合不起来。小样本下 dropout 从 0.1 起步,如果模型在训练集上表现良好但验证集崩了,可以逐步加到 0.2、0.3,但不要超过 0.4。

5. 现场实录:一个 few shot SFT 项目的防过拟合复盘

5.1 项目背景与参数选择

这是一个模拟项目,我做过的某个知识问答场景,目标是让开源底座学会面向特定业务领域输出规范化回答。手里的人工标注样本一共四十条,覆盖六种常见意图。因为样本量不够,我一开始就做了总体规划:32 条扩展训练、8 条验证、另有 10 条预留在训练完全结束后做最终泛化评估。

模型侧我选择了冻结底座全部参数,只训练 LoRA 适配器。初始参数如下:

参数设定值说明
LoRA rank8小样本默认起点
LoRA alpha16两倍秩
LoRA dropout0.1后期视情况调整
学习率3e-5余弦调度,前 10% 步数 warmup
有效 batch size24单批 6,梯度累积 4 步
训练轮数3配合早停窗口
梯度裁剪1.0防止个别样本梯度尖峰
weight_decay0.05对 LoRA 矩阵施以轻量约束

第一轮就跑了完整的三轮训练,得到的结论是:训练损失到第二个 epoch 已经降得很低,验证损失却从第二个 epoch 开始反弹。这也是我判断该启动防过拟合调整的典型信号。

5.2 训练过程中的异常信号与调整动作

训练过程中,我重点盯三个信号。第一是训练 loss 与验证 loss 的差距,这是最直接的。第二是模型在验证集上不同意图间的表现差异,如果某个意图的验证结果比其他意图差很多,说明训练样本没有覆盖到该意图的多样性表达。第三是观察生成的句式长度和复杂度,如果模型反复输出同一长度的固定句式,甚至用词和训练集里的模板高度重合,那就是记忆在生成端的外显。

第一轮跑完,我做了三个调整。第一,把 LoRA dropout 从 0.1 提到 0.2。第二,把 weight_decay 从 0.05 提到 0.1。第三,减少一个 epoch,从三降为二,同时把早停窗口设置得更灵敏一些。调整后训练损失略高,但验证损失重新变得平滑,生成的答案也更自然,不再像背课文。

还有一个调整我特别想分享,就是把训练数据里“过于一致的表达”手动打散。当时训练集里有一半样本都有“你好,我需要”这种开头,模型很快学会了把所有输出都挂到“你好”后面。我后来对这批样本做了改写,把开头换成“麻烦问下”“想了解一下”“给个方案”等多种表达。虽然训练集的“平均损失”上升了零点几,但验证集明显扛住了压力,模型对不同前置语气的鲁棒性增强了许多。

5.3 常见问题速查表

把我在 few shot SFT 项目里踩过的坑整理成一张速查表,方便大家对照排查。

表格如下:

现象可能原因处理办法
训练 loss 下降,验证 loss 上升过拟合正在发生提前早停,降低学习率,提高 LoRA dropout 或 weight_decay
验证 loss 波动剧烈,无法判断趋势验证集过小或分布不均匀扩大验证集,或改用 k 折交叉验证
输出高度重复训练集句式模型记住样本表层模式做语义改写类数据扩展,尝试降低 LoRA rank
某些意图表现特别差该意图训练样本太单调对该意图做定向改写增强,增加表达多样性
训练 loss 也很高,且验证 loss 同样高欠拟合或学习率过低先提高学习率试验,确认 loss 开始下降后再回到防过拟合策略
一个样本的微小改动影响全局输出模型过度依赖个别样本适当增加 dropout,限制适配器参数更新幅度
生成变得模棱两可、缺乏确定性标签平滑或 dropout 过高取消标签平滑,把 dropout 调回 0.1 左右

这张表是我后来反复检查项目的“体检单”。每次开始一个新 few shot SFT 项目,我都会先跑一两轮基线,看有哪几个现象命中,再有针对性地做调整。这样不会一开始就把所有参数同时改掉,避免了“血清病”——你改了十个参数,最后效果好,但根本不知道是哪个起的作用。

6. 最后再分享两个现场小技巧

正好聊到这儿,我再分享两个在几个项目里都验证过的小技巧。

第一个技巧是,条件允许的话,把训练集、验证集之外的“空白样本”也纳入评测维度。什么意思?就是你准备一些和业务毫无关系的通用输入,比如“讲个笑话”“写一首诗”,交给微调后的模型生成。如果模型面对这些输入时表现异常,比如不自觉地都输出业务固定话术,那说明微调已经产生了很重的偏向。这个检验不需要经过复杂计算,几分钟就能看到结果,却能快速暴露模型是否被训练样本“驯化”。

第二个技巧是低秩适配器合并之后的“二次验证”。很多人训练完 LoRA 只是简单加载权重测几个 case 就收工。我会把适配器合并回底座,导出一个完整模型,再用同一套验证数据重新跑一遍。合并过程中偶尔会出现适配器放大或者抵消某些底座的表达,导致合并前后行为不一致。很多线上模型表现不如本地调试,根子可能就出在这一步。先合并再测,才能保证最后的部署形态和验证结果一致。

这些经验都是我一次次在少量样本训练里被现实怼过之后攒下来的。没有高深理论,都是每一步能直接试、直接复现的做法。你要是手里也有一个 few shot SFT 项目正在头疼,我建议先从数据扩展和 LoRA dropout 这两个方向下手,它们是性价比最高的防过拟合手段。等你把这两个工具用顺手了,再回头看学习率和早停的精细化调整,就会容易很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询