做持续学习(Continual Learning)的朋友应该都有同感,模型越学越多,反而越学越“笨”:旧任务上原本稳定的指标,一旦引入新任务数据,立刻开始跳水。我以前在某个图像识别项目上就栽过这个跟头,模型先学 A 类识别再学 B 类,学完 B 之后 A 的准确率直接掉了接近三成。后来我把 Memory Aware Synapses(MAS)引入这套流程,旧任务的精度损失被压到了三个百分点以内。这篇文章就围绕 MAS 展开,讲清楚它的原理、它和 EWC 这类经典方法的区别、PyTorch 里怎么落地,以及我在反复调参过程中踩过的坑。无论你是刚开始接触持续学习,还是已经在项目里被遗忘问题折磨,都可以把文章当一份参考笔记用。
1. MAS 想解决的,不只是“遗忘”这个现象
1.1 灾难性遗忘到底是怎么发生的?
要理解 MAS 的定位,得先搞清楚灾难性遗忘(Catastrophic Forgetting)的根源。很多人第一反应是“模型容量不够”,但实际上哪怕一个参数量很大的网络,在顺序学习多个任务时照样会忘。原因出在梯度更新上。神经网络学新任务时,反向传播会把所有参与前向计算的参数都往新任务的损失梯度方向推,而这些更新并不会区分哪些参数对旧任务更重要。于是,新任务把旧任务的重要特征表示“顶”掉了,保留了能拟合新数据的权重,旧任务的决策边界就被破坏了。
我用一个比较生活化的类比:假设你在一张白纸上先画了一幅精细的素描,然后用同一支笔在同一张纸上画新的卡通图,新笔迹会覆盖掉旧轮廓。除非你知道哪些线条是素描的关键骨架,在画新内容时避开它,否则旧画必然毁掉。神经网络也一样,如果能在更新时知道哪些权重是旧任务的“骨架”,给它们加一道保险,遗忘问题就能从源头缓解。
1.2 MAS 的核心思路:给每个参数发一张“重要度身份证”
MAS 的全称是 Memory Aware Synapses,中文可以理解成“具备记忆意识的突触”。它属于基于参数重要性的正则化方法,这一类方法还有一个比较常见的名字:弹性权重巩固。它的做法概括起来就两步:先估计每个参数对于已学任务的重要性,得到一个重要性矩阵(论文里记为 Omega),然后在训练新任务时,对那些重要性高的参数额外施加惩罚,阻止它们被大幅改动。
这里的关键问题是:怎么估计重要性才靠谱?早期最经典的 EWC(Elastic 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) Σ_{k=1..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 第一步:计算 Omega
import torch def compute_omega(model, dataloader, device="cpu"): """ 在旧任务数据上估计每个参数的重要程度(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, p=2) 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_reg=1.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_reg=1.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, mode="sum"): 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 年前后提出的,SI(Synaptic Intelligence)也在 2017 年出现。它们共享同一个框架:训练新任务时,用旧任务上估计的重要性给参数变化加惩罚。区别就在怎么算重要性。
EWC 用 Fisher 信息矩阵,本质上是损失函数对于参数的二阶敏感度,具体计算时需要任务标签和任务损失。Fisher 信息有一个很好的数学解释:它衡量了参数在给定数据分布下的信息量,是理论最稳的重要性度量之一。缺点前面说过了,必须有标签。
SI 不直接保存参数快照,而是用路径积分的方式,在训练过程中实时累积“这个参数帮助损失下降了多少”,从而判断它有多重要。SI 的优点是不需要额外的反向传播,缺点是重要性估计受训练路径影响较大,超参更敏感。
MAS 走的是另一条路:不看损失,只看输出的敏感度。它在数学上其实是在近似输入-输出映射的“局部 Lipschitz 系数”,也就是模型输出对参数扰动的稳定性。这个视角让 MAS 在很多无标签、弱标签场景下成为首选。
4.2 用一张表快速对比
| 方法 | 重要性来源 | 是否需标签 | 额外反向传播 | 超参数敏感度 |
|---|---|---|---|---|
| EWC | Fisher 信息 | 需要 | 0次 | 中等 |
| SI | 训练路径积分 | 需要 | 0次 | 较高 |
| MAS | 输出范数梯度 | 不需要 | 1次(估计Omega时) | 中等偏低 |
这里“额外反向传播”指训练新任务前为了估计重要性多花的一次 forward+backward。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 比 SGD+Momentum 更容易调,但 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 实现开始,先把管线跑通,再去琢磨变体。只要记住一点:重要参数要温柔以待,不重要参数才能放手让它学新知识。这条原则听起来简单,真正实践起来能绕开不少持续学习的大坑。