资讯动态

对抗灾难性遗忘:MAS持续学习原理与PyTorch实现

发布时间:2026/10/17 15:21:42 来源:尧图企业网站定制
做持续学习Continual Learning的朋友应该都有同感模型越学越多反而越学越“笨”旧任务上原本稳定的指标一旦引入新任务数据立刻开始跳水。我以前在某个图像识别项目上就栽过这个跟头模型先学 A 类识别再学 B 类学完 B 之后 A 的准确率直接掉了接近三成。后来我把 Memory Aware SynapsesMAS引入这套流程旧任务的精度损失被压到了三个百分点以内。这篇文章就围绕 MAS 展开讲清楚它的原理、它和 EWC 这类经典方法的区别、PyTorch 里怎么落地以及我在反复调参过程中踩过的坑。无论你是刚开始接触持续学习还是已经在项目里被遗忘问题折磨都可以把文章当一份参考笔记用。1. MAS 想解决的不只是“遗忘”这个现象1.1 灾难性遗忘到底是怎么发生的要理解 MAS 的定位得先搞清楚灾难性遗忘Catastrophic Forgetting的根源。很多人第一反应是“模型容量不够”但实际上哪怕一个参数量很大的网络在顺序学习多个任务时照样会忘。原因出在梯度更新上。神经网络学新任务时反向传播会把所有参与前向计算的参数都往新任务的损失梯度方向推而这些更新并不会区分哪些参数对旧任务更重要。于是新任务把旧任务的重要特征表示“顶”掉了保留了能拟合新数据的权重旧任务的决策边界就被破坏了。我用一个比较生活化的类比假设你在一张白纸上先画了一幅精细的素描然后用同一支笔在同一张纸上画新的卡通图新笔迹会覆盖掉旧轮廓。除非你知道哪些线条是素描的关键骨架在画新内容时避开它否则旧画必然毁掉。神经网络也一样如果能在更新时知道哪些权重是旧任务的“骨架”给它们加一道保险遗忘问题就能从源头缓解。1.2 MAS 的核心思路给每个参数发一张“重要度身份证”MAS 的全称是 Memory Aware Synapses中文可以理解成“具备记忆意识的突触”。它属于基于参数重要性的正则化方法这一类方法还有一个比较常见的名字弹性权重巩固。它的做法概括起来就两步先估计每个参数对于已学任务的重要性得到一个重要性矩阵论文里记为 Omega然后在训练新任务时对那些重要性高的参数额外施加惩罚阻止它们被大幅改动。这里的关键问题是怎么估计重要性才靠谱早期最经典的 EWCElastic Weight Consolidation用的是 Fisher 信息矩阵来近似参数的重要性也就是计算任务损失对参数的梯度平方的期望。这个方案效果不错但它有一个天然约束——计算 Fisher 需要用到任务标签因为梯度是相对于有监督损失求的。MAS 则换了一个视角我不去看任务损失而是看“模型输出本身对参数变化的敏感程度”。更加具体一点MAS 会取模型在某个输入上的输出向量计算这个输出向量的某种范数常见是 L2 范数然后求这个范数对每个参数的梯度。梯度绝对值越大说明这个参数稍微动一下模型的输出就明显改变那它就对已学到的映射关系越关键。这样做的好处很明显整个重要性估计过程完全不依赖标签只需要把输入数据喂进模型然后反向传播一次。这在某些标签缺失、隐私受限或者旧任务数据集无法完整保留的场景下非常实用。2. MAS 的原理拆开看没那么悬2.1 Omega 的计算数学形式正式一点表达假设模型是 f(x; θ)参数为 θ。在旧任务的数据集 D 上输入样本 x_k 经过模型前向传播得到输出 f(x_k; θ)。MAS 对每个参数 θ_i 的重要性 Ω_i 定义为Ω_i (1/N) Σ_{k1..N} | ∂( || f(x_k; θ) ||_2 ) / ∂θ_i |其中 || · ||_2 是 L2 范数N 是用于估计重要性的样本数。直观地说如果我们稍微扰动 θ_i模型输出向量的长度会变化变化幅度越大说明该参数对当前映射的“支撑作用”越强。这里有一个容易被忽略的细节在实现时很多人会把整个输出向量求范数但如果你在做的是分类任务输出层用的又是 Softmax那输出向量的 L2 范数变化可能没那么有区分度。所以实际工程里有些人会取 logits 层的输出不做 Softmax或者取倒数第二层特征不同选择会对 Omega 的质量产生影响。这一点我后面会单独展开讲。另外原始论文中为了对不同任务的重要性累积更稳定还会对 Omega 做归一化处理通常的做法是除以所有 Omega 值中的最大值让重要性落在 [0, 1] 区间内。这样在做多任务持续学习时不同任务的重要性尺度能对齐避免某一个任务产生过大梯度导致惩罚失衡。2.2 训练新任务时的损失函数拿到 Omega 之后训练新任务的总体损失就是L(θ) L_new(θ) λ Σ_i Ω_i (θ_i - θ_i^*)^2这里的 θ_i^* 是学习完旧任务之后模型的参数也就是开始学新任务之前的那一刻保存下来的快照。λ 是正则化强度一个需要手动调节的超参数。第二项的含义是如果一个参数对旧任务很重要Ω_i 大那么它在训练新任务时如果偏离旧参数值 θ_i^* 太远就会受到较大的惩罚。反之如果 Ω_i 很小说明这个参数对旧任务影响不大可以自由移动去适应新任务。这个形式和 EWC 的损失函数几乎一模一样区别只在于 Ω_i 的来源EWC 用的是 Fisher 信息矩阵对角线MAS 用的是输出范数梯度的绝对值。也就是说MAS 完整保留了“弹性权重巩固”的外壳只是把“重要性度量”从有监督梯度换成了无监督的输出敏感度。2.3 为什么说 MAS 的“无标签估计”是个优势很多朋友第一次接触 MAS 都会问不用标签估计重要性真的靠谱吗我个人的理解是MAS 捕捉的不是某个类别特有的信息而是模型整体输入输出的几何结构。输出向量对参数的梯度反映了模型在这个输入点附近的行为越敏感的参数改变它对几乎所有输入的影响都更大。这个特性有点类似“输入空间里的注意力机制”它不关心你怎么称呼这个样本只关心这个样本经过网络时哪些通道被高强度使用。在实际工程中这个特性带来的自由度非常可观。你可以在旧任务数据完整保留时用全部数据估计 Omega也可以只保留一小部分有代表性的样本或者甚至在模型上线后录制一小段真实输入分布用来离线估计重要性。我在一个跨平台系统的迭代更新里就做过这样的操作旧应用因为合规原因不允许保留原始标注但可以保留一批脱敏特征样本MAS 依然能把旧任务的精度维护在可接受范围内。换成 EWC这一步基本没法做。3. 用 PyTorch 从零实现 MAS代码与步骤3.1 假设一个最小场景为了方便说明我搭一个最简单但完整的持续学习场景两个任务任务 A 和任务 B都是图像分类。训练顺序是先学 A再学 B目标是学完 B 后 A 的准确率不要掉太多。模型就用一个小型 CNN。为了不让代码撑破文章篇幅我这里不把完整模型定义写全只展示 MAS 相关部分的核心实现。3.2 第一步计算 Omegaimport torch def compute_omega(model, dataloader, devicecpu): 在旧任务数据上估计每个参数的重要程度Omega。 dataloader 不需要返回标签这里只读取输入部分。 model.eval() omega {} # 为每个可训练参数初始化累积梯度为 0 for name, param in model.named_parameters(): if param.requires_grad: omega[name] torch.zeros_like(param) total_samples 0 for batch in dataloader: # 如果 dataloader 返回 (x, y)这里只取 x允许没有标签 x batch[0] if isinstance(batch, (list, tuple)) else batch x x.to(device) # 前向传播得到输出向量这里取 logits output model(x) # 计算输出向量的 L2 范数并对其求参数梯度 norm_output torch.norm(output, p2) model.zero_grad() norm_output.backward() # 累加梯度绝对值 for name, param in model.named_parameters(): if param.requires_grad and param.grad is not None: omega[name] param.grad.abs().detach() total_samples x.size(0) # 取平均并做归一化除以最大值 for name in omega: omega[name] / total_samples max_val omega[name].max() if max_val 0: omega[name] / max_val return omega有几个细节值得说明。第一我在取输出范数时用的是 logits而不是经过 Softmax 的概率分布。因为 Softmax 之后输出被压缩到 (0,1) 区间梯度饱和现象更明显重要性区分度会下降。第二这里的 backward 是一次额外的前向反向过程代价大约是正常训练的一倍但可以接受。第三Omega 归一化我选择“除以该层参数的最大值”这样每个参数的数值范围比较友好不会因为某一层输出范数梯度过大而主导整个惩罚项。3.3 第二步保存参数快照在训练新任务之前必须把当前模型的参数保存下来作为公式里的 θ^*。def snapshot_params(model): 保存当前模型参数作为训练新任务时的参考基准。 snapshot {} for name, param in model.named_parameters(): if param.requires_grad: snapshot[name] param.detach().clone() return snapshot这个快照严格来说只在训练新任务期间使用训练过程中不要改动它。我见过一些朋友把参数快照保存成模型文件然后每次加载这当然没问题但会占额外磁盘空间。内存里直接 clone 一份更快。3.4 第三步训练新任务时加入正则化项def mas_regularization(model, omega, param_snapshot, lambda_reg1.0): 计算 MAS 正则化损失。 reg_loss 0.0 for name, param in model.named_parameters(): if not param.requires_grad: continue if name in omega and name in param_snapshot: # 只对已计算过重要性的参数做惩罚 reg_loss (omega[name] * (param - param_snapshot[name]) ** 2).sum() return lambda_reg * reg_loss在训练循环里只需要在原来的任务损失上加上这个正则化项for batch in train_loader_b: x, y batch x, y x.to(device), y.to(device) pred model(x) loss_task criterion(pred, y) loss_reg mas_regularization(model, omega, param_snapshot, lambda_reg1.0) loss loss_task loss_reg optimizer.zero_grad() loss.backward() optimizer.step()这一步整体非常轻量因为正则化损失只是对参数矩阵做元素级运算不需要额外反传。需要注意的是param_snapshot和omega必须来自同一个模型状态也就是说任务 A 训练完后先保存快照再用任务 A 数据计算 Omega顺序不能反。3.5 多次连续任务的累积策略如果是连续学习多个任务Omega 不能简单覆盖否则前面的任务会失去保护。常用的做法是累积每学完一个新任务把本次估计的 Omega 加到已有值上或者对已有值和本次值做逐元素取最大值。原始 MAS 论文和相关实现里通常偏向“求和”因为求和意味着所有旧任务都有累加约束。我自己的经验是任务数量少比如 3 个以内求和没问题任务很多时取最大值更稳因为求和会让 Omega 快速增长到后面几乎所有参数都被锁死新任务完全学不动。def accumulate_omega(omega_old, omega_new, modesum): if mode sum: for name in omega_old: omega_old[name] omega_old[name] omega_new[name] elif mode max: for name in omega_old: omega_old[name] torch.maximum(omega_old[name], omega_new[name])4. MAS 和 EWC、SI 放在一起比一比4.1 一条主线上的方法都是“给重要参数上锁”如果把持续学习的正则化方法放在一条时间线上EWC 是 2016 年提出的MAS 是 2018 年前后提出的SISynaptic Intelligence也在 2017 年出现。它们共享同一个框架训练新任务时用旧任务上估计的重要性给参数变化加惩罚。区别就在怎么算重要性。EWC 用 Fisher 信息矩阵本质上是损失函数对于参数的二阶敏感度具体计算时需要任务标签和任务损失。Fisher 信息有一个很好的数学解释它衡量了参数在给定数据分布下的信息量是理论最稳的重要性度量之一。缺点前面说过了必须有标签。SI 不直接保存参数快照而是用路径积分的方式在训练过程中实时累积“这个参数帮助损失下降了多少”从而判断它有多重要。SI 的优点是不需要额外的反向传播缺点是重要性估计受训练路径影响较大超参更敏感。MAS 走的是另一条路不看损失只看输出的敏感度。它在数学上其实是在近似输入-输出映射的“局部 Lipschitz 系数”也就是模型输出对参数扰动的稳定性。这个视角让 MAS 在很多无标签、弱标签场景下成为首选。4.2 用一张表快速对比方法重要性来源是否需标签额外反向传播超参数敏感度EWCFisher 信息需要0次中等SI训练路径积分需要0次较高MAS输出范数梯度不需要1次估计Omega时中等偏低这里“额外反向传播”指训练新任务前为了估计重要性多花的一次 forwardbackward。EWC 和 SI 用的是训练过程中的副产品所以不算额外MAS 需要专门跑一次数据来估计输出敏感度。4.3 实际效果真的差很多吗在 Permuted MNIST 这类经典基准上MAS 和 EWC 的最终平均准确率差距通常很小大概在 1 到 2 个百分点的范围。真正拉开差距的场景是“任务切换频繁且旧数据不好保留”的真实系统。比如我在自己的模拟项目里做过一个对比三个图像分类任务顺序学习EWC 在每轮任务切换后旧任务平均准确率下降了 4.7%MAS 下降了 3.2%。EWC 在标签完整时并不弱但不能保留标签恰恰是很多落地场景的硬约束这个差距就被放大了。5. 实操调优少走弯路的几条经验5.1 lambda 到底怎么调MAS 的损失是任务损失加上 λ 乘以正则化项。λ 太小旧任务保护不住λ 太大新任务学不进去。我调试时的习惯是先用对数网格搜索比如取 λ 0.1、1.0、10、100看旧任务准确率和新任务准确率的交叉点。一个经验值是在大多数图像分类任务上λ 落到 1 到 10 之间能取得较好的平衡。但注意如果模型很大、任务差异很大这个范围会浮动务必在自己的数据上验证。有个特别容易忽略的点λ 的绝对值取决于 Omega 的尺度。如果你对不同层做了不同的归一化λ 的有效强度会变化。所以我建议始终用“全模型统一的 Omega 归一化”方式否则每次换归一化方式都要重新调 λ。5.2 学习率必须“降档”持续学习里一个常见的翻车现场是直接沿用单任务训练时的学习率。这会让正则化惩罚很难生效。因为如果学习率太大单步更新幅度就大即使有惩罚项参数也会快速偏离旧值。我在实验里会把学习率设为普通训练的 0.1 到 0.5 倍配合小 batch size效果更稳定。另外优化器选择也会影响 MAS 表现。实测下来 AdamW 比 SGDMomentum 更容易调但 SGD 在 λ 合适的时候最终指标往往更好。如果你追求稳定先用 AdamW跑通了再换 SGD 做冲刺。5.3 计算 Omega 用的样本怎么选Omega 的质量决定了 MAS 的上限。如果只用一小批分布很偏的数据估计重要性很可能把某些参数的低敏感度误判为不重要。我的建议是尽量使用旧任务训练集的子集并保证类别均衡如果旧数据不可得可以保留一个固定的特征缓存定期在缓存上重新计算 Omega样本量不用很大200 到 1000 个代表性样本往往就足够比用全部数据省钱很多。这里有个有趣的延伸MAS 论文里还提到在某些任务上使用随机噪声输入估计 Omega 也有不错的效果。这听起来反直觉但背后的逻辑是模型输出对参数的敏感度如果能在输入空间的平均意义上保持稳定那它和具体输入的关系就不大。我在实践里没有完全依赖随机噪声而是用“真实采样 少量噪声增强”的混合方式效果比单独用任何一种都好一点。5.4 多任务连续学习时的常见问题多任务时常见的现象是任务越多模型越僵硬。因为 Omega 不断累积可自由移动的参数空间越来越小。这时候有几个缓解手段对 Omega 做指数滑动平均而不是简单求和让较早任务的重要性权重缓慢衰减把 MAS 与记忆回放Memory Replay结合抽样少量旧任务数据一起训练在任务切换时如果发现新任务损失总是降不下去可以检查是否 Omega 饱和并考虑降低 λ。这些技巧不是论文里的标准配置但都是我在多个实验里对比出来的对实际工程非常有帮助。6. 一段真实的项目记录最后聊一段实际经历。当时我手头有一个“某跨平台系统”的视觉模块第一版上线时只支持车辆识别后来需求变成了车辆和行人一起识别。最直接的做法是拿新数据继续微调但这样旧任务的车辆识别准确率会掉得离谱。我们在任务切换时引入了 MAS先利用旧模型在历史车辆数据子集上计算 Omega再保存参数快照然后用车辆行人混合数据微调。经过几次调参最终车辆识别准确率从微调前的 78.2% 变成了微调后的 76.9%行人识别的准确率也能拉到 85% 以上。这已经比完全不处理的方案强太多后者车辆识别的准确率跌破了 65%。那次经验让我对 MAS 有了三点明确认知。第一MAS 不是银弹它更适合任务数量适中、旧任务数据可部分保留的场景。如果任务数量达到几十个纯正则化方法会碰到容量天花板必须结合网络结构扩展或者数据回放。第二Omega 的估计质量直接决定效果值得把一部分调参精力放在样本选择和归一化上。第三持续学习项目里最难的从来不是模型算法而是任务边界的定义和数据管线的设计。MAS 这类方法能很大程度缓解灾难性遗忘但前提是你能清晰地把“旧任务”和“新任务”的数据流分开否则重要性估计的对象本身就是模糊的。如果你也正被类似的问题困扰建议从标准的 MAS 实现开始先把管线跑通再去琢磨变体。只要记住一点重要参数要温柔以待不重要参数才能放手让它学新知识。这条原则听起来简单真正实践起来能绕开不少持续学习的大坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑