读少样本学习的论文,绕不开早期几篇经典工作。这篇《Meta-Transfer Learning for Few-Shot Learning》(下称 MTL)我一直放在精读清单里。它没有靠堆特别复杂的模块刷点,而是把“迁移学习”和“元学习”两条并行的技术路线拧成了一套两阶段的训练框架。这篇论文阅读笔记我会按自己读论文的顺序展开:先讲它想解决的问题,再拆方法主体,然后落到实验和复现细节,最后聊聊我认为值得继续做的方向。如果你想入少样本学习,或者做小样本分类时总觉得微调不稳定,花二十分钟读这篇应该会有收获。
1. 少样本学习到底难在哪
1.1 从一个人工标注的困境说起
先抛一个实际问题:你现在需要给某种稀有植物做品种识别,每个品种手里只有三五张照片,怎么训练一个可靠的分类器?按常规思路,要么继续找人拍更多照片,要么用一个已经训练好的模型直接做迁移。但前者成本高,后者因为目标类别样本太少,微调时很容易把预训练学到的特征搞坏。这类问题就是少样本学习(Few-Shot Learning)的典型场景:训练阶段每个类别只有极少标注样本,通常就是一张或五张。
少样本学习的常规评测方式是 N-way K-shot。N-way 表示每个任务里随机抽几个类别,K-shot 表示每个类别放几张标注样本。比如 5-way 1-shot,就是从数据集里随机抽 5 个类别,每个类别只给 1 张标注图,剩下的图作为查询集用来计算准确率。这样一来,模型必须在“只看过非常少的样本”条件下快速适应新任务。设置虽然简单,但它的难度在于:同一个数据集里,任务要随机抽取不同的类别组合来反复训练,模型要学的不是某一类长什么样,而是“如何快速适应一个没见过的新分类任务”。
1.2 常见解法为什么不够用
针对这个问题,当时主要有两条技术路线。
第一条是纯迁移学习:先在基类大数据集上训练一个特征提取器,然后在小样本任务上微调分类层或全部参数。这条路的问题很明显——微调全部参数时,每类只有一两张样本,网络很容易在训练任务上过拟合;如果只微调分类层,又受限于特征空间不够自适应,遇到和基类分布差异较大的新类别时表现会急剧下降。
第二条是元学习,也就是“学习如何学习”。典型代表是 MAML 这类方法,目标是从大量小任务中学到一个好的初始化参数,使模型在新任务上只需要几步梯度更新就能快速收敛。思路很优雅,但实际跑起来有几个硬伤:一是每次任务更新都要计算二阶梯度,显存和算力开销非常大;二是直接在特征提取器所有参数上做元学习,需要大量任务来支撑训练,任务数量不够时,最终学到的初始化参数反而不如简单预训练来得稳。当时不少实验也证实,元学习方法在小规模训练任务上很容易发生过拟合,收敛速度还慢。
1.3 论文核心思路一句话概括
面对这两条路各自的短板,MTL 给的方案说穿了也很朴素:先用大规模基类数据把特征提取器训好,然后在元学习阶段只更新一组轻量的缩放器参数,而不是更新整个网络。这样做的好处是,特征表示由数据量充足的预训练阶段负责,元学习阶段只负责学习“如何根据当前任务重新校准特征分布”。参数一少,过拟合风险大幅度下降,训练速度也快了很多。
这个思路放在当时挺反直觉。因为很多人默认元学习就应该动整个模型的参数,不然怎么算“适应”新任务?但 MTL 用实验说明,适不适应新任务不一定要改所有参数,有时候只需要对那些已经很有用的特征做点“微调”,效果反而更好。这也是标题里 Meta-Transfer 两个词的含义所在——把元学习和迁移学习放在同一个框架里用,各取所长。
2. MTL 的两阶段框架
2.1 阶段一:大规模基类上的预训练
MTL 的训练流程分成两个阶段,第一步是在基类数据集上做标准分类预训练。这个步骤和我平时训练普通分类模型没有本质区别:把基类图片输入特征提取器,经过一个分类头,算交叉熵损失,更新网络参数。唯一需要注意的是,预训练阶段的目的不是为了拿到最终分类结果,而是为了获得一个泛化能力足够强的特征表示,以及一组适合后续缩放的特征通道。
论文里的做法是在预训练的同时,额外引入一组缩放器参数,用于对特征做缩放和平移。这组参数在预训练阶段会参与训练,初始值通常设成 identity 的形式,也就是缩放为 1、平移为 0,让预训练一开始不改变原有特征的分布。随着训练进行,缩放器会逐渐学习到“哪些通道对整体分类更重要”。这一步比较像给每个特征维度都装了一个可学习的门控开关。
这里有个关键细节:预训练阶段的缩放器参数虽然能帮助特征表达,但它跟具体任务无关,不能直接用来处理新的小样本任务。真正的适应要在第二阶段完成,所以第一阶段更像是在准备一套“半成品工具”,而不是完整的解决方案。
2.2 阶段二:元学习阶段只动缩放器
第二阶段进入元学习。训练时每次从基类集合里采样一组小任务,每个任务像评测时一样只给少量标注样本。与阶段一的关键区别是:特征提取器的参数此时完全冻结,不再参与梯度更新,模型只会更新与缩放器相关的模块。也就是说,无论采样多少任务,真正被训练的参数只有缩放器那一小部分。
每个任务进入模型后,先用冻结的特征提取器提取支持集和查询集的特征,然后根据支持集的特征分布生成一组缩放参数。这组参数会被应用到特征上,让特征分布适应当前任务。比如在某个任务里,某些通道方差很大、区分度不强,缩放器就给它乘小一点系数;某些通道在少数样本上不容易激活,缩放器就给它加个偏移,把响应拉起来。整个过程可以理解成“对特征做一次轻量级的重标定”。
因为需要更新的参数数量很少,第二阶段训练速度明显快于从头训练元模型。更重要的是,特征提取器不参与更新,意味着模型不会因为每个任务只有几张图而产生剧烈的梯度波动。经验上,这种参数越少越稳的规律在少样本场景里非常明显,MTL 能在一个相对小船模型上跑出不错的效果,很大程度上靠的就是这个减法思路。
2.3 为什么两阶段比单阶段更稳
我读这篇论文时,一直在想一个问题:为什么不能直接端到端联合训练,非要拆成两个阶段?试着复现之后就明白了。如果第一阶段和第二阶段同时训,特征提取器会因为大量小任务的稀疏监督信号而学得不够充分,最后得到的特征泛化能力反而更差;如果只保留第二阶段,又缺少足够的数据来支撑特征提取器收敛。两阶段设计的本质是把一个难问题拆成了两个更容易控制的子问题:先用大样本解决“表示学习”,再用小样本解决“任务适应”。
这种拆分还有一个好处,阶段性评估更清晰。第一阶段结束能直接看基类分类准确率,判断特征提取器是否可靠;第二阶段结束再看小样本评测分数,判断缩放器是否真正学到了适应能力。如果最终分数不理想,可以比较快地定位是哪个阶段出了问题,而不是在联合训练的混沌里瞎调。
3. 缩放器与任务编码器
3.1 缩放器的数学形式和直觉
论文里缩放器的设计并不复杂,对特征图的每个通道做仿射变换:
一个特征图上通道级别的缩放和平移。如果某层特征的形状是 C×H×W,那缩放器负责生成两组和通道数一致的向量,一组做乘、一组做加。对每个通道而言,等于对整张特征图做了一个统一的线性变换。之所以选择通道维度而不是空间维度,是因为卷积网络不同通道通常对应不同的语义响应,有的通道对纹理敏感,有的对轮廓敏感,通道级重标定已经能够起到特征增强的效果,同时也减少了缩放器的参数量。
这个操作和生活里的“音量均衡”有点像。同一个音箱放不同风格的音乐,低频、高频的响应不一样,最简单的办法不是换一套音响系统,而是调一下均衡器,针对当前音乐的特点强化某些频段、削弱某些频段。缩放器扮演的就是这个均衡器角色,只是它作用在特征通道上,而且每次面对新任务都会自动调节。
3.2 任务编码器的输入与输出
缩放器不是固定参数,而是由任务编码器根据当前任务动态生成的。任务编码器需要输入什么信息才能判断该缩放哪些通道?论文选择利用支持集样本的特征和类别原型之间的关系。具体是:先由冻结的特征提取器得到支持集每个样本的特征表示,再对每个类别的支持样本特征求平均作为原型,然后计算支持集样本向量与各个原型的相似度向量。这个相似度向量经过一个多层感知机处理后,最终输出缩放参数。
把流程拆开理解就是:任务编码器接收的是一个任务描述,这个描述以相似度矩阵的形式存在,包含了“当前类别的样本在特征空间里分布成什么样”的信息。如果一个类别的样本互相靠近,说明该类特征聚集度高,分类信心足;如果样本分散严重,说明当前类别用现有特征不太容易分开,缩放器就需要更大幅度地调整特征。任务编码器的作用就是量化这种判断,并把它转成具体的通道缩放值。
我复现时有一个比较深的感受:任务编码器输出的缩放参数虽然数量少,但它需要非常敏感的输入信号。如果支持集样本本身就很有代表性,相似度区分度高,任务编码器学到的缩放规律就很可靠;反之,如果输入噪声大,缩放参数就会不稳定。因此,特征提取器的质量直接决定了整个 MTL 的性能上限,这也是为什么第一阶段预训练不能敷衍。
3.3 Hardness Attention:关注困难样本
MTL 的另一个细节是困难样本注意力(Hardness Attention)。简单说,不同的支持集样本对任务适应的贡献不应该完全一样。有些样本处在类别边界附近,特征模糊,如果它们对缩放参数的影响和那些特征清晰的核心样本一样大,反而会带来噪声。
论文中通过比较样本特征与所属类别原型的相似度来定义样本的困难程度,相似度越低说明越难。任务编码器会使用这种困难程度信息,让困难样本获得更高或更低的注意力权重,从而影响缩放参数的生成。这个机制让我想到平时训练分类时使用的难例挖掘,只是 MTL 把难例挖到了任务编码器的输入里,而不是直接用在分类损失上。
实际操作中,这个注意力模块需要留意数值稳定性。因为困难程度往往要经过 softmax 归一化才能变成注意力权重,如果特征分布过于集中,softmax 的输出会接近 one-hot,梯度信号会变得非常弱。我在实验里给相似度除过一个温度系数,让注意力分布更平滑,训练稳定性提升比较明显。
3.4 与传统特征复用方法的区别
支持小样本场景的特征复用方法在 MTL 之前就有很多,做法大多是训练好特征提取器之后,直接拿特征做最近邻或者训练一个小分类器。这类方法对特征质量要求极高,一旦新任务的数据分布与预训练集差异明显,固定特征就很难做调整。
MTL 与传统特征复用的最大区别在于,它允许特征在推理阶段被动态重标定。每来一个新任务,模型都会根据支持集重新生成一组缩放参数,相当于为当前任务定制了一种特征变换。这种调整不需要反向传播更新主网络,只在特征层面做线性操作,所以推理成本很低,实现起来也很方便。从效果上看,它比完全冻结特征更灵活,又比全部微调更稳定。
4. 实验设置与关键结果
4.1 数据集与评测设置
论文实验主要在 miniImageNet 和 tieredImageNet 两个少样本学习基准上做评测。miniImageNet 是从大规模图像数据集中抽取的 100 个类别、每类 600 张左右的子集,通常按 64 类作为基类、16 类作为验证、20 类作为新类来划分。tieredImageNet 分层结构更明显,基类、验证类、新类之间语义差异更大,跨域挑战也更高。
评测任务常用 5-way 1-shot 和 5-way 5-shot。前面提到过,5-way 表示每次任务随机抽取 5 个类,1-shot 表示每类只有 1 张支持样本,5-shot 则是每类有 5 张支持样本。查询集通常每类再抽 15 张作为测试。结果取多个随机任务的平均准确率,同时报告 95% 置信区间。这个标准设置一直沿用到现在,读论文时先看任务设置,再对比数据,判断方法好坏才不容易出错。
4.2 主干网络与训练细节
MTL 实验中使用了两类主干网络:一类是较简单的卷积网络,带有 4 个卷积块;另一类是残差网络结构,通道数按一定比例逐层扩大。特征提取器最后输出的特征图没有直接展平当作向量,而是保留了空间信息,方便后续做通道级缩放。实验结果显示,残差网络作为主干时整体水平明显更高,这也说明 MTL 并不依赖于某种特殊结构,只要特征提取器足够强,收益就很明显。
训练时有一些细节值得留意。预训练阶段会训练较长时间,让特征提取器充分收敛;元学习阶段则不会训练太多轮次,因为缩放器参数少,往往几十个 epoch 内就能达到稳定。学习率方面,预训练阶段用常规的阶梯下降,元学习阶段因为更新参数少、梯度规模小,学习率可以设置得相对保守一点,防止缩放参数来回震荡。优化器选择上,论文用 SGD 或 Adam 都对,我自己的经验是 Adam 配合小学习率更容易稳定复现。
4.3 在基准上的整体表现
从论文报告的结果看,MTL 在 5-way 1-shot 和 5-way 5-shot 上都明显优于当时其他常见做法,尤其是 1-shot 场景,领先幅度会更大。原因也好解释:样本越少,微调全部参数的方案越容易崩溃,而 MTL 在这个场景下只依赖预训练特征和轻量缩放,风险小得多。5-shot 场景因为支持样本增多,很多方法都能正常工作,优势幅度会收窄,但总体仍然保持靠前。
这里我建议读论文时不要只看一个数字,重点观察不同方法在不同主干下的对比。有些方法在 4 层卷积网络上很强,换到更深的残差网络后优势缩小,说明它可能只是在特定结构上有效。MTL 在两种主网上都能保持稳定提升,说明其核心设计不太受网络深度影响,这种鲁棒性我认为比几个百分点的提升更有价值。
4.4 结果背后的可解释性
论文还做了可视化实验,分析缩放器学到的参数在不同任务里的模式。结果让整个方法非常可信:在简单任务上,缩放参数比较平缓,特征基本保持原样;在困难任务上,缩放参数变化幅度明显增大,某些通道被显著抑制或增强。这说明缩放器确实在做“按需调整”,而不是简单地把所有特征都拉成同一分布。
这种可解释性在少样本学习里很难得。很多元学习方法像一个黑盒,能涨点但说不清为什么;MTL 却可以让研究者直观看到模型“注意”到了哪些通道、调整了哪些响应。对于做实际项目的人来说,这种可视化也能辅助判断:如果困难任务里某些通道被长期抑制,可能需要回到预训练阶段检查那些通道对应的语义模式是否本身就没学好。
5. 复现流程与避坑指南
5.1 简化实现伪代码
读论文如果只停留在理解阶段,过一个月基本就会忘。我自己复现 MTL 时把整个训练流程简化成了一个主干流程,在这里分享出来,方便想跑实验的读者快速搭底子。
import torch def meta_train_step(task, backbone, scale_net, optimizer): # 任务解包:支持集、支持标签、查询集、查询标签 support_x, support_y, query_x, query_y = task # 特征提取器完全冻结 with torch.no_grad(): support_feat = backbone(support_x) query_feat = backbone(query_x) # 计算每个类的原型 n_way = support_y.max() + 1 prototypes = torch.stack([ support_feat[support_y == i].mean(dim=0) for i in range(n_way) ]) # 用支持集特征与原型的距离构造任务描述 task_desc = -torch.cdist(support_feat, prototypes) # 任务编码器生成缩放参数 scale, shift = scale_net(task_desc) # 对特征做缩放和平移 support_feat = support_feat * scale + shift query_feat = query_feat * scale + shift # 使用距离分类,计算查询集损失 logits = -torch.cdist(query_feat, prototypes) loss = torch.nn.functional.cross_entropy(logits, query_y) optimizer.zero_grad() loss.backward() optimizer.step()这段代码不是我按照论文逐行誊的,而是把核心逻辑抽出来的最小实现。实际使用时要根据特征维度调整缩放器的输出结构,但整个训练流程基本就是这三板斧:冻结主干、生成缩放、更新缩放器。
5.2 几个容易被忽视的超参数
复现 MTL,有四个超参数很容易被轻视。
第一个是“缩放器的初始化方式”。缩放初始化为 1、平移初始化为 0 是必须的,如果随机初始化,元学习开始时特征分布会被破坏得非常严重,模型要花很长时间才能恢复到正常水平。
第二个是“支持集特征是否做标准化”。有些实现会在任务编码器前对特征做 L2 标准化,有些则不做。这个选择对结果影响不小,标准化会让相似度矩阵更稳定,但对噪声也更敏感。我的做法是先在支持集特征上做一次 channel-wise 标准化,再送进任务编码器,效果更好。
第三个是“查询集每类的样本数量”。论文常用每类 15 个查询样本,这个数量不能太少,否则每个 step 的梯度噪声太大,缩放器学出来的参数会乱跳。我试过每类只取 5 个查询样本,收敛速度明显变慢,最终分数也低一些。
第四个是“元学习阶段的任务数”。一个 epoch 里采样多少任务直接影响训练代价。缩放器参数量少,理论上不需要特别多的任务,但太少又会让任务编码器的泛化性不足。建议先用 100 个任务试一轮,观察 loss 是否平滑下降,再逐步调试。
5.3 踩过的坑
复现过程中我踩过几个比较典型的坑,写出来供参考。
第一个坑是预训练基类集合和元学习训练集合没有完全分开。如果两边类别重叠,模型在评估时相当于见过部分测试类别的特征,分数会虚高,而且自己还很难察觉。正确的做法是在数据集划分阶段就固定类别清单,基类、验证类、新类完全不相交。
第二个坑是缩放器输出的 scale 太大导致特征溢出。如果激活函数选用不当,scale 可能被推到很大值,特征乘完后数值范围暴涨,后续距离计算会出现大数吞小数问题。可以给 scale 加一个上下限约束,或者要求网络输出的规模在某个区间内,这样训练最稳。
第三个坑是显存管理和梯度图累积。特征提取器虽然冻结,但如果用 no_grad 包住,依然能省不少显存。一开始图省事没有包 no_grad,显存占用直接翻倍,后来改成上面伪代码的写法才好转。另外,每个任务更新完一定要清空计算图,否则多任务累积会让显存持续膨胀。
5.4 训练速度优化
MTL 最大的优势之一就是训练快。因为冻结了特征提取器,支持集和查询集的特征不需要重复计算。实际优化时,可以先把所有基类图片的特征提前抽出来缓存到内存里,元学习阶段直接读取缓存特征,省去每一轮重新forward的耗时。我实验的基类图片量不算太大,缓存后训练速度提升大约三倍。
如果手工特征缓存实现起来麻烦,也可以退一步只做特征提取器的权重冻结,配合自动混合精度训练。普通数据精度下,冻结主干已经比端到端元学习快很多了,混合精度主要是在任务编码器那一层节省显存,顺便让 batch size 可以适度增大。
6. 论文的局限和值得延伸的方向
6.1 对基类数据集的依赖
MTL 效果好,但前提是有一个规模不小、和目标任务分布相近的基类数据集。预训练阶段的基类如果与最终评测类别差异过大,比如基类都是自然图像,评测对象变成医学影像,那特征提取器本身就没有提供有价值的表示,缩放器再怎么做通道重标定也救不回来。这也是 MTL 在跨域少样本场景下面临的主要挑战。
另一个隐蔽问题是基类数据本身的标注质量。缩放器学的是“特征通道和分类难度的关系”,如果基类标签有噪声,预训练特征会被污染,任务编码器学到的规律也就不准。在实际项目中如果想套用 MTL,建议先人工检查一遍基类数据质量,把钱花在预训练数据的清理上,比盲目调元学习阶段的超参更有效。
6.2 与后续工作的关联
MTL 不是终点,它开启了一个很重要的方向:用轻量适配模块做少样本学习。后来有不少方法在 MTL 的基础上发展,比如用更复杂的调制网络替代缩放器,或者在特征提取器中间层也插入适配模块,而不仅仅在最外层做通道缩放。这些工作的共同点是遵循“主干参数冻结、适配模块轻量”的原则,只是把适配模块做得越来越精细。
MTL 的缩放思想也和当时的一些视觉模型内在机制有共通之处。通道注意力、特征重标定、动态滤波这类方法,本质上都是在特征图上做自适应变换。区别在于,注意力往往由单张输入图片自己决定,而 MTL 的缩放由支持集样本共同决定,属于任务级的调制。这种从“图片自适应”到“任务自适应”的思路升级,是理解后续很多元学习工作的钥匙。
6.3 可以做的扩展
如果要在 MTL 基础上自己做扩展,我会优先尝试两个方向。第一个是把缩放器作用域从特征提取器末端扩展到多个中间层。末端特征语义信息丰富,但中间层特征包含更多局部细节,对于细粒度小样本分类,多层缩放可能更有效。代价是缩放器参数总量增加,需要重新平衡参数量和过拟合风险。
第二个方向是在缩放器输出中加入一些可解释的约束。比如让缩放参数满足稀疏性,只有少数通道会被调动,启发模型关注最有区分力的特征通道,而不是把所有通道都调一遍。稀疏约束做得好,不但能提高一点准确率,还能让可视化结果更有说服力,方便在实际项目中向团队解释模型行为。
我自己读完这篇论文后最大的体会其实是一句话:少样本场景下,少更新往往比多更新更有效。很多时候我们默认模型在目标任务上训练得越多越好,但样本量撑不起这种训练时,把大多数参数冻住、只学少量适配模块,反而能把有限的信息用在刀刃上。这个思路后来我也用到了别的视觉任务上,轻量适配模块的价值比想象中要宽广。如果你想复现,我建议先照着一套迷你版数据跑通整体流程,再逐步对齐论文里的完整细节,这样踩的坑最少,花的时间也最值。