
简介基于持续学习的图像分类Python源码项目针对机器学习课程设计与毕业设计场景提供一套完整可运行的增量学习图像分类方案。项目支持CIFAR100数据集涵盖多任务类别递增、旧样本经验重放、余弦归一化、边缘排序损失与less-forget约束等关键机制便于理解灾难性遗忘问题及缓解策略。压缩包共86个文件其中32个Python源码负责训练、验证、特征选择与模型构建48个pyc为编译缓存另有4个txt和2个Markdown文档含项目说明与配置指引整包仅115KB轻量易部署。已有111人学习下载适合计算机相关专业学生作为大作业、课程设计或毕设参考。代码结构清晰main.py支持通过命令行参数灵活调节数据集、每轮类别数、样本保留策略等可基于此快速复现实验并二次开发拓展功能。1. 持续学习图像分类这份 Python 源码解决的增量学习痛点做过机器学习课程设计的人应该都遇到过这个场景分类模型在固定类别上训练得很好准确率 90% 以上但一旦加入几类新数据model 重新训练后旧类别准确率直接崩盘。这就是持续学习领域最核心的灾难性遗忘问题。这份基于持续学习的图像分类 Python 源码不是简单的 CNN 分类器而是一套完整可跑的增量学习基线它用经验重放配合两种约束损失Less-Forget 蒸馏约束和 Margin Ranking Loss来控制遗忘支持 CIFAR100 分阶段增量训练。对于正在做机器学习大作业、需要课程设计源码或者想入门增量学习的从业者来说它比换个数据集跑 resnet18 有价值得多——你拿到的不只是分类器而是一整套「如何让模型记住旧知识」的完整方案。2. 项目结构逐层拆解main.py、模型库与样本回放是怎么咬合的拿到压缩包先别急着运行我建议先花十分钟把目录理清楚。这个项目的代码组织方式很典型入口脚本、模型定义、损失函数、工具模块四层分离比那种把全部逻辑塞进一个 train.py 的大作业源码要规范得多。下面按调用顺序逐层拆。2.1 先看入口main.py 的参数分发与训练流程main.py是唯一需要直接运行的入口所有实验配置都通过命令行参数控制。项目说明里给出的参数表很完整我把它整理成了实际运行时的样子python main.py --dataset CIFAR100 --start 50 --increment 10 --rehearsal 20 --selection herding --exR True --less_forg True --lambda_base 10 --ranking True --class_balance_finetuning True注意这段命令对应一个典型的 LUCIR 增量场景初始任务先学 50 类之后每轮增量 10 类每类保留 20 个旧样本做回放。main.py内部用 argparse 解析这些参数核心逻辑是按task循环每个增量阶段先取出当前任务的数据类列表然后在已保存的旧样本和当前新数据上训练模型任务结束后调用存储模块更新 exemplar 集合。从代码结构看main.py 的训练循环大致是这样一个骨架# main.py 训练主循环结构示意 for task_id in range(num_tasks): # 1. 取出当前任务要学习的类别集合 cur_classes class_order[task_id * increment: (task_id 1) * increment] # 2. 构建训练集新类别数据 旧类别 exemplar如果开启回放 train_dataset build_task_dataset(cur_classes) if args.exR and exemplar_set.size() 0: train_dataset combine_with_exemplars(train_dataset, exemplar_set) # 3. 在扩展后的数据上训练模型 model train_one_task(model, train_dataset, args) # 4. 任务结束后为当前类别挑选 exemplar 存入回放池 exemplar_set.update(model, cur_classes, args.rehearsal)这里的参数联动关系很关键--exR False时纯增量学习不做回放--rehearsal完全不起作用--ranking True时 margin ranking loss 会叠加到总损失上配合--lambda_base控制 less-forget 损失的权重。我一般建议第一次跑通时把--rehearsal设成 20 到 50 之间太小了旧类很快被遗忘太大了训练速度会明显变慢。2.2 模型层拆解增量 ResNet 与余弦归一化分类器的分工models/目录下有一堆模型文件第一次看容易懵。我拆开说incremental_resnet.py是核心它定义了增量学习专用的 ResNet 变体cosine_classifier.py是余弦归一化分类头这是 LUCIR 方法的一个关键设计my_resnet.py、resnet_cifar.py、resnet_imagenet.py分别是不同数据集和网络深度对应的骨干实现。文件作用关键点incremental_resnet.py增量主干网络继承标准 ResNet按增量任务动态扩展分类头维度CosineClassifier.py余弦分类器特征与权重做 L2 归一化去掉偏置项乘可学习缩放因子resnet_cifar.pyCIFAR 系列骨干适配 32x32 输入去掉第一个池化层my_resnet.py自定义 ResNet 变体可调 block 和层数一般用于快速实验resnet_imagenet.pyImageNet 骨干标准 224 输入用于大规模数据集扩展增量模型和普通分类模型最大的区别在分类头普通模型最后一层是全连接加 softmax维度固定增量模型的分类头维度会随着任务推进不断扩展。incremental_resnet.py里每次新类别到来时会把最后一层权重大小从旧的类别数扩展到新的类别数新增的权重行用随机初始化或者基于旧权重的拓展策略。只扩展最后一层会带来一个问题——新旧类别特征分布不一致CosineClassifier 的归一化处理缓解的正是这个问题。# CosineClassifier.py 核心逻辑示意 class CosineClassifier(nn.Module): def __init__(self, in_features, num_classes, scale16.0): super().__init__() self.fc nn.Linear(in_features, num_classes, biasFalse) self.scale scale # 可学习缩放因子默认 16 def forward(self, features): # 特征和权重都做 L2 归一化再算余弦相似度 norm_feat F.normalize(features, dim1) norm_weight F.normalize(self.fc.weight, dim1) logits self.scale * (norm_feat norm_weight.t()) return logits这段代码是理解了为什么增量分类用余弦分类器的关键特征和权重都投影到单位超球面上logits 只取决于方向、不受模长影响。增量训练后期新类别样本的梯度容易主导网络归一化能有效缓解新旧类别特征尺度不一致带来的分类偏置。scale16是论文里验证过的经验值项目里也是这个默认设定。2.3 数据流ExemplarSet、feature_selection 与旧样本回放ExemplarSet.py 和 feature_selection.py 是回放机制的两块拼图。ExemplarSet.py负责管理每类固定数量的代表性样本feature_selection.py提供了三种样本选择策略Herding、Random、Closest to Mean。项目说明里参数--selection对应的就是这三种方式。Random 最简单从旧类样本里随机抽固定数量Closest to Mean 是挑选特征最接近类别均值的样本计算量小、效果中等Herding 是论文里验证效果最好的策略核心思路是贪心每一轮选出一个样本使得当前已选样本集合的均值向量离类别原型均值最近。Herding 的名字也由此而来——像牧羊犬把羊群往中心聚集。# feature_selection.py Herming 选择策略示意 def herding_selection(features, k): # features: 某类别所有样本的特征矩阵 # 1. 计算类别原型特征均值 class_mean features.mean(dim0) selected [] current_mean torch.zeros_like(class_mean) for _ in range(k): # 2. 每次挑选让已选集合均值最接近类原型的样本 distances ((features - current_mean) ** 2).sum(dim1) distances[selected] float(inf) idx distances.argmin().item() selected.append(idx) current_mean features[selected].mean(dim0) return selected这个策略在选择每一个样本时都重新计算已选集合的均值保证选出来的样本整体上能代表类别的分布中心。Rehearsal 数量直接影响回放池大小20 个样本时 Herding 比 Random 在旧类准确率上能高出 3 到 5 个百分点但样本量增加到 50 以后差距会缩小。如果追求快速验证用 Closest to Mean 也能得到不错的基线。3. 两个核心损失函数Less-Forget 约束与 Margin Ranking Loss 的实现与调参增量学习方法按机制分三大流派正则化、回放、参数隔离。这个项目主要走的是回放 正则化的混合路线。Less-Forget 蒸馏约束和 Margin Ranking Loss 是损失函数层面的核心直接决定模型遗忘旧知识的速度这一章把这两个文件讲透。3.1 less_forget.py知识蒸馏如何对抗遗忘Less-Forget 约束的本质是知识蒸馏在每个增量任务开始前先把旧模型上一个任务训练好的冻结保存训练新任务时除了标准交叉熵分类损失外额外计算新模型在旧类别上的输出分布与旧模型输出分布之间的差异通过最小化这个差异来强制新模型保留旧类别的判别信息。# loss/less_forget.py 蒸馏损失逻辑示意 def less_forget_loss(new_logits, old_logits, old_classes, temperature2.0): # 只取旧类别对应的 logits 做蒸馏 new_old new_logits[:, old_classes] / temperature old_old old_logits[:, old_classes] / temperature # soft target 蒸馏KL 散度 loss nn.functional.kl_div( F.log_softmax(new_old, dim1), F.softmax(old_old, dim1), reductionbatchmean ) return loss * (temperature ** 2)这里temperature是蒸馏温度常见的取值是 2 到 4温度越高软标签分布越平滑、对新模型的约束越宽松。蒸馏损失只作用于旧类别对应的输出节点新类别的学习不受干扰。--lambda_base控制这个损失在总损失里的权重项目默认搭配是lambda_base10如果发现旧类准确率掉得快把lambda_base加大到 20 到 30 能明显改善但代价是新类别学习变慢、训练时间变长。3.2 margin_lucir.py在余弦空间里拉开新旧类的距离Margin Ranking Loss 是针对增量分类的另一个关键约束它解决的是新类别样本被误分到旧类别的问题。思路是把当前样本的 embedding 与它所属类的原型向量正向以及其他类别的原型向量负向做相似度排序强制正向相似度比负向相似度高出至少一个 margin。# loss/margin_lucir.py 边缘排序损失逻辑示意 def margin_ranking_loss(features, class_centers, target_class, margin0.5): pos_sim cosine_similarity(features, class_centers[target_class]) # 对比的负类排除当前类别取相似度最高的几个类别作为负样本 neg_sim cosine_similarity(features, class_centers[exclude(target_class)]) max_neg neg_sim.max() loss torch.relu(max_neg - pos_sim margin) return lossmargin 是核心超参数LUCIR 论文里推荐 0.5 左右。margin 设太大超过 1.0会让 loss 长期不收敛新类别永远达不到要求的间隔设太小低于 0.2又对分类边界几乎没有约束力。这个 loss 的计算需要维护每个类别的原型向量即类特征均值我们在 2.3 节 feature_selection 里计算类别原型时这部分信息同时也会被 Margin Ranking Loss 用到。3.3 参数搭配建议几个开关的组合逻辑这个项目的参数矩阵看起来很灵活但实际有内在的依赖关系我把常见组合整理成表格方便对照场景exRless_forgrankingclass_balance_finetuning预期效果纯增量基线FalseFalseFalseFalse旧类快速遗忘不推荐只回放TrueFalseFalseFalse有一定效果长尾偏置明显回放蒸馏推荐起点TrueTrueFalseTrue旧类准确率稳定可作主基线完整 LUCIR 配置TrueTrueTrueTrue论文复现效果新类稍慢但整体最佳class_balance_finetuning这个开关值得单独说每个增量任务结束后它会用各类别均匀采样的数据对模型做少量微调纠正数据不均衡导致的分类边界偏移。开启它几乎总是必要的尤其是当increment比较大比如 20 类一批时没开这个微调的模型在新类上表现会明显偏科。我习惯的顺序是先跑通「回放蒸馏」的基线记录准确率曲线再依次打开 ranking 和 finetuning 观察各自带来的提升幅度这样交大作业时能清楚说明每个模块的贡献。4. 复现训练全流程从环境准备到 validate.py 验证理论部分讲透了这一章进入实战复现。按我的习惯跑一个新项目先做环境备份、再做最小化验证、最后才跑完整实验这一章的三节正好对应这三步。4.1 环境准备Python 版本、依赖安装与路径问题首选压缩包里的 pyc 文件是 cpython-38 编译的说明作者用的是 Python 3.8。我建议直接用 Python 3.8 或 3.9PyTorch 选 1.7 到 1.10 之间的版本都兼容——这个项目不涉及新的算子torch 2.x 理论上也能跑但没必要冒险。先创建虚拟环境再安装依赖# 创建并激活虚拟环境conda 和 venv 都可以 conda create -n continual python3.8 conda activate continual # 安装基础依赖 pip install torch1.10.0 torchvision0.11.0 # 安装项目 requirements注意项目说明里漏了 -r直接按它的写法会报错 pip install -r requirements.txt项目说明里写的是pip install requirements.txt这实际上会报错正确写法要用-r参数这一条踩坑记录后面还会细说。requirements.txt 里主要是 numpy、tqdm 这类常规库torch 系列建议单独安装以便控制版本。还有一个所有 Windows 用户都绕不过去的坑解压路径不能有中文。项目说明里已经提示了「项目名字和项目路径不要用中文」原因在于 Python 的 pyc 文件加载和某些库的路径编码处理在中文路径下会抛UnicodeDecodeError或者ModuleNotFoundError。解压后把文件夹重命名为continual_image_classification这类英文名再放到纯英文路径下。4.2 两组可复现的训练命令快速验证与完整实验环境配好后建议先跑一个快速验证命令确认代码链路是通的# 快速验证小增量步长、少回放样本几分钟跑完一个任务 python main.py --dataset CIFAR100 --start 40 --increment 5 --rehearsal 10 --selection herding --exR True --less_forg True --lambda_base 10 --ranking True --class_balance_finetuning True这个配置把初始类别设成 40 类每轮只加 5 类rehearsal 只有 10整体训练时间会明显缩短适合先跑通流程、观察输出日志是否正常。确认无报错后再跑完整配置# 完整实验常见 LUCIR 增量场景6 个任务50 10 x 5 python main.py --dataset CIFAR100 --start 50 --increment 10 --rehearsal 20 --selection herding --exR True --less_forg True --lambda_base 10 --ranking True --class_balance_finetuning True数据集第一次运行时会自动下载到项目目录下的 data 文件夹。如果下载速度慢或者失败可以手动从 CIFAR100 官网下载 tar.gz 包放到data/cifar-100-python/目录程序解码逻辑是一样的。这里值得一提的是--start 50 --increment 10的组合意味着总共 6 个学习阶段初始 1 个 增量 5 个每阶段结束都会输出该阶段在所有已见类别上的准确率这个输出正好可以做增量学习曲线的数据点。4.3 validate.py 与结果解读每阶段的准确率是怎么算的每个任务训练结束后validate.py会执行评估将测试集里所有已学习过的类别全部拿出来跑一遍前向推理统计 top-1 准确率。这是增量学习评估的关键——它不仅要看新类别的准确率还要看旧类别有没有被遗忘所以测试集是「所有已见类」而不是「当前任务类」。# 单独运行验证脚本需要确保模型权重已保存在 checkpoints 目录 python validate.py --dataset CIFAR100 --start 50 --increment 10验证输出一般会打印每一类的准确率和总体准确率重点关注两个指标当前任务的新类准确率以及从第 1 个任务到第 6 个任务的整体准确率衰减曲线。正常的实验结果是新类准确率在 60% 到 75% 之间浮动而旧类准确率相比单任务训练会下降 10 到 15 个百分点——这就是增量学习的代价项目的各类约束损失做的就是把遗忘率压在这个可控范围内。如果旧类准确率掉到 30% 以下说明参数配置有问题优先检查lambda_base和rehearsal这两个值。5. 避坑排查增量训练中最常见的 6 个翻车点增量学习项目的调试比普通分类项目更隐蔽普通分类训练 loss 不降一看便知增量训练可能在每个任务上看单独准确率都正常但旧类在不知不觉中崩塌。以下 6 个坑是我实跑这个项目时逐一踩过的按环境层和训练层分开讲。5.1 环境与路径层三个必踩的坑坑 1pip install requirements.txt 报错现象运行项目说明里的安装命令pip 直接报ERROR: Invalid requirement或者找不到包。原因说明里写的是pip install requirements.txt但 pip 正确用法是pip install -r requirements.txt少了-r参数pip 把文件名当成了包名去解析。解决用带-r的完整命令。这是文档笔误造成的低级坑但每个拿到压缩包的人几乎都会踩一次直接按 4.1 节正确命令安装即可。坑 2项目路径带中文运行报 UnicodeDecodeError现象程序启动后在加载 pyc 文件或读取数据时报错错误栈指向FeatureSelection.py或ExemplarSet.py的文件读取环节。原因Windows 控制台默认编码和 Python 的文件路径处理在含中文目录下编码不一致尤其是source_code_all_bk这种同时包含中文目录名和重复嵌套目录的情况解析会更混乱。解决解压后立刻把整个项目文件夹重命名为英文比如continual_learning确保从项目根目录到数据目录的完整路径中不含任何中文字符。这也是项目说明里重点提示的一条。坑 3CIFAR100 数据集下载卡死或下载失败现象运行main.py时日志长时间停在Downloading CIFAR100...网络慢或断连时直接报download error中止。原因自动下载用的是官方源国内网络访问不稳定数据包大约 160MB容易超时。解决手动下载 CIFAR100 压缩包放到data/cifar-100-python/目录下再重新运行。数据集文件校验通过后程序会跳过下载直接进入解压流程。也可以换成--dataset CIFAR10先做小规模验证CIFAR10 数据包小得多第一次跑通流程更快。5.2 训练与效果层遗忘了但看不出来的隐性坑坑 4显存 OOM增量到第三个任务时崩了现象前两个任务训练正常第三个任务开始报CUDA out of memory。原因CIFAR100 每类 500 张训练图rehearsal 设置得大比如 50增量到 3 个任务后回放池样本量累积每个 epoch 的批次数暴涨同时增量 ResNet 的分类头维度扩展显存占用逐阶段递增。解决把--rehearsal从 50 降到 20同时把 batch size 调小。CIFAR100 这种 32x32 小图batch size 64 足够稳定不要照搬 ImageNet 的 256。如果还爆显存用torch.cuda.set_per_process_memory_fraction(0.8)限制显存分配或者直接换--increment 5减少每个任务的类别数。坑 5加了 Margin Ranking Loss 后新类准确率反而下降现象没有开--ranking时新类准确率 70%开了之后新类掉到 58%旧类也没提升多少。原因Margin Ranking Loss 本质上是给新类的特征学习加了「拉开距离」的约束如果 margin 设得过大比如代码默认或自己改了 1.0网络需要更多 epoch 才能让新类特征收敛到满足间隔要求的位置训练轮数不变的情况下新类欠拟合。解决先不开启 ranking把 less-forget 和回放调好作为基线再开 ranking 并把 margin 设为 0.5。如果开了之后新类准确率下降超过 5 个百分点把训练 epoch 增加 20% 到 30%给排序损失留出收敛时间。坑 6每轮增量任务准确率波动剧烈同一套参数两次结果差 8%现象固定参数跑两次完整实验第 5 个任务后的整体准确率分别是 62% 和 71%相差明显。原因CIFAR100 下载后类别顺序是按文件归档顺序固定的算法内部没有固定随机种子ExemplarSet 初始化和样本洗牌的随机性在增量后期被放大。增量学习对类别顺序和样本选择极其敏感随机种子不同结果波动比普通训练大得多。解决在main.py开头加固定种子# main.py 开头固定随机种子 import random import numpy as np import torch def set_seed(seed2020): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(2020)固定种子之后同参数两次实验差异会压到 1% 以内。这个坑几乎每个做增量学习的人都会遇到固定种子是实验可复现的前提也是写好实验报告的数据可信度保障。6. 进阶改造接入自有数据集并量化遗忘率基础跑通后如果想要更好的课程设计评价或者毕设加分不要停留在跑通 CIFAR100 这一步下面两个改造方向是投入产出比最高的。方向一替换成自己的数据集。在 utils.py 里新增一个数据集分支只需要实现load_dataset(path)返回(train_images, train_labels, test_images, test_labels)然后把类别按顺序切分成不同增量任务即可。注意保持类别数整除规则假设你的数据有 20 类--start 10 --increment 2意思是先学 10 类、每轮加 2 类总共 6 轮任务。如果类别总数不能被start - increment整除最后一轮会类别不足程序容易在你意想不到的地方崩溃务必先整除再开跑。方向二量化遗忘率。增量学习里只报准确率是不够的遗忘率才是审稿人和老师更看重的指标。你可以用 validate.py 输出的历史数据在每次评估后记录所有已见类别的准确率然后计算平均遗忘率# 遗忘率计算越高说明记忆保持越差 def compute_forgetting(acc_matrix): # acc_matrix[t][i]: 第 t 个任务结束后第 i 个任务类别的准确率 num_tasks len(acc_matrix) total_forgetting 0.0 for i in range(num_tasks - 1): # 对每个旧任务 acc_first acc_matrix[i][i] # 刚学完时准确率 acc_last acc_matrix[-1][i] # 所有任务结束后准确率 total_forgetting (acc_first - acc_last) return total_forgetting / (num_tasks - 1)把每次 validate.py 输出的准确率按任务记录成行就能得到一张任务 x 任务的准确率矩阵。对角线是每个任务的峰值准确率矩阵下方是遗忘后的表现。我通常把这份矩阵连同遗忘率一起放进大作业的附录比单纯贴一张 loss 曲线更有说服力。从那以后我每次跑增量实验都强制走一遍「固定种子 → 记录准确率矩阵 → 算遗忘率」这套流程否则结果根本没法对比。希望帮到你。本文还有配套的精品资源点击获取