☰
大模型持续学习:灾难性遗忘、EWC 与 LoRA 参数隔离
2026/10/31 2:07:48 网站建设 项目流程

1. 面试现场:一个看似简单的问题

面试官问你:
一个大模型微调,先微调了法律知识,又微调了医疗知识,结果法律能力全丢了,加数据混合能解决吗?

你回答:不能。旧数据拿不到,比例也调不好。

追问:那为什么有的方案能持续学习新任务,还不忘记旧的?

又答不上来了吧。

这里涉及的就是持续学习中的
灾难性遗忘、弹性权重固化(EWC)和参数隔离机制。
考的是模型知识保持和参数更新边界的工程理解。

2. 微调到底调了啥:从权重说起

先把最基础的问题说清楚:微调调的就是模型的权重(参数),而且是神经网络里那一堆权重矩阵和偏置。

一个神经网络可以看成一张巨大的参数表。以 Transformer 为例,每一层里都有:

  • 注意力层的投影矩阵:W_q、W_k、W_v、W_o,负责把输入映射成 query/key/value 并做输出投影;
  • 前馈网络(FFN)的两层矩阵:先升维再降维,参数量通常占整层的大头;
  • LayerNorm 的缩放和偏置:γ、β,数量少但影响数值稳定性;
  • 词嵌入矩阵和输出头:词表有多大,这一块就有多宽。

这些矩阵里的每一个数字,都是训练时通过反向传播一点点更新出来的。所谓"微调",就是拿一个已经预训练好的权重作为起点,用新任务的数据继续跑梯度下降,把这些数字往新任务更优的方向挪一挪。

# 微调的本质:对权重做梯度更新forx,yindataloader:loss=criterion(model(x),y)loss.backward()# 算出每个权重的梯度optimizer.step()# 权重 -= lr * 梯度

所以"全参微调"调的是全部权重,"局部微调"调的是其中一部分权重——这正是后面 EWC 和 LoRA 的分歧所在:前者想办法约束哪些权重不能乱动,后者干脆只让一小部分权重参与训练。

2.1 权重里到底存了什么

一个常见的误解是"知识存在某一层"。实际上,预训练模型学到的知识是分散在大量权重里的分布式表示,没有哪个矩阵专门存"法律知识"、哪个矩阵专门存"医疗知识"。

这也解释了为什么灾难性遗忘难以避免:新任务的梯度更新不区分权重的重要性,只要某个权重对降低新任务损失有用,它就会被改,而它可能恰好也是旧任务依赖的。权重是共享的,知识就是耦合的。

2.2 训练数据到底是什么

训练数据是外部样本(文本、问答对、图片标签),不进模型结构;参数(权重)是模型内部那堆数字,是训练后沉淀下来的"学到的知识"。训练就是拿数据反复前向、算损失、反向传播,把梯度作用到参数上——数据是输入,参数是输出,数据用完可丢,参数会留下。

# 数据 → 梯度 → 参数forx,yindataloader:# x, y 是训练数据(样本)loss=criterion(model(x),y)# 数据喂进模型,算出误差loss.backward()# 误差反传,得到每个参数的梯度optimizer.step()# 梯度更新参数(权重)# 训练结束后:数据可以不再保存,参数被固化下来

这也解释了旧数据丢了为什么麻烦——参数里只留下旧数据训练后的结果,原始样本没了就没法再算梯度、做回放,只能从参数层面保护,这正是 EWC 和 LoRA 要解决的问题。

至于"自己存一份旧数据",真实工程里往往卡在几道坎上:数据不归你(合同常写明"仅用于本次训练、不得留存")、隐私合规(医疗金融法律样本脱敏授权审计成本比训练还高)、流式一次性(线上日志训完即弃)、存了也未必能用(分布会变,全量回放可能比重训还贵)、跨团队跨版本(数据在别的部门或基座换了版本,对应关系已断)。

一句话:能存最好,存不了才是常态——方案要按"旧数据拿不到"这个最坏情况来设计。

3. 先搞清楚:持续学习到底在问什么

持续学习到底怎么判断哪些参数能改,哪些必须冻结,旧知识存在哪一层?

很多人的第一反应是:把新旧数据混在一起训练就行了。但真实情况要细得多。

  • 全参微调时,新任务的梯度会覆盖旧任务学到的权重,模型容量被新任务占满,旧任务的表现断崖式下跌。
  • 混旧数据时,旧数据可能已经拿不到了;比例调不好,还会两头不讨好。

所以在默认全参数微调、无参数保护的场景下,答案就是不能。

但面试时如果只答"不能",其实只答对了一半。
更完整的回答是分情况:
默认全参微调下,新任务会覆盖旧任务的参数,旧知识发生灾难性遗忘,这条路走不通;可一旦引入弹性权重固化 + 参数隔离 + LoRA 适配器这套配置,答案就变成"可以"——EWC 负责保护对旧任务重要的参数,LoRA 则为每个任务提供独立的适配器,两者配合,新任务学得进、旧任务忘不掉。

4. 灾难性遗忘的成因

一个模型要学习新任务,本质上是调参数让新任务的损失下降。默认规则下,全参微调对所有参数一视同仁,新任务的梯度更新不区分哪些参数对旧任务重要。

旧任务的知识分散在大量参数里,新任务训练时这些参数被大幅修改,旧知识被覆盖,这就是常见的知识遗忘。

更麻烦的是,旧任务的训练数据因为隐私或者存储原因已经不可用,没法做数据回放(Replay)。数据回放这条路走不通,就只能从参数层面想办法。

4. 方案一:弹性权重固化(EWC)

持续学习提供了多个层面的方案,其中一个叫弹性权重微调,也就是 EWC(Elastic Weight Consolidation)。

EWC 的核心思路是Fisher 信息:先估计每个参数对旧任务的重要性,重要性越高的参数,在学新任务时越要"拉住"它,不让它被大幅改动。

这样新任务仍然能学,但不会把旧任务的关键权重冲掉。代价是需要额外计算 Fisher 信息矩阵,并且要为每个旧任务保存一份重要性权重,任务一多,存储和计算开销都会上升。

4.1 PyTorch 实现:Fisher 信息 + 惩罚项

光讲思路不够,面试官很可能让你"写两行看看"。下面这段代码把 EWC 的三个关键动作都覆盖了:算 Fisher 信息、存重要性权重、在 loss 里加惩罚项。

importtorchimporttorch.nnasnnfromcopyimportdeepcopydefcompute_fisher(model,dataloader,criterion,device="cuda"):"""在旧任务数据上估计每个参数的 Fisher 信息(对角近似)"""fisher={n:torch.zeros_like(p)forn,pinmodel.named_parameters()}model.eval()forx,yindataloader:x,y=x.to(device),y.to(device)model.zero_grad()loss=criterion(model(x),y)loss.backward()# 只求梯度,不更新参数forn,pinmodel.named_parameters():ifp.gradisnotNone:fisher[n]+=p.grad.detach()**2# Fisher ≈ 梯度的平方,逐样本累加forninfisher:fisher[n]/=len(dataloader)# 取平均,得到参数重要性returnfisherdefsave_ewc_state(model,fisher,path="ewc_state.pt"):"""保存旧任务参数快照 + 重要性权重,供后续任务使用"""torch.save({"params":{n:p.detach().cpu()forn,pinmodel.named_parameters()},"fisher":{n:f.cpu()forn,finfisher.items()},},path)defewc_loss(model,criterion,outputs,targets,ewc_state,lam=1000.0):"""新任务 loss + EWC 惩罚项:λ/2 * Σ F_i * (θ_i - θ*_i)^2"""loss=criterion(outputs,targets)penalty=0.0forn,pinmodel.named_parameters():ifnnotinewc_state["fisher"]:continuef=ewc_state["fisher"][n].to(p.device)# 旧任务对该参数的重要性theta_star=ewc_state["params"][n].to(p.device)# 旧任务收敛时的参数值penalty+=(f*(p-theta_star)**2).sum()# 重要性越高,偏离惩罚越重returnloss+lam/2.0*penalty,loss,penalty

几个容易踩坑的点:

  • Fisher 要在旧任务数据上算,算完立刻save_ewc_state,因为旧数据后面可能就拿不到了;
  • lam是弹簧硬度:调大更护旧知识、但新任务学得慢,调小则相反,通常从 100~10000 之间试;
  • 对角近似只存每个参数一个标量,显存友好,这也是工程上最常用的做法;
  • 多任务累积时,把每个旧任务的fisher和params都存一份,惩罚项逐任务累加。

5. 方案二:参数隔离 + 低秩适配(LoRA)

另一个思路是参数隔离,典型代表就是 LoRA(Low-Rank Adaptation)。

LoRA 的做法是:冻结预训练模型的大量权重矩阵,只引入低秩矩阵作为适配器参与训练。不同任务各自训练一套适配器,参数天然隔离,互不干扰。

5.1 多科室医疗问答系统

在多科室医疗问答系统里,先上线了神经内科模型,最后上线儿科。如果每次都用全参数微调,后面科室会覆盖前面科室。

用 LoRA 微调,每个科室一个适配器,推理时根据科室路由到对应的适配器,互不干扰。

5.1.1 代码示例:按科室路由到对应 LoRA 适配器

思路很直接:主干模型只加载一次并全程冻结,每个科室各自训练一套 LoRA 权重,推理时按科室把对应的适配器挂上去。下面用 PEFT 演示适配器的加载与切换。

importtorchfromtransformersimportAutoModelForCausalLM,AutoTokenizerfrompeftimportPeftModel BASE_MODEL="Qwen/Qwen2.5-7B-Instruct"ADAPTER_DIR={"neurology":"./lora/neurology",# 神经内科适配器"pediatrics":"./lora/pediatrics",# 儿科适配器"cardiology":"./lora/cardiology",# 心内科适配器}tokenizer=AutoTokenizer.from_pretrained(BASE_MODEL)# 1. 主干只加载一次,并冻结全部参数(LoRA 训练时主干本就不更新)base_model=AutoModelForCausalLM.from_pretrained(BASE_MODEL,torch_dtype=torch.bfloat16,device_map="auto")forpinbase_model.parameters():p.requires_grad=False# 2. 把第一个适配器挂到主干上,得到一个可切换的 PeftModelfirst_dept=next(iter(ADAPTER_DIR))model=PeftModel.from_pretrained(base_model,ADAPTER_DIR[first_dept],adapter_name=first_dept)# 3. 其余科室的适配器一次性加载进来,但同一时刻只有一个处于激活状态fordept,pathinADAPTER_DIR.items():ifdept==first_dept:continuemodel.load_adapter(path,adapter_name=dept)# 只加载权重,不激活model.eval()defroute_inference(dept:str,question:str,max_new_tokens:int=256)->str:"""按科室路由:激活对应适配器 → 推理 → 其他适配器自动失效"""ifdeptnotinADAPTER_DIR:raiseValueError(f"未知科室:{dept}")# 关键一步:切换激活的适配器,主干权重不动,只换低秩分支model.set_adapter(dept)messages=[{"role":"user","content":question}]inputs=tokenizer.apply_chat_template(messages,add_generation_prompt=True,return_tensors="pt").to(model.device)withtorch.no_grad():outputs=model.generate(inputs,max_new_tokens=max_new_tokens,do_sample=False)# 只解码新生成的部分,跳过输入 promptreturntokenizer.decode(outputs[0][inputs.shape[-1]:],skip_special_tokens=True)# 4. 同一个进程里连续服务不同科室,互不干扰print(route_inference("neurology","患者突发右侧肢体无力伴言语不清,考虑什么?"))print(route_inference("pediatrics","3 岁儿童发热 39℃ 两天,如何处理?"))

几个工程上必须注意的点:

  • 主干只加载一份:所有科室共享同一个基座模型,显存占用约等于「一个基座 + N 份小适配器」,而不是 N 个完整模型;
  • set_adapter是切换开关:它只改变前向时走哪套低秩分支,不会改动主干权重,所以科室之间天然零干扰;
  • 适配器要按科室命名并做版本管理:adapter_name建议直接用业务标识,方便灰度、回滚和 A/B 对比;
  • 路由层要能兜底:科室识别不出来时,走通用适配器或直接回退到基座模型,别让请求直接报错;
  • 并发场景下别共享可变状态:多线程/多请求同时推理时,set_adapter是全局状态,建议按适配器做模型实例池,或改用支持 per-request adapter 的推理框架(如 vLLM 的 multi-LoRA)。

5.2 其他典型场景

  • 推荐系统:用户喜好不断变化,不同阶段、不同人群可以挂不同适配器。
  • 企业知识库:新文档不断进来,按业务域拆分适配器,避免新知识冲掉旧知识。

6. 怎么判断一个持续学习方案好不好

面试里如果被追问"那你说说,怎么评价一个方案行不行",别只背名词,直接给四条判断标准,一条一条对着看:

  • 旧任务关键参数有没有做保护:看方案是否显式估计了参数对旧任务的重要性(比如 Fisher 信息),并对关键参数施加约束。没有这层保护,新任务的梯度就会无差别地冲刷旧权重。
  • 参数隔离有没有做到任务间零干扰:看不同任务的更新是否落在互不重叠的参数子空间上。LoRA 冻结主干、每个任务一套低秩适配器,天然满足;如果多个任务共用同一份可训练参数,就谈不上隔离。
  • 适配器有没有做冲突消解:任务一多,适配器之间会出现语义重叠甚至相互打架。要看有没有路由策略、合并策略或者冲突检测机制,而不是简单堆一堆适配器了事。
  • 线上分布偏移有没有做监控和回滚:模型上线后数据分布会漂移,旧任务的表现可能悄悄退化。要有指标监控、灰度发布和快速回滚能力,否则离线指标再好看,线上也可能翻车。
6.1 适配器冲突消解具体怎么做

"冲突消解"听起来抽象,落到工程上其实是四层动作,从粗到细依次是:路由、合并、检测、隔离。

第一层:路由(Route)——先保证"该用谁就用谁"

冲突的根源往往是"用错了适配器"。所以第一件事是把路由做准:

  • 显式路由:请求里带业务标识(科室、业务域、租户 ID),直接映射到adapter_name,最稳、最可控;
  • 隐式路由:请求里没有标识时,用一个轻量分类器(小模型或规则)判断意图,再选适配器;
  • 兜底路由:识别不出来就走通用适配器或直接回退基座模型,绝不让请求报错。
# 路由层:显式标识优先,其次分类器,最后兜底defpick_adapter(request,classifier,known_adapters,fallback="general"):ifrequest.deptinknown_adapters:# 1. 显式标识returnrequest.dept pred=classifier(request.text)# 2. 意图分类ifpred.confidence>=0.8andpred.labelinknown_adapters:returnpred.labelreturnfallback# 3. 兜底

第二层:合并(Merge)——多个适配器都要用时怎么合

当一个请求同时涉及多个领域(比如"儿科用药对肝肾功能的影响"),需要同时激活多个适配器。常见做法:

  • 加权合并:把多个 LoRA 的B@A低秩增量按权重相加,W' = W + Σ w_i · B_i A_i,PEFT 里可用add_weighted_adapter实现;
  • 串行/交替:先走 A 适配器再走 B,适合有明确先后顺序的场景;
  • 任务算术(Task Arithmetic):把适配器当成"任务向量"做加减,用于合并同类任务、抵消冲突方向。
frompeftimportPeftModel# 把多个适配器按权重合并成一个组合适配器model.add_weighted_adapter(adapters=["pediatrics","pharmacy"],# 要合并的适配器weights=[0.7,0.3],# 权重:主领域高、辅领域低adapter_name="pediatrics_pharmacy",# 合并后的新名字combination_type="linear",# 线性加权合并)model.set_adapter("pediatrics_pharmacy")

第三层:检测(Detect)——怎么知道它们打架了

合并之前要先判断"能不能合",否则越合越差。可用的检测手段:

  • 输出一致性检测:同一批探针问题分别用 A、B、A+B 推理,比较答案差异,差异过大说明冲突;
  • 参数相似度:计算两个适配器低秩矩阵的余弦相似度,方向越接近越容易合并;
  • 回归测试集:每个适配器维护一组"能力探针"(golden set),合并后跑一遍,看各自领域指标掉没掉。
importtorchdefadapter_similarity(adapter_a,adapter_b):"""粗略衡量两个适配器的方向相似度,越接近 1 越容易合并"""sims=[]for(na,pa),(nb,pb)inzip(adapter_a.named_parameters(),adapter_b.named_parameters()):ifpa.shape!=pb.shape:continuea,b=pa.flatten().float(),pb.flatten().float()sims.append(torch.cosine_similarity(a,b,dim=0).item())returnsum(sims)/len(sims)ifsimselse0.0

第四层:隔离(Isolate)——实在合不了就分开

如果两个适配器冲突严重(比如法律和医疗对同一表述的倾向完全相反),最稳妥的做法是不合并:

  • 按请求维度拆实例,各自加载各自的适配器,物理隔离;
  • 用支持 per-request adapter 的推理框架(如 vLLM 的 multi-LoRA),一次前向只挂一个适配器;
  • 冲突无法调和时,宁可多训一个"融合适配器",也不要在推理时硬拼。

一句话总结:冲突消解不是某一个算法,而是一套流程——先用路由保证选对,再用合并处理多领域,用检测判断能不能合,最后用隔离兜住合不了的情况。面试时能把这四层讲清楚,比只答"用路由"要扎实得多。

这四条其实对应了持续学习的两个核心问题:参数更新边界(前两条)和工程可控性(后两条)。能同时答上来的,才算真正理解了持续学习,而不只是记住了 EWC 和 LoRA 两个缩写。

7. 工程落地:为什么实际工作几乎不做全参微调

实际工作当中,全参数微调基本不会做,因为很大概率会遇到灾难性遗忘问题。这很恐怖,很有可能一个项目被拖了再拖,最后黄了。

所以都会选择局部微调,比如 LoRA 和 Adapter。

模型微调基本集中在这些场景:

  • 文本提取
  • 短文本生成
  • 报告格式
  • 语气基调

实际工作中,微调需要考虑成本和效率的天平。所以 LoRA 几乎就是工作中微调大模型的性价比之王。

可以理解为:在保留预训练模型大量权重矩阵的同时,引入低秩矩阵作为适配器,这样就能做到有效分离。

8. 面试回答

得看情况。默认全参微调、不做任何保护,那不能——旧数据拿不到,比例也调不好。

因为全参微调时梯度对所有参数一视同仁,不区分哪个对旧任务重要,新任务一训就把旧权重冲了,这就是灾难性遗忘。

但持续学习有两条路:EWC 用 Fisher 信息找出关键参数,加惩罚项拉住它;LoRA 干脆做隔离,冻结主干,每个任务一套适配器,推理时按任务路由,互不干扰。两个还能组合,EWC 保参数、LoRA 隔任务,就能学新的不忘旧的。

工程上基本不做全参微调,风险太大,都是局部微调,LoRA 性价比最高。

判断方案行不行,我就看四点:关键参数有没有保护、参数隔离是不是真零干扰、适配器有没有冲突消解、线上分布偏移有没有监控和回滚。

追问细节时,补一张对比表兜底:

方案核心思想优点代价
数据混合新旧数据一起训实现简单旧数据可能不可得,比例难调
EWCFisher 信息约束关键参数单模型承载多任务需存重要性权重,开销随任务增长
LoRA / Adapter参数隔离,低秩适配互不干扰,成本低需管理多套适配器与路由

一句话收尾:面试不是回答"能不能",而是分情况——默认全参微调会让新任务覆盖旧参数、造成灾难性遗忘;而 EWC 保护关键参数、LoRA 为每个任务提供独立适配器,这套组合配置就能做到持续学习新任务而不忘旧的。判断一个方案好不好,就看四点:关键参数有没有保护、参数隔离有没有做到零干扰、适配器有没有冲突消解、线上分布偏移有没有监控和回滚。

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

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

立即咨询