深蓝黑背景封面。左侧是一张坐标图:横轴为微调步数,纵轴为能力。青色实线从左下向右上爬升,标注「目标任务能力:越训越好」;紫色虚线从左上向右下持续下滑,标注「通用能力:悄悄劣化」;两条曲线之间右侧的区域以紫色半透明填充,标注「遗忘区」。图下注「新任务学会了,旧本事丢了」。右侧大标题「微调后的灾难性遗忘是怎么回事」,副标题「成因是参数被新数据梯度覆盖,对策是四类缓解手段」。左上标注「面试经典 · 大模型微调」,右上圆角胶囊标注「Day 10 · 11-27」。 图 1:本文封面,微调中目标任务上升、通用能力下滑的双曲线。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「微调后通用能力下降」是每个做大模型微调的人都会撞上的现象,面试里它的正式名字叫灾难性遗忘(catastrophic forgetting):模型学会新任务的同时,把预训练阶段积累的通用能力「挤掉」了。

成因:同一份参数,被新数据的梯度改写。

预训练参数之所以有通用能力,是因为它们在万亿级 token 上被调到了一个对「语言整体分布」都不错的位置。微调时,梯度只按新任务数据的分布计算,参数朝着新任务的低损失方向移动——而新数据里没有任何信号阻止它远离旧任务的好位置。网络里没有「这块参数管通用问答、那块管拼写纠错」的隔断,所有能力共享同一套权重,新梯度覆盖到哪里,哪里的旧能力就劣化。任务越窄、数据越单一、训练越久,覆盖得越狠。

四种缓解手段,各管一段。

  • 数据混合:在微调数据里按比例回混通用指令数据或旧任务数据。这是最直接的手段——旧分布的信号重新出现在梯度里,参数就没有理由单方面漂移。代价是要准备和维护回混数据,比例也得调。
  • 小学习率:微调学习率明显小于预训练末期,每一步对参数的改写量小,旧能力被覆盖得慢。代价是收敛慢,新任务要学更久。
  • 正则约束:给损失加一项「不许重要参数动太多」的惩罚。经典做法是 2016 年提出的 EWC(Elastic Weight Consolidation,arXiv:1612.00796):用费雪信息估计每个参数对旧任务的重要性,越重要的参数改动惩罚越大,相当于给旧能力上了一圈松紧不一的锚。
  • 参数冻结:干脆不让原权重动。LoRA 这类参数高效微调只训练一小撮增量参数,预训练权重全程冻结,通用能力几乎不掉——这是它在实践中大受欢迎的重要原因之一。

没有免费的午餐:缓解越强,新任务学得越慢。

四种手段本质上是同一个权衡的不同切面:参数离旧位置越远,新任务学得越透、旧能力掉得越多。工程上的目标不是「零遗忘」,而是「遗忘控制在够用线以内」——上线前用通用能力评测集(而不是只看新任务指标)卡住下限,是基本的自保动作。

用代码把遗忘和缓解跑一次最小复现。

下面这段 Node.js 22.15.0 程序(node forget.js)把问题压缩到极限:一个单参数线性模型 y = w·x,任务 A(模拟通用能力)的最优 w 是 2,任务 B(微调目标)的最优 w 是 1.5。先充分学好 A,再用三种策略各微调 40 步。

const mse = (w, data) => data.reduce((a, [x, y]) => a + (w * x - y) ** 2, 0) / data.length;
const grad = (w, data) => data.reduce((a, [x, y]) => a + 2 * (w * x - y) * x, 0) / data.length;
const A = [[1, 2], [2, 4], [3, 6], [-1, -2], [-2, -4]];          // y = 2x
const B = [[1, 1.5], [2, 3], [3, 4.5], [-1, -1.5], [-2, -3]];    // y = 1.5x
const report = (tag, w) =>
  console.log(`${tag}:w = ${w.toFixed(3)} | 任务A误差 = ${mse(w, A).toFixed(4)} | 任务B误差 = ${mse(w, B).toFixed(4)}`);

let w = 0;
for (let i = 0; i < 2000; i++) w -= 0.05 * grad(w, A);            // 充分学好任务 A
report('学完任务A(微调前)', w);

let w1 = w;
for (let i = 0; i < 40; i++) w1 -= 0.05 * grad(w1, B);            // 大学习率 + 纯新数据
report('大学习率纯B数据', w1);

let w2 = w;
for (let i = 0; i < 40; i++) w2 -= 0.005 * grad(w2, B);           // 学习率缩小 10 倍
report('小学习率纯B数据', w2);

let w3 = w;
for (let i = 0; i < 40; i++) w3 -= 0.05 * grad(w3, [...B, ...A]); // 回混 50% 旧数据
report('大学习率+回混旧数据', w3);

实测输出:

学完任务A(微调前):w = 2.000 | 任务A误差 = 0.0000 | 任务B误差 = 0.9500
大学习率纯B数据:w = 1.500 | 任务A误差 = 0.9500 | 任务B误差 = 0.0000
小学习率纯B数据:w = 1.606 | 任务A误差 = 0.5894 | 任务B误差 = 0.0428
大学习率+回混旧数据:w = 1.750 | 任务A误差 = 0.2375 | 任务B误差 = 0.2375

三行数字正好对应三种权衡:大学习率纯新数据把 B 学到满分、A 彻底遗忘;小学习率保住了大半 A,但同样步数下 B 没学透;回混旧数据让参数停在两个任务的折中点,两边误差都不归零但都可控。真实大模型里发生的就是这件事的高维版本。

深蓝黑背景概念图。上半部分是一条三节点成因链:「预训练参数承载通用能力」箭头指向「微调梯度只按新数据分布更新」,再指向紫色高亮的「旧任务相关权重被覆盖 —— 遗忘」。下半部分是四张并排缓解卡,每张带一条表示缓解力度的横向刻度条:数据混合(旧数据按比例回混,力度条最长,注「最直接,成本是回混数据要准备」);小学习率(步子小覆盖少,力度条中等偏长,注「代价是收敛慢」);正则约束(EWC:按重要性惩罚参数改动,力度条中等,注「重要参数少动」);参数冻结(LoRA 类只训练增量,原权重不动,力度条长,注「通用能力几乎不掉」)。底部总结条:「没有免费的午餐:缓解越强,新任务学得越慢 —— 权衡点是够用就好」。 图 2:遗忘成因链与四种缓解手段的力度对比。

面试现场的答题结构。

先定义现象——微调后通用能力劣化叫灾难性遗忘;再讲成因——共享参数被新分布梯度单方面覆盖,任务越窄、训练越久越严重;然后按「数据混合、小学习率、正则约束(EWC)、参数冻结(LoRA 类)」四种手段逐一讲机制与代价;最后落到权衡:目标是控制在够用线以内,用通用评测集卡下限。你做微调时遇到过遗忘吗?最后是靠调学习率还是回混数据救回来的?欢迎聊聊。