
先从结论说起持续学习Continual Learning解决的不是“把单任务模型训得更准”而是让模型在连续到达的任务流上持续更新同时尽量不破坏已经学到的旧任务能力。如果一个业务场景根本没有数据流模型一次训练上线后不再更新那持续学习暂时用不上但凡涉及“今天来了新类别、明天来了新领域、后天数据分布又变了”持续学习就应该被纳入选型范围。为什么要强调“in Transition”因为持续学习这个方向这几年正在发生明显转向评价场景从 Split MNIST 这类玩具基准转向大语言模型和视觉-语言模型上的持续微调学习方式从离线多任务串行训练转向在线、少样本、无任务边界的数据流研究对象也从纯粹的算法指标延展到模型部署、数据预算和系统稳定性。换句话说持续学习正从一个“论文指标方向”变成一个需要考虑工程可行性的落地方向。对大部分读者来说这篇文章能回答四个问题第一持续学习到底解决什么问题、什么时候才该用它第二当前主流方法分几类、各自的代价是什么第三怎么用 PyTorch 搭一个最小可跑的持续学习基线并正确评估第四真正复现和落地时最容易踩哪些坑。文中代码以教学验证为主直接粘到自己的实验目录里就可以改。1. 核心内容速览能力项说明核心问题灾难性遗忘Catastrophic Forgetting学习范式任务流/数据流上的增量学习不依赖静态 i.i.d. 数据三种经典设置Task-Incremental任务增量、Domain-Incremental领域增量、Class-Incremental类别增量方法主线经验重放Replay、参数正则化Regularization、动态架构Architecture常见评测数据集Split MNIST、Permuted MNIST、Split CIFAR-100、CORe50、TinyImageNet、DomainNet核心指标平均准确率ACC、后向迁移/遗忘BWT、前向迁移FWT主流实验框架Avalanche、Mammoth或基于 PyTorch 自写基线硬件门槛小模型实验 8G 左右显存可跑CL LLM 需要更高配置需按实际模型测试与普通微调的区别普通微调只看新任务精度持续学习还要量化旧任务精度保持情况当前趋势CL 大模型、CL 多模态、CL 智能体以及在线/无任务边界持续学习从这张表也能看出来持续学习不是一个“模型文件”而是一套训练协议和评估协议的组合。这也是为什么单纯下载一个开源模型无法解决问题真正要关注的是任务流怎么定义、数据预算怎么分配、指标怎么算。2. 适用场景与使用边界2.1 什么场景真的需要持续学习持续学习不是万能的但下面几类场景天然符合它的假设第一类是数据流持续到达的业务系统。典型的例子是推荐系统。用户的兴趣分布会随时间漂移新商品、新话题不断出现模型如果每隔一段时间全量重训一次训练成本很高而且重训数据里旧样本的时效性已经变差。持续学习可以按天或按小时增量更新模型减少全量重训频率。第二类是类别持续扩展的识别系统。视觉质检、安防识别、医疗影像筛查这类业务中新类别会陆续被标注出来。如果每次增加一个类别都重新训练整个模型标注成本和计算成本都不可控。类增量设置Class-Incremental就是为这种场景设计的。第三类是个性化设备端学习。手机、机器人、边缘设备上的模型无法把全部历史数据保存在本地但可以保存一个小型重放缓冲在设备端持续适配用户习惯。这种场景对隐私敏感不能把数据上传到中心服务器持续学习提供了一种“本地增量更新”的替代方案。第四类是对齐大模型知识时效性。大语言模型的持续预训练、持续指令微调已经变成研究热点。每过一段时间就有新的文档、新的对话数据、新的工具调用格式需要注入模型。直接全量微调成本太高只在新数据上继续微调又会出现灾难性遗忘因此需要持续学习或基于 PEFT 的持续微调方案。2.2 什么场景不适合用持续学习如果业务本身是静态数据集训练模型训练完直接上线之后再无新数据那持续学习没有任何增益常规训练流程更简单、更稳定。如果数据分布变化非常大旧任务和新任务几乎没有共享结构持续学习也很难同时保住“高可塑性”和“高稳定性”。比如前一个任务学猫狗图像后一个任务直接变成雷达波形分类再好的持续学习算法也保存不了太多旧能力。如果旧数据因为合规要求必须删除且任务又与旧数据强相关那基于重放的持续学习方法就不合规。这时候要么选无数据重放的架构类或正则化方法要么在算法设计上彻底避免存储原始样本。2.3 使用边界与合规提醒持续学习涉及数据保存、任务流划分、模型更新因此有几点必须提前确认重放缓冲中保存的旧样本是否有版权和隐私限制。涉及人脸、声音、医疗、儿童数据时需要先确认授权和保留期限。增量更新的模型是否用于生产环境。发布前要做效果复核尤其是旧任务精度不能低于可接受阈值。不要在未获授权的数据上进行去标识化、伪造或识别类实验。如果项目中使用开源数据集如 CORe50、DomainNet 等需要保留数据许可证信息并遵守引用要求。3. 持续学习的方法分类与理论基础3.1 为什么普通训练会遗忘普通监督学习假设训练集是独立同分布采样模型在同一份数据上来回迭代直到收敛。当数据变成流式输入后模型每到一个新任务梯度都在向新任务的目标方向更新旧任务的决策边界就会被覆盖。这种现象叫灾难性遗忘。遗忘的本质是参数共享冲突。神经网络的同一组权重同时承担旧任务和新任务的表示当优化目标切换到新任务时旧任务方向的梯度被新任务梯度取代。如果新任务数据量足够大旧任务的局部最优解会被破坏得比较彻底。3.2 三条主流技术路线当前持续学习算法大致分三类理解它们之后再选择会更容易。基于重放Replay的方法。这类方法在遇到新任务时从旧任务中保留一部分样本或生成样本与新任务数据混合在一起训练。最简单的形式是经验回放Experience Replay简称 ER直接维护一个固定大小的缓冲每次训练时从缓冲中采样一批旧样本和新样本组成同一个 batch。iCaRL 在回放基础上加入了类别均值特征和近邻分类A-GEM 则用投影梯度方式保证旧任务损失不上升。重放类方法实现简单、效果稳定是多数业务场景的首选基线。基于正则化Regularization的方法。这类方法不保存旧样本而是在损失函数中增加一个惩罚项限制对旧任务重要的参数变化。EWCElastic Weight Consolidation用 Fisher 信息矩阵估计每个参数对旧任务的重要程度重要参数尽可能不动。SISynaptic Intelligence则在训练过程中在线估算参数重要度。知识蒸馏类方法也属于这个方向LwFLearning without Forgetting用旧模型输出来约束新模型的输出分布避免旧任务特征被过度改写。正则化方法省内存但通常稳定性不如重放类方法。基于架构Architecture的方法。这类方法为不同任务分配不同子网络或掩码从结构上隔离任务冲突。PackNet 用剪枝思路把网络划分成多个子网络HAT 用 Hard Attention Mask 让不同任务各走不同的前向路径动态扩展网络则在新任务到来时增加新的分支。架构类方法在任务边界已知时效果很好但部署时子网络管理复杂不适合任务数量无限扩展的场景。三类方法不是互斥的。现代持续学习算法经常混用重放、蒸馏和参数隔离比如先用 PEFT 隔离一部分参数再配合回放缓冲稳定旧任务。3.3 任务边界与在线学习持续学习实验设计里有一个关键区别任务边界是否已知。Task-Incremental训练时知道当前属于哪个任务推理时也知道任务 ID模型可以使用多头输出头。这个问题相对简单。Domain-Incremental训练时知道任务 ID但推理时不知道模型必须用同一个输出头处理所有领域。Class-Incremental训练时知道任务 ID推理时不知道模型要在不断增长的类别集合上做分类。这是最难也最接近实际业务的设置。如果数据流是逐样本到达而非逐任务到达就进入 Online Continual Learning。在线设置下每个类别可能只出现几十个样本模型见一次就必须学一次。这种场景更依赖重放缓冲和快速适应算法。4. 实验环境准备与复现前置条件这里给一套通用环境准备清单适合本地模型研究与复现。如果你用的是学校或公司的 GPU 服务器流程相同只是路径不同。4.1 基础软件依赖操作系统Linux 优先Windows 也可以跑 PyTorch 小实验。Python推荐 3.10 或以上版本。深度学习框架PyTorch 2.xCPU 版本也能跑通 MNIST 级实验但 CIFAR 以上建议用 GPU。可选实验框架Avalanche、Mammoth。数据集存储提前准备目录存放 MNIST、CIFAR-100 等数据集后续拆分任务使用。4.2 创建环境命令conda create -n clab python3.10 -y conda activate clab # 安装 CPU 或 GPU 版 PyTorchGPU 版请到官网按 CUDA 版本选择命令 pip install torch torchvision # 如果使用 Avalanche可以安装完整版 pip install avalanche-lib # 可视化与实验数据记录 pip install matplotlib tensorboard安装完成后用一小段代码确认环境可用import torch print(torch.__version__) print(torch.cuda.is_available())如果torch.cuda.is_available()返回 False不代表不能做持续学习实验只代表所有训练会落到 CPU 上。Split MNIST 这类实验在 CPU 上几分钟就能跑完。4.3 目录结构建议推荐把实验代码、数据、结果分开管理避免后续批量实验时目录混乱。continual-learning-lab/ ├── data/ ├── models/ ├── methods/ │ ├── replay.py │ ├── ewc.py │ └── baseline.py ├── utils/ │ ├── metrics.py │ └── data_loader.py ├── configs/ │ └── exp1.yaml ├── outputs/ │ └── logs/ └── run_experiments.py这个结构不是必须照搬但建议至少分离数据、代码和输出。批量跑实验时输出目录会很快塞满日志和模型文件。5. 最小可跑的持续学习基线经验回放下面我从零写一个最小可跑的经验回放ER基线只用 PyTorch 基础 API不依赖 Avalanche。这样做的好处是代码完全可控方便你改成 EWC、LwF 等其它算法。5.1 准备任务流数据先定义一个按任务切分数据集的函数。以 Split MNIST 为例把 10 个数字按顺序切分成 5 个任务每个任务包含 2 个类别。import torch from torch.utils.data import DataLoader, TensorDataset from torchvision import datasets, transforms def split_mnist_by_task(root./data, n_tasks5, seed0): transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST(rootroot, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(rootroot, trainFalse, downloadTrue, transformtransform) classes_per_task 10 // n_tasks train_tasks, test_tasks [], [] for t in range(n_tasks): cls list(range(t * classes_per_task, (t 1) * classes_per_task)) train_idx [i for i, label in enumerate(train_set.targets) if label in cls] test_idx [i for i, label in enumerate(test_set.targets) if label in cls] train_sub TensorDataset(train_set.data[train_idx].float() / 255.0, train_set.targets[train_idx]) test_sub TensorDataset(test_set.data[test_idx].float() / 255.0, test_set.targets[test_idx]) train_tasks.append(DataLoader(train_sub, batch_size128, shuffleTrue)) test_tasks.append(DataLoader(test_sub, batch_size256)) return train_tasks, test_tasks5.2 定义简单 MLP 模型这里用两层 MLP方便 CPU 快速验证。如果你想替换成 ResNet只需要保证输入输出维度。import torch.nn as nn class SimpleMLP(nn.Module): def __init__(self, input_size784, hidden256, num_classes10): super().__init__() self.net nn.Sequential( nn.Flatten(), nn.Linear(input_size, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, num_classes), ) def forward(self, x): return self.net(x)5.3 实现重放缓冲区重放缓冲区只保留最老或最近的部分样本代码里用一个列表保存展平后的图像和标签。核心逻辑是当缓冲超过buffer_size时淘汰最老的样本。import random class ReplayBuffer: def __init__(self, buffer_size2000): self.buffer_size buffer_size self.samples [] def add(self, x, y): # x: [N, C, H, W] 或 [N, D] x x.detach().cpu() y y.detach().cpu() for i in range(x.size(0)): self.samples.append((x[i], y[i].item())) if len(self.samples) self.buffer_size: self.samples self.samples[-self.buffer_size:] def sample(self, batch_size): batch random.sample(self.samples, batch_size) xs torch.stack([s[0] for s in batch]) ys torch.tensor([s[1] for s in batch], dtypetorch.long) return xs, ys5.4 每个任务的训练循环持续学习训练循环与普通训练的区别在于每个任务内部可以正常迭代多轮但任务切换后旧任务数据不能再全量获取只能从重放缓冲区采样。import torch.optim as optim import torch.nn.functional as F def train_one_task(model, optimizer, task_loader, replay_buffer, device, replay_batch_size64): model.train() for x, y in task_loader: x, y x.to(device), y.to(device) if len(replay_buffer.samples) 0: rx, ry replay_buffer.sample(min(replay_batch_size, len(replay_buffer.samples))) rx, ry rx.to(device), ry.to(device) x torch.cat([x, rx], dim0) y torch.cat([y, ry], dim0) optimizer.zero_grad() logits model(x) loss F.cross_entropy(logits, y) loss.backward() optimizer.step()这里有一个细节如果新任务的类别和旧任务类别都放进同一个分类头训练类别增量场景下可能会出现分类头偏差。最直观的处理方式是在每个任务训练完之后用重放缓冲区重新校准分类头或者使用最近类均值分类器。这个后续在最佳实践里再展开。5.5 评估函数评估时对每个任务单独算准确率并记录到矩阵中。矩阵的第 i 行第 j 列表示训练完任务 i 之后在任务 j 测试集上的准确率。def evaluate_all(model, test_tasks, device): model.eval() accs [] with torch.no_grad(): for test_loader in test_tasks: correct 0 total 0 for x, y in test_loader: x, y x.to(device), y.to(device) logits model(x) pred logits.argmax(dim1) correct (pred y).sum().item() total y.size(0) accs.append(correct / total) return accs5.6 主循环这里串起五个任务每个任务训练 5 个 epoch。训练完每个任务后立刻评估并记录结果。def main(): device cuda if torch.cuda.is_available() else cpu train_tasks, test_tasks split_mnist_by_task(root./data) model SimpleMLP().to(device) replay_buffer ReplayBuffer(buffer_size2000) acc_matrix [] optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9) for t, task_loader in enumerate(train_tasks): for epoch in range(5): train_one_task(model, optimizer, task_loader, replay_buffer, device) # 训练结束后把当前任务数据加入缓冲 for x, y in task_loader: replay_buffer.add(x, y) accs evaluate_all(model, test_tasks, device) acc_matrix.append(accs) print(fTask {t 1} done: test accs {[round(a, 4) for a in accs]}) return torch.tensor(acc_matrix) if __name__ __main__: acc_matrix main()这个基线已经能把“只在新任务上训练”的遗忘现象明显压制住。你可以跑两遍一遍把 replay 部分去掉一遍保留 replay对比任务 2 结束之后旧任务准确率的差异。6. 评估协议准确率、遗忘率与迁移指标持续学习的评估不是只看“最后一个模型在所有任务上的平均准确率”还要看训练过程中的稳定性。先定义一个指标矩阵然后从矩阵推导三个常用指标。假设一共有 N 个任务acc_matrix[i][j]表示训练完任务 i 之后在任务 j 测试集上的准确率。6.1 平均准确率ACC平均准确率有两种常见算法。第一种是取对角线准确率的平均衡量每个任务刚学完时的表现diagonal_acc torch.diag(acc_matrix).mean().item()第二种是取训练完所有任务后模型在所有任务上的最终准确率也叫最终平均准确率final_acc acc_matrix[-1, :].mean().item()实际论文中ACC 通常指后者因为它反映了模型在任务流结束时保留了多少能力。6.2 后向迁移/遗忘BWTBWT 衡量模型学完后续任务后对之前任务准确率的影响。如果 BWT 为负说明发生了遗忘如果为正说明新任务帮助提升了旧任务表现这种情况称为正迁移。def calculate_bwt(acc_matrix): n acc_matrix.shape[0] bwt 0.0 for t in range(n - 1): bwt (acc_matrix[-1, t] - acc_matrix[t, t]) return bwt / (n - 1)6.3 前向迁移FWTFWT 衡量模型在尚未见过的任务上是否因为之前学习积累了更好的初始化。通常用训练任务 i 时在任务 jj i测试集上的准确率减去随机初始化模型在该任务上的准确率基线。def calculate_fwt(acc_matrix, random_baseline): n acc_matrix.shape[0] fwt 0.0 count 0 for i in range(n): for j in range(i 1, n): fwt acc_matrix[i, j] - random_baseline[j] count 1 return fwt / count6.4 评估协议注意事项评估时最容易被忽视的是任务顺序和随机种子。持续学习结果对任务顺序极其敏感同一个数据集交换任务顺序后结果可能差异很大。因此论文实验通常要跑多个种子报告均值和标准差。另一个问题是每个 epoch 结束后的评估频率。在线持续学习中数据是可流式的如果只在任务边界评估看不到模型在新任务早期阶段的波动。建议在任务内部固定间隔做一次快速评估观察稳定性。7. 训练资源占用与性能观察持续学习训练过程的资源占用和普通深度模型训练类似但多了一个重放缓冲的内存开销。这里讨论怎么观察显存、内存变化以及哪些因素会放大成本。7.1 显存占用观察方法使用 GPU 训练时可以用命令行实时观察显存# 每隔 1 秒刷新一次 GPU 占用 watch -n 1 nvidia-smi在代码里也可以打印当前已显存if torch.cuda.is_available(): allocated torch.cuda.memory_allocated() / 1024**2 print(fallocated memory: {allocated:.1f} MB)显存占用主要来自三个部分模型参数和梯度、当前 batch 的中间激活、重放样本的临时张量。重放缓冲本身如果保存的是原始小图占用的主要是内存而非显存只有把重放样本送到 GPU 时才会临时占用显存。7.2 影响性能的关键因素重放缓冲区大小。缓冲区越大每个 batch 中旧样本占比越高显存和内存开销越大但对旧任务的保持效果通常越好。需要根据数据规模调整。batch 大小。新任务样本和重放样本合在一个 batch 时总 batch 增大显存占用上升。如果显存紧张可以降低 replay batch size。模型宽度和输入分辨率。MLP 在 CPU 上就能跑ResNet 或 ViT 上显存需求明显上升。输入分辨率从 224 降到 128显存和计算量下降很多。任务数。任务数增加不会显著增加单次训练显存但会增加评估耗时和重放缓冲的总样本量。7.3 降低资源占用的建议如果实验环境显存有限优先把输入归一化放到 CPU 端完成再用pin_memory预加载。重放样本可以直接以 npy 格式压缩保存到磁盘训练时随机读取一个 mini-batch而不是把所有重放样本一次性常驻内存。# DataLoader 启用 pin_memory可以减少 GPU 拷贝等待 DataLoader(..., num_workers2, pin_memoryTrue)如果使用大模型做持续微调建议优先采用 LoRA 等 PEFT 方案只训练低秩适配器。这样旧任务参数基本不变同时显存占用可以大幅下降。8. 常见问题与排查方法持续学习实验跑起来之后会遇到几类高频问题。这里整理成排查清单。问题现象可能原因排查方式解决方案新任务训练后旧任务精度大幅下降缓冲区过小或没有重放打印每个任务后评估矩阵观察对角线之后旧任务准确率变化增大 buffer_size增加 replay batch size类增量场景下模型倾向预测新类别分类头被新类数据偏置统计推理时新类别预测占比观察是否严重失衡任务结束后用重放缓冲校准分类头或改用最近类均值分类器同一个数据集不同任务顺序结果差异大任务顺序敏感性随机打乱任务顺序跑 3 到 5 个种子报告多个种子的均值和标准差不要依赖单次结果在线持续学习时准确率波动剧烈每个 batch 数据量太少、学习率太高记录每个 epoch 的评估曲线观察波动幅度降低学习率增大缓冲区考虑用 AdamW 替代 SGD重放缓冲中样本类别不平衡有些任务样本量少统计缓冲区类别分布按类别均衡采样或使用类别比例采样显存不足batch 过大、模型过宽、输入分辨率过高用 nvidia-smi 观察显存曲线降低 batch、降低输入分辨率、使用梯度累积任务数增多后评估时间越来越长每个任务后都要评估所有旧任务检查评估代码是否串行遍历所有 task loader只在关键任务边界全量评估中间用抽样子集评估引入蒸馏损失后训练不收敛蒸馏软标签质量差检查新旧模型输出分布是否差异过大调整蒸馏温度和蒸馏损失权重排障时最重要的一件事是先固定一个简单基线。比如先跑纯 Naive没有回放的持续学习确认代码逻辑正确再逐步加回放、加蒸馏、加正则化。不要一上来就往复杂算法上堆。9. 最佳实践与落地建议9.1 实验设计层面第一每个方法都要控制相同的存储预算。持续学习论文里比较重放和正则化方法时重放类方法存储旧样本会占用额外空间。公平比较应该在“同等额外存储开销”下进行否则结论不公正。第二多个随机种子取平均。持续学习结果方差大单次实验可能得出完全相反的结论。至少跑 3 个种子报告均值和标准差。第三记录每个任务结束后的模型快照。这样回溯遗忘曲线时可以直接加载对应模型不需要重新训练。9.2 算法选择层面如果你的业务允许存储少量旧样本优先考虑基于重放的方法。实现简单、调参门槛低、效果稳定。如果业务不允许保留原始样本选择正则化类方法但要对旧任务的精度保持设置一个可接受阈值。EWC 对超参数敏感需要单独调 Fisher 采样数量和正则化权重。如果任务边界清晰、任务数量有限架构类方法能提供最强的旧任务保护。但部署时要注意子网络索引管理任务数量过多时不能无限制扩展。如果业务数据是流式逐样本到达不要使用离线分任务的评估方式要改为在线评估协议并按时间窗口记录数据到达顺序。9.3 工程化建议把任务流定义成配置文件包含任务顺序、数据路径、重放缓冲大小、训练轮数保证实验可复现。批量实验时使用独立的日志文件记录每次运行的任务顺序、随机种子、超参数和评估结果。涉及人脸、声音、版权数据时必须确认授权。重放缓冲区保存的旧样本同样要合规。生产环境发布前用一套固定“任务流回归集”验证旧任务精度是否低于阈值。如果低于阈值回滚到上一版本模型或触发一次全量重训。使用大模型做增量微调时优先尝试 LoRA 或前缀微调并配合小型重放缓冲成本可控效果也容易评估。9.4 从算法验证到业务部署持续学习落地不是把训练脚本跑通就行。实际生产环境里任务流来自真实数据管道可能会有标签延迟、数据缺失、类别采样不均等问题。建议先做一个小范围影子测试用真实数据流的一个月切片重建历史任务流对比“全量重训基线”和“持续学习增量更新基线”重点看旧任务精度保持、新任务适应速度、训练耗时和存储占用。只有对比通过再考虑替换生产训练链路。10. 总结与下一步持续学习最有价值的点是把“模型上线后就不能再动”或者“每次更新都要全量重训”的二元选择变成了一种平滑过渡方案。你可以用小批量、低代价的增量更新在尽量不破坏旧能力的前提下让模型跟随新任务流持续演进。这篇文章里最值得先验证的是那个最小经验回放基线。它能直观展示灾难性遗忘现象也能让你在 10 分钟内在 CPU 上跑通一轮完整任务流评估。最容易踩的坑集中在类增量评估和任务顺序敏感度上尤其是分类头偏置问题初学者很容易忽略。后续想继续深入可以从这几个方向展开先给上面的 ER 基线加上知识蒸馏复现一个 LwF再把 EWC 的 Fisher 正则项加进去对比两类方法在相同存储预算下的表现然后试着把模型换成 ResNet把数据集从 MNIST 换成 CIFAR-100观察类增量场景下的精度变化如果条件允许再尝试在开源大语言模型上用 PEFT 做持续指令微调验证不同 LoRA 配置对旧指令集的保持效果。持续学习不是银弹但它提供了一套值得研究的“过渡技术”从静态训练过渡到增量更新从离线评估过渡到在线验证从单模型过渡到持续演进的模型状态机。建议有数据流和增量更新需求的团队先收藏这篇文章把基线代码跑通再决定要不要把持续学习引入自己的业务链路。