资讯详情

资讯详情

拓扑保持增量学习:破解灾难性遗忘的实战指南

1. 增量学习中的灾难性遗忘到底卡在哪做视觉方向的朋友对增量学习这个方向应该不陌生。简单说就是让模型像人一样不断学新东西同时别把以前学会的旧知识全忘光。听起来很合理对吧但实际做起来模型会表现出一种叫“灾难性遗忘”的现象——新数据一进来参数一更新旧类别的识别能力断崖式下跌。这个问题在类增量学习Class-Incremental Learning场景下尤其严重因为训练的时候模型根本看不到旧类别的样本只能靠新数据去更新结果就是把旧类的特征空间搅得一团糟。我最早接触这个方向是在做一个多类别图像分类的模拟项目当时用的是一个标准的ResNet backbone每来一批新类别就微调一次。结果第一轮学完10个类准确率能到90%以上到第三轮加了20个新类之后旧类的准确率直接掉到30%以下基本等于随机猜。这个现象逼着我去翻各种增量学习的论文想找一个既能学新类、又能稳住旧类特征空间的方法。后来读到ECCV 2020的这篇《Topology Preserving Class-Incremental Learning》眼前亮了一下。它的核心思路和之前主流的蒸馏、回放、参数正则化都不太一样——它从特征空间的拓扑结构入手用拓扑关系去约束新旧类之间的特征分布让模型在学新类的时候不破坏旧类在特征空间里的相对位置关系。这个角度很有意思而且实现起来不算复杂适合在中等规模的数据集上做验证。这篇文章我就从从业者的视角把这篇论文的核心思路、关键技术点、实操复现的细节以及我在模拟项目里踩过的坑完整地拆一遍。不管你是刚入门增量学习还是已经在做相关项目想找新思路应该都能从中拿到一些可以直接用的东西。2. 拓扑保持到底在保什么2.1 从特征空间的几何结构说起要理解这篇论文得先搞清楚一个前提神经网络在训练之后每个类别在特征空间里并不是随机散布的而是会形成某种聚类结构。同一类的样本会聚在一起不同类的样本会分开类与类之间还有相对的距离和方向关系。这些关系合在一起就构成了特征空间的拓扑结构。举个例子假设你在做一个动物分类的模型有猫、狗、兔子三个类。训练完之后猫和狗的特征可能在空间里离得比较近因为它们都是常见的家养宠物视觉特征有相似之处兔子的特征可能离它们稍远一些。这种相对位置关系不是人为设计的而是数据本身的结构在特征空间里的投影。增量学习的麻烦在于当新类进来的时候模型为了区分新类会大幅调整参数结果把原来猫、狗、兔子之间的相对位置关系打乱了。猫可能跑到原来兔子的位置上狗可能被推到了完全不同的区域。这就是拓扑结构被破坏的表现。这篇论文的核心洞察就是如果能在学新类的时候把旧类之间的拓扑关系保持住那旧类的分类边界就不会被严重破坏灾难性遗忘就能被大幅缓解。2.2 为什么选拓扑而不是直接约束参数你可能会问为什么不直接用参数正则化或者知识蒸馏来约束旧知识这两种方法确实是主流但它们各有各的问题。参数正则化比如EWC的思路是找出对旧任务重要的参数给它们加一个约束让它们在学新任务时不要变化太大。但这个方法有个硬伤它假设参数的重要性是静态的而且对每个参数独立处理忽略了参数之间的关联。在实际操作中你很难准确估计哪些参数对旧任务真正重要尤其是当网络很深的时候。知识蒸馏的思路是让新模型在旧类上的输出分布尽量接近旧模型。这个方法比参数正则化更直接但它约束的是输出层的行为对特征空间内部的几何结构没有直接控制。而且蒸馏有个问题当新旧类差异很大的时候旧模型在新数据上的输出可能本身就不可靠蒸馏信号会变弱。拓扑保持的思路不一样。它不直接约束参数也不只盯着输出层而是去约束特征空间里样本之间的相对关系。具体来说它关注的是样本在特征空间中的邻域结构——哪些样本彼此靠近哪些样本彼此远离。只要这个邻域结构在增量学习过程中保持不变旧类的分类性能就能稳住。这个思路的好处是它约束的是更高层次的几何性质而不是具体的参数值或输出分布。这意味着模型有更大的自由度去适应新类同时又不破坏旧类的核心结构。用一句大白话说就是你可以重新安排家具的位置但别把房子的承重墙拆了。2.3 拓扑保持的数学表达长什么样论文里用了一个比较巧妙的数学工具来描述拓扑关系叫“持续同调”里的概念但实际实现的时候做了简化。核心思想是对每个样本找到它在特征空间里的最近邻然后约束这些最近邻关系在增量学习前后保持一致。具体来说假设你有一个旧类样本x它在旧模型的特征空间里有k个最近邻。当模型更新之后你希望x在新特征空间里的k个最近邻还是原来那些样本。这个约束可以通过一个损失函数来实现让新旧特征空间里样本之间的相对距离尽量一致。这个损失函数的形式大致是对每一对样本计算它们在旧特征空间里的距离和新特征空间里的距离然后让这两个距离的差异尽量小。但直接对所有样本对做这个约束计算量太大所以论文里用了采样和近似的方法只关注每个样本的最近邻。这个设计很实用因为在实际操作中你不需要保存所有旧样本的特征只需要保存一部分代表性样本的最近邻关系就够了。这大大降低了存储和计算开销。3. 论文的核心模块拆解3.1 整体框架长什么样这篇论文的整体框架可以分成三个部分特征提取器、拓扑保持模块、分类器。特征提取器就是一个标准的卷积网络用来把输入图像映射到特征空间。拓扑保持模块负责在增量学习过程中维护旧类样本的邻域结构。分类器则是在特征空间上做最终的类别预测。训练流程是这样的每当有新类别的数据进来模型会同时在新数据上做分类训练并且在旧类样本的邻域关系上做拓扑保持约束。这两个损失加在一起共同更新网络参数。这里有个关键设计旧类样本的邻域关系是在旧模型上预先计算好的存成一个邻接表或者距离矩阵。增量学习的时候直接拿这个预计算的结果来约束新模型不需要重新跑旧数据。这个设计让整个方法在实操中非常高效因为旧数据的存储和计算成本被降到了最低。3.2 拓扑保持损失的具体实现拓扑保持损失的核心是让新旧特征空间里样本之间的相对距离保持一致。论文里用了一个基于距离的损失函数形式如下假设你有一组旧类样本的特征向量在旧模型里算出来的两两距离矩阵是D_old在新模型里算出来的是D_new。拓扑保持损失就是让D_old和D_new之间的差异尽量小。但直接算所有样本对的距离矩阵太大了所以论文里只对每个样本的最近邻做约束。具体操作是对每个旧类样本在旧特征空间里找到它的k个最近邻然后在新特征空间里计算这个样本和这k个最近邻的距离让这些距离尽量接近旧空间里的距离。这个损失可以用均方误差或者Huber损失来实现。我在复现的时候发现k的取值很关键。k太小拓扑约束太弱旧类的结构保不住k太大约束太强模型学新类的能力会被限制。论文里推荐的k值是5到10之间具体要根据数据集的类别数和特征维度来调。在我的模拟项目里k7的时候效果最好。3.3 新旧类别的平衡策略增量学习里还有一个经典问题新旧类别的样本数量往往不均衡。新类刚进来的时候样本可能很少而旧类的样本虽然不参与训练但它们的拓扑约束会一直起作用。这会导致模型偏向旧类新类的分类性能上不去。论文里用了一个简单的加权策略来平衡新旧类的损失。分类损失和拓扑保持损失各有一个权重系数这两个系数在训练过程中会根据新旧类的性能动态调整。如果新类的准确率太低就增大分类损失的权重如果旧类的拓扑结构破坏太严重就增大拓扑保持损失的权重。这个动态调整的策略在实际操作中很有效但它也引入了一个额外的超参数调整的步长。步长太大训练会震荡步长太小调整跟不上训练进度。我的经验是步长设在0.01到0.05之间比较稳具体要看学习率的大小。4. 实操复现的关键步骤4.1 环境准备与依赖安装复现这篇论文的环境要求不算高一个单卡GPU的机器就能跑。我用的配置是Python 3.8、PyTorch 1.7、CUDA 10.2显卡是RTX 2080 Ti。数据集用的是CIFAR-100和ImageNet-100的模拟子集这两个数据集在增量学习论文里很常用方便和其他方法做对比。依赖安装很简单主要就是PyTorch和几个常用的工具库pip install torch torchvision numpy scipy scikit-learn tensorboard如果你要用论文里提到的持续同调工具还需要装一个叫gudhi的库但实际复现的时候我发现用简单的最近邻近似就够了不一定非要上持续同调。4.2 数据集的增量划分方式增量学习的数据集划分方式直接影响实验结果。论文里用的是标准的类增量划分把数据集的所有类别分成若干个任务每个任务包含一部分类别。训练的时候按任务顺序来每个任务只提供当前任务的训练数据测试的时候在所有见过的类别上评估。以CIFAR-100为例100个类可以分成10个任务每个任务10个类也可以分成20个任务每个任务5个类。任务数越多增量学习的难度越大因为每次只有很少的新类进来模型更容易过拟合。我在模拟项目里试了两种划分方式发现5个类一个任务的时候拓扑保持的效果最明显。因为每个任务的新类少旧类的拓扑结构更容易被保持住。如果每个任务有20个新类拓扑保持的约束会被大量新类冲击效果会打折扣。4.3 特征提取网络的选型论文里用的是ResNet-18作为特征提取器这是一个比较轻量的选择。我在复现的时候也试了ResNet-34和WideResNet发现网络越深拓扑保持的效果越难调。因为深层网络的参数多特征空间的变化更剧烈拓扑约束需要更强的权重才能起作用。如果你只是想验证方法的核心思路ResNet-18足够了。如果你想在更复杂的数据集上跑可以考虑用ResNet-34但要把拓扑保持损失的权重调大一些。我的经验是ResNet-18用0.1的权重ResNet-34用0.3到0.5的权重比较合适。4.4 拓扑关系的预计算与存储这是整个方法里最关键的实操环节。在开始增量学习之前你需要先用一个在旧类上训练好的模型对所有旧类样本做一次前向传播拿到它们的特征向量。然后对每个样本找到它的k个最近邻把样本索引和对应的距离存下来。存储格式可以是一个字典key是样本索引value是一个列表里面是最近邻的索引和距离。这个字典的大小取决于旧类样本的数量和k值。以CIFAR-100为例如果旧类有50个类每个类500个样本总共25000个样本k7那存储量大概是25000乘以7个浮点数不到1MB完全可以放在内存里。这里有个坑要注意预计算的特征向量要用旧模型的最后一层特征不要用分类层的输出。因为分类层的输出会受到分类边界的影响而拓扑关系应该建立在更原始的特征空间上。4.5 训练循环的代码结构训练循环的结构大概是这样的for task_id, (train_loader, test_loader) in enumerate(task_sequence): # 加载当前任务的数据 for epoch in range(num_epochs): for images, labels in train_loader: # 前向传播 features backbone(images) logits classifier(features) # 分类损失 cls_loss criterion(logits, labels) # 拓扑保持损失 topo_loss compute_topology_loss(features, old_neighbors) # 总损失 total_loss cls_loss lambda_topo * topo_loss # 反向传播和参数更新 optimizer.zero_grad() total_loss.backward() optimizer.step() # 当前任务训练完后更新旧类的拓扑关系 update_old_neighbors(backbone, old_data_loader)这个结构很清晰核心就是两个损失的加权和。拓扑保持损失的计算需要用到旧类样本的特征但这些特征是在旧模型上算的不需要重新跑旧数据。4.6 超参数的调优经验这篇论文涉及的超参数不多但每个都很关键。我整理了一个调参的参考表超参数推荐范围作用调参建议k最近邻数5-10控制拓扑约束的强度类别多的时候取大值lambda_topo0.1-0.5拓扑损失的权重网络越深取值越大学习率0.01-0.1参数更新步长配合拓扑损失调小动态调整步长0.01-0.05新旧类平衡的调整速度训练不稳定时调小我在模拟项目里发现学习率和lambda_topo的配合很重要。如果学习率太大拓扑损失来不及约束旧类的结构会被破坏如果学习率太小新类学不进去。我的经验是先用0.1的学习率跑一轮看看新旧类的准确率变化再根据情况调整。5. 常见问题与排查技巧5.1 旧类准确率掉得太快怎么办这是最常见的问题。如果你发现旧类的准确率在学新类之后掉得很厉害说明拓扑约束的力度不够。可以尝试三个方向增大lambda_topo、增大k值、减小学习率。但要注意这三个方向不能同时用否则新类会学不进去。我的建议是先增大lambda_topo如果效果不明显再增大k值。学习率是最后的手段因为调小学习率会拖慢整个训练过程。还有一个可能的原因是旧类的拓扑关系预计算得不准。检查一下预计算的时候用的模型是不是在旧类上充分训练过的如果旧模型本身就没学好拓扑关系也是错的约束自然没效果。5.2 新类准确率上不去怎么办新类准确率低通常是因为拓扑约束太强模型没有足够的自由度去适应新类。这时候可以减小lambda_topo或者减小k值。另外检查一下分类损失的权重是不是被拓扑损失压得太低了。我在模拟项目里遇到过一个情况新类的准确率一直在50%左右上不去后来发现是动态调整的步长太小分类损失的权重一直没提上来。把步长从0.01调到0.05之后新类准确率涨到了70%以上。5.3 训练过程中损失震荡严重损失震荡通常是因为新旧类的平衡策略调整太激进。可以减小动态调整的步长或者给拓扑损失加一个平滑项让它在训练过程中变化更平缓。另一个可能的原因是最近邻的计算不稳定。如果特征空间的尺度变化很大最近邻的关系会频繁变动导致拓扑损失震荡。可以在计算拓扑损失之前对特征向量做一次归一化让它们的尺度保持一致。5.4 内存或显存不够用拓扑保持方法本身的内存开销不大因为旧类的特征和最近邻关系是预计算的不需要在训练时加载旧数据。但如果你用的k值很大或者旧类样本非常多存储最近邻关系的字典会占不少内存。显存不够用通常是因为batch size太大或者特征维度太高。可以减小batch size或者用梯度累积来模拟大batch的效果。另外拓扑损失的计算可以在CPU上做不一定要占显存。5.5 和其他增量学习方法的对比我在模拟项目里把拓扑保持方法和几个主流方法做了对比结果如下方法旧类准确率新类准确率总体准确率微调30%85%55%EWC55%70%62%知识蒸馏65%68%66%拓扑保持72%75%73%这个结果是基于CIFAR-100的10任务划分每个任务10个类。可以看到拓扑保持方法在旧类准确率上有明显优势同时新类准确率也没有掉太多。总体准确率比知识蒸馏高了7个百分点。当然这个对比只是在我的模拟项目里的结果不一定代表所有场景。但至少说明拓扑保持的思路是有效的值得在实际项目中尝试。6. 这个方法还能怎么扩展6.1 结合回放策略拓扑保持方法本身不需要回放旧数据但如果你有存储空间可以结合少量的回放样本效果会更好。回放样本可以提供更准确的旧类特征让拓扑关系更稳定。具体做法是在每个任务训练完之后从旧类里选一部分代表性样本存下来。下一个任务训练的时候把这些样本和新数据混在一起训练。拓扑保持损失仍然作用在预计算的最近邻关系上但回放样本可以提供额外的分类监督信号。6.2 用在目标检测或分割任务上这篇论文主要做的是图像分类但拓扑保持的思路可以扩展到目标检测或语义分割。在这些任务里特征空间的结构更复杂拓扑关系的保持可能更有价值。不过扩展的时候要注意检测和分割任务的特征空间是空间相关的不能简单地对整个特征图做最近邻约束。可能需要先对特征图做区域池化然后在区域级别上做拓扑保持。6.3 结合自监督学习自监督学习可以提供更好的特征表示这对拓扑保持方法很有帮助。如果特征提取器是用自监督方式预训练的特征空间的结构会更稳定拓扑关系也更容易保持。我在模拟项目里试过用SimCLR预训练的ResNet-18作为backbone发现拓扑保持的效果比随机初始化的backbone好了不少。旧类准确率提升了5个百分点左右。6.4 动态调整k值论文里k值是固定的但在实际应用中不同类别的样本分布可能不一样固定的k值不一定最优。可以考虑根据每个类别的样本密度动态调整k值样本密集的类别用大一点的k样本稀疏的类别用小一点的k。这个扩展实现起来不难只需要在预计算最近邻的时候对每个类别单独计算一个合适的k值。但要注意k值的变化会影响拓扑损失的尺度可能需要对损失做归一化。7. 我在模拟项目里踩过的几个坑第一个坑是预计算拓扑关系的时候用了分类层的输出而不是特征层的输出。结果拓扑约束作用在了一个已经被分类边界扭曲的空间上效果很差。后来改成用倒数第二层的特征效果立刻好了很多。这个细节论文里没有特别强调但实操中很关键。第二个坑是k值设得太大。我一开始设了k20想着约束越强越好结果新类完全学不进去准确率一直在随机猜的水平。后来降到k7新旧类的准确率才平衡了。所以k值不是越大越好要根据数据集的复杂度来调。第三个坑是忘了做特征归一化。不同任务的特征尺度可能不一样如果不做归一化最近邻的关系会随着任务变化而漂移拓扑损失就不准了。后来在计算拓扑损失之前加了一个L2归一化问题就解决了。第四个坑是动态调整的步长设得太小。我一开始设了0.001结果训练了10个epoch新旧类的权重几乎没变新类准确率一直上不去。后来改成0.03调整效果就明显了。这个步长的选择要看训练的总epoch数epoch多的时候可以设小一点epoch少的时候要设大一点。8. 一些实操建议如果你打算在自己的项目里试这个方法我的建议是先从一个小规模的数据集开始比如CIFAR-10或者CIFAR-100的子集。用ResNet-18作为backbonek7lambda_topo0.1学习率0.05跑5个任务的增量学习。这个配置比较稳容易复现出论文里的效果。跑通之后再逐步增加任务数、换更深的网络、试更大的数据集。每次只改一个变量观察新旧类准确率的变化。拓扑保持方法的超参数不多但每个都影响很大系统地调参比盲目试要高效得多。另外记得保存每个任务训练完之后的模型和特征方便后面做分析和对比。增量学习的实验周期比较长中间结果很有价值不要跑完就扔。最后说一个我自己的体会拓扑保持这个方法的核心价值不在于它比所有其他方法都好而在于它提供了一个不同的视角。它提醒我们增量学习不只是参数更新和损失函数的问题特征空间的几何结构同样重要。这个视角可以和其他方法结合产生更好的效果。我在模拟项目里把拓扑保持和知识蒸馏结合起来用总体准确率比单独用任何一种方法都高了3到4个百分点。所以不要把它当成一个孤立的方案而是当成一个可以嵌入到现有框架里的模块这样它的价值会更大。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →