MAS:用参数重要性正则化缓解灾难性遗忘的PyTorch实践

发布时间:2026/10/16 11:01:21
MAS:用参数重要性正则化缓解灾难性遗忘的PyTorch实践 一个人工智能模型如果只学会了一个任务转头去学第二个任务往往会把第一个任务忘得干干净净。这种情况在业内叫灾难性遗忘是连续学习Continual Learning领域最核心、也最让人头疼的问题。今天要拆解的方法是 Memory Aware SynapsesMAS一篇思路清奇、实现简单、效果又很稳的经典老工作。它不需要保存旧数据、不需要额外网络、不需要任务专属结构只要计算每个参数对输出的重要性在后续训练里“重点保护”这些重要参数就能让旧知识尽量不被冲掉。这篇文章不打算只讲论文复述我会从连续学习的技术路线说起把 MAS 的原理掰开揉碎给出可直接操作的 PyTorch 实现细节、超参数调节经验以及我实际项目里踩过的坑。只要你在做增量学习、模型微调、多任务训练或者只是想让自己手里的模型别那么“脆”这篇文章都值得往下看。MAS 这种“按参数重要性保护知识”的思路到现在依然是很多新方法的底层组件搞懂它对理解后续的 EWC、SI、PackNet 也都有帮助。1. 先把“灾难性遗忘”这件事看明白1.1 为什么模型会“学了新的就忘旧的”不管是分类网络还是生成模型本质上都是在调整网络里的几百万、几千万个参数让输出尽可能匹配目标。当你训练数据来自任务 A 时梯度下降会沿着让任务 A 损失下降的方向更新参数。等切换到任务 B数据分布变了、损失形态变了继续用梯度下降更新参数网络就会倾向满足任务 B 的目标。问题在于任务 A 和任务 B 可能对同一批参数有不同甚至矛盾的更新方向于是那些本来承载着任务 A 信息的参数值就被改写任务 A 的能力自然就蒸发了。这个“改写”本质上是参数共享副作用除非你把每个任务完全隔离开否则无法彻底避免。很多人天真地觉得在训练任务 B 时把任务 A 的损失也一起算进去不就行了。想把这个思路落地首先你得存着任务 A 的数据这在很多真实场景下根本不现实旧数据可能涉及隐私、可能因为存储限制被清理、可能已经不存在了。就算数据还在每加一个新任务就要把历史所有数据重新训练一遍计算开销也会随时间线性膨胀这就不叫连续学习了那叫频繁的全量重训。MAS 这类正则化方法不存旧数据而是在任务 A 训练完以后用很小的代价计算一组参数重要性权重。之后学任务 B 时给重要的参数加上一个“锁”让它们尽量不变不重要的参数则可以自由更新。这个思路本质上是告诉优化器这里有块地雷你踩的时候轻一点。1.2 三条主流技术路线MAS 站在哪一边连续学习的方法大致可以分成三类参数隔离法每个任务使用独立的参数子网络。典型代表有 PackNet、Progressive Networks、Hard Attention。这类方法不容易遗忘但参数开销大而且要在网络里找“空闲参数”结构设计受限制。回放法显式保存一小部分旧样本或旧特征训练新任务时混着一起用。代表有 Experience Replay、A-GEM、LwF特征蒸馏也算半个回放。效果好但涉及存储、隐私、样本选择策略工程上没那么干净。正则化法不保存旧数据而是在损失函数里加一项约束限制参数更新的自由度。EWC 是最早出名的一个SI 是后续改进MAS 也是这一支里的经典。MAS 在正则化法里又有一点特殊很多正则化方法需要知道任务边界比如 EWC 在每个任务之后都要算一次 Fisher 信息矩阵然后基于新任务和旧任务构建惩罚项。MAS 的办法更简单你在任务 A 训练完以后只需要跑一次前向和反向传播对网络里的每个参数算一个“重要性分数”然后把这个分数积累下来。之后学任务 B 时在通常的损失函数后面加一个 L2 惩罚项惩罚内容就是“所有参数的变化量乘以重要性”。这个方法的好处是计算成本低、实现难度小而且不依赖任务之间的明确分界可以在线持续更新。我后来在好几个项目里测过作为 baselineMAS 的稳定性和可调性都相当不错。2. MAS 的核心思想与重要性权重2.1 从“哪些参数不能乱动”这个问题切入我记得第一次看 MAS 论文时最先记住的就是它的问题框架一个参数重要不重要不看它的值有多大也不看它被更新了多少次而是看改动它以后网络输出会不会发生剧烈变化。这个思路很直觉。网络最终的输出是决策的载体如果某个参数稍微一动输出就大幅改变说明这个参数承载了关键的模式信息。反过来如果参数怎么改输出都不怎么变说明它对当前任务贡献很小后面学新任务时尽管改就行。怎么量化“输出对参数的变化敏感程度”常规正则化方法 EWC 用的是 Fisher 信息矩阵本质是“损失对参数的二阶导近似”它回答的是改这个参数会让旧任务损失增加多少。MAS 的视角不同它不直接算损失而是算“网络输出的范数对参数的敏感程度”。具体来说给定输入 x网络输出记为 (F(x, \theta))那么参数 (\theta_i) 的重要性定义为[ \Omega_i \frac{1}{N} \sum_{k1}^{N} \left| \frac{\partial F(x_k, \theta)}{\partial \theta_i} \right| ]这里乘上了一个与输入有关的加权项实际实现时通常直接取输出向量对参数的梯度绝对值再求和。这种做法的潜台词是网络输出是任务能力的最直接体现只要输出不变说明旧任务的行为模式保留住了至于内部特征怎么重排、特征提取器怎么变化都无所谓。2.2 “经验”积累得到重要性权重的完整过程在具体实现 MAS 时重要性权重是通过一次前向、一次反向拿到的。分几步来看任务 A 正常训练训练结束以后固定住模型参数。随机抽取一批当前任务的输入样本可以来自任务 A 的训练集如果没有训练集用无标签数据也行这是 MAS 很友好的地方。把这些输入依次传入网络得到输出 (F(x))。大多数分类网络会输出一个 logits 向量我们取这个向量对网络参数的梯度。对每个参数将梯度张量的绝对值累加起来就得到这个参数在任务 A 上的重要性权重。注意这里没说用任务 A 的标签。MAS 计算重要性时不需要标签只要有输入数据就行。这一点在无标签数据、自监督场景下特别好用我是实际用过的总觉得在标签稀少的业务场景里MAS 这种不依赖标签的敏感性估计方式很有战斗力。为什么取“输出的范数对参数的梯度”通常比“损失对参数的梯度”更稳。因为损失函数往往带有特定任务的信息比如交叉熵会让梯度集中在最后一层 softmax 的输出变化上而中间层参数梯度被任务目标“淹没”了。MAS 直接对网络原始输出求梯度能更均衡地反映所有参数对表达空间的贡献。这也是 MAS 在不少迁移学习任务上比 EWC 表现更好的原因之一。2.3 和 EWC、SI 放一起对比MAS 的差异在哪EWC需要保存旧任务的 Fisher 信息本质是梯度平方的期望计算时用训练数据的标签而且需要走一遍完整数据来累计。Fisher 信息矩阵是对角的近似EWC 解释性更强但计算量更大对任务边界依赖强。SISynaptic Intelligence在训练过程中记录每个参数的历史路径和梯度贡献在线累积重要性。SI 不单独做一次“重要性计算”更偏在线模式MAS 则可以非常干净地在训练后一次性完成。MAS用输出对参数的梯度模长来衡量重要性不需要标签可以一次性算完也可以在线累积。三个方法放在一起MAS 最简单、最不依赖任务边界。我在实际项目中经常把 MAS 当作速度最快的 baseline主要是因为它的计算逻辑太简单了只需要一次反向传播重要性权重算完以后直接存成一个字典后面训练随时取用。相比 EWC 每次都要扫一遍旧任务数据算 FisherMAS 的工程成本低了一个量级。3. 代码实战给任意 PyTorch 模型接上 MAS3.1 网络准备与任务划分假设你现在要在一个分类任务序列上做连续学习比如先学 CIFAR-10 的前 5 类再学 CIFAR-10 的后 5 类然后是 CIFAR-100 的子集。没有特殊要求的网络结构简单 ConvNet 或者 ResNet 都能跑。下面是一段“把网络参数按名字收集好”的代码这里面的named_parameters()是关键后续算重要性时要按参数名对应而不是用parameters()的索引否则一旦模型结构有变化名字就乱了。import torch import torch.nn as nn def collect_params(model): params_dict {} for name, param in model.named_parameters(): if param.requires_grad: params_dict[name] param return params_dict任务序列用最简单的方式实现每个任务对应一个分类头。如果你的任务共享同一个num_classes那其实不用改网络结构直接按不同数据分布来分任务就行。3.2 核心重要性权重计算与在线更新训练完任务 A 后跑这样一段代码得到omega_dict也就是每个参数的重要性权重def compute_mas_importance(model, data_loader, devicecuda): model.train() omega_dict {} grad_dict {} for name, param in model.named_parameters(): if param.requires_grad: omega_dict[name] torch.zeros_like(param) grad_dict[name] 0.0 for batch in data_loader: if isinstance(batch, (list, tuple)): inputs batch[0] else: inputs batch inputs inputs.to(device) outputs model(inputs) # 取输出向量计算其范数 if isinstance(outputs, (list, tuple)): outputs outputs[0] loss torch.norm(outputs, p2) ** 2 model.zero_grad() loss.backward() for name, param in model.named_parameters(): if param.grad is not None: # 用梯度的一阶矩来近似注意这里梯度可能是均值后的 # 我们直接用 abs 累加效果最好 omega_dict[name] param.grad.abs().detach() * inputs.size(0) grad_dict[name] inputs.size(0) # 归一化 for name in omega_dict: if grad_dict[name] 0: omega_dict[name] omega_dict[name] / grad_dict[name] return omega_dict这段代码里有几个关键选择我单独说明一下。torch.norm(outputs, p2) ** 2这是对输出向量做 L2 范数平方。论文里用的是 L2 范数实际实现时平方与否会改变重要性数值的尺度。我自己的经验是取平方后梯度量级更平稳尤其是输出向量维度很高的时候直接取范数会让所有参数的梯度都偏小。outputs如果是 logits 向量那么它是整个 batch 所有样本的输出拼在一起形状是[batch_size, num_classes]。对outputs整体做范数意味着所有样本的梯度都混在一起实际上隐式地把 batch 内样本的贡献平均了。这种简化在 batch size 稳定时问题不大。归一化时用的是inputs.size(0)去做加权平均相当于每次梯度除以该 batch 的样本数。严格来说梯度的均值已经做了这一步这里再做一次会使得重要性量级跟 batch 大小无关减小了超参调优的难度。计算完之后可以立刻把重要性权重存成.pth文件下次训练新任务时直接加载。3.3 训练新任务时如何把 MAS 惩罚加进损失训练任务 B 时损失函数要改写成[ L L_{CE} \lambda \sum_i \Omega_i (\theta_i - \theta_i^A)^2 ]其中 (\theta_i^A) 是任务 A 训练完后的参数值。实现时不能直接改模型结构而是在 backward 之后手动叠加梯度或者更推荐直接在损失张量上做加法。下面这个代码片段是我用着最顺的写法def mas_loss(model, omega_dict, old_params_dict, lambda_reg100.0): reg_loss 0.0 for name, param in model.named_parameters(): if not param.requires_grad: continue if name not in omega_dict or name not in old_params_dict: continue # (theta - theta_A).pow(2) 与 omega 逐元素相乘再求和 reg_loss (omega_dict[name] * (param - old_params_dict[name]).pow(2)).sum() return lambda_reg * reg_loss然后在你的训练循环里这么用for batch_idx, (inputs, targets) in enumerate(train_loader_b): inputs, targets inputs.to(device), targets.to(device) outputs model(inputs) ce_loss criterion(outputs, targets) reg_loss mas_loss(model, omega_dict, old_params_dict, lambda_reg) loss ce_loss reg_loss optimizer.zero_grad() loss.backward() optimizer.step()有个细节值得注意old_params_dict必须在任务 A 训练完以后立刻深拷贝一份而且不能用param.clone()然后随手丢要存成 CPU 上的 tensor否则你后来把模型搬到 GPU 时会遇到设备不一致的问题。最好这样做old_params_dict {} for name, param in model.named_parameters(): old_params_dict[name] param.detach().cpu().clone()3.4 超参数 λ 怎么选MAS 的实践效果对lambda_reg非常敏感。如果这个值太小相当于没加保护旧任务照样忘光如果太大任务 B 根本学不进去因为所有参数都在原地锁死。最稳妥的办法是先用一个小验证集做任务 B 的快速测试分别试[10, 50, 100, 300, 1000]这几个量级。我个人的经验是lambda_reg的范围跟你的任务 B 数据量有强关系。如果任务 B 每类样本很少比如只有几十张那么主导损失很容易被 MAS 正则项压住导致学不到新东西这时反而要把lambda_reg调小。反过来任务 B 数据量大自己就能学到足够强的特征lambda_reg可以稍微大一些防止旧任务被冲太狠。初始值可以定 100然后观察两个指标任务 B 的验证集准确率不能比不用 MAS 时低太多任务 A 的验证集准确率不能掉得太快。这两个指标是跷跷板不可能同时完美你要按照业务需求把平衡点找出来。4. 项目实操中的踩坑记录与问题排查4.1 算重要性权重时到底取哪个输出很多第一次实现 MAS 的人会直接把model(inputs)当作outputs如果模型结构简单这没问题。但如果你用了带辅助输出的网络比如带中间监督的分类网络outputs往往是一个 tuple这时必须取主分类头对应的输出。我当时在某跨平台系统里用过一个带多个辅助头的网络忘了取主输出重要性权重算出来完全不准任务 A 的准确率在训练任务 B 后直接崩到接近随机排查了很久才发现是这个细节。解决方案也很简单在compute_mas_importance里加一段判断把 tuple 拆开if isinstance(outputs, (list, tuple)): outputs outputs[0]4.2 BN 层对梯度估计的干扰BatchNorm 层在训练模式和评估模式下行为差异非常大。训练模式时BN 会使用当前 batch 的均值和方差做归一化并且会更新 running 统计量。如果你在计算 MAS 重要性权重时把模型保持在train()状态那么每过一个 batchBN 的 running mean 和 running variance 都在变化这会影响后续任务的网络状态。我的做法是计算重要性权重之前把模型先切到eval()模式这样 BN 使用固定的 running 统计量输出对参数的梯度更能反映当前模型的真实状态。等到真正训练任务 B 时再切回train()。如果你用的是 LayerNorm 或者没有 BN 的网络这一步影响不大但保持统一更稳。还有一个更隐蔽的问题BN 层自身的weight和bias也是参数它们在 MAS 里也会被纳入named_parameters()。这本身没问题但 BN 参数的梯度经常很奇怪因为 BN 的 affine 变换很短小重要性权重通常很大。如果你发现lambda_reg加在哪里都不对试试把 BN 层的参数从重要性计算里排除掉只保留卷积层和全连接层。我在几个实验里这样做之后旧任务准确率稳定了不少。4.3 任务边界不明确时怎么在线更新标准的 MAS 是在每个任务结束后一次性计算重要性权重但有些项目场景下你根本不知道数据什么时候换了一批。比如一个推荐系统用户口味是逐渐漂移的没有明确的“任务 B 开始”这样的信号。这时可以用滑动窗口式的 MAS 更新。设定一个固定间隔每隔 K 个 batch 就当前模型计算一次新的重要性权重然后和旧的重要性权重做衰减合并decay 0.9 for name in omega_dict: omega_dict[name] decay * omega_dict[name] (1 - decay) * new_omega[name]这里的decay是人为设定的它控制着历史重要性和当前重要性的平衡。衰减越小模型越能适应新数据衰减越大模型越保守。通常我会选0.9~0.99之间具体数值可以用一个小规模离线模拟来确定。不过要提醒一句在线更新模式下 MAS 的稳定性比离线一次性计算要差一些因为窗口式的重要性权重更新可能带来震荡。如果你发现任务 A 的指标出现周期性波动优先降低decay值。4.4 常见问题速查表这个问题集是从各种连续学习任务里整理出来的碰到类似现象可以按表排查现象可能原因解决办法旧任务准确率骤降正则项权重 lambda 太小调大 lambda对比多个候选值新任务学不进去lambda 太大压住了梯度调小 lambda或增大新任务学习率重要性权重全是 NaN输出向量出现 inf 或 NaN检查输入归一化给输出加 clamp加了 BN 后效果差异大BN 层 running stats 在重要性计算时变化计算重要性时切 eval 模式旧任务稳定但新任务很差重要性权重没排除 BN 层将bn.weight、bn.bias排除 in compute权重投影不对齐参数名变了固定参数收集顺序统一用名字索引多个任务累积时保护不足只用了最后一个任务的重要性使用衰减合并保留历史累计权重4.5 实际项目的“避坑”补充说明有一类问题是结构性的很难靠调参解决。当任务 B 的数据分布和任务 A 差得特别远时比如一个学猫狗分类另一个学黑白街景分割参数共享本身就很牵强。MAS 只能做到“尽量别破坏旧参数”但不可能给模型凭空增加容量来容纳新知识。这个认知很重要不要指望一个固定容量的小网络通过 MAS 就能无限学下去。真到了那种场景要么加参数隔离要么用回放方法要么就把模型容量做大。另外值得一提的是MAS 的方法很依赖网络输出的连续变化如果网络是极端稀疏的比如某些剪枝后的网络大量参数为 0梯度在稀疏处不稳定MAS 重要性权重的计算也会不可靠。如果要在剪枝网络上做连续学习我建议先恢复稠密再算重要性。5. 用 MAS 搭建一个完整的连续学习实验5.1 实验配置和评估流程实话说只把 MAS 嵌入训练循环还不够你得有一整套实验流程这样才能确认 MAS 确实在起作用。下面是我在模拟项目 X 里用过的评估脚本结构# 流程示意 task_list [...] model YourModel() old_params_dict None omega_dict None for t, (task_name, train_loader, val_loader) in enumerate(task_list): # 训练当前任务 train_one_task(model, train_loader) # 如果是第一个任务初始化重要性权重 if t 0: omega_dict compute_mas_importance(model, train_loader) old_params_dict deep_copy_params(model) else: # 合并当前重要性到历史重要性 current_omega compute_mas_importance(model, train_loader) omega_dict merge_omega(omega_dict, current_omega, decay0.99) old_params_dict deep_copy_params(model) # 每学完一个任务评估所有历史任务 eval_all_tasks(model, task_list[: t1])这里的关键点是每训练完一个任务先计算当前任务的重要性再与历史重要性合并而不是只保留最后一次。5.2 我在实践中的经验评估口径比你想象中更重要连续学习的公开评估口径有很多种常见的是“对所有已学任务的平均准确率”。但如果你只汇报平均准确率会掩盖一个重要事实模型可能为了保住旧任务把新任务完全牺牲了。均衡的评估方式是同时记录旧任务遗忘率Backward TransferBWT和新任务学习能力Forward TransferFWT。有些论文对 BWT 的定义是“训练完所有任务后再测一遍旧任务”这个指标能反映多次推进后旧知识保持得如何。我自己做项目时只看两个数字所有任务训练完成后的平均准确率每个任务训练完新任务后的即时旧任务准确率后者更贴近真实业务因为生产中没人会等你把所有任务都训完再上线。5.3 和回放方法结合使用的更优解纯正则化方法在数据分布漂移比较温和时很好用但在剧烈漂移场景表现会明显弱于回放方法。我试过把 MAS 和一个小型样本回放池结合效果出乎意料地好。做法是在每个任务训练完后随机保留该任务 5% 的样本放到缓冲区里训练新任务时每批次混合 10% 的回放样本同时再加上 MAS 正则项。这种情况下 MAS 负责“参数级别”的保护回放样本负责“数据级别”的纠偏两个机制互补。比单独使用其中任何一个都稳定尤其适合任务类别跨度大的场景。代价是你要额外管理一个回放缓冲区但只存 5% 样本的话存储开销很小。写在最后的一点个人体会用了一阵子 MAS 以后我最明显的感受是这个方法的门槛低到让人觉得不像学术工作但它给你留出来的调参空间又非常宽。lambda_reg一改整套系统的性格就变了从“极度保守”到“极度激进”都能呈现。这种灵活性既是优点也是坑你必须为自己的场景定好一套评估脚本否则你根本不知道自己调到的是“更稳了”还是“更蠢了”。我自己平时做连续学习相关项目MAS 始终是我默认的第一个 baseline。不是因为它在所有 benchmark 上都是第一而是因为它足够简单、足够透明出了任何问题都能很快定位。想在工程里快速验证“参数保护”思路是否可行时写一个 MAS 出来往往只需要半小时这比上任何花哨框架都来得实际。如果你也是刚接触连续学习我建议先把 MAS 亲手复现一遍然后拿它和 EWC、SI 做对比感受一下“输出敏感度”和“损失敏感度”这两种视角的差别。等你把这几个经典方法都玩明白再看模型扩容、动态结构这些更高阶的手段时思路会清楚很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询