小样本学习(Few-shot Learning)是我这两年投入时间最多的方向之一。坦白讲,刚开始看到这个名字时,我以为只是“数据少的时候训练一个分类器”,等到真正去读综述、复现论文才发现,里面有完全不同的任务设定、眼花缭乱的方法,以及大量被论文引言轻描淡写带过的评测陷阱。这篇文章是我自己阅读多篇小样本学习综述论文后的系统性总结,也是一份从“看懂概念”到“落地复现”的个人路线图。
适合谁看:刚入门小样本学习的研究生、正在做技术方案调研的工程师,以及所有不想被论文堆吓跑的朋友。读完你至少能搞清楚:这个领域到底在解决什么问题,方法之间有什么本质差别,为什么同一张表里不同论文的精度不能直接比,以及复现时最容易在哪个环节翻车。
1. 先搞懂小样本学习在解决什么,再看综述不迷路
1.1 N-way K-shot 到底是什么
小样本学习最常见的任务定义用“N-way K-shot”描述。举个例子,5-way 5-shot 表示每个任务里有 5 个类别,每个类别只给 5 张标注样本作为参考,模型要根据这 25 张图,去判断查询集里一堆新图片分别属于哪一类。对人类来说这并不难,孩子见过一次“猫”就能建立起概念;但深度学习模型天生依赖大量标注数据,少样本场景下很容易只记住训练时的类别,换一批新类就失灵。
我一开始以为这个问题就是把普通分类任务的数据量调小,后来才发现完全不是一回事。在普通分类里,训练集和测试集的类别是同一个集合,模型只要学会区分见过的类即可。小样本学习测试时遇到的是训练阶段从未见过的类别,所以它真正要学的是“能够快速适应新类别”的通用能力。这也是为什么所有综述都会反复强调:不要把它当作小数据分类问题,而要当作一个泛化问题来看。
1.2 为什么绕不开元学习
读综述时你会发现,小样本学习和元学习几乎是绑定的。元学习的核心思想是“学会学习”:不是只学一个固定的分类器,而是学一套适应规则,让模型在见到新任务时能快速自我调整。具体做法是 episodic training,也叫任务式训练。
每次迭代时,从训练集的类别池里随机抽出 N 个类别,每个类别取 K 个样本作为支持集(support set),再取若干样本作为查询集(query set),组成一个虚拟任务。模型在这个任务上计算损失、更新参数。跑很多个这样的任务之后,模型就“见过”大量“只有几个样本的情况”,新任务到来时不至于手足无措。
我当初总觉得元学习是在绕远路,但后来亲手做了对比实验才明白:如果不用任务式训练,只是在小数据上微调一个预训练模型,模型很快会把已有类别的偏好固化住,遇到不认识的类时基本是在瞎猜。任务式训练相当于逼着模型在每次迭代都面临一次“新环境”,这种压力才是小样本泛化能力的来源。
2. 综述里的方法地图:四条主流路线与一条暗线
2.1 基于度量的方法:让“像不像”变成学习目标
基于度量的方法是我个人最喜欢的一类,思路足够直观:把样本都映射到一个特征空间里,然后用距离来衡量相似度。查询样本离哪个支持样本或类原型更近,就判成哪个类。整个问题从“分类”变成了“比较像不像”。
经典路线包括:
- 孪生网络(Siamese Networks):双塔结构,两个输入共享网络权重,输出一个相似性分数,训练目标是让同类别样本对得分高、不同类别样本对得分低。思路朴素,但奠定了“比较式”思维的基础。
- 匹配网络(Matching Networks):提出用注意力机制计算查询样本与每个支持样本的匹配程度,再按类别汇总得到预测分布。这篇论文也是 episodic training 范式的重要推手。
- 原型网络(Prototypical Networks):每个类别不再保留单一样本,而是把支持集里该类所有样本的特征取平均,得到一个“类原型”。查询样本与各类原型的欧氏距离经过 softmax 后变成类别概率。公式简洁、实现容易、效果不差,所以我通常建议新手从这里入手复现。
- 关系网络(Relation Networks):不固定距离函数,而是把查询特征和候选特征拼在一起,让一个小网络去判定“相关程度有多高”。表达能力更强,但训练量和过拟合风险也更大。
选型体会:度量方法在特征空间清晰、训练过程比较平顺,推理时只需一次前向计算,工程上很友好。缺点是它对特征质量要求很高,如果 backbone 学出的嵌入本身分不开不同类别,再好的距离函数也没用。
2.2 基于优化的方法:学习一个“更容易适应”的初始点
第二类方法不改变模型结构,而是改变模型参数的更新方式。它的目标是找到一个“好的初始参数”,让模型面对新任务时只需几步梯度下降就能收敛到不错的结果。
代表性工作就是 MAML(Model-Agnostic Meta-Learning)。它的训练分两层:内层对每个任务做一次或几次普通梯度更新,得到临时参数;外层基于所有任务的临时表现,去计算初始参数应该怎么调。这个外层更新涉及经过梯度更新后的损失对初始参数的求导,也就是二阶导数,所以实现起来又复杂又吃显存。Reptile 通过直接比较初始参数与任务更新后的参数来近似这个更新过程,省去了二阶求导,速度上友好很多。Meta-SGD 则更进一步,不光学初始化参数,还学习每一步更新的方向和步长。
这类方法的优势是通用性强,不只能做图像分类,还能迁移到回归、强化学习等场景。代价是训练不稳定,超参数很敏感,尤其是内层学习率和外层学习率的比例。我在复现 MAML 时最深的感受是:论文看着漂亮,但工程落地需要反复调参,没有度量方法那么随手。
2.3 基于模型结构与数据增强的路线
还有一类方法从结构和数据两个角度做文章。基于模型的方法,比如记忆增强网络(Memory-Augmented Neural Networks),给网络增加一个可读写的外部记忆模块,遇到新类别时把支持集样本写进记忆,查询时再从记忆里检索相关信息。这种设计在处理“类别持续增加”的场景时很自然,但在标准小样本分类中实现成本偏高,我在复盘时认为性价比一般。
数据增强路线则更贴近直觉:既然样本少,那就想办法造样本。简单做法包括随机裁剪、旋转、颜色扰动、Mixup 混合等;进阶做法是用生成模型或图像翻译模型合成新的支持样本。增强确实能缓解支持集稀缺的问题,尤其是对姿态、光照变化很大的数据集。但它的风险也很明显:若是增强方式改变了类别语义,比如把一张“轮胎裂纹”图片旋转后变成“背景纹理”,模型就会学到错误模式。用生成模型造样本时还要警惕生成分布偏差,生成的图像与真实分布差得太远,反而拉低测试精度。
2.4 被很多综述单独拎出来的暗线:预训练基线
过去几年有一条容易被新手忽略的暗线:预训练基线的强势回归。以论文A Closer Look at Few-shot Classification为代表的工作发现,一个普通的特征提取器在大规模数据集上预训练,然后冻结特征、只训练一个线性分类头,或者做简单的微调,其精度经常超过复杂的元学习模型。这个结论当时让不少人震惊,因为它动摇了“元学习必须复杂”的假设。
我自己复现时也验证过这一点:先把 ResNet 在基类数据上预训练好,再只学一个分类头,miniImageNet 上的 5-way 5-shot 精度就已经能摸到许多度量学习方法的水平。这给我的启示是:对比方法前,必须把 baseline 做得足够扎实,否则你兴冲冲提的新方案,本质上只是在弥补 baseline 的缺陷。近期预训练大模型、CLIP 这样的视觉语言模型兴起后,少样本适配更多转向了 prompt、adapter 这类轻量微调方案,几乎都是在“预训练特征 + 少量参数适配”这条暗线上往前走。
| 路线 | 核心思想 | 代表工作 | 优点 | 适合场景 |
|---|---|---|---|---|
| 基于度量 | 在特征空间中比较相似度 | 原型网络、匹配网络 | 简单直观、推理快 | 类别语义清晰、特征可分性好 |
| 基于优化 | 学习更好的初始参数 | MAML、Reptile | 通用性强、可跨任务 | 任务间差异大、需要快速适应 |
| 基于模型/记忆 | 用外部记忆存储与检索样本 | 记忆增强网络 | 支持持续学习 | 类别动态增加的场景 |
| 数据增强 | 合成或变换出更多样本 | Mixup、生成模型 | 落地成本低 | 样本过少且增强不破坏语义 |
| 预训练基线 | 大规模预训练+少量适配 | 线性探针、微调 | 简单稳定、效果好 | 有充足基类数据或预训练模型 |
3. 从综述到复现:评测协议和参数选择才是关键
3.1 数据集划分与信息泄漏
读综述时如果只看方法不看评测协议,很容易被数字误导。小样本学习的常用数据集里,miniImageNet 是 100 类、每类 600 张图,按照 64/16/20 划分训练、验证、测试类别;tieredImageNet 则按超类划分,保证三个集合在语义上更不相干,难度更高;CUB-200 这种鸟类细粒度数据集则常用于测试跨领域泛化能力。
我强调“划分”的原因是信息泄漏问题。如果基类与新类在语义上高度相似,或者图像本身有重复,模型在基类上看到的模式可以直接套用到新类上,测试精度就会虚高。有些论文为了刷分,会刻意选择容易的划分或使用验证集类别做反复调优,这在综述里通常不会写明白。因此我养成了一个习惯:先看数据集构建和划分方式,再看实验结果,最后才看方法细节。
3.2 一个标准 episodic 训练循环
许多综述会配一张任务构建示意图,但实际复现时还是容易在数据加载上出错。这里给出一段我常用的原型网络训练伪代码,基于的是该领域最常见实践,大家在自己代码里可以对照调整。
for epoch in range(max_epochs): # 从训练集中随机采样 N 个类别,每类 K 个支持样本 + Q 个查询样本 support_x, support_y, query_x, query_y = sample_episode( train_set, N=5, K=5, Q=15 ) # 共享权重的特征提取器,分别对支持和查询样本编码 support_emb = model(support_x) # shape: [N*K, d] query_emb = model(query_x) # shape: [N*Q, d] # 原型网络:计算每个类别的原型(类内特征均值) prototype = [] for cls in range(N): mask = support_y == cls proto = support_emb[mask].mean(dim=0) prototype.append(proto) prototype = torch.stack(prototype) # shape: [N, d] # 用欧氏距离得到查询样本属于每个类的概率 dist = torch.cdist(query_emb, prototype) logits = -dist loss = F.cross_entropy(logits, query_y) optimizer.zero_grad() loss.backward() optimizer.step()这段逻辑里最容易出错的是标签映射。因为每个 episode 的类别是临时抽出来的,类别编号需要重新从 0 到 N-1 编码,不能用原始数据集的全局标签。以前我就吃过这个亏,模型训练时 loss 明明在降,测试准确率却一直停在 1/N 附近,查了半天发现是标签错位。
测试阶段同样重要。在 novel 类上构建固定数量的 episode,比如 600 个或 1000 个,每个 episode 也包含支持集和查询集,最终报告平均准确率和 95% 置信区间。注意测试时不能再更新模型参数,除非你用的是 transductive 设置,可以借助查询集本身的信息做特征调整,但那样就和 inductive 设置的结果不能直接对比了。
3.3 常见超参与 Backbone 选择
关于 backbone,miniImageNet 上早期论文常用 4 层卷积网络,参数量小、训练快,适合验证想法。稍微复杂一点的数据集上,ResNet12 或 ResNet18 是更常见的选择。我的经验是不要一味堆深度,小样本场景下大模型容易过拟合,如果预训练数据不够多,ResNet18 和 ResNet50 的差距没有想象中那么大。
优化器方面,SGD 加动量在元学习里表现往往比 Adam 更稳,但 Adam 的调参成本低。学习率可以从 0.001 到 0.01 之间尝试,配合余弦退火效果更好。训练时每个 epoch 内采多少个 episode 也值得关注,我一般让每个 epoch 采样几百个 task,训练 500 到 1000 个 epoch,具体还要根据训练集大小和损失下降曲线调整。查询集大小 Q 常用 15 或 16,太小会让梯度噪声偏大,太大则增加显存压力。
4. 复现小样本学习项目时,我最常踩的坑
4.1 训练不收敛或效果很差的排查
小样本学习项目调试起来比普通分类麻烦,因为“任务”是动态采样的,问题不一定出在模型上。我列几个最常见的现象和排查思路。
- support loss 很低,query loss 很高:说明模型过度记住了支持集,泛化到查询集时失效。试着降低模型容量、增加数据增强、调低训练任务内迭代次数,或者增加任务数量。
- 准确率一直维持在 1/N:大概率是代码逻辑问题,优先检查 episode 的标签映射、支持集和查询集是否重叠、损失函数是否把维度传错。
- 训练 loss 震荡很厉害:先降低学习率,再看 episode 采样是否引入了类别不均衡。如果采样函数有 bug,某些类别的出现频率过高,模型就会产生偏好。
- 显存溢出:MAML 这类二阶优化方法最明显。可以调小 batch 大小、减少内层更新步数,或者换用一阶近似实现。
4.2 复现结果与论文对不上的原因
复现精度差一两个点是很常见的,但如果差出五六个点,通常不是模型问题,而是评测设置不一致。我整理了一个排查表,供大家对照。
| 常见原因 | 现象 | 解决办法 |
|---|---|---|
| 图像预处理不一致 | 比论文低很多 | 核对输入尺寸、归一化均值标准差、裁剪方式 |
| 随机种子差异 | 每次结果波动大 | 固定种子,跑多个种子取平均,报告置信区间 |
| 查询集数量不同 | 精度趋势不同 | 统一 Q 的大小,至少对齐到论文设置 |
| Backbone 初始化不同 | certain方法相差大 | 使用相同的预训练权重或同样的随机初始化方式 |
| 测试 episode 数量过少 | 结果不可靠 | 至少 600 个 episode,有条件就上 2000 个 |
| transductive 与 inductive 混淆 | 数字虚高 | 明确是否使用了查询集信息,在论文和表格中标注清楚 |
我在评估时踩过最深的一个坑是:为了省时间测试时只用 100 个 episode,结果两次实验之间相差 5 个百分点,根本分不清是方法改进还是随机波动。后来改成 1000 个 episode,对比才变得有意义。
4.3 业务数据上的血泪经验
标准 benchmark 上的方法搬到真实业务数据时,往往会打不少折扣。我之前做过一个工业缺陷检测项目,缺陷类别的样本只有三十来张,与正常样本数量完全不成比例。第一版方案直接在原图上跑原型网络,结果 F1 值惨不忍睹。分析之后发现问题不在算法本身,而在域差异:公开数据集里的图像是中心目标、背景干净,而产线图片背景杂乱、目标尺度不固定、拍摄角度千奇百怪。模型在预训练和基类上学到的特征,根本无法直接迁移到这种图像分布上。
后来我做了三件事:一是把图像先做目标检测裁剪和对齐,去掉无关背景;二是用一个更大的通用预训练模型提取嵌入特征,冻结 backbone 只在顶层做原型计算;三是引入了一些无标注产线图像做无监督域适配,让特征空间更贴近真实分布。这三步做完,F1 从不到 0.3 提到了 0.6 以上。结论很简单:小样本方法不是“开箱即用”的魔法,特征分布对齐才是业务落地真正的胜负手。
5. 小样本学习选型建议:从论文到项目落地
5.1 不同场景怎么选方法
结合综述里的方法分类和我自己的项目经验,我整理了一套比较实用的选型思路,很建议大家先按这套方法做初步筛选,再针对自己的数据做小规模消融。
- 类别语义清晰、特征空间好分:直接从原型网络开始,它是效果与复杂度之间最平衡的选项。
- 任务之间的差异非常大:比如训练任务与测试任务来自完全不同的领域,优先试 MAML 这类基于优化的方法。
- 每类样本只有几张,且数据增强不会破坏语义:先做增强加预训练特征,简单便宜,效果提升很直接。
- 业务系统里已经有现成的大模型:考虑线性探针、prompt 或 adapter 这类轻量适配,不要从零训练一个小模型。
- 新类别会不断出现、系统需要持续上新:记忆增强或带动态类别扩展的度量方法更合适,因为每来一类只需更新原型,不需要重训整个网络。
5.2 工程落地时还需要考虑的约束
技术指标之外,工程约束常常决定方法选型。推理速度方面,基于度量的方法只需要一次前向计算,最容易满足线上实时性要求;MAML 系列在新任务上仍需要几步梯度更新,更适合离线批处理或低频更新场景。显存和训练成本方面,二阶优化的代价很高,如果团队只有单卡 GPU,我建议慎选。
可解释性是另一个容易被忽略的点。生产环境里算法输出需要能被复核,原型网络天然可以把每个类别的原型和具体样本导出,业务方能够直观看到“这张查询图为什么被分到这个类”;而 MAML 学习到的初始化参数则很难向非技术人员解释。我在项目复盘时发现,算法能否上线,有时候并不取决于精度高的那一个点,而取决于出了问题之后能否定位和解释。
6. 读完这些综述后,我留在手上的几个判断
先说方法之外的第一条心得:读任何一篇小样本学习综述,先用半小时把评测协议彻底吃透,再去看方法。评测协议决定了你看到的实验结果是否可信,也决定了两个方法之间能不能放在一起比。
我的第二个判断是:小样本学习的本质不是某个精巧的模块,而是跨任务先验的习得方式。只要抓住“如何从有限样本中提炼可泛化规律”这个命题,再去理解度量、优化、记忆、增强这些流派,思路就会特别清楚。
最后分享一个我一直在用的工作习惯:把任何一篇综述都按“问题定义、评测协议、方法分类、复现验证、应用限制”五步来读。这个方法让我从读论文容易陷入细节的毛病里解脱出来,也建议你们试试。以我经验来说,小样本学习这个领域真正有价值的工作,往往不是把精度刷高一个点,而是把评测或者问题定义往前推一步;这也是我在读综述和做项目时最想抓住的东西。