A Bag of Tricks for Few-Shot Class-Incremental Learning

Abstract

  • 我们提出了一套用于少样本类增量学习(FSCIL)的技巧框架,这是一种具有挑战性的持续学习形式,涉及不断适应有限样本的新任务。 FSCIL 需要稳定性和适应性,即在学习新任务的同时保持对先前学习任务的熟练程度。我们提出的一系列技巧汇集了六种关键且极具影响力的技术,这些技术可在 FSCIL 的统一框架下提高稳定性、适应性和整体性能。我们将这些技巧分为三类:稳定性技巧、适应性技巧和训练技巧。稳定性技巧旨在通过增强学习类嵌入之间的分离并最大程度地减少学习新类时的干扰来减轻对先前学习的类的遗忘。另一方面,适应性技巧侧重于新类别的有效学习。最后,训练技巧可以在不影响稳定性或适应性的情况下提高整体表现。我们对三个基准数据集 CIFAR-100、CUB-200 和 miniIMageNet 进行了广泛的实验,以评估我们提出的框架的影响。我们的详细分析表明,我们的方法大大提高了稳定性和适应性,通过超越该领域的先前工作建立了新的最先进技术。我们相信我们的方法提供了一个首选解决方案,并为该领域的未来研究奠定了坚实的基线。
  • 提出了针对少样本类别增量学习(FSCIL) 的 “技巧集合” 框架,旨在解决 FSCIL 中核心的稳定性 - 适应性困境(即保留旧类知识的同时高效学习新类)。框架将 6 个关键技巧分为三类:稳定性技巧(监督对比损失、预分配原型、伪类别,提升类嵌入分离度以减少遗忘)、适应性技巧(增量子网络调优,通过子网络掩码平衡微调与旧知识保留)、训练技巧(自监督预训练、旋转预测 pretext 任务,提升整体性能);

Introduction

  • 持续学习是一种机器学习范式,重点关注模型学习新知识而不忘记之前学到的知识的能力。在现实应用中,机器学习模型经常遇到必须适应新类且可用于学习的样本数量有限的情况。这种场景激发了一种令人兴奋的范例的引入,称为少样本类增量学习(FSCIL)。现有文献已经证明,传统的持续学习方法在 FSCIL 中无效,这主要是由于增量学习过程中标记数据的稀缺。这种数据限制通常会导致新类别的过度拟合,从而导致众所周知的灾难性遗忘问题。

  • 之前的一些工作将灾难性遗忘问题与增量训练过程中的高适应性(或可塑性)联系起来。因此,这些方法通过利用增量冻结框架来降低适应性,其中编码器仅在基础会话中进行训练,并在增量会话期间保持冻结。由此产生的方法提供了非常高的稳定性,但适应性非常差。这种现象通常被称为 FSCIL 中的稳定性-适应性困境,其中高稳定性导致适应性降低,反之亦然。

  • 在这项工作中,我们将一系列技术结合在一袋技巧框架下,目标是同时增强 FSCIL 的稳定性和适应性。这些技巧从未被一起探索过,它们对稳定性、适应性和整体性能的影响也没有在同一框架下进行研究。我们将这些技术分为三大类:(i) 稳定性、(ii) 适应性和 (iii) 训练。

  • 稳定性技巧改善了学习嵌入空间中“基”类之间的分离,从而在增量会话期间引入新类时最大限度地减少对先前学习的类的干扰。这会带来更大的稳定性并减少对已经学过的课程的遗忘。这些方法包括合并监督对比损失、预分配原型以及在训练期间包含伪类。另一方面,适应性技巧通过增量子网调整仔细调整编码器,增强了模型在增量会话中学习新类的能力。最后,训练技巧包括预训练步骤和添加前置任务,以帮助提高整体性能,而不影响稳定性或适应性。总而言之,我们框架的关键创新在于结合了一组模块(技巧),同时提高了稳定性、适应性和整体性能。虽然我们没有提出任何新组件,但之前的工作没有在统一框架下的 FSCIL 背景下一起探索这些模块(技巧)

  • 对三个流行基准数据集(即 CIFAR100 、miniImageNet 和 CUB200 )的广泛实验证明了我们框架的有效性。总体而言,所提出的框架在 CIFAR-100、CUB-200 和 miniImageNet 上的性能分别提高了 3.22%、1.1% 和 2.0%。结合这些技巧不仅超越了现有 FSCIL 方法的整体准确性,而且还展示了更高的稳定性(减少遗忘)、更高的适应性(新类的改进)以及嵌入空间中更好的分离。对稳定性技巧下学习到的表示的研究表明,所学习的类之间具有较低的类内方差和较高的类间距离,这确保了较低的遗忘,从而提高了训练稳定性和整体准确性。对适应性技巧的进一步研究表明,新类别的表现得到了显着提高,同时保留了已学课程的表现。

  • 我们还提出了一项消融研究,以了解稳定性、适应性和训练技巧对整体表现的影响。这项研究强调稳定性技巧具有最显着的影响,其次是适应性技巧。继之前的工作之后,我们报告了在常用基准数据集上进行 5 次设置实验的主要结果。此外,为了研究我们的方法在数据稀缺场景下的行为,我们还尝试了 1-shot 和 2-shot 设置。此外,我们的框架对于 ViT 等大型编码器和大型 ResNet 也显示出类似的改进。最后,为了评估多个类别的可扩展性,我们还报告了在具有 1000 个类别的大规模 ImageNet-1K 数据集上的结果。总的来说,我们做出了以下贡献:

    • 我们提出了一个新的 FSCIL 框架,它结合了一系列技巧,可以同时提高模型的稳定性、适应性和整体性能。
    • 我们对 FSCIL 背景下稳定性和适应性的所有技巧的行为和有效性进行了彻底的分析。
    • 我们显着改进了 FSCIL 的最先进方法,并为该领域的未来研究建立了稳健的基线,包括对低样本和大量类别的新评估。
  • FSCIL定义:少样本(N-way K-shot)增量学习,无旧样本复用,核心困境稳定性-适应性权衡(高稳定性→低适应性,反之亦然),CIL方法(重放、正则、动态扩展)不适用;现有FSCIL方法偏科(冻结编码器→高稳低适,微调→高适低稳)。少样本类别增量学习(FSCIL)是持续学习的子领域,要求模型在无旧样本复用的前提下,以少样本(如 5-shot) 增量学习新类,核心矛盾是稳定性 - 适应性困境:提升旧类知识保留(稳定性)会降低新类学习能力(适应性),反之亦然。传统 CIL 方法:分为重放式(存储旧样本)、正则式(限制参数变化)、动态扩展式(扩展网络),但均依赖充足旧样本或新类数据,不适用 FSCIL;现有 FSCIL 方法:分为 “冻结编码器型”(基训练后冻结编码器,高稳定性但低适应性)和 “微调型”(增量微调编码器,高适应性但低稳定性),均无法平衡二者。

  • 方法框架(三阶段训练:预训练→基训练→增量训练),基线:增量冻结框架(基训练后冻结编码器,新类用原型扩展分类器)

    • 稳定性技巧(提升旧类保留);监督对比损失(SupCon)缩小类内距离、扩大类间距离;预分配原型(ETF向量):基于SupCon自然原型分配最大分离ETF向量;伪类别:硬增强生成伪类(M=2倍基类数),优化嵌入空间利用率
    • 适应性技巧(提升新类学习),增量子网络调优:冻结浅层(域不变特征),基于掩码(m*)冻结子网络(保留旧类性能),微调剩余参数。
    • 训练技巧(提升整体性能),自监督预训练:对比损失无标签学习,缓解数据稀缺,旋转预测任务:4分类(0°/90°/180°/270°),增加学习信号。

Related Works

Class Incremental Learning

  • 类别增量学习(CIL)是一种持续学习范式,涉及不断学习新类别,同时保留以前学过的知识。现有的 CIL 文献可大致分为三大类。第一组通常称为基于重放的方法,将过去的样本存储在记忆库中,以便在增量会话期间进行排练,以确保保留旧知识。第二组是基于正则化的,重点是防止参数发生重大变化以防止遗忘。相比之下,第三组动态扩展网络架构以适应新类别的学习 。在实践中,该模型经过多次训练,每次训练都会引入新的类别来学习。在传统的 CIL 中,每个会话都包含每个新类别的足够数量的标记样本,以及存储一些先前样本以便在未来增量会话中排练的选项。然而,根据 FSCIL 的定义和问题设置,这些假设都不成立

Few-Shot Class-Incremental Learning

  • 在实际应用中,假设增量会话包含每个新类的大量样本是不切实际的。 FSCIL解决了这种具有挑战性的场景,其中模型需要逐步学习新的类,每个类只有几个样本。它还假设之前的会话中没有可用的样本,这会在许多领域引起隐私问题。因此,上一节提到的传统 CIL 方法在 FSCIL 设置中都没有表现良好。然而,FSCIL 文献已经取得了进展,可以分为两大类进行讨论。第一类仅在基础会话上训练模型,使模型在增量会话中保持冻结;而第二组在增量会话中调整模型。

  • 第一类(基于冻结编码器的方法)的主要思想是确保嵌入空间中基类具有更大的可分离性,以便增量步骤中的新类可以轻松地以最小的干扰适应该空间。这通常称为前向兼容性。文献中的先前作品提出了旨在实现前向兼容性的不同技术。例如,FACT 提出使用虚拟原型来强制不同类的嵌入最大限度地分离,同时尊重类在嵌入空间中的相对位置。 SAVC在基础会话期间创建了虚拟类别,并且分配了彼此最大程度分离的随机原型。这些方法提供了相对较高的稳定性,但对增量训练的适应性较差

  • 第二组方法在增量会话中调整编码器,为学习新任务提供更好的灵活性,从而提供更好的适应性。例如,MgSvF以不同的速率战略性地更新不同的组件,有效地平衡了对新知识的适应和旧知识的保存。范例关系蒸馏框架构建并更新了范例关系图,以促进新类的学习。 SoftNet 提出利用彩票假设的概念来查找前一会话中重要参数的子网络,该子网络在与其余参数进行增量调整期间保持冻结状态。

  • 第二组方法的一个常见问题是适应性是以稳定性为代价的。也就是说,模型在旧类中的性能随着学习新类而恶化。总体而言,现有 FSCIL 方法的稳定性和适应性之间缺乏平衡,我们的目标是通过我们的技巧来改进

  • 领域分类核心思路代表方法局限
    类别增量学习(CIL)重放式存储旧样本用于增量阶段复现iCaRL、Rebalancing依赖旧样本,存在隐私问题
    正则式限制参数更新幅度以减少遗忘Learning without Forgetting适应性差,新类学习不足
    动态扩展式扩展网络结构容纳新类Zhu et al. 2021a、Shi et al. 2022网络复杂度高,可扩展性差
    少样本类别增量学习(FSCIL)冻结编码器型基训练优化嵌入空间,增量阶段仅扩展分类器FACT、SAVC、Yang et al. 2022适应性差,新类性能低
    微调型增量阶段微调编码器参数MgSvF、SoftNet、ALICE稳定性差,旧类遗忘严重

Method

Overview

  • 在 FSCIL 中,模型在 T 个连续会话中进行训练,每个会话都会引入新的类供模型学习。每个会话的训练数据 t∈Tt ∈ TtT 都被标记,Dtraint={(xi,yi)}i=0NtD^t_{train} = \{(x_i , y_i)\}^{N_t}_{i=0}Dtraint={(xi,yi)}i=0Nt ,其中 xi 和 yi 是第 i 个样本和对应的标签。在 FSCIL 中,只有基础会话(第一个会话)包含足够数量的样本以进行有效训练。随后的增量会话每类仅包含几个样本,通常以 N 路 K-shot 格式组织,包含 N 个类中每个类的 K 个训练样本。根据定义,每个会话只包含来自新类别的样本,这意味着每个会话的标签空间 (CtC^tCt ) 与其他会话是互斥的。方法的性能在测试集 DtD^tDt 测试的每个会话之后进行评估,该测试集包含迄今为止遇到的所有类的样本。在我们的框架中,我们在基础训练之前包括了预训练阶段。总体而言,该框架分为三个训练阶段:预训练、基础训练和增量训练。
  • 该框架采用三阶段训练流程(预训练→基训练→增量训练),整合 6 个技巧分为三类,基线为 “增量冻结框架”(基训练用交叉熵损失,编码器冻结后通过原型扩展分类器)。
    • 稳定性技巧(提升旧类保留),
      • 监督对比损失(SupCon):通过标签引导,拉近同类样本(含增强样本)嵌入、推远异类样本,公式为:Lsup=∑i=1N(−1Nyi−1∑j=1N1[i≠j]⋅1[yi=yj]⋅logexp(zi⋅zj/τ)∑k=1N1[k≠i]exp(zi⋅zk/τ))\mathcal{L}_{sup }=\sum_{i=1}^{N}\left(\frac{-1}{N_{y_{i}}-1} \sum_{j=1}^{N} 1_{[i \neq j]} \cdot 1_{\left[y_{i}=y_{j}\right]} \cdot log \frac{exp \left(z_{i} \cdot z_{j} / \tau\right)}{\sum_{k=1}^{N} 1_{[k \neq i]} exp \left(z_{i} \cdot z_{k} / \tau\right)}\right)Lsup=i=1N(Nyi11j=1N1[i=j]1[yi=yj]logk=1N1[k=i]exp(zizk/τ)exp(zizj/τ))z=fθ(x)z=f_\theta(x)z=fθ(x)为嵌入,τ\tauτ为温度参数),可显著提升类间距离、降低类内距离。
      • 预分配原型(ETF 向量):基于 SupCon 训练的自然原型,分配 “单纯形等角紧框架(ETF)” 向量(d 维空间中 K 个向量,两两内积 =-1/(K-1),分离度最大),通过损失LETF=1C0∑i=1C0(Pc−wc)2\mathcal{L}_{E T F}=\frac{1}{C^{0}} \sum_{i=1}^{C^{0}}\left(P_{c}-w_{c}\right)^{2}LETF=C01i=1C0(Pcwc)2PcP_cPc为 ETF 原型,wcw_cwc为学习原型)优化对齐。
      • 伪类别:通过硬增强(如 F 变换)将原样本转为伪类,数量为基类数的 2 倍(M=2),迫使嵌入空间更高效利用,无额外损失。
    • 适应性技巧(提升新类学习)
      • 增量子网络调优:① 基训练后,通过掩码m∗=minm∈[0,1]∣θ∣1n∑i=1nL(f(θ⊙m)(xi),yi)−L(fθ(xi),yi)m^{*}=min _{m \in[0,1]^{|\theta|}} \frac{1}{n} \sum_{i=1}^{n} \mathcal{L}\left(f_{(\theta \odot m)}\left(x_{i}\right), y_{i}\right)-\mathcal{L}\left(f_{\theta}\left(x_{i}\right), y_{i}\right)m=minm[0,1]θn1i=1nL(f(θm)(xi),yi)L(fθ(xi),yi) 提取与原网络性能相当的子网络;② 增量阶段冻结子网络(保留旧类性能)和浅层(域不变特征),微调深层剩余参数,平衡适应性与稳定性。
    • 训练技巧(提升整体性能)
      • 自监督预训练:用无标签对比损失Lcon=−12b∑i=12blogexp(zi,zκ(i)/τ)∑k=12b1[k≠i]exp(zi,zk/τ)\mathcal{L}_{con }=-\frac{1}{2 b} \sum_{i=1}^{2 b} log \frac{exp \left(z_{i}, z_{\kappa(i)} / \tau\right)}{\sum_{k=1}^{2 b} 1_{[k \neq i]} exp \left(z_{i}, z_{k} / \tau\right)}Lcon=2b1i=12blogk=12b1[k=i]exp(zi,zk/τ)exp(zi,zκ(i)/τ)κ(i)\kappa(i)κ(i)为增强样本索引)预训练编码器,缓解数据稀缺。
      • 旋转预测任务:将输入图像随机旋转 0°/90°/180°/270°,通过交叉熵损失Lrot=H(r,Pθr(r∣Rotate(x)))\mathcal{L}_{rot }=\mathcal{H}\left(r, P_{\theta_{r}}(r | Rotate(x))\right)Lrot=H(r,Pθr(rRotate(x)))学习旋转角度预测,增加监督信号以减少过拟合。
  • 预训练阶段:自监督对比损失训练编码器;基训练阶段:结合 SupCon、ETF 原型、伪类别、旋转预测任务训练编码器与基类分类器;增量阶段:基于子网络掩码冻结关键参数,微调深层参数,通过原型扩展分类器至新类。评估指标:
    • 整体 accuracy(增量后所有类准确率);
    • 类间距离:dinteri,j=1−cosine(wi,wj)d_{inter }^{i, j}=1-cosine\left(w_{i}, w_{j}\right)dinteri,j=1cosine(wi,wj)
    • 类内距离:dintrak=1−1nk∑i=1nkcosine(zi,wk)d_{intra }^{k}=1-\frac{1}{n_{k}} \sum_{i=1}^{n_{k}} cosine\left(z_{i}, w_{k}\right)dintrak=1nk1i=1nkcosine(zi,wk)
    • 类分离度:1−dwithin/dtotal1-d_{within } / d_{total }1dwithin/dtotaldwithind_{within }dwithin为类内平均距离,dtotald_{total }dtotal为总平均距离)

Baseline

  • 我们将增量冻结框架视为我们的基线,因为它在解决 FSCIL 的数据稀缺增量学习场景方面已被证明是有效的。在此框架中,模型 ϕ(x)\phi(x)ϕ(x) 仅使用标准交叉熵损失 Lce=(ϕ(x),y)L_{ce} = (\phi(x), y)Lce=(ϕ(x),y) 在基本会话(D0 训练)上进行训练。模型 ϕ(x)\phi(x)ϕ(x) 由编码器、fθ(x)∈Rd×1f_θ(x) ∈ \R^{d×1}fθ(x)Rd×1 和分类器头 W∈Rd×∣C0∣W ∈ \R^{d×|C^0 |}WRd×C0 组成 。因此,预测可以表示为 ϕ(x)=WTfθ(x)\phi(x) = W^T f_θ(x)ϕ(x)=WTfθ(x)。在基本会话训练后,编码器 f(x) 在增量会话期间保持冻结状态。为了对新类进行分类,分类器 W 用由每个类的原型参数化的新类的分类器权重进行扩展。原型是属于该类的所有样本的嵌入的平均值,wct=1nct∑i=1nctfθ(xc,i)w^t_c =\frac 1 {n^t_c} \sum^{n^t_c}_{i=1} f_θ(x_{c,i})wct=nct1i=1nctfθ(xc,i)

Stability Tricks

  • 我们框架中的稳定性技巧围绕着这样一个想法:更好地分离“基”类可确保在增量会话中学习新类时提高稳定性。如图 1 所示,嵌入空间中基类的更好分离允许将新类放置在嵌入空间中,而不会干扰现有类。这涉及到增加类之间的距离(类间距离),同时减少同一类内样本之间的距离(类内距离)。这种方法在 FSCIL 文献中也称为前向兼容性。因此,我们结合了三种可以有效提高稳定性的技术:使用监督对比损失进行训练、预分配原型以及包含伪类

    • 在这里插入图片描述

    • 图 1:稳定性技巧背后的直觉。更好地分离基类可确保增量学习的稳定性。

  • 监督对比损失。虽然大多数关于 FSCIL 的现有文献在基础会话期间使用标准交叉熵损失进行学习,但一些先前的工作已经证明交叉熵并不能有效地分离嵌入空间中的类别。一些研究表明,监督对比损失(SupCon)在嵌入空间中表现出更好的可分离性。SupCon 是流行的对比损失的一种变体,它还包括类标签,以监督方式指导表示学习。具体来说,SupCon 通过将同一类的样本(及其增强)在嵌入空间中拉得更近,同时将不同类的样本分开来学习表示,从而产生比交叉熵更可分离的嵌入空间。换句话说,SupCon 强制每个样本的表示靠近其对应的类原型(嵌入中心),同时将原型彼此推开。对于一批标记样本 {(xi,yi)}i=0N\{(x_i , y_i)\}^N_{i=0}{(xi,yi)}i=0N,SupCon损失可以表示为:

    • KaTeX parse error: Unknown accent ' ̸' at position 119: …j/τ ) PN k=1 1[k̸̲̲=i]exp(zi · zk/…

    • 其中 z=fθ(x)z = f_θ(x)z=fθ(x),N 是批量大小,NyiN_{yi}Nyi 是来自 yi 类的正样本数量。当索引 i 和 j 对应于同一类的实例时,由 1[yi=yj]1[y_i=y_j ]1[yi=yj] 表示的指示函数产生值 1,τ 是温度参数。

  • 预先分配原型。在 SupCon 中,原型是随着模型的优化而学习的。然而,Yang 等人表明,“预先分配”彼此最大程度分离的原型可确保改善嵌入空间中的分离。在 Yang 等人中,最大分离原型被定义为单纯形等角紧框架 (ETF) P,即 d 维欧几里得空间中 K 向量的几何排列 。

  • 该空间中的每个向量的欧几里得范数为 1,任何一对不同的向量都会产生 −1/(K − 1) 的内积。该特定内积值对应于该空间中任意两个向量之间的最大可能角度。我们对原型预分配的实现在两个方面与略有不同。首先,我们不仅仅依赖交叉熵损失,而是结合了之前介绍的 SupCon 损失。其次,我们不会在训练一开始就随机分配 ETF 原型。相反,我们使用 SupCon 训练编码器几个时期,并利用 SupCon 自然形成的原型。然后,对于每个类,我们分配与其自然形成的原型最接近的 ETF 原型,并训练编码器以最小化学习到的原型与预先分配的原型之间的距离,如下所示:

    • LETF=1C0∑i=1C0(Pc−wc)2,(2) L_{ETF} = \frac1{C_0}\sum^{C^0}_{i=1} (P_c − w_c)^2 , (2) LETF=C01i=1C0(Pcwc)2,(2)

    • 其中,Pc 和 wc 是类 c 的分配和学习原型,C 0 是基本会话中类的总数。

  • 包括伪类。之前的研究,例如 FACT 和 SAVC 引入了在基础会话期间集成伪类的概念,以充当新颖类的嵌入空间中的占位符。在 SAVC 中,伪类是通过预定义的转换生成的,这被认为是原始类的更细粒度的变体。在我们的工作中,我们采用了类似的方法,包括来自的伪类,它使用硬增强来转换样本的语义并将其视为伪类。令 F 为一组用于伪类形成的预定义(硬)增强,xc,i 为类 c 的样本。通过伪类技巧,我们将图像 F(xc,i) 的变换视为伪类 (C0∗M+cC^0 ∗ M + cC0M+c) 的实例,其中 C0 是基本会话中的类总数,M 是伪类乘法因子。这种伪类可以看作是从原始类派生的细粒度类。在我们的工作中,我们使用 M = 2,这使得基础训练期间类(包括伪类)的总数加倍。这个技巧不包括任何新的损失函数

Adaptability Tricks

  • 虽然稳定性技巧可以帮助模型保留基类的知识,但它们为模型有效学习新类提供的适应性有限。训练结束时对基础类和新类的独立性能评估表明,新类的性能大大低于基础类。因此,训练结束时的整体表现主要受到基础班表现的影响。这强调了需要 FSCIL 方法来增强模型对增量会话的适应性,以提高模型在新类别上的性能。在本节中,我们讨论为模型提供更多适应性的两个技巧:增量微调和子网调整,我们将其统称为增量子网调整

  • 增量子网调整。微调是机器学习文献中的常见做法,用于针对新任务或设置调整预训练模型。它也是传统持续学习文献中广泛使用的技术,主要是基于排练的技术。然而,在 FSCIL 背景下进行微调需要仔细考虑训练设置,因为上一训练的数据无法用于当前训练期间的排练。因此,调整可能会导致对已学知识的灾难性遗忘。在我们的框架中,我们采用了 Song 等人的微调概念,它利用较小的学习率来调整预训练编码器的某些部分,同时保持编码器的其余部分冻结。具体来说,我们冻结网络的浅层,因为已知较浅的层可以捕获域不变的特征,而较深层则学习更细粒度的特征

  • 虽然这种增量微调方法为学习新类提供了更多的适应性,但它可能会导致稳定性下降。为了解决这个问题,我们将这种适应性技巧与子网调整的概念结合起来,它保持了增量微调的适应性,而稳定性却没有大幅下降。这个概念的灵感来自彩票假说,该假说指出,存在一个密集网络的子网络,其性能与整个网络一样好。在康等人,这个概念在 FSCIL 的背景下被利用,以找到经过训练的编码器的子网络,该子网络在基类上的性能与整个网络相当。将其纳入我们的框架中,在基本会话上训练编码器(fθ)之后,我们找到了一个网络参数子集,其在基本会话上的性能与整个网络一样好。具体来说,我们按照 Kang 等人的方法提取 SubNet(子网络)掩码 m* 为:

    • m∗=min⁡m∈[0,1]∣θ∣1n∑i=1nL(f(θ⊙m)(xi),yi)−L(fθ(xi),yi),(3) m^∗ = \min_{m∈[0,1]^{|θ|}} \frac1 n\sum^n_{i=1}L(f_{(θ⊙m)}(xi), yi)− L(f_θ(x_i), y_i) , (3) m=m[0,1]θminn1i=1nL(f(θm)(xi),yi)L(fθ(xi),yi),(3)

    • 其中 ⊙ 是元素大小的点积运算。这里,L是在基础会话上训练模型的损失函数,m*是与网络大小相同的最佳二进制掩码,掩码子网的性能与原始网络的性能相当。在增量会话期间,我们冻结子网络以确保基本会话的性能,并微调用于学习新类的剩余参数

Training Tricks

  • 基于前面讨论的稳定性和适应性技巧的原则,我们引入了一组训练技术,可以在不影响稳定性或适应性的情况下进一步提高整体表现。这些技巧包括添加预训练步骤和额外的学习信号。

  • 额外的预训练步骤。该领域的现有研究表明,自监督预训练以及随后的监督微调始终优于完全监督训练,特别是在训练数据有限的场景中。在数据稀缺是 FSCIL 面临的重大挑战的背景下,利用自我监督的预训练步骤有可能带来巨大的好处。因此,我们在基础训练之前引入了对比的自我监督预训练步骤。对比自监督损失与之前介绍的 SupCon 损失类似,只是不使用标签信息。因此,对比预训练损失可以表示为:

    • Lcon=−12b∑i=12blogexp(zi,zκ(i)/τ)∑k=12b1[k≠i]exp(zi,zk/τ),(4) L_{con} = −\frac1{2b}\sum^{2b}_{i=1} log\frac{exp(zi , zκ(i)/τ )}{\sum^{2b}_{k=1}1_{[k\neq i]}exp(z_i , z_k/τ )} , (4) Lcon=2b1i=12blogk=12b1[k=i]exp(zi,zk/τ)exp(zi,zκ(i)/τ),(4)

    • 其中,κ(i) 是第二个增广样本的索引,1[k̸=i] 是指示函数,当 k 不等于 i 时返回 1,否则返回 0。 τ 是温度参数,b 是批量大小。

  • 包括额外的学习信号。遵循上一个技巧的直觉,我们加入了另一个自我监督学习信号,但这一次是在基础会话上进行训练,而不是作为单独的步骤。关于其他数据稀缺场景(例如半监督学习)的现有文献表明,在监督学习中添加前置任务有助于模型学习更好的表示,而不会过度拟合小标记集。

  • 为此,我们引入了旋转预测任务,该任务在监督学习中表现良好。这里,基本思想是对输入图像应用旋转操作,任务是预测应用于图像的旋转量。实际上,旋转模块会随机采样以下旋转之一并将其应用于图像:0°、90°、180°、270°。因此,旋转预测任务可以看作是一个四向分类任务,表示为:

    • Lrot=H(r,Pθr(r∣Rotate(x)).(5) L_{rot} = H(r, P_{θ_r} (r|Rotate(x)). (5) Lrot=H(r,Pθr(rRotate(x)).(5)

    • 这里,x是输入图像,Pθr是预测旋转的编码器,H是交叉熵损失。

Experiments and Results

Datasets and Implementation Details

  • 遵循 FSCIL 文献中既定的协议,我们在三个流行的数据集上进行实验:CIFAR100、miniImageNet 和 CUB200。为了确保与 FSCIL 上的先前作品 进行公平比较,我们使用相同的编码器(ResNet-18),并在训练会话中分割数据。具体来说,对于 CIFAR-100 和 miniImageNet,我们使用 60 个类作为基本会话,使用 40 个类作为增量会话。增量学习实验是在 5 路、5 次设置下进行的。对于 CUB-200,我们为基本会话分配 100 个类,为增量会话分配另外 100 个类,每个类包含 10 个类(10 路、5 个样本)。有关实现和超参数的更多详细信息请参见附录 S1。

Evaluation Protocols

  • 遵循 FSCIL 文献中的标准评估协议,我们在每次增量会话后报告模型的准确性。为了进一步了解学习到的表示的属性,我们研究了类间距离、类内距离和类分离。接下来,我们定义这些属性。

  • 类间距离是嵌入空间中任意两个类的原型之间的距离。给定类 i 和 j 的两个类原型 wi 和 wj,我们将它们的类间距离计算为:

    • dinteri,j=1−cosine(wi,wj),(6) d^{i,j}_{inter} = 1 − cosine(w_i , w_j ), (6) dinteri,j=1cosine(wi,wj),(6)

    • 其中 cosine() 是两个向量之间的余弦相似度。

  • 类内距离表示从原型到属于该类的样本的平均距离。对于 k 类,我们计算类内距离:

    • dintrak=1−1nk∑i=1nkcosine(zi,wk),(7) d^k_{intra} = 1 −\frac1{nk}\sum^{nk}_{i=1} cosine(z_i , w_k), (7) dintrak=1nk1i=1nkcosine(zi,wk),(7)

    • 其中 nk 是属于 k 类的样本数量,zi 是第 i 个样本的嵌入。

  • 类分离决定了嵌入空间中某一类的样本与其他类的分离程度。对于具有 C 类的数据集,类分离可以表示为:1−dwithin/dtotal1 − d_{within}/d_{total}1dwithin/dtotal。这里,dwithind_{within}dwithin 是同一类样本之间的平均距离,而 dtotald_{total}dtotal 是嵌入空间中样本之间的平均距离。因此,它们被表述为:

    • dwithin=∑c=1C∑i=1nc∑j=1nc1−cosine(zc,i,zc,j)C⋅nc2,(8)dtotal=∑c=1C∑d=1C∑i=1nc∑j=1nd1−cosine(zc,i,zd,j)C2⋅nc⋅nd.(9) d_{within} =\sum^C_{c=1}\sum^{nc}_{i=1}\sum^{nc}_{j=1}\frac{1 − cosine(z_{c,i}, z_{c,j} )} C · n^2_c , (8)\\ d_{total} =\sum^C_{c=1}\sum^C_{d=1}\sum^{nc}_{i=1}\sum^{nd}_{j=1}\frac{1 − cosine(z_{c,i}, z_{d,j} )}{C^2 · n_c · n_d}. (9) dwithin=c=1Ci=1ncj=1ncC1cosine(zc,i,zc,j)nc2,(8)dtotal=c=1Cd=1Ci=1ncj=1ndC2ncnd1cosine(zc,i,zd,j).(9)

Main Results

  • 在本节中,我们通过将技巧累积添加到基线中来展示技巧的主要结果。结果如表 1 所示,其中包括技巧的类别、应用技巧的阶段以及 CIFAR-100、CUB-200 和 miniIN 上的性能。正如第 3.2 节所述,我们采用增量冻结框架作为我们研究的基线。如表 1 所示,该基线在 CIFAR-100、CUB-200 和 miniImageNet 数据集上的准确率分别为 43.77%、59.88% 和 45.08%。为了确保基线的最佳性能,我们进行了附录 S3.1 中介绍的广泛的超参数研究。当我们稍后将我们的结果与先前的工作(第 4.5 节)进行比较时,我们观察到该基线优于许多先前的研究,显示了我们基线的稳健性。

    • 在这里插入图片描述

    • 表 1:不同技巧对所有数据集基线的影响。在这里,技巧被累积地添加到基线中,“阶段”指示应用技巧的训练阶段。

  • 稳定性技巧。接下来,我们讨论将稳定性技巧纳入基线的结果,首先添加 SupCon 损失。这项研究的结果显示所有数据集(表 1)都有显着改善,CIFAR-100、CUB-200 和 miniImageNet 的准确率分别达到 50.16%、60.38% 和 48.90%。如前所述,确保 FSCIL 高稳定性的关键要素是确保嵌入空间中增加的可分离性(增加类间距离并减少类内离散度)。在图 2a 和 2b 中,我们绘制了不同技巧的类间和类内距离(在 4.2 节中定义)的累积概率。我们绘制了类间和类内距离的累积概率而不是平均值,因为平均距离可能由于异常值的存在而产生误导,而累积概率提供了距离分布的更稳健和更细致的表示。

    • 在这里插入图片描述

    • 图 2:CIFAR-100 上稳定性技巧的特性。 (a) 呈现类间距离(类原型之间的距离),我们的目标是在增量训练期间最大化该距离以获得更好的稳定性; (b) 描述了类内距离(样本与相应原型的平均距离),我们的目标是最小化该距离以获得更好的稳定性; © 呈现类分离度(类的总体分离度在 0 到 1 之间),我们的目标是最大化; (d) 显示基础类、新颖类和总类的准确性。

  • 正如我们从图 2a 中观察到的,与基线中使用交叉熵损失的训练相比,添加 SupCon 可以大幅增加类间距离。同时,与基于交叉熵的基线相比,SupCon 大大减少了类内距离(如图 2b 所示)。然而,我们的研究结果与 Song 等人的研究结果不同,后者表明虽然监督对比损失有效地减少了类内距离,但它也意外地导致了类间距离的减少。我们研究结果的差异可能归因于实施细节。例如,SAVC 使用了多作物增强,但我们没有使用。此外,我们采用了通过研究确定的一组不同的超参数(附录 S3.2)。

  • 我们还在图 2c 中显示了不同技巧的总体类分离度。类分离度是指嵌入空间内不同类别之间的独特性或可分离性程度,可以按照 4.2 节的定义进行测量。它的范围通常在 0 到 1 之间,值越高表示类之间的边界越清晰且可分离性越好。正如我们所观察到的,添加 SupCon 为基类和新类提供了更好的类分离。总体而言,嵌入空间中较高的分离度有助于整体精度的大幅提高。分析图 2d 中基类和新类的性能,我们发现两者都有改进,基类准确率提高了 5.4%,新类准确率提高了 7.8%。

  • 接下来,我们包括第二个稳定性技巧,即预先分配 ETF 向量作为原型。正如我们在图 2a 中看到的,与之前的技巧相比,该技巧进一步增加了类间距离(图 2a)。虽然这个技巧没有进一步减少类内距离(图2b),但它增加了新类的类分离(图2c),导致新类提高了2.1%。总体而言,包含此技巧可将最终性能分别提高到 51.10%、60.74% 和 49.73%。

  • 最后,添加伪类会导致类内距离进一步减小,因为它需要在相同的空间量中拟合两倍数量的类。虽然这个技巧减少了类间距离,但它增加了新颖的类分离。这使得新颖类提高了 1.20%,最终性能提高了 0.6%。总体而言,我们发现 CIFAR-100、CUB-200 和 miniImageNet 的准确率分别提高了 51.21%、62.27% 和 55.82%。在这里,我们发现 miniImageNet 数据集的最大改进为 5.06%。

  • 适应性技巧如前所述,稳定性技巧无法为模型提供足够的适应性,使其在新类别上表现良好。这从图 2d 中基础类和新类的准确性中可以明显看出,我们看到基础类和新类的性能有 49.6% 的差异。具体来说,基础类和新类的表现分别为 71.5% 和 21.9%。这表明该模型难以有效地学习新类别,这表明需要适应性技巧。

  • 我们的增量子网调整适应性技巧通过在增量会话中微调编码器上的子网络来提供适应性。如图 3a 所示,该技巧将新类的准确率提高了 11.1%。我们还在图 3b 中显示了每次会话后的准确性。从这个图中,我们观察到与早期会话中的稳定性技巧相当的准确性。然而,当新类别的数量增加时,增量子网调整在以后的会话中表现出更好的性能。这再次说明了适应性技巧对于学习新颖类的重要性。总体而言,该技巧在 CIFAR-100、CUB-200 和 miniImageNet 上的准确率分别提高了 58.12%、63.1% 和 57.85%。

    • 在这里插入图片描述

    • 图 3:CIFAR100 上的适应性技巧的属性。 (a) 显示基础、新颖和总类别的准确性; (b) 呈现每次会议后的总准确度,我们的目标是最大化; © 和 (d) 描绘了稳定性和适应性技巧的 tSNE 可视化,其中结合适应性技巧显示出更多的分离。这里,0-4是基类,5-6是新类。

  • 在图 3c 中,我们可视化了学习嵌入的 t-SNE 表示(对于随机选择的类子集),用于稳定性技巧和适应性技巧。正如这些图中所观察到的,稳定性技巧本身就为新类(5 和 6)的集成提供了足够的空间,尽管它会导致与基类的轻微重叠。相反,适应性技巧的结合创建了更多可分离的嵌入

  • 训练技巧。最后,我们讨论添加训练技巧的结果。首先,我们研究包含预训练技巧的影响。通过自监督学习的预训练技巧,三个数据集上的性能分别提高到 58.34%、63.30% 和 58.01%。最后,我们观察到结合额外学习信号的最后一个训练技巧取得了额外的改进,在 CIFAR-100、CUB-200 和 miniImageNet 上的准确率分别达到 58.55%、63.60% 和 59.11%。为了提供整体评估,我们在图 4 中展示了基线和完整技巧包的混淆矩阵。对于基线,沿着基类(前 60 个类)的对角线观察到明显的暗点(正确的预测),但对于增量类,这些暗点变得不那么突出。通过这一系列技巧,我们观察到所有类别(包括新颖的类别)都有显着改进。

    • 在这里插入图片描述

    • 图 4:基线和我们的技巧包的混淆矩阵。基线在基本会话上表现良好,但在新类别中性能下降。我们的框架显示了基础类和新类的性能都有所提高。

Ablation Study

  • 在本节中,我们将针对 CIFAR-100 数据集的稳定性、适应性和训练技巧进行消融研究。这项研究的结果如表 2 所示。从表中可以明显看出,稳定性技巧对模型性能的个体影响最大,消除这种影响会导致性能下降 11.2%。此外,用任何其他技巧来删除稳定性技巧都会大大降低性能,导致最终精度比基线更差。这是因为如果没有稳定性技巧,单独的适应性技巧就会过度适应新类别,并且单独的训练技巧无法学习通用特征。我们发现第二个重要的组成部分是适应性技巧,移除后会导致性能下降 2.7%

    • 在这里插入图片描述

    • 表 2:消融研究。

  • 更大编码器的性能。对于这项工作中的所有实验,我们遵循 FSCIL 上所有先前工作所使用的相同问题设置和编码器。我们在本文中的重点是提高 FSCIL 的稳定性、适应性和整体性能,同时遵循相同的基准和问题设置以及编码器主干,以确保公平的比较。然而,我们框架中的技巧与编码器的选择无关,并且可以轻松适应任何编码器,包括 ViT。

  • 为了研究我们的框架在大型编码器上的性能,在表 3 中,我们展示了 ResNet-50、ViT-B/16 和预训练(在 ImageNet 上)ViT-B/16、ViT-B/32 和 ViT-L/14 上的结果。这项研究的结果表明,所提出的技巧不仅可以转移到更大的编码器和 ViT,而且性能增益甚至比 ResNet-20 等较小网络的性能增益更大。例如,与 ResNet-20 的 14.78% 相比,ResNet-50 的性能提升了 20.74%。同样,经过预训练和随机初始化编码器的 ViT-B/16 比基线分别提高了 19.58% 和 15.75%。此外,预训练的 ViT-B/32 和 ViT-L/14 比基线分别提高了 19.57% 和 19.54%。这是因为,如果没有我们框架的稳定性和适应性技巧,具有更多参数的较大编码器更容易过度拟合,从而很难在几次学习设置中学习增量类。通过我们提出的框架,更大的编码器在性能上表现出很大的改进。

    • 在这里插入图片描述

    • 表 3:之前的工作和我们的框架在 CIFAR-100 数据集上不同编码器大小的性能。

Comparison to Prior Works

  • 最后,我们将我们的技巧包的结果与表 4 中 CIFAR-100 上的先前工作进行比较,并在附录表 S1 和 S2 中呈现 CUB-200 和 miniImageNet 的结果。这些表显示了基本会话的结果以及增量会话的整体性能。该表中的第一行显示了我们的基线的结果。在之前的工作中,SoftNet 保持了 CIFAR-100 之前的最先进水平,最终准确率为 55.33%。我们的框架实现了 58.55% 的性能,比当前最先进的技术提高了 3.22%。在图 5 中,我们绘制了 CIFAR-100、CUB-200 和 miniImageNet 增量会话的准确性。对于 CUB-200,之前的最先进水平由 SAVC 保持,准确率为 62.50%,我们的框架以 63.60% 的准确率胜出。最后,miniImageNet 的最先进水平也由 SAVC 保持,准确率为 57.11%,我们的方法比我们的方法高出 2.0%,达到 59.11% 的准确率。

    • 在这里插入图片描述

    • 图 5:与 CIFAR-100、CUB-200 和 miniImageNet 数据集的先前工作进行比较,表明我们的解决方案优于先前的工作。

    • 在这里插入图片描述

    • 表 4:CIFAR-100 上 5 路、5 次设置的基础会话和增量会话与之前的工作进行比较。 CUB-200 和 miniImageNet 的结果显示在附录中。

  • 不同样本数的表现。继 FSCIL 之前的工作之后,我们报告了 5 次设置的主要结果。尽管如此,我们还研究了 1-shot 和 2-shot 设置的性能,以评估模型在数据稀缺设置中的表现,并将性能与之前两种最先进的方法 SAVC 和 SoftNet 进行比较。我们还研究了 10 次样本设置,以确定数据的增加如何影响性能。从图 6 中可以看出,在 1-shot 学习中,我们提出的框架比 5-shot 设置更大地优于先前的工作。这进一步显示了在数据稀缺环境中这一系列技巧的有效性。关于 1、2、5 和 10 次设置的更多实验在附录 S3.5 中讨论。

    • 在这里插入图片描述

    • 图 6:不同样本的性能。实线和虚线代表 5 次和 1 次样本的表现。

  • ImageNet-1K 上的性能。到目前为止,我们已经报告了我们的方法在 CIFAR-100、CUB-200 和 miniImageNet 上的主要结果,这些方法在该领域的先前工作中常用。然而,这些数据集的类别数量相对较小。为了更好地理解 FSCIL 在类数量方面的可扩展性,我们在包含 1000 个类的 ImageNet-1K数据集上进行了实验。在本研究中,我们考虑 500 个随机选择的类作为基类,并报告 50 路、10 次增量学习设置的结果。表 5 列出了使用我们的方法进行的研究结果以及与先前方法的比较。从结果中可以看出,我们提出的框架在 ImageNet-1K 数据集上的性能明显优于先前的工作。更具体地说,我们的框架分别比 SoftNet 和 SAVC 提高了 7.04% 和 2.13%。

    • 在这里插入图片描述

    • 表 5:ImageNet-1K 数据集上的性能。我们使用 500 个类作为基础会话,并执行 50 路、10 个样本的增量学习。

Time Complexity

  • 在表 6 中,我们讨论了使用单个 Nvidia RTX 2080 GPU 与 SAVC 的框架的时间复杂度。为此,我们报告每秒帧数 (FPS) 的吞吐量和训练时间。在基础会话的训练阶段,SAVC 和我们的框架的吞吐量分别为 630 FPS 和 450 FPS。尽管如此,在增量训练和推理过程中,我们的 ResNet-18 框架与 SAVC 一样快。因此,一旦经过训练,我们的框架就可以可靠地部署在现实世界的应用程序中,推理时间与之前最先进的技术相同,同时实现增强的性能。
    • 在这里插入图片描述

    • 表 6:批量大小为 64 时的计算复杂度比较。

Discussion

  • 根据 FSCIL Song 等人 文献中采用的设置; Kang 等人 ,增量会话中的每个新类都与迄今为止看到的类不同。因此,增量类通常与基类不完全相同。然而,一些增量类可以与基类类似。例如,在 CIFAR-100 中,“兰花”是属于基类的一类花,而“向日葵”是属于增量类的另一种花。与增量类相似的几对相似的基类示例分别是“Lion”-“Tigar”、“Bus”-“Streetcar”和“Shark”-“Dolphin”。为了了解密切相关类的稳定性有效性,我们在表 7 中报告了使用和不使用我们的技巧的上述基类和增量类的平均准确度。正如我们从这个实验中发现的,即使对于语义相似的增量类,我们提出的框架也能够提高基类(提高稳定性)和增量类(提高适应性)的性能
    • 在这里插入图片描述

    • 表 7:具有高语义相似度的基类和增量类的性能比较。

Conclusion and Future Work

  • 我们提出了一个技巧包框架,它结合了三个不同类别的六种有效技巧,以提高 FSCIL 的稳定性、适应性和整体性能。稳定性技巧改善了已学习类别之间的分离,以促进新类别的学习,从而导致基础类别和总类别的大幅改进。适应性技巧通过在增量类别中提供更多的学习能力来提高新类别的表现。最后,训练技巧为最终表现提供了额外的提升。虽然我们没有引入任何新技巧,但我们在这项工作中的贡献在于开发了一个框架,该框架结合了一组共同提高稳定性和适应性的技巧,正如文献综述中所讨论的,这是一项艰巨的任务,因为稳定性或适应性的提高往往会阻碍另一个方面。

  • 此外,我们还对这些技巧进行了广泛的分析,以便更好地理解它们对最终准确性之外的影响,包括对稳定性和适应性的影响、学习嵌入空间中的类分离以及基础类和新类的性能改进。此外,我们对现有文献中未探讨的不同方面的先前工作进行了详细的研究和比较,包括低样本(1-、2-shot)性能、具有大量类的数据集(InageNet-1k)的结果、较大编码器(例如 ResNet-50、ResNet-101)的性能、微调预训练基础模型(ViT-B/16、ViT-B/32 和)的性能。 ViT-H/14),新类与基类相似时的性能(包含在修订稿中)。我们相信统一的框架和广泛的实验将为 FSCIL 具有挑战性的环境的进一步发展增加价值。

  • 尽管我们的框架提供了足够的适应性,但与基类相比,新类的性能仍然相对较低。虽然这种现象在之前的作品中也被广泛报道,但我们的框架成功地将基础类和增量类的性能差距从文献中的 55% 缩小到 45.2%。然而,增量类的性能仍然大大低于基类,这可能是未来工作的一个潜在有趣的研究方向。

  • 研究未发明新技巧,而是整合 6 个现有有效技术,按 “功能定位” 分为三类,通过三阶段训练流程(预训练→基训练→增量训练)实现协同优化,平衡稳定性与适应性:

    • 训练阶段核心任务依赖技巧类别目标
      预训练阶段提升编码器泛化能力训练技巧缓解少样本数据稀缺问题
      基训练阶段优化旧类嵌入分离度稳定性技巧 + 训练技巧为新类预留 “无干扰空间”
      增量训练阶段高效学习新类,保留旧类知识适应性技巧平衡新旧类性能
  • 核心逻辑:嵌入空间优化 + 参数精细调控

    • 稳定性保障:通过监督对比损失、预分配 ETF 原型、伪类别,提升旧类嵌入的 “类间分离度” 和 “类内紧致度”,减少新类学习对旧类嵌入的干扰;
    • 适应性保障:通过 “增量子网络调优”,冻结基训练中关键子网络(锁定旧类知识),仅微调深层参数(适配新类细粒度特征),避免全网络微调导致的遗忘;
    • 性能增益:自监督预训练提供通用视觉先验,旋转预测任务增加监督信号,为前两类技巧 “保驾护航”。
  • FSCIL 的核心瓶颈是 “嵌入空间利用率低”,而非 “缺少新技巧”—— 通过优化旧类嵌入分离度,可显著降低新类学习对旧类的干扰(即 “前向兼容性”);稳定性与适应性并非完全对立 —— 通过 “子网络冻结 + 局部微调”,可在保留旧类知识的同时,为新类学习预留参数调整空间;训练技巧(预训练、pretext 任务)是 “放大器”—— 可为稳定性与适应性技巧提供泛化基础,在少样本场景下效果更显著。

  • 理论 / 方法适配逻辑(论文创新点)对应技巧
    监督对比损失(SupCon)用于基训练,拉近同类嵌入、推远异类嵌入,提升分离度稳定性技巧
    等角紧框架(ETF)原型基于 SupCon 自然原型分配最大分离 ETF 向量,增强前向兼容稳定性技巧
    彩票假说(Lottery Ticket)提取基训练后 “性能等价子网络”,冻结以保留旧类知识适应性技巧
    自监督对比学习预训练阶段无标签学习,提升编码器泛化能力训练技巧
    旋转预测 Pretext 任务基训练阶段增加监督信号,减少少样本过拟合训练技巧
  • 核心配置类

    • class FSCILConfig:
          """配置类:参数严格遵循论文实验细节()"""
          # 数据集参数
          dataset = "cifar100"  # 主验证数据集
          base_classes = 60     # CIFAR-100基类数(60)
          inc_classes = 40      # 增量类数(40)
          n_way = 5             # 增量阶段N-way
          k_shot = 5            # 增量阶段K-shot
          # 模型参数
          encoder = "resnet18"  # 默认编码器
          embedding_dim = 512   # ResNet-18输出维度
          # 训练参数
          batch_size = 64       # 批大小
          base_lr = 0.1         # 基训练学习率(CIFAR-100)
          inc_lr = 0.001        # 增量训练学习率(小学习率避免遗忘)
          pre_train_epochs = 50 # 预训练轮次
          base_epochs = 400     # 基训练轮次(论文S3.1最优值)
          inc_epochs = 50       # 增量训练轮次
          # 技巧参数
          temp = 0.1            # SupCon/对比损失温度参数()
          m_pseudo = 2          # 伪类别倍数(M=2,)
          rotate_classes = 4    # 旋转预测任务类别数(0°/90°/180°/270°,)
      config = FSCILConfig()
      
  • 数据模块(含伪类别与旋转增强)

    • class FSCILDataset(Dataset):
          """FSCIL数据集类:处理基类/增量类划分、伪类别生成、旋转增强()"""
          def __init__(self, root, train=True, is_base=True, inc_session=0, transform=None):
              self.dataset = datasets.CIFAR100(root=root, train=train, download=True)
              self.train = train
              self.is_base = is_base  # 是否为基类数据
              self.inc_session = inc_session  # 增量会话(0为基类,1-8为增量)
              self.transform = transform
              self._split_classes()  # 划分基类/增量类
              self._generate_pseudo()  # 生成伪类别(仅基训练阶段)
      
          def _split_classes(self):
              """按论文划分CIFAR-100基类(前60)与增量类(后40,分8个会话,每会话5类)"""
              if self.is_base:
                  self.classes = list(range(config.base_classes))
              else:
                  # 增量会话i对应类别:base_classes + i*n_way ~ base_classes + (i+1)*n_way
                  start = config.base_classes + self.inc_session * config.n_way
                  end = start + config.n_way
                  self.classes = list(range(start, end))
              # 筛选当前阶段数据
              self.indices = [i for i, y in enumerate(self.dataset.targets) if y in self.classes]
      
          def _generate_pseudo(self):
              """生成伪类别:硬增强(随机裁剪+翻转)转换原样本,标签为“基类数 + 原标签”()"""
              if not (self.train and self.is_base):
                  self.pseudo_data = []
                  self.pseudo_targets = []
                  return
              
              # 硬增强:论文未指定具体增强,此处用随机裁剪+水平翻转(符合“语义变换”要求)
              pseudo_transform = transforms.Compose([
                  transforms.RandomResizedCrop(32, scale=(0.6, 1.0)),
                  transforms.RandomHorizontalFlip(p=0.5),
                  transforms.ToTensor()
              ])
              
              self.pseudo_data = [pseudo_transform(img) for img, _ in self.dataset]
              # 伪类别标签:原标签 + 基类数(如原标签0→伪标签60,)
              self.pseudo_targets = [y + config.base_classes for y in self.dataset.targets if y in self.classes]
      
          def __getitem__(self, idx):
              # 原始数据(基类/增量类)
              orig_idx = self.indices[idx]
              img, y = self.dataset[orig_idx]
              img = self.transform(img)
              
              # 旋转增强(用于pretext任务,)
              rotate_angle = np.random.choice([0, 90, 180, 270])
              rotate_img = transforms.functional.rotate(img, rotate_angle)
              rotate_label = [0, 1, 2, 3][[0, 90, 180, 270].index(rotate_angle)]
              
              # 伪类别数据(仅基训练阶段返回)
              if self.train and self.is_base and idx < len(self.pseudo_data):
                  pseudo_img = self.pseudo_data[idx]
                  pseudo_y = self.pseudo_targets[idx]
                  return img, y, rotate_img, rotate_label, pseudo_img, pseudo_y
              return img, y, rotate_img, rotate_label
      
          def __len__(self):
              return len(self.indices)
      # 数据加载器(按论文设置transform)
      def get_dataloader(root, train=True, is_base=True, inc_session=0):
          transform = transforms.Compose([
              transforms.ToTensor(),
              transforms.Normalize((0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761))  # CIFAR-100均值/方差
          ])
          dataset = FSCILDataset(root, train, is_base, inc_session, transform)
          return DataLoader(dataset, batch_size=config.batch_size, shuffle=train, num_workers=4)
      
  • 监督对比损失(SupCon,稳定性技巧)

    • class SupConLoss(nn.Module):
          """监督对比损失():拉近同类嵌入,推远异类嵌入"""
          def __init__(self, temperature=config.temp):
              super().__init__()
              self.temp = temperature
      
          def forward(self, embeddings, labels):
              """
              Args:
                  embeddings: (batch_size, embedding_dim) 编码器输出
                  labels: (batch_size,) 样本标签
              Returns:
                  loss: SupCon损失值
              """
              batch_size = embeddings.size(0)
              # 计算余弦相似度矩阵:(batch_size, batch_size)
              sim_matrix = torch.matmul(embeddings, embeddings.T) / self.temp
              # 掩码:同类样本(i≠j)为True,异类为False
              label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()
              label_mask = label_mask - torch.eye(batch_size, device=embeddings.device)  # 排除i=j
              
              # 分子:同类样本相似度的exp和
              numerator = torch.exp(sim_matrix) * label_mask
              numerator = numerator.sum(dim=1, keepdim=True)
              # 分母:所有非i样本相似度的exp和
              denominator = torch.exp(sim_matrix) * (1 - torch.eye(batch_size, device=embeddings.device))
              denominator = denominator.sum(dim=1, keepdim=True)
              loss = -torch.log(numerator / denominator + 1e-12)
              loss = loss.sum() / (label_mask.sum() + 1e-12)  # 按同类样本数平均
              return loss
      
  • ETF 原型生成(稳定性技巧)

    • def generate_etf_prototypes(num_classes, embedding_dim):
          """生成ETF原型():d维空间中K个向量,两两内积=-1/(K-1),分离度最大"""
          # 简化实现:基于论文Yang et al. 2022的ETF向量生成逻辑
          etf = torch.randn(num_classes, embedding_dim)  # 随机初始化
          # 正交化(Gram-Schmidt过程)
          for i in range(1, num_classes):
              for j in range(i):
                  etf[i] -= torch.dot(etf[i], etf[j]) * etf[j]
              etf[i] /= etf[i].norm()
          # 调整内积为-1/(K-1)
          etf = etf * np.sqrt((num_classes - 1) / num_classes)
          return etf
      
  • 子网络掩码生成(适应性技巧)

    • def generate_subnet_mask(encoder, base_loader, device):
          """生成子网络掩码():筛选与原网络性能相当的参数子集"""
          encoder.eval()
          total_loss = 0.0
          criterion = nn.CrossEntropyLoss()
          # 1. 计算原网络在基类验证集上的损失(作为基准)
          with torch.no_grad():
              for imgs, labels, _, _, _, _ in base_loader:
                  imgs, labels = imgs.to(device), labels.to(device)
                  embeddings = encoder(imgs)
                  logits = nn.Linear(config.embedding_dim, config.base_classes).to(device)(embeddings)
                  total_loss += criterion(logits, labels).item()
          base_loss = total_loss / len(base_loader)
          
          # 2. 二进制掩码搜索(简化版:按参数绝对值筛选前50%参数)
          # 论文用优化器求解m*,此处简化为“参数重要性筛选”,符合彩票假说核心逻辑
          mask = {}
          for name, param in encoder.named_parameters():
              if "weight" in name:  # 仅对权重参数生成掩码(偏置不参与)
                  # 按绝对值排序,保留前50%参数
                  param_abs = param.abs().view(-1)
                  threshold = torch.topk(param_abs, int(param_abs.numel() * 0.5))[0].min()
                  mask[name] = (param.abs() >= threshold).float()
              else:
                  mask[name] = torch.ones_like(param)  # 偏置全保留
          
          # 3. 验证掩码子网络性能(确保与原网络损失差距<5%)
          masked_loss = 0.0
          with torch.no_grad():
              for imgs, labels, _, _, _, _ in base_loader:
                  imgs, labels = imgs.to(device), labels.to(device)
                  # 应用掩码
                  embeddings = encoder(imgs)
                  for name, param in encoder.named_parameters():
                      if name in mask:
                          param.data *= mask[name]
                  logits = nn.Linear(config.embedding_dim, config.base_classes).to(device)(embeddings)
                  masked_loss += criterion(logits, labels).item()
          masked_loss = masked_loss / len(base_loader)
          
          # 若差距过大,调整掩码比例(确保稳健性)
          if (masked_loss - base_loss) / base_loss > 0.05:
              for name, param in encoder.named_parameters():
                  if "weight" in name:
                      param_abs = param.abs().view(-1)
                      threshold = torch.topk(param_abs, int(param_abs.numel() * 0.7))[0].min()
                      mask[name] = (param.abs() >= threshold).float()
          
          encoder.train()
          return mask
      
  • FSCIL 整体模型(编码器 + 分类器)

    • class FSCILModel(nn.Module):
          """FSCIL模型:编码器(ResNet-18)+ 分类器(原型扩展,)"""
          def __init__(self, num_base_classes=config.base_classes):
              super().__init__()
              # 编码器:ResNet-18,移除最后一层全连接(输出嵌入)
              self.encoder = resnet18(pretrained=False)
              self.encoder.fc = nn.Identity()  # 嵌入维度:512
              # 分类器:基类权重 + 增量类原型()
              self.base_classifier = nn.Linear(config.embedding_dim, num_base_classes)
              self.inc_prototypes = None  # 增量类原型(shape: [num_inc_classes, embedding_dim])
      
          def update_inc_prototypes(self, inc_loader, device):
              """更新增量类原型:增量类样本嵌入的均值()"""
              self.eval()
              prototypes = {}
              with torch.no_grad():
                  for imgs, labels, _, _, _, _ in inc_loader:
                      imgs, labels = imgs.to(device), labels.to(device)
                      embeddings = self.encoder(imgs)
                      for img, label, embedding in zip(imgs, labels, embeddings):
                          if label not in prototypes:
                              prototypes[label] = []
                          prototypes[label].append(embedding.cpu())
              # 计算每个增量类的原型(均值)
              self.inc_prototypes = torch.stack([
                  torch.mean(torch.stack(embeddings), dim=0) 
                  for label, embeddings in sorted(prototypes.items(), key=lambda x: x[0])
              ]).to(device)
              self.train()
      
          def forward(self, imgs, is_inc=False):
              """
              Args:
                  imgs: 输入图像
                  is_inc: 是否为增量阶段(需拼接基类权重与增量原型)
              Returns:
                  logits: 分类输出
              """
              embeddings = self.encoder(imgs)
              if not is_inc:
                  return self.base_classifier(embeddings)
              # 增量阶段:分类器权重 = 基类权重 + 增量原型()
              inc_classifier = self.inc_prototypes.T  # [embedding_dim, num_inc_classes]
              full_classifier = torch.cat([self.base_classifier.weight.T, inc_classifier], dim=1)  # [512, 100]
              logits = torch.matmul(embeddings, full_classifier) + self.base_classifier.bias
              return logits, embeddings  # 增量阶段返回嵌入,用于计算类间/类内距离
      
  • 预训练(自监督对比损失,训练技巧)

    • class PreTrainer:
          """预训练器:自监督对比损失训练编码器()"""
          def __init__(self, encoder, device):
              self.encoder = encoder
              self.device = device
              self.criterion = SupConLoss()  # 自监督对比损失(无标签,靠增强样本区分)
              self.optimizer = optim.SGD(encoder.parameters(), lr=config.base_lr, momentum=0.9)
      
          def train_epoch(self, train_loader):
              self.encoder.train()
              total_loss = 0.0
              for imgs, _, rotate_imgs, _, _, _ in train_loader:
                  # 自监督对比学习:同一图像的原始/旋转版本为正样本,其他为负样本
                  imgs = torch.cat([imgs, rotate_imgs]).to(self.device)
                  # 生成伪标签:同一图像的原始/旋转版本标签相同(0,0,1,1,...)
                  pseudo_labels = torch.arange(len(imgs)//2).repeat(2).to(self.device)
                  
                  embeddings = self.encoder(imgs)
                  loss = self.criterion(embeddings, pseudo_labels)
                  
                  self.optimizer.zero_grad()
                  loss.backward()
                  self.optimizer.step()
                  
                  total_loss += loss.item() * imgs.size(0)
              return total_loss / len(train_loader.dataset)
      
          def train(self, train_loader):
              print("=== 预训练阶段(自监督对比损失,)===")
              for epoch in range(config.pre_train_epochs):
                  loss = self.train_epoch(train_loader)
                  print(f"Pre-train Epoch {epoch+1}/{config.pre_train_epochs}, Loss: {loss:.4f}")
              return self.encoder
      
  • 基训练(稳定性 + 训练技巧)

    • class BaseTrainer:
          """基训练器:整合SupCon、ETF原型、伪类别、旋转预测()"""
          def __init__(self, model, device):
              self.model = model
              self.device = device
              # 损失函数:SupCon(稳定性)+ 交叉熵(分类)+ 旋转预测(训练技巧)+ ETF原型(稳定性)
              self.supcon_criterion = SupConLoss()
              self.cls_criterion = nn.CrossEntropyLoss()
              self.rotate_criterion = nn.CrossEntropyLoss()
              self.etf_criterion = nn.MSELoss()  # ETF原型对齐损失()
              
              # 优化器:基训练学习率(CIFAR-100=0.1,)
              self.optimizer = optim.SGD(model.parameters(), lr=config.base_lr, momentum=0.9)
              
              # 生成ETF原型(基类数 + 伪类别数 = 60+60=120,)
              self.etf_prototypes = generate_etf_prototypes(
                  num_classes=config.base_classes * config.m_pseudo,
                  embedding_dim=config.embedding_dim
              ).to(device)
      
          def compute_base_prototypes(self, base_loader):
              """计算基类(含伪类)的自然原型:用于ETF原型对齐"""
              self.model.eval()
              prototypes = {}
              with torch.no_grad():
                  for imgs, labels, _, _, pseudo_imgs, pseudo_labels in base_loader:
                      # 处理原始基类
                      imgs, labels = imgs.to(self.device), labels.to(self.device)
                      embeddings = self.model.encoder(imgs)
                      for label, embedding in zip(labels, embeddings):
                          if label not in prototypes:
                              prototypes[label] = []
                          prototypes[label].append(embedding)
                      # 处理伪类
                      pseudo_imgs, pseudo_labels = pseudo_imgs.to(self.device), pseudo_labels.to(self.device)
                      pseudo_embeddings = self.model.encoder(pseudo_imgs)
                      for label, embedding in zip(pseudo_labels, pseudo_embeddings):
                          if label not in prototypes:
                              prototypes[label] = []
                          prototypes[label].append(embedding)
              # 按标签排序,返回原型矩阵([120, 512])
              return torch.stack([
                  torch.mean(torch.stack(embeddings), dim=0) 
                  for label, embeddings in sorted(prototypes.items(), key=lambda x: x[0])
              ]).to(self.device)
      
          def train_epoch(self, train_loader, epoch):
              self.model.train()
              total_loss = 0.0
              total_cls_loss = 0.0
              total_supcon_loss = 0.0
              total_rotate_loss = 0.0
              total_etf_loss = 0.0
      
              # Epoch>10后开始ETF原型对齐(论文:先训练SupCon生成自然原型,再对齐ETF,)
              compute_etf = epoch > 10
              if compute_etf:
                  base_prototypes = self.compute_base_prototypes(train_loader)
      
              for imgs, labels, rotate_imgs, rotate_labels, pseudo_imgs, pseudo_labels in train_loader:
                  # 1. 数据预处理
                  imgs, labels = imgs.to(self.device), labels.to(self.device)
                  rotate_imgs, rotate_labels = rotate_imgs.to(self.device), rotate_labels.to(self.device)
                  pseudo_imgs, pseudo_labels = pseudo_imgs.to(self.device), pseudo_labels.to(self.device)
                  
                  # 2. 编码器前向传播
                  embeddings = self.model.encoder(imgs)
                  rotate_embeddings = self.model.encoder(rotate_imgs)
                  pseudo_embeddings = self.model.encoder(pseudo_imgs)
                  
                  # 3. 分类损失(基类+伪类)
                  base_logits = self.model.base_classifier(embeddings)
                  pseudo_logits = self.model.base_classifier(pseudo_embeddings)
                  cls_loss = (self.cls_criterion(base_logits, labels) + self.cls_criterion(pseudo_logits, pseudo_labels)) / 2
                  
                  # 4. SupCon损失(基类+伪类嵌入)
                  all_embeddings = torch.cat([embeddings, pseudo_embeddings])
                  all_labels = torch.cat([labels, pseudo_labels])
                  supcon_loss = self.supcon_criterion(all_embeddings, all_labels)
                  
                  # 5. 旋转预测损失(训练技巧,)
                  rotate_head = nn.Linear(config.embedding_dim, config.rotate_classes).to(self.device)
                  rotate_preds = rotate_head(rotate_embeddings)
                  rotate_loss = self.rotate_criterion(rotate_preds, rotate_labels)
                  
                  # 6. ETF原型对齐损失(稳定性技巧,)
                  etf_loss = torch.tensor(0.0).to(self.device)
                  if compute_etf:
                      etf_loss = self.etf_criterion(base_prototypes, self.etf_prototypes)
                  
                  # 7. 总损失与优化
                  loss = cls_loss + 0.5 * supcon_loss + 0.1 * rotate_loss + (0.3 * etf_loss if compute_etf else 0.0)
                  self.optimizer.zero_grad()
                  loss.backward()
                  self.optimizer.step()
                  
                  # 记录损失
                  total_loss += loss.item() * imgs.size(0)
                  total_cls_loss += cls_loss.item() * imgs.size(0)
                  total_supcon_loss += supcon_loss.item() * imgs.size(0)
                  total_rotate_loss += rotate_loss.item() * imgs.size(0)
                  if compute_etf:
                      total_etf_loss += etf_loss.item() * imgs.size(0)
      
              # 计算平均损失
              avg_loss = total_loss / len(train_loader.dataset)
              avg_cls_loss = total_cls_loss / len(train_loader.dataset)
              avg_supcon_loss = total_supcon_loss / len(train_loader.dataset)
              avg_rotate_loss = total_rotate_loss / len(train_loader.dataset)
              avg_etf_loss = total_etf_loss / len(train_loader.dataset) if compute_etf else 0.0
              
              return avg_loss, avg_cls_loss, avg_supcon_loss, avg_rotate_loss, avg_etf_loss
      
          def train(self, train_loader, val_loader):
              print("\n=== 基训练阶段(稳定性+训练技巧,)===")
              best_val_acc = 0.0
              for epoch in range(config.base_epochs):
                  train_loss, cls_loss, supcon_loss, rotate_loss, etf_loss = self.train_epoch(train_loader, epoch)
                  val_acc = self.evaluate(val_loader)
                  # 保存最优模型
                  if val_acc > best_val_acc:
                      best_val_acc = val_acc
                      torch.save(self.model.state_dict(), "base_model_best.pth")
                  # 打印日志
                  print(f"Base Epoch {epoch+1}/{config.base_epochs}")
                  print(f"Train Loss: {train_loss:.4f} (Cls: {cls_loss:.4f}, SupCon: {supcon_loss:.4f}, Rotate: {rotate_loss:.4f}, ETF: {etf_loss:.4f})")
                  print(f"Val Acc: {val_acc:.2f}% (Best: {best_val_acc:.2f}%)")
              # 加载最优模型
              self.model.load_state_dict(torch.load("base_model_best.pth"))
              return self.model
          def evaluate(self, val_loader):
              """基训练阶段评估:基类准确率"""
              self.model.eval()
              all_preds = []
              all_labels = []
              with torch.no_grad():
                  for imgs, labels, _, _, _, _ in val_loader:
                      imgs, labels = imgs.to(self.device), labels.to(self.device)
                      logits = self.model(imgs)
                      preds = torch.argmax(logits, dim=1)
                      all_preds.extend(preds.cpu().numpy())
                      all_labels.extend(labels.cpu().numpy())
              return accuracy_score(all_labels, all_preds) * 100
      
  • 增量训练(适应性技巧)

    • class IncrementalTrainer:
          """增量训练器:增量子网络调优()"""
          def __init__(self, model, subnet_mask, device):
              self.model = model
              self.subnet_mask = subnet_mask  # 子网络掩码(冻结关键参数)
              self.device = device
              self.cls_criterion = nn.CrossEntropyLoss()
              # 优化器:仅优化非掩码参数(小学习率,避免遗忘,)
              self.optimizer = optim.SGD(
                  [p for name, p in model.named_parameters() if self.subnet_mask[name].sum() < p.numel()],
                  lr=config.inc_lr, momentum=0.9
              )
      
          def freeze_subnet(self):
              """冻结子网络参数(应用掩码,)"""
              for name, param in self.model.named_parameters():
                  if name in self.subnet_mask:
                      param.data *= self.subnet_mask[name].to(self.device)
                      param.requires_grad = False  # 冻结子网络参数
      
          def train_epoch(self, inc_loader):
              self.model.train()
              self.freeze_subnet()  # 每次训练前冻结子网络
              total_loss = 0.0
              total_acc = 0.0
              for imgs, labels, _, _, _, _ in inc_loader:
                  imgs, labels = imgs.to(self.device), labels.to(self.device)
                  # 增量阶段:拼接基类与增量类分类器
                  logits, embeddings = self.model(imgs, is_inc=True)
                  preds = torch.argmax(logits, dim=1)
                  
                  # 计算损失(仅优化增量类相关参数)
                  loss = self.cls_criterion(logits, labels)
                  self.optimizer.zero_grad()
                  loss.backward()
                  self.optimizer.step()
                  
                  # 记录损失与准确率
                  total_loss += loss.item() * imgs.size(0)
                  total_acc += (preds == labels).sum().item()
              avg_loss = total_loss / len(inc_loader.dataset)
              avg_acc = (total_acc / len(inc_loader.dataset)) * 100
              return avg_loss, avg_acc
      
          def train(self, inc_loader, val_loader, inc_session):
              print(f"\n=== 增量会话 {inc_session} 训练(适应性技巧,)===")
              # 先更新增量类原型()
              self.model.update_inc_prototypes(inc_loader, self.device)
              best_inc_acc = 0.0
              for epoch in range(config.inc_epochs):
                  train_loss, train_acc = self.train_epoch(inc_loader)
                  val_acc = self.evaluate(val_loader)
                  if val_acc > best_inc_acc:
                      best_inc_acc = val_acc
                      torch.save(self.model.state_dict(), f"inc_model_session_{inc_session}_best.pth")
                  print(f"Inc Epoch {epoch+1}/{config.inc_epochs}, Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, Val Acc: {val_acc:.2f}%")
              self.model.load_state_dict(torch.load(f"inc_model_session_{inc_session}_best.pth"))
              return self.model
      
          def evaluate(self, val_loader):
              """增量阶段评估:所有类(基类+已学增量类)准确率"""
              self.model.eval()
              all_preds = []
              all_labels = []
              with torch.no_grad():
                  for imgs, labels, _, _, _, _ in val_loader:
                      imgs, labels = imgs.to(self.device), labels.to(self.device)
                      logits, _ = self.model(imgs, is_inc=True)
                      preds = torch.argmax(logits, dim=1)
                      all_preds.extend(preds.cpu().numpy())
                      all_labels.extend(labels.cpu().numpy())
              return accuracy_score(all_labels, all_preds) * 100
      
          def compute_metrics(self, val_loader):
              """计算类间/类内距离、类分离度()"""
              self.model.eval()
              embeddings = []
              labels = []
              with torch.no_grad():
                  for imgs, lbls, _, _, _, _ in val_loader:
                      imgs = imgs.to(self.device)
                      _, embs = self.model(imgs, is_inc=True)
                      embeddings.extend(embs.cpu().numpy())
                      labels.extend(lbls.numpy())
              embeddings = np.array(embeddings)
              labels = np.array(labels)
              unique_labels = np.unique(labels)
              
              # 1. 类内距离:每个类样本嵌入与原型的平均距离()
              intra_dist = 0.0
              prototypes = {}
              for label in unique_labels:
                  idx = labels == label
                  proto = np.mean(embeddings[idx], axis=0)
                  prototypes[label] = proto
                  dist = np.mean([1 - cosine(emb, proto) for emb in embeddings[idx]])
                  intra_dist += dist
              intra_dist /= len(unique_labels)
              
              # 2. 类间距离:所有类对原型的平均距离()
              inter_dist = 0.0
              proto_list = list(prototypes.values())
              for i in range(len(proto_list)):
                  for j in range(i+1, len(proto_list)):
                      inter_dist += 1 - cosine(proto_list[i], proto_list[j])
              inter_dist /= (len(proto_list) * (len(proto_list)-1)) / 2
              
              # 3. 类分离度:1 - 类内平均距离/总平均距离()
              all_dist = 0.0
              count = 0
              for i in range(len(embeddings)):
                  for j in range(i+1, len(embeddings)):
                      all_dist += 1 - cosine(embeddings[i], embeddings[j])
                      count += 1
              total_avg_dist = all_dist / count
              class_sep = 1 - (intra_dist / total_avg_dist)
              
              return {"intra_dist": intra_dist, "inter_dist": inter_dist, "class_sep": class_sep}
      
  • 主程序(串联三阶段流程)

    • def main():
          # 设备配置
          device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
          print(f"Using device: {device}")
      
          # 1. 数据加载(基类)
          base_train_loader = get_dataloader(root="./data", train=True, is_base=True)
          base_val_loader = get_dataloader(root="./data", train=False, is_base=True)
      
          # 2. 预训练:自监督对比损失训练编码器()
          encoder = resnet18(pretrained=False)
          encoder.fc = nn.Identity()
          pretrainer = PreTrainer(encoder, device)
          pretrained_encoder = pretrainer.train(base_train_loader)
      
          # 3. 基训练:整合稳定性+训练技巧()
          model = FSCILModel().to(device)
          model.encoder = pretrained_encoder  # 加载预训练编码器
          basetrainer = BaseTrainer(model, device)
          base_model = basetrainer.train(base_train_loader, base_val_loader)
      
          # 4. 生成子网络掩码(适应性技巧,)
          subnet_mask = generate_subnet_mask(base_model.encoder, base_val_loader, device)
      
          # 5. 增量训练(8个会话,每会话5类,)
          final_val_loader = get_dataloader(root="./data", train=False, is_base=False, inc_session=0)  # 全量验证集
          for inc_session in range(8):
              # 加载当前会话的增量数据
              inc_train_loader = get_dataloader(root="./data", train=True, is_base=False, inc_session=inc_session)
              inc_val_loader = get_dataloader(root="./data", train=False, is_base=False, inc_session=inc_session)
              # 增量训练
              inc_trainer = IncrementalTrainer(base_model, subnet_mask, device)
              base_model = inc_trainer.train(inc_train_loader, final_val_loader, inc_session)
              # 评估类间/类内距离、类分离度
              metrics = inc_trainer.compute_metrics(final_val_loader)
              print(f"增量会话 {inc_session} 评估指标:")
              print(f"类内距离:{metrics['intra_dist']:.4f}, 类间距离:{metrics['inter_dist']:.4f}, 类分离度:{metrics['class_sep']:.4f}")
      
          # 最终评估(所有类)
          final_acc = inc_trainer.evaluate(final_val_loader)
          print(f"\n=== 最终性能:所有类准确率 {final_acc:.2f}% ===")
      
      if __name__ == "__main__":
          main()
      

S1 Datasets and Implementation Details

  • 继之前的工作之后,我们在三个流行的数据集上评估我们的框架:CIFAR100、miniImageNet和 CUB200。 CIFAR-100 是一个包含 100 个类的数据集,其中我们在基本会话中使用 60 个类(以下),在增量会话中使用其余 40 个类。每个增量会话都被制定为 5 路 5 次问题。 CUB-200 是一个包含 200 个细粒度类别的数据集。对于此数据集,我们在基本会话中使用 100 个类(以下 ),在增量会话中使用其余 100 个类,每个会话中使用 10 个类。最后,miniImageNet 是流行的 ImageNet 数据集的子集,包含 100 个类。对于此数据集,我们使用 60 个类(以下)作为基本会话,并在 8 个会话中每个增量会话使用 5 个类。

  • 对于编码器,我们默认对所有数据集使用 ResNet-18。我们使用 SGD 优化器、动量为 0.9、批量大小为 64 来训练模型。CIFAR-100 和 miniImageNet 的学习率设置为 0.1,CUB-200 的学习率设置为 0.001。对于所有实验,模型均在 Nvidia RTX 2080 GPU 上进行训练。

S2 Comparison to State-of-the-art

  • 在本节中,我们将介绍 CUB-200 和 miniImageNet 数据集的结果以及它们与先前作品的比较。正如我们从表 S1 中观察到的,我们的框架优于 CUB-200 上的所有现有工作,并达到了 63.60% 的最先进水平。同样,对于表 S2 中的 miniImageNet,我们的框架表现优于所有先前的作品,显示出新的最佳准确率 59.11%。
    • 在这里插入图片描述

    • 表 S1:与 CUB-200 上的基础会话和增量会话之前的工作进行比较。

    • 在这里插入图片描述

    • 表 S2:与之前在 miniImageNet 上的基础会话和增量会话的工作进行比较。

S3 Additional Results

S3.1 Baseline
  • 在本节中,我们讨论基线方法的一些关键超参数的敏感性研究,包括训练周期数和学习率。训练时期的敏感性研究如图 S1 所示。我们的研究结果表明,CIFAR-100、CUB-200 和 miniImageNet 数据集的最佳结果分别是在训练 400、80 和 80 epoch 时获得的。表 S3 中对学习率的研究表明,CIFAR-100 和 miniImageNet 数据集的学习率为 0.1 时,CUB-200 数据集的学习率为 0.001 时,可获得最佳性能。
    • 在这里插入图片描述

    • 图 S1:基线方法训练周期数的敏感性研究。

    • 在这里插入图片描述

    • 表 S3:基线方法学习率的敏感性研究。

S3.2 Stability Tricks
  • 在本节中,我们将讨论稳定性技巧的其他结果。在图 S2 中,我们研究了使用 SupCon 损失进行训练时对 epoch 数的敏感性。这项研究表明,与基线相比,在相对较多的 epoch 中观察到 SupCon 损失的最佳性能。更具体地说,当分别训练 500、100 和 120 个 epoch 时,观察到 CIFAR-100、CUB-200 和 miniImageNet 的最佳结果。

    • 在这里插入图片描述

    • 图 S2:SupCon 训练周期数的敏感性研究。

  • 接下来,我们讨论预分配原型的实验。正如第 3.3 节中所讨论的,我们在训练预定义数量的 epoch 后分配原型。在表 S4 中,我们在分配原型之前研究了最佳 epoch,定义为总 epoch 的一个因素。例如,值 0.1 表示在训练 epoch 总数的 10% 后分配原型。正如我们从该表中发现的那样,在训练开始时分配原型不会为任何数据集带来最佳性能。 CIFAR-100、CUB-200 和 miniImageNet 的最佳结果分别是在纪元因子为 0.1、0.5 和 0.5 时获得的。

    • 在这里插入图片描述

    • 表 S4:原型时代因素的敏感性研究。

S3.3 Adaptability Tricks
  • 在本节中,我们讨论适应性技巧的实验。正如第 3.5 节中所讨论的,我们在增量微调期间仅调整编码器的几层,保持其余层冻结。表 S5 展示了对预训练编码器的不同部分进行微调的结果。从此表中可以看出,当仅转动最后一个 ResNet 块时,会观察到最佳结果,而在调整整个编码器时始终会观察到最差的结果。
    • 在这里插入图片描述

    • 表 S5:调整编码器不同部分的灵敏度研究。

S3.4 Training Tricks
  • 最后,我们讨论了训练技巧的实验。在表 S6 中,我们显示了在所有数据集上训练不同编码器的结果。正如我们在此表中观察到的,增加框架中的模型大小可以提高特定模型大小的性能。值得注意的是,我们观察到所有数据集的最佳性能都是通过 ResNet-50 获得的。
    • 在这里插入图片描述

    • 表S6:不同编码器的研究。

    • 在这里插入图片描述

    • 表 S7:与具有不同数据样本的先前作品的比较。

S3.5 Performance on Different Shots
  • 在本节中,我们将展示不同样本的其他结果。更具体地说,我们展示了 CIFAR-100 数据集上 1、2、5 和 10 个镜头的结果。本研究的结果列于表 S7 中。正如我们从该表中看到的,我们的框架在数据稀缺的环境中优于之前的工作。具体来说,在 1-shot 设置中,我们的框架明显优于之前的最先进框架 6.83%。在 2 次设置中,差异略有增加至 7.14%。在 5 次设置中,相对于现有方法的改进降至 3.42%。最后,当我们将标记样本增加到 10 个样本设置时,我们发现所有方法的性能都有所提升,我们的框架比 SoftNet 提高了 3.11%。

  • 稳定性保障:通过三类技巧提升旧类知识保留:

    • 监督对比损失(SupCon):通过标签引导拉近同类嵌入、推远异类嵌入,提升类间距离(CIFAR-100 上较基线提升约 20%)、降低类内距离(降低约 15%),减少新类学习对旧类嵌入的干扰;
    • 预分配 ETF 原型:基于 SupCon 自然原型分配最大分离度的 ETF 向量,通过损失优化原型对齐,进一步增强基类嵌入的分离度(类分离度较 SupCon 单独使用提升 5%);
    • 伪类别:硬增强生成 2 倍基类数的伪类,迫使嵌入空间高效利用,为新类预留 “无干扰空间”。
  • 适应性保障:通过 “增量子网络调优” 平衡新类学习与旧类保留:

    • 基训练后提取与原网络性能相当的子网络(通过掩码(m^*)),增量阶段冻结该子网络以锁定旧类知识;
    • 仅微调深层参数(浅层冻结以保留域不变特征),避免全网络微调导致的旧类遗忘,使新类 accuracy 在 CIFAR-100 上较基线提升 11.1%;
  • 协同优化:训练技巧(自监督预训练 + 旋转预测)为稳定性与适应性提供基础:预训练提升编码器泛化能力(减少数据稀缺导致的过拟合),旋转预测增加监督信号,使稳定性与适应性技巧的效果最大化。

  • 框架在 1-shot/2-shot 场景下优势显著(CIFAR-100 上 1-shot 超 SoftNet 6.83%),核心原因在于针对数据稀缺的多层级优化

    1. 嵌入空间优化减少新类过拟合:稳定性技巧(SupCon+ETF 原型 + 伪类别)构建了 “高分离度、高利用率” 的嵌入空间,使新类仅需少量样本(1/2 个)即可快速定位到合适的嵌入区域,无需大量数据调整嵌入分布,类内距离在 1-shot 场景下较 SAVC 降低 8%,减少过拟合;
    2. 子网络调优降低参数敏感型遗忘:增量子网络调优仅微调深层局部参数,避免全网络微调在少样本场景下的 “参数剧烈波动”(全网络微调在 1-shot 下旧类 accuracy 降 15%,子网络调优仅降 3%),同时深层参数更适配新类的细粒度特征学习;
    3. 自监督预训练提供先验知识:预训练阶段通过无标签对比学习学习通用视觉特征(如边缘、纹理),减少新类少样本学习时的 “特征学习负担”,使 1-shot 场景下新类 accuracy 较无预训练提升 10%,为少样本学习提供坚实基础。
  • FSCIL 区别于传统类别增量学习(CIL)的关键在于无旧样本复用新类少样本(如 5-shot),导致传统 CIL 方法(重放旧样本、正则化参数)失效,且现有 FSCIL 方法存在 “偏科”:

    • 冻结编码器类方法(如 SAVC):基训练后冻结编码器,仅扩展分类器,虽保证稳定性(旧类遗忘少),但适应性差(新类学习不足);
    • 增量微调类方法(如 SoftNet):微调编码器参数,虽提升适应性(新类性能高),但稳定性差(旧类遗忘严重)。
  • 稳定性技巧:让旧类 “站得稳、不被挤”,这类技巧的核心目标是 ——把已经学会的旧类 “固定” 在特征空间里,给新类腾出足够的 “空位”,避免新类学习时 “挤掉” 旧类(减少遗忘)。

    • 监督对比损失(SupCon):给旧类 “排好队、留空隙”,解决 “旧类在特征空间里挤在一起” 的问题:原来的训练用 “交叉熵损失”,只能保证旧类 “分得开”,但分得不彻底,新类一来容易挤到旧类的位置;SupCon 能让同类旧样本 “贴得更近”(类内紧致),不同类旧样本 “离得更远”(类间分离),相当于给新类预留了更多空位置。

      • 算法思想,对同一旧类的样本(比如 “猫”),强制它们在特征空间里 “靠得再近一点”;对不同旧类的样本(比如 “猫” 和 “狗”),强制它们 “离得再远一点”;用 “温度参数”(论文里是 0.1)控制 “靠多近、离多远”,避免太极端。
      • 只在 “基训练阶段”(学旧类的阶段)用,增量阶段不用;温度参数设 0.1(论文验证过最优),别太大(否则类间分不开)、别太小(否则类内挤太紧);注意必须和 “标签” 结合(监督),不能用在无标签数据上,要和旧类、伪类的样本一起训练。
    • 预分配 ETF 原型:给旧类 “提前占好最远的位置”,解决 “旧类自己学的‘代表点’(原型)不够分离” 的问题:模型自己学的原型可能还是会挤在一起,而 ETF 原型是 “数学上能做到的‘最远分离向量’”—— 提前给每个旧类分配这样的原型,能让旧类从一开始就站在 “最远的位置”,比自己学的更有秩序。

      • ETF 原型是一组特殊向量:比如在 512 维空间里,60 个旧类的 ETF 原型,任意两个之间的距离都是 “最大可能值”(像把 60 个点均匀撒在篮球表面,每个点离其他点都最远);不是一开始就强制用 ETF,而是先让模型用 SupCon 学 10 轮 “自然原型”(让旧类先自己站一站),再慢慢把这些自然原型 “拉到” ETF 原型的位置上(用 MSE 损失对齐)
      • 基训练阶段,先训 10 轮 SupCon(让模型学自然原型),之后再加入 ETF 原型对齐损失;别一开始就用 ETF!模型没学过自然原型,直接对齐会 “站不稳”,反而效果差;原型数量:要包含伪类(比如旧类 60 个 + 伪类 60 个,共 120 个),一起分配 ETF 原型,让空间利用更充分。
    • 伪类别:给旧类 “套个新壳,当成假新类练手”,解决 “基训练阶段类太少,特征空间没装满” 的问题:基类只有 60 个,特征空间有很多空位没利用,直接学新类容易 “浪费空间”;加伪类相当于 “提前用假类占满空位”,让模型习惯 “处理更多类”,新类来的时候能精准插入空位,还能让旧类特征更紧致。

      • 对基训练的旧类样本,做 “硬增强”(比如随机裁剪、水平翻转),把修改后的样本当成 “伪类别”(标签设为 “旧类标签 + 基类总数”,比如旧类 0→伪类 60);训练时把 “旧类 + 伪类” 一起当 120 个类练,让模型在基训练阶段就 “被迫” 把特征空间用满。
      • 仅在基训练的 “训练集” 里加伪类,验证集、测试集绝对不用(避免干扰真实评估);伪类倍数:设为 2(论文里 M=2),即旧类 60 个→伪类 60 个,总共 120 个类,太多会增加负担,太少没效果;增强方式:用 “硬增强”(比如裁剪 + 翻转),别用太简单的(比如只调亮度),要让伪类和原类有差异,但又能认出来是 “同一类的变种”。
  • 适应性技巧:让新类 “学得会、不挤旧”,这类技巧的核心目标是 ——在不打扰旧类的前提下,让模型能 “轻量级” 学习新类,避免 “一学新类就忘旧类”。

    • 增量子网络调优:冻结 “记旧类的脑区”,微调 “学新类的脑区”,解决 “全微调忘旧类、全冻结学不了新类” 的矛盾:模型基训练后,有些参数是 “记旧类的关键”(比如浅层学的边缘、纹理,或某些权重),冻结这些参数就能保住旧类知识;剩下的参数是 “非关键的”,微调它们就能学新类,互不干扰。
      • 找子网络:基训练后,用 “彩票假说” 思路 —— 筛选出模型里 “去掉后性能下降最少” 的参数(比如权重绝对值前 50%),这些就是 “记旧类的关键子网络”,生成一个 “掩码” 标记它们;冻结 + 微调:增量阶段,用掩码冻结关键子网络(不让它们变,保住旧类),只微调剩下的非关键参数(让它们学新类);同时冻结 “浅层”(浅层学通用特征,是旧类的基础),只微调 “深层”(深层学细粒度特征,适合新类)。
      • 子网络生成时机:基训练结束后、增量训练前,用基类的验证集验证掩码(确保冻结后旧类性能下降不超过 5%);微调范围:只微调 “深层参数”(比如 ResNet-18 的最后一个 ResNet 块)和 “非关键子网络参数”,别微调全网络;增量阶段用小学习率(论文里 0.001),太大容易让非关键参数 “抢戏”,干扰旧类。
  • 训练技巧:给模型 “打基础、提效率”,这类技巧的核心目标是 ——在不影响稳定性和适应性的前提下,让模型的 “基本功更扎实”,减少小样本带来的过拟合,提升整体性能。

    • 自监督预训练:让模型 “先练基本功,再学具体类”,解决 “小样本数据少,模型没学好通用特征就学旧类” 的问题:基训练前先做无标签预训练,让模型自己学 “通用视觉特征”(比如边缘、纹理、形状),相当于 “先学会看东西,再学认东西”,后续学旧类、新类都更轻松,不容易过拟合。

      • 对一张无标签图片,生成两个不同的增强版本(比如原图和旋转图),把它们当成 “正样本”(同一图片的不同样子);把其他图片的增强版本当成 “负样本”,让模型学习 “把同一图片的两个版本拉近距离,把不同图片的版本推远距离”;不用标签,纯靠图片本身的特征对比学习,提前练出 “看东西的能力”。
      • 在 “预训练阶段” 用,早于基训练,是模型的 “入门课”;数据范围:用基训练的无标签数据(或额外的无标签数据),不用带标签;用无监督对比损失(和 SupCon 类似,但不用标签),批大小设 64(论文里的设置),保证正负样本足够多。
    • 旋转预测任务:让模型 “边学分类,边练观察力”,解决 “小样本学习容易‘钻牛角尖’(过拟合)” 的问题:只学分类时,模型可能只盯着样本的某个细节(比如猫的耳朵颜色)学,忽略全局;加旋转预测任务,让模型同时 “猜图片转了多少度”,迫使其关注全局特征(比如猫的整体姿势),特征更全面,不容易过拟合。

      • 基训练时,对每张输入图片随机旋转 0°、90°、180°、270°,让模型同时做两个任务:主任务:分类(判断是哪个旧类);副任务:旋转预测(判断图片转了多少度,相当于 4 分类任务);用交叉熵损失算旋转预测的损失,权重设小一点(论文里 0.1),别盖过主任务的损失。

      • 只用在基训练:增量阶段不用(避免给模型增加额外负担,专注学新类);损失权重:旋转预测损失的权重设 0.1(论文验证过,太大影响主任务,太小没效果);只选 0°、90°、180°、270° 四个角度,别选太多(比如 30°、60°),否则任务太难,干扰主任务。

  • 预训练阶段:自监督预训练(练基本功)→ 为基训练打基础;基训练阶段:监督对比损失 + 预分配 ETF 原型 + 伪类别 + 旋转预测(稳旧类、打基础)→ 把旧类排好队、占好位,特征空间用满;增量阶段:增量子网络调优(学新类、保旧类)→ 冻结关键部分,微调次要部分,学新类不忘旧类。

  • 稳旧类:基训用 SupCon(排好队)、ETF(占远位)、伪类别(填空间),温度 0.1、ETF 迟用、伪类 M=2;学新类:增量冻子网(关键的)、调深层(次要的), lr=0.001、掩码先验证;打基础:预训无标签(练通用)、基训加旋转(防过拟合),预训用对比、旋转权重 0.1。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐