AI/ML 详解:持续学习与灾难性遗忘

Day 52 · 2026-07-09
面向:有编程经验的非 AI 方向工程师

灾难性遗忘 & 稳定性-可塑性权衡Catastrophic Forgetting

机制神经网络核心矛盾
一句话类比

神经网络的权重是一份全局共享的可变状态(shared mutable state)。学新任务 B 时做的是原地更新(in-place update)——直接覆写这份状态。没有版本隔离、没有事务,任务 A 依赖的那些权重被 B 的梯度悄悄改写,A 就灾难性地全忘了。这不是"记忆容量满了",而是并发写冲突(write-write conflict)没有隔离机制

它解决什么问题 + 工作机制

灾难性遗忘(catastrophic forgetting)指:网络顺序学多个任务时,学后面的会急剧抹掉前面学到的能力。McCloskey & Cohen 1989 年就发现了它。根因在于:任务 A 的知识分布式地编码在所有权重的一组特定取值里,而 SGD 学任务 B 时只优化 B 的损失——它不知道也不在乎哪些权重是 A 的"承重墙",只要能降低 B 的 loss 就往哪个方向推。

这引出持续学习的核心矛盾——稳定性-可塑性困境(stability-plasticity dilemma):权重太稳定(不许改)→ 学不进新任务(没有可塑性);权重太可塑(随便改)→ 一学新的就忘旧的。这跟你熟悉的系统权衡同构:一个只读副本永远一致但没法写入;一个人人可写的缓存吞吐高但一致性全无。持续学习就是要在这条谱系上找折中:

稳定性 ←──────────────────────→ 可塑性

冻结全部权重· EWC 正则· 经验重放· 参数隔离· 自由微调
记得牢但学不动                          学得快但全忘光

下面 3 个概念,就是这条谱系上三种主流对策:约束权重(正则)、复习旧数据(重放)、给每任务独立参数(隔离)

代码示例
# 用最小实验"看见"灾难性遗忘:先学任务A,再学任务B,回头测A
import torch, torch.nn as nn
net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 2))
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()

def make_task(seed):                       # 两个不同的二分类任务
    g = torch.Generator().manual_seed(seed)
    X = torch.randn(400, 20, generator=g)
    y = (X @ torch.randn(20, generator=g) > 0).long()
    return X, y

def train(X, y):
    for _ in range(200):
        opt.zero_grad(); loss_fn(net(X), y).backward(); opt.step()
def acc(X, y): return (net(X).argmax(1) == y).float().mean().item()

Xa, ya = make_task(1); Xb, yb = make_task(2)
train(Xa, ya); print("学完A,A准确率:", acc(Xa, ya))   # ~0.95
train(Xb, yb); print("再学B,A准确率:", acc(Xa, ya))   # 掉到 ~0.55 → 遗忘
常见误区 + 实践场景
误区:"遗忘是因为网络容量不够,参数量堆大就好了。"——错。灾难性遗忘在严重过参数化的大网络里照样发生:容量足够同时装下 A 和 B,但 SGD 没有理由去找那个"两个都兼顾"的解,它只沿着当前任务的梯度走。问题出在优化目标,不在容量。
📌 超级个体场景:你拿业务数据持续微调一个开源模型,跑第二批数据后发现它在第一批任务上退化了——这就是灾难性遗忘。理解它,你才知道该"混着训"还是"加约束",而不是盲目加大模型或调 lr。
Takeaway + 思考题
💡 灾难性遗忘的本质是"共享可变状态 + 无隔离的原地写",稳定性-可塑性是它逼出的核心权衡。
🤔 生物大脑边学新东西边保留旧记忆,几乎不"灾难性遗忘"。它靠什么隔离机制做到的?(提示:海马体 vs 新皮层的双系统)

弹性权重固化(EWC)Elastic Weight Consolidation

正则化派Fisher 信息
一句话类比

EWC 相当于给每个权重装一把软写锁(soft write-lock),锁的力度按"这个权重对旧任务有多重要"来定。像数据库里给热点行、承重索引加高优先级保护:不是禁止改,而是"你想改这些关键权重?可以,但代价很大"。改无关权重几乎免费,改承重权重要付高额惩罚——学习被弹性地拉回。

它解决什么问题 + 工作机制

重放需要存旧数据,隔离要额外参数。EWC(Kirkpatrick 等 2017)想做到零旧数据、零新增参数,纯靠约束权重防遗忘。关键问题是:怎么知道哪些权重是任务 A 的"承重墙"?答案是 Fisher 信息(Fisher information)——衡量"轻微扰动权重 i,任务 A 的输出会变多剧烈"。变化剧烈 = 这个权重承重 = 锁紧;变化几乎为零 = 无关紧要 = 放开。

学任务 B 时,EWC 在 B 的损失上加一个二次惩罚项

L(θ) = L_B(θ) + Σ_i (λ/2) · F_i · (θ_i − θ*_A,i)²

逐符号拆解:L_B(θ) 是新任务 B 的损失(要降低它);θ*_A,i 是学完 A 后权重 i 的取值("旧记忆锚点");F_i 是权重 i 的 Fisher 信息(重要度);λ 调节"多在乎不忘 A"。直觉:每个权重上挂一根弹簧,把它往"任务 A 时的老位置"θ*_A,i 拉;弹簧劲度就是 F_i·λ——承重权重的弹簧很硬(几乎拉不动),无关权重弹簧很软(自由让 B 用)。于是 B 只能在"不惊动 A 的方向"上优化。这正是稳定性-可塑性谱系上的一个显式旋钮:λ 越大越稳定、越小越可塑。

代码示例
# EWC 核心:学完A后估计 Fisher,学B时加二次惩罚
def estimate_fisher(net, X, y, loss_fn):
    fisher = {n: torch.zeros_like(p) for n, p in net.named_parameters()}
    for i in range(len(X)):
        net.zero_grad()
        loss_fn(net(X[i:i+1]), y[i:i+1]).backward()
        for n, p in net.named_parameters():
            fisher[n] += p.grad.pow(2) / len(X)   # 梯度平方 ≈ Fisher 对角
    return fisher

# 学完A后:star = 权重快照(θ*_A),fisher = 重要度
star = {n: p.clone().detach() for n, p in net.named_parameters()}
fisher = estimate_fisher(net, Xa, ya, loss_fn)

def ewc_penalty(net, lam=1000.):     # 学B时加进总loss
    return lam * sum((fisher[n] * (p - star[n]).pow(2)).sum()
                     for n, p in net.named_parameters())
# total = loss_fn(net(Xb), yb) + ewc_penalty(net)  → A 的准确率被保住
常见误区 + 实践场景
误区:"EWC 是完美方案,一劳永逸。"——不。EWC 的弱点是惩罚会累积、Fisher 是近似:任务一多,一堆二次约束把权重锁得越来越死,可塑性枯竭,学第 10 个任务时几乎动不了;且 Fisher 只取对角、忽略权重间相关性,估计有偏。诚实结论:任务数少、无法存旧数据时 EWC 划算;长序列任务上,重放通常更稳。
📌 决策辅助场景:把 λ 想成"保守度旋钮"。给模型注入新领域知识又怕它忘掉通用能力时,EWC 式思路(约束关键参数别乱动)比"无脑全参微调"更可控——这也是很多 LLM 持续预训练里"只动部分层/加正则"的直觉来源。
Takeaway + 思考题
💡 EWC = 给每个权重按"重要度(Fisher)"挂一根弹簧,把它拉回旧任务的取值,用二次惩罚换取零旧数据、零新参数。
🤔 Fisher 信息用"梯度平方"近似权重重要度。这和你判断"数据库哪张表是热点、动了会全站崩"的直觉,有什么共通之处?

经验重放 & 生成重放Experience / Generative Replay

重放派生成模型
一句话类比

重放就像数据库的 WAL(write-ahead log)重演:为了让新旧一致,把旧事务混进来一起重跑。经验重放存一小份旧样本,训练时新旧混着喂。生成重放(generative replay)更聪明——不存原始旧数据,而是训一个生成器去合成"假的旧数据",相当于用一个压缩快照生成器替代全量备份,兼顾隐私与存储。

它解决什么问题 + 工作机制

遗忘的根源是"学 B 时看不到 A 的数据"。最直接的解法:学 B 时顺便复习 A。经验重放(experience replay)留一个小 记忆缓冲区(memory buffer)存 A 的少量样本,每个 batch 混入若干旧样本,梯度就同时压低新旧任务的 loss,权重不会跑偏。简单粗暴但极其有效——它几乎是持续学习里最强的基线。

但存旧数据有两个硬约束:存储爆炸(任务无限增长)和隐私/合规(医疗、用户数据不能留档)。生成重放(Shin 等 2017,Deep Generative Replay)的解法优雅:训一对协作模型——生成器(generator)学会产出"像旧任务的数据",求解器(solver)是真正干活的分类/预测模型。学新任务时,生成器先吐出一批合成旧样本、并由旧求解器打上标签,再和新数据混合训练。灵感来自海马体在睡眠中"回放"经历以巩固记忆。流程:

生成器 G──合成旧样本──▶混合 batch◀──新任务数据──新数据


求解器 Solver 训练──▶更新 G(也学新分布)
↑ 下一任务时 G 已能同时生成"旧+新",滚动传承,无需存原始数据
代码示例
# 经验重放:最小、最强的基线。留一个缓冲区,混着训
import random
buffer = []                                  # 存 (x, y) 旧样本

def reservoir_add(buf, sample, cap=200):    # 蓄水池采样:等概率保留
    if len(buf) < cap: buf.append(sample)
    else:
        j = random.randint(0, len(buf))      # 随机替换,保持代表性
        if j < cap: buf[j] = sample

def train_with_replay(net, opt, loss_fn, Xb, yb, k=32):
    for i in range(200):
        xb, yb_ = Xb, yb                     # 当前任务 batch
        if buffer:                           # 混入旧样本一起算 loss
            old = random.sample(buffer, min(k, len(buffer)))
            ox = torch.stack([o[0] for o in old])
            oy = torch.tensor([o[1] for o in old])
            xb = torch.cat([xb, ox]); yb_ = torch.cat([yb_, oy])
        opt.zero_grad(); loss_fn(net(xb), yb_).backward(); opt.step()
# 学A时把样本 reservoir_add 进 buffer;学B时自动复习A → 遗忘大幅缓解
常见误区 + 实践场景
误区:"生成重放能完美复现旧数据,等价于全量重放。"——不。生成器本身也在持续学习,会逐代退化(生成质量漂移):它合成的"旧样本"越来越不像真的,误差在任务链上累积放大,长序列后旧任务照样崩。生成重放的效果被生成器的保真度封顶——旧任务越复杂(高清图像、长文本),越难指望它救场。
📌 超级个体场景:想让本地模型持续吸收新知识又不忘旧的,最省心的其实是经验重放——留一份有代表性的旧数据小样本混进新一轮训练即可,比复杂正则简单且更稳。这也解释了为什么 LLM 持续训练时业界普遍"新旧数据按比例混合"而非只喂新数据。
Takeaway + 思考题
💡 重放 = 学新时复习旧;生成重放用"生成器合成旧数据"换掉存储,代价是受生成保真度封顶。混合旧数据是持续学习最强也最朴素的基线。
🤔 生成重放靠"合成假的旧记忆"防遗忘。人脑的记忆本就是重构而非回放——每次回忆都在改写。这种"不精确的重放"是 bug 还是 feature?

参数隔离Parameter Isolation

隔离派PackNet / 渐进网络
一句话类比

参数隔离 = 分区/分片(partitioning / sharding)思路:既然共享可变状态会写冲突,那就给每个任务划一块专属参数,学完就把它设成只读(read-only)。别的任务用别的分片,物理上没有写冲突,旧任务被结构性地保护——就像微服务各自独立部署,互不覆盖。

它解决什么问题 + 工作机制

正则和重放都是"软"保护——旧知识仍可能被慢慢侵蚀。参数隔离给出"硬"保证:凡是分配给旧任务的权重,一律冻结不再更新,遗忘从根上被杜绝。两个代表思路:

① PackNet(Mallya & Lazebnik 2018)——固定容量里"打包"多任务。 大网络高度冗余,很多权重可裁剪而几乎不掉精度。做法:学完任务 A → 剪枝掉一批不重要权重 → 剩下的"A 权重"冻结锁死 → 用刚释放出的空闲权重去学任务 B → 再剪枝、再冻结……像在一块固定磁盘上不断"划新分区"给新任务,直到空间用尽。存储零增长,但任务数受容量硬上限约束

② 渐进网络(Progressive Networks,Rusu 等 2016)——每任务加一列新网络。 每来一个任务就新增一列(column)参数,旧列全部冻结;新列通过横向连接(lateral connections)读取旧列学到的特征(能正向迁移),但反过来改不动旧列。彻底零遗忘、且能复用旧知识——代价是参数随任务数线性膨胀,像每来一个任务就起一个新副本、只允许读旧副本。

PackNet(固定容量 · 剪枝腾位)
A 权重·冻结B 权重·冻结C 权重·空闲待用

渐进网络(每任务加一列 · 横向读旧)
列A·冻结─横向→列B·冻结─横向→列C·训练中
旧列只读、可被新列借用特征;新列的梯度进不了旧列
代码示例
# PackNet 风格:学完A后剪枝+冻结,把"关掉的"权重留给B
import torch

def prune_and_freeze(weight, keep=0.5):
    # 按幅度保留 top-keep 的权重作为"任务A的锁定分区"
    flat = weight.abs().flatten()
    thresh = flat.kthvalue(int(len(flat) * (1 - keep))).values
    taskA_mask = weight.abs() >= thresh    # True = 属于A、冻结
    weight.data *= taskA_mask                     # 剪掉不重要的(置0,留给B)
    return taskA_mask

def masked_grad_hook(mask):
    # 反向时把"A分区"的梯度清零 → A权重永不被B更新
    def hook(grad): return grad * (~mask)
    return hook

W = net[0].weight
maskA = prune_and_freeze(W, keep=0.5)   # 学完A调用
W.register_hook(masked_grad_hook(maskA))  # 此后学B,A分区被硬保护
# 训练B时:只有 ~50% 空闲权重会更新,A的准确率精确保持不变
常见误区 + 实践场景
误区:"参数隔离零遗忘,所以最好。"——它的零遗忘是有代价的:(1) 必须知道当前是哪个任务(推理时要选对分区/列),做不到"任务边界模糊"的场景;(2) PackNet 容量有硬上限,渐进网络参数线性膨胀;(3) 任务间只能单向迁移(新借旧),学好旧任务后来的新数据反哺不了它。它适合任务清晰、数量有限、要求旧任务绝对不退化的场景。
📌 个人项目场景:给不同客户/领域各训一套 LoRA 适配器(adapter),主干冻结、每个 adapter 是独立"分区"——这正是参数隔离在 LLM 时代的实用变体:零互相干扰、按需加载,用哪个任务挂哪个 adapter。
Takeaway + 思考题
💡 参数隔离用"分区 + 冻结只读"给旧任务硬保证:PackNet 在固定容量里剪枝腾位,渐进网络每任务加一列。零遗忘,代价是需知任务身份、容量/参数受限。
🤔 三派对策(正则/重放/隔离)恰好对应你熟悉的三种并发控制:加锁、重演日志、数据分片。哪一派最像"乐观锁",哪一派最像"悲观锁"?

深入资源Further Reading

深入思考Deep Questions

1. 正则(EWC)、重放、隔离三派,本质上都在"约束优化不要毁掉旧解"。若把它们统一看成同一件事的不同实现,那件事是什么?
共同内核是:在参数空间里,为旧任务的"好解区域"加保护,让新任务的优化只在"不破坏旧解"的子空间里进行。三派只是用不同手段刻画并保护那个子空间:EWC 用 Fisher 二次惩罚,近似地把旧解画成一个椭球,惩罚离开它;重放/GEM 用旧数据(真实或合成)采样出旧解约束——GEM 更直接,它要求新梯度和旧样本梯度的内积非负("别往增加旧 loss 的方向走");参数隔离最暴力,直接把旧解占用的维度冻结成常数、根本不进优化。所以它们在稳定性-可塑性谱系上的位置不同:隔离最稳(硬约束、零遗忘、可塑性受限),EWC 居中(软约束、可塑但会累积僵化),重放最灵活(数据驱动、效果强但要存储/生成)。理解这个统一视角,你选方案时就不是背 3 个 trick,而是问一句:"我的场景里,旧解的保护子空间该用什么代价来刻画?"——这和你在分布式系统里选一致性协议是同一种思维。
2. 有观点认为"大模型 + 混合数据全量重训"让持续学习成了伪问题——反正每次都从头训。这个论断成立吗?
部分成立,但有硬边界。成立的一面:当你能拿到全部历史数据、且算力预算允许,"周期性全量重训"(含新旧混合)确实是工业界最稳的做法,直接绕开了遗忘——这也是为什么很多产品级模型不搞花哨的持续学习算法,而是攒够数据就重训一版。经验重放的成功本质上印证了"混合数据"这条路。不成立的边界:(1) 数据不可留——隐私/合规/授权到期,旧数据物理消失,只能靠 EWC 或生成重放这类"无旧数据"方法;(2) 算力不允许——边缘设备、机器人要在部署中在线学习,没有"回炉重训"的机会;(3) 实时性——需要分钟级适应新分布(推荐、风控),等不起重训周期;(4) 成本——前沿大模型全量重训成本极高,为一点增量知识重训整个模型不经济。所以持续学习不是伪问题,而是被推向了"约束更苛刻"的场景:数据留不住、算力受限、要在线适应。对超级个体,本地小模型的持续微调恰恰落在这些边界内。
3. 灾难性遗忘在人脑里几乎不发生。生物大脑的哪些机制正好对应了本期的三派对策?这对 AI 有何启发?
惊人地一一对应。(1) 互补学习系统(海马体+新皮层)↔ 重放:海马体快速编码新经历,睡眠中反复"回放"给新皮层缓慢巩固——正是生成重放的直接灵感(Shin 等明说受海马体启发)。(2) 突触固化(consolidation)↔ EWC:重要突触通过分子机制变得"更难改写"——EWC 的 Fisher 惩罚就是它的数学化身,论文标题里"consolidation"正是这个词。(3) 功能分区/神经发生 ↔ 参数隔离:不同脑区处理不同功能——类似给新任务分配新容量。启发在于:大脑不靠单一机制,而是三者协同;当前 AI 多数方法只用一招,而最强的持续学习系统往往也是混合的(重放+正则)。更深一点:大脑的记忆是有损重构(回忆即改写),它主动遗忘不重要的——"聪明的遗忘"或许是特性而非缺陷,这是多数 CL 方法尚未认真对待的维度。
4. 若把"BigCat 与 AI 长期共事"看成一个持续学习系统,你(人)该扮演三派中的哪一派?
有趣的错位:今天的 AI 工具几乎没有真正的持续学习——每次对话无状态、权重不更新。"记忆"被外包到了系统外层(Day 8 的 context/memory 架构),而非模型内部。这意味着:在扮演"重放"派——通过共享 memory / 操作手册把"旧上下文"重新喂给无状态模型,就是人肉经验重放;厂商的"周期性重训新版本"则是全量重训。启发:(1) 别指望现在的模型"记住"你,关键记忆要结构化沉淀在外层(隔离派思维:每项目一份独立记忆分区);(2) 定期"复习"——重要偏好/结论周期性重新注入,否则新会话等于遗忘;(3) 警惕你自己的遗忘:过度外包,脑中的"承重权重"(核心判断力)若不加"EWC 式保护"(刻意练习、不外包核心思考),也会被日常便利悄悄侵蚀。
5. "稳定性-可塑性困境"能否被彻底解决,还是像 CAP 一样是本质权衡?
倾向于:本质权衡,但可被"抬高帕累托前沿"——类似 CAP 不可能同时满足,工程却能在给定分区容忍度下把 C、A 都做好。在固定容量、固定信息下,学新必然改写某些表示,改写就有损旧任务,这是信息论层面的张力,不可能凭空消除。但抬高前沿的手段很多:(1) 加容量(隔离派)——用参数换两者兼得,代价是规模;(2) 加信息(重放派)——保留旧数据把"顺序学习"变回"联合学习",代价是存储;(3) 更好的表示——若不同任务表示天然正交/稀疏(不共享权重),冲突就小,这是模块化/稀疏架构的希望;(4) 正向迁移——理想下学新甚至能改善旧任务(GEM 追求的),此时不再零和。务实答案:原理不可消除,但通过容量、信息、表示、迁移四条路,可压到"可接受"甚至"接近双赢"——与你对待 CAP 的成熟心态一致:不是消灭权衡,而是调到具体场景的最优点。