ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

小样本学习入门:从问题定义到原型网络实战与避坑指南

小样本学习入门:从问题定义到原型网络实战与避坑指南

我最早被“小样本学习(Few-shot Learning)”吸引,是因为一个特别接地气的画面:家里小朋友只看过两三张大象照片,到了动物园却能一眼认出真大象。而我当时正被业务里一个“新品类识别”的需求折腾得焦头烂额——新品类刚上线,所有可用数据只有几十张实拍图,用标准分类模型去训练,准确率惨不忍睹。那一刻我突然意识到,人类学习新概念的样本效率,和深度学习模型的胃口差距有多大。小样本学习想做的,就是让模型也拥有这种“看几张图就学会一个新类别”的能力,而不是动辄需要成千上万张标注样本。

这篇文章不堆术语。我会把问题定义、三大主流技术路线、常用数据集与评估协议讲清楚,再用最少的代码带大家实现一个经典的小样本分类器,最后聊一聊我在实际实验里踩过的坑,以及一些大概率能帮你省时间的判断方法。适合两类读者:一类是刚转向视觉方向、想在Few-shot Learning领域建立整体认识的研究者;另一类是业务里经常遇到“新类数据太少”的工程师,你可以把本文当作一份可直接参考的入门笔记。

1. 先搞清楚小样本学习到底在解决什么问题

1.1 一个容易被误解的问题定义

很多初学者一听到“小样本学习”,会觉得它只是“让模型在小数据集上训练”。但实际上,小样本学习真正要解决的是新类别的快速泛化。

常规监督学习有一个隐含假设:训练集和测试集共享类别空间。你训练了一个10类分类器,测试时也只能在同样的10类里做选择。而小样本学习引入了一个完全不同的场景:训练阶段模型可能已经接触过大量类别,但这些类别跟测试阶段出现的新类别是完全不同的。测试时,每个新类别只给出极少数标注样本供参考,模型需要凭借这些参考样本,把没有标注的查询数据正确分类。

这个设定的标准说法叫N-way K-shot。N是测试时面临的新类别数量,K是每个类别提供的参考样本数。比如5-way 1-shot,就是某一轮测试中,模型看到5个新类别,每类只有1张标注样本,接下来要根据这5张图去判断一批查询图片属于哪一类。这里的“1”非常关键——只有一张参考图,模型仍然要能举一反三。

一个常见误区是:既然有少量标注了,直接把一个预训练模型拿过来在K个样本上微调不就行了吗?

其实这条路不是不能走,也确实是研究分支之一。但在K很小(比如1或5)的时候,纯微调极易过拟合:模型会很快记住这几张图的细节,比如背景、光线、甚至边框上的噪声,而对新数据的泛化能力很差。这也是为什么小样本学习领域中,度量学习、元学习等方法会更有存在感——它们的设计目标就是在极少量样本下降低过拟合风险。

1.2 小样本学习为什么近几年才开始火

这个研究方向其实上世纪就出现了,但真正成为一种“显学”是这几年的事。推动它的主要有三个力量:

  • 应用需求找到痛点:推荐系统的冷启动、电商新品类的识别、工业质检里新缺陷的发现、医疗影像中的罕见病识别,本质上都是“新类数据稀少”的问题。企业不是没有数据,而是每类数据太少,来不及为几百个新类逐一标注大量样本。
  • 方法论的成熟:以原型网络、匹配网络为代表的度量学习方法,以MAML为代表的元学习方法,配合大规模预训练模型提供的优质特征,让小样本准确率从“勉强能看”提升到“业务可用”。
  • 评测标准化:早年间各自用私有数据集跑实验,结果很难横向对比。后来Omniglot、miniImageNet等基准成为事实标准,科研和工程之间终于有了共同语言。

随着业界对大模型、多模态模型的关注增加,小样本学习的含义也在扩展:给模型几个示例,让它完成“从未见过的下游任务”——本质上也是Few-shot的思想,只是把数据从图片扩展到了文本、音视频等各种模态。

1.3 它和迁移学习、元学习、预训练是什么关系

这一点非常容易把人绕晕。我的经验是以“任务-方法”的区分去理解:

  • 小样本学习是一类任务:特征是新类别样本稀缺。只要符合“N-way K-shot”这种设定,都可以算。
  • 元学习是一类方法:它的目标是让模型具备“学会如何去学习”的能力,MAML和Reptile是最典型的代表。
  • 度量学习是一类方法:通过特征空间中的距离关系进行分类判断,原型网络和匹配网络都属于这个阵营。
  • 迁移学习和预训练是更大的底座:先用大数据预训练好一个特征提取器,再在少量样本上做适配。这套思路今天几乎渗透进了所有小样本方法之中,即使论文里目标是元学习,底层也往往用到一个在大型数据集上预训练过的backbone。

也就是说,你没必要把“预训练+微调”和“Few-shot Learning”对立起来,更合理的理解方式是:预训练提供了强大的起点,而Few-shot Learning研究的是在极少量标注下如何高效使用这个起点。

2. 三条主流技术路线,背后其实是三种世界观

小样本学习的方法论百花齐放,但梳理下来,主流无非三条路线:度量学习、元学习、数据增强和先验知识注入。三条路线解决同一个问题的思路有很大差别。

2.1 度量学习:让样本在特征空间里“抱团”

度量学习的想法最直观:把图片映射到一个特征空间,同类样本在这个空间里挤在一起,异类样本离得越远越好。测试时,把新类别的support样本也映射到这个空间,然后看没有标注的query图片离哪个类更近,就归为哪个类。

早期代表是匹配网络(Matching Networks),思想是对support集做记忆和加权投票。后来出现的原型网络(Prototypical Networks)在它基础上做了极大简化:直接把每类的support样本向量求平均,得到“类原型”向量,然后计算query向量与每个类原型之间的距离。距离最近的那个类就是预测结果。

别小看这个“求平均”操作。它让方法变得简单、可解释、计算高效,甚至在标准benchmark上依旧相当能打,所以我后面第4节的入门demo会选择它。

给一个不严谨但好懂的生活类比:你刚到一个陌生城市,朋友告诉你“川菜馆一般集中在这两条街,火锅店集中在那个区域”。你把这些店按特征画在一张地图上,每个类别形成了一个“区域中心”。当一家新店出现时,你判断它属于哪个类别,其实就是看它离哪个区域中心最近。特征空间就是这张地图,类原型就是每个区域的“中心点”。

为什么度量学习在小样本下特别有效?因为它绕开了“从头训练一个复杂分类器”的困境。模型的核心工作从“学会分类边界”变成了“学到一个好特征空间”,而特征空间的质量靠大型训练集和良好的损失函数来保证。当新类别数据极少时,类别边界参数化得越复杂越容易过拟合,度量学习这种简单决策方式反而更稳。

2.2 元学习:学会“学习”这件事本身

元学习是另一条完全不同的路线。它不直接关心“这一轮分类怎么分”,而是关心“模型如何快速适应一个新任务”。

以MAML为代表的方法,训练阶段的目标是找到一组初始参数θ,使得当模型遇到任何新的Few-shot任务时,只需要在该任务的support数据集上做很少几步梯度更新,就能在query数据集上取得很好效果。也就是说,它优化的是“起点”,而不是“终点”。

这个思想的厉害之处在于,它把“学习能力”本身变成了优化目标。训练过程通常会构造大量episode(每个episode对应一个N-way K-shot小任务),模型不断在一个任务上学几步,再换到下一个任务,最终学到的初始参数,对新任务有极强的可适应性。

举一个生活化的例子:普通人学新球类运动,不会每次从零开始学发力、学跑动、学平衡,而是先掌握通用的体能、协调性、球感,之后再学具体项目会很快。MAML希望模型也能这样,通过大量任务的磨练,找到那个“学什么都快”的初始状态。

需要注意的是,MAML原版的训练过程涉及二阶梯度计算,实现复杂度较高,工程上有一定门槛。它的变体Reptile通过只保留一阶近似,让实现难度大大降低。但对第一次接触小样本学习的读者,我建议先把度量学习玩明白,再考虑涉足元学习,否则容易在优化细节里迷失。

2.3 数据增强与先验知识:把一张图拆成十张用

第三条路线更直接:既然“数据少”是原罪,那我就想办法把数据变多。

经典的数据增强手段包括随机旋转、裁剪、颜色抖动、混合样本等。近年也有一些基于生成模型的方法,比如用扩散模型根据新类别的语义描述生成额外训练样本,或者从无标注数据中检索相似的辅助样本。这个方向通常不单独作为完整方案,而是作为数据层面的补充,叠加在度量学习或元学习之上。

另外一个越来越重要的趋势是“利用先验知识”。大规模预训练模型本身就是一个巨大的先验知识库。很多实用的小样本系统做法是:直接用预训练图像模型提取特征,只在特征后面加一个小分类头,甚至只是用最近邻分类。实测下来,这种方案的性价比往往异常高。你会发现有些时候并不是方法不够先进,而是没有把预训练特征用好。

对于实际业务,我的建议是把三者看作工具箱里互补的零件:度量学习决定“怎么比较”,元学习决定“怎么快速适应”,数据增强和先验知识决定“信息从哪里来”。成熟的项目通常不会只用一条路线,而是混合使用。

3. 动手之前先读懂数据集和评估协议,否则分数没有意义

很多人跑Few-shot实验的最大问题不是模型不会写,而是评估协议搞错了。在Few-shot领域,评测方法直接决定实验结果的可信度。真不是危言耸听——协议错了,你的准确率再高也没有比较价值。

3.1 几个绕不开的基准数据集

入门阶段,下面几个数据集是绕不开的。这是学术界常用的公开基准,你在任何小样本学习论文里都会反复看到它们。

数据集规模特点常用配置
Omniglot1623个手写字符类别,每类20个样本类别多、每类样本少,字符风格多样20-way 1-shot / 5-shot
miniImageNet100个类别,每类600张图片ImageNet子集,图片内容更接近真实场景5-way 1-shot / 5-shot
tieredImageNet约608个类别按ImageNet语义层级划分,训练和测试的类别分布差异更大5-way 1-shot / 5-shot
CIFAR-FS100个类别,每类600张建立在CIFAR-100之上,图像分辨率低5-way 1-shot / 5-shot
FC100100个类别同样来自CIFAR-100,但类别被更严格地区分为训练/验证/测试集,挑战性更高5-way 5-shot

你不需要把每个数据集都跑一遍。作为入门,我推荐先在Omniglot上把代码跑通,再换到miniImageNet上体验真实图像的难度。两个数据集跑下来,基本就能建立起Few-shot的体感。

3.2 N-way K-shot评估协议到底怎么玩

Few-shot评估不是在固定测试集上一次性跑完,而是通过反复采样“episode”来评估。

一个episode是这样构建的:从测试类别里随机抽N个类别,每个类别随机抽K张图片作为support集,再抽若干张作为query集。模型拿到support集作为参考,对query集的每张图片做预测,计算一个准确率。然后再抽下一个episode,重复几百上千次,把平均准确率和95%置信区间作为最终结果。

为什么要设计成这种“抽卡式”评估?因为小样本学习强调的是模型在“任意新类别组合”下都能快速适应,而单个固定组合无法代表所有可能情况。只有通过大量随机的类别组合采样,才能比较全面地反映模型面对新类别时的真实能力。

这也是它和普通分类评测最大的区别:普通分类评测关心“在训练过的类别上表现如何”,Few-shot评测关心“面对从没见过的类别,在极少参考样本下表现如何”。所以跑实验时,千万不要把训练集的分类准确率和Few-shot准确率混为一谈。

3.3 评估里最容易犯的错

我见过不少跑实验的人,准确率刷得很漂亮,结果评审一问才发现评估协议就是错的。最常见的三类问题:

  • 类别泄漏:训练阶段见过测试类的任何样本。一旦模型在预训练或主训练中接触过测试类,它就不再是“从没见过的新类”了,Few-shot就退化成普通分类,分数虚高。miniImageNet的经典协议是训练用64类、验证用16类、测试用20类,这三组类别绝不能重叠。
  • 随机种子不一致:episode采样依赖随机种子。不同实现、不同种子下跑出的结果差异可能达到两三个百分点,不固定种子就无法互相比较。规范的实验会固定随机种子,并报告多次运行的平均值和方差。
  • 反复筛选最优验证结果:有些实现会在验证集上试很多次,挑表现最好的结果写进论文或报告。这本质上是把验证集当测试集在用,会让最终结论失真。正确做法是:验证集只用来调参,测试集只跑一次,并提前固定评估用的episode池。

3.4 预处理与评价指标中的细节

图像大小、归一化方式、是否做随机裁剪,这些听起来很小的预处理选择,在Few-shot实验中影响可能很大。同一个方法,有人用224×224输入,有人用84×84低分辨率,结果差了5个点都是正常的。

正因如此,论文复现时最重要的不是模型结构,而是先对齐数据预处理和评估协议。我通常在复现工作里做的第一件事,就是把数据路径、图像预处理、episode采样逻辑和评估指标全部列成一张清单,逐项跟原始实现核对。这个过程看起来繁琐,却能帮你规避大量“跑不通、跑不对”的问题。

4. 用原型网络从零搭一个最少代码量的Few-shot分类器

理论讲了这么多,是时候动手了。我选取原型网络(ProtoNet)作为入门Demo,因为它原理简单、代码量小、效果稳定,而且能帮助你直观理解“度量学习”这一大分支的核心逻辑。

4.1 核心公式与实现逻辑

原型网络的训练和推理可以拆解为三步:

  1. 用特征提取器f把图片编码成向量。
  2. 对每个类别c的support样本向量求平均,得到类原型p_c。
  3. 将query图片编码成向量,计算其与每个类原型的距离(常用欧氏距离),距离最近的类就是预测结果。

之所以用“类平均”作为类别代表,是因为它能在特征空间中最大程度保留类别中心的信息,同时对个别样本的噪声不敏感。用代码表达,核心推理逻辑大概是这个样子:

# 伪代码:原型网络分类逻辑 def proto_predict(feature_extractor, support_images, support_labels, query_images): support_features = feature_extractor(support_images) # 按类别计算原型 protos = [] for cls in unique(support_labels): mask = support_labels == cls proto = mean(support_features[mask], dim=0) protos.append(proto) protos = stack(protos) # [num_way, feat_dim] query_features = feature_extractor(query_images) # 计算query到每个原型的欧氏距离 distances = euclidean_distance(query_features, protos) # [num_query, num_way] # 距离越小概率越大,通常取负距离作为logits logits = -distances probs = softmax(logits, dim=1) return probs

你不需要一次把代码全部写完。我建议先把特征提取器替换成一个最简单的4层卷积网络,把图片转成向量,其余部分保持这个结构。跑通后再考虑替换为更强的预训练backbone。

4.2 Episode采样怎么写

训练原型网络时,最关键的是“如何构造一个episode”。思路是:先从训练类别集合中随机抽N个类,再从每个类中随机抽K个样本来组成support集,再抽一定数量的query样本。

下面这段简化代码展示了采样的核心逻辑。你只需替换成自己数据集的接口,就可以直接复用:

def sample_episode(dataset, classes, n_way, k_shot, n_query): # 随机抽出n_way个类别 chosen_classes = random.sample(classes, n_way) support_images, support_labels = [], [] query_images, query_labels = [], [] for label, cls in enumerate(chosen_classes): indices = dataset.get_indices(cls) # 获取该类的所有样本索引 random.shuffle(indices) # 前k_shot作为support,接下来n_query作为query support_images.extend(indices[:k_shot]) support_labels.extend([label] * k_shot) query_images.extend(indices[k_shot:k_shot + n_query]) query_labels.extend([label] * n_query) return support_images, support_labels, query_images, query_labels

这套采样逻辑跟模型结构无关。将来你切换到匹配网络、关系网络,只要训练流程还是“基于episode”,这段代码都能继续用。

训练阶段,每个episode都会计算一次交叉熵损失,通过反向传播更新特征提取器的参数。测试阶段则冻结模型,在测试类上采样一批episode,计算平均准确率。

4.3 关键超参数与效果观察

原型网络需要关注的超参数并不多,但有几个会影响最终效果:

  • 特征维度:一般64到512之间。特征维度太低可能信息不足,太高则可能让距离计算变得不稳定。
  • 距离函数:很多论文发现,对于原型网络来说,欧氏距离比余弦相似度更稳定。不要凭直觉乱改,先跑出baseline,再逐一尝试其他距离函数。
  • 学习率与优化器:常用Adam或SGD,学习率从1e-3起步,配合余弦退火或StepLR。
  • backbone选择:入门阶段先用4层卷积网络,后面再替换为预训练模型。

我个人的观察是,在miniImageNet的5-way设置下,1-shot和5-shot的难度差异非常大。1-shot几乎没有容错余地,一个特征位置偏了,分类就可能错;5-shot给了模型“平均”的机会,明显更稳。一个合格baseline的入门结果大致是:4层卷积网络配合ProtoNet在miniImageNet 5-way 5-shot上跑到60%以上,替换更强backbone后能接近或超过70%。不同训练细节会有浮动,不必纠结绝对值,更重要的是观察曲线是否平滑上升、验证集是否稳定。

4.4 训练时间与算力门槛

对入门者来说,Few-shot实验对大算力的依赖并不算高。用Omniglot数据集训练一个原型网络,普通消费级显卡几分钟到十几分钟就能完成;miniImageNet规模更大,也基本在一两个小时以内。相比训练大规模语言模型或大视觉模型,这算是非常友好的入门方向。

如果你的机器资源有限,可以用更小的数据集子集、更小的图像分辨率先验证逻辑,再把规模放大。

5. 实际跑实验时最容易踩的坑

代码能跑通只是第一步。真正有价值的经验,往往藏在那些“看着没毛病、结果却不对劲”的瞬间里。下面这些坑,我基本都亲身踩过。

5.1 特征提取器“抢戏”的问题

在小样本实验里,特征提取器的强弱对结果影响巨大,甚至大到让人怀疑方法本身不重要。如果直接用一个规模很大的预训练模型做backbone,通常无论叠加什么小样本方法,结果都会不错。这时候要注意,如果预训练模型的训练数据覆盖了测试类别,那就等于把答案泄漏给了模型。

另一种典型情况是微调策略选错。把整个模型端到端微调,效果有时好、有时突然崩溃。我个人的建议是:先用冻结特征的方式跑一次baseline,再试全量微调,观察验证集上的表现来决定要不要解冻全部参数。一上来就全量微调,可能会因为样本太少而迅速过拟合。

5.2 验证集过拟合与“刷榜幻觉”

Few-shot实验天然样本少,如果在固定验证episode池上反复调参,模型的状态很可能是在“记住验证集上的随机波动”,而不是真正变强。结果一到测试集,效果立刻打回原形。

我给自己定过一条规矩:同一个实验方向最多在验证集上跑两三轮,最终测试集只跑一次,并固定随机种子。这样做虽然会让自己少一些“我再试试”的投机空间,但能有效减少自我欺骗。做工程交付时,这种保守做法反而更受客户信任。

5.3 数据增强的收益与反效果

数据增强在小样本场景下通常很有用。随机裁剪、水平翻转、适度的颜色扰动,都能明显提升模型对新类别的泛化能力。但增强不是越猛越好。如果颜色抖动过于强烈,或者使用了过强的正则化,模型很可能学不到有效特征,在本来就稀缺的样本上“越增越虚”。

我印象最深的是一次5-shot项目:盲目堆叠了五六种增强手段后,验证准确率反而下跌了两三个点。后来逐项消融,发现只有随机裁剪和水平翻转带来了正向收益,其他增强要么没帮助、要么拖后腿。所以实验时一定要做增强消融,不要“大锅乱炖”。

5.4 评估随机性:同一份代码跑三次结果不一样

Few-shot实验的评测结果对episode采样非常敏感。即使训练过程完全一致,测试时换一批episode,准确率也可能波动两三个点。这在5-way 1-shot场景下尤其明显,因为一次预测只依赖单张图片。

正确的做法是:要么固定一个测试episode池,所有方法共用同一批episode;要么跑多个种子取平均值并报告标准差。如果只跑一次、只挑最好结果,很容易得出不可复现的结论。对普通业务项目来说,这种随机性也意味着:上线前最好在多个随机种子下各跑几遍,确认性能稳定,再谈部署。

最后说点个人体会。小样本学习不是万能药,接到一个业务需求时,我会先问自己三个问题:新类别在训练阶段是否完全不可见?每个新类别是否真的只有极少量标注样本?标注人员能否为每个新类别提供足够一致的参考样本?如果三个答案都是“是”,那这个场景才真正适合Few-shot这一套。如果只是“历史数据少,但未来数据会不断涌来”,那更好的方向可能是无监督预训练加主动学习,而不是硬套小样本框架。

这几年小样本学习的方法论还在快速演进,新的技巧不断出现。但无论外面怎么变,只要你在入门阶段把度量学习和评估协议这两块基本功打牢,后面读新论文、做新实验都会轻松很多。希望这篇初探能帮你少走一点弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进