☰
小样本图像分类实战:从原型网络到工程落地避坑指南
2026/10/23 5:12:45 网站建设 项目流程

你在实际项目中大概率遇到过这种情况:新产线上的一种产品缺陷,从试产到量产只积累了三十几张现场照片,却要求当天就给出一个分类结果,把它和十几种正常状态区分开。按传统思路,每个类别凑齐几千张标注样本再训练分类器,这条路根本走不通。这正是小样本图像分类(Few-shot Image Classification)要解决的:在每类只有一张到十几张标注样例时,让模型学会识别新类别。这篇文章不打算复述论文综述,而是从一个工程落地者的角度,把核心逻辑、代码实现和实际踩坑讲清楚,适合正要拿小样本学习去解决真实任务的读者参考。

1. 小样本学习到底在解决什么"不现实"的问题

1.1 传统分类器默认的三个条件,现实里几乎都不成立

常规图像分类的训练流程,默认了三个前提:第一,类别集合固定,模型最后一层的输出维度直接绑定类别数量,加一个新类别就要改结构、重新训练;第二,每个类别的样本量充足,几百张是起步,上千张才敢说稳妥;第三,训练数据和部署数据来自同一个分布,采集环境、相机参数、光照条件都相对一致。

这三个前提放到真实业务里,几乎每个都站不住。我做过的项目里,最常见的是工业质检和安防场景:正常样本成千上万,但每种缺陷或异常事件往往只有几十张,而且这个类别列表还在不断膨胀。今天上线时是十类,下周客户说又发现了一种新瑕疵,现场只能挤出二十来张照片,你说要不要支持?这就是长尾分布的真实写照——头部类别的数据多到用不完,尾部类别却少得可怜。

小样本学习就是针对这种情况提出的一套方法论:模型在大量"不同小样本任务"上训练,学会一种快速适应新类别的方式,最终面对一个全新的类别集时,每类只给一到十几张支撑样本(support set),也能做出像样的分类。

1.2 N-way K-shot:先把术语对齐

在往下聊之前,必须把几个术语对齐,否则后面看代码和论文都会懵圈。

N-way K-shot指的是一个分类任务里有 N 个类别、每个类别有 K 张标注样本。常见设定是 5-way 1-shot 和 5-way 5-shot。1-shot 就是每个类只给你一张图,5-shot 就是每个类给你五张图。这些给定的少量样本叫支撑集(support set),用来"教会"模型这几个新类别长什么样。另外还会准备一批同类别但不在支撑集里的图片,叫查询集(query set),用来评估模型学得怎么样。

举个例子:5-way 1-shot 任务中,支撑集就是 5 张图(每类一张),查询集可能有 75 张图(每类 15 张),模型只看过支撑集,然后要在查询集上做分类。测试时 K 越大通常越简单,因为每类可参考的信息多了。

这个设定和人类的学习方式很像。一个孩子见过几次猫,就能认出一只从没见过的猫,不需要先看几千张猫的照片。小样本学习想模仿的,正是这种"从少量样例归纳出类别概念"的能力。

1.3 为什么这个问题值得单独研究

可能有人会问:直接用数据增强多复制几份样本,或者拿预训练模型提取特征再训练一个分类器,不就能解决吗?

数据增强确实能涨点,但本质上只是让模型在同一分布里多看了几个"变形版本",解决不了类别本身信息不足的问题。迁移学习也有效,但真实场景里的新类别往往来自一个新域,预训练特征未必能直接覆盖,而且微调本身也需要一定数据量。小样本学习更关注"任务结构":专门设计训练方式,让模型在每个任务里都处于"少量样本"的状态,从而逼它学会利用类别间的共性和差异。打个比方,数据增强是给新员工塞更多重复培训资料,小样本学习则是教新员工掌握一套举一反三的工作方法。

2. 三大技术路线的底层逻辑与代表方法

2.1 度量学习:把分类问题变成比较远近的问题

度量学习(metric learning)是目前落地最稳的一类方法,核心思路是:学一个嵌入函数 f,把图片映射到一个特征空间,在这个空间里同类图片靠近、异类图片远离。分类的时候不再直接输出类别概率,而是计算查询图片和每个类别"代表点"的距离,谁近就归谁。

这条路线里,早期有孪生网络,把两张图分别送进两个共享权重的分支,输出一个相似度分数,本质上是在训练一个"图片对比器"。后来匹配网络引入了注意力机制,用查询图片与所有支撑样本的相似度做加权求和。再往后就是最有名的原型网络(Prototypical Network),把每个类的支撑样本嵌入向量取均值,作为该类的"原型"或者叫类中心,查询图片和哪个原型最近就属于哪类。关系网络更进一步,连"距离怎么算"都不固定了,直接把查询特征和支撑特征拼接起来,送给一个小网络学出一个关系分数。

这里有个值得琢磨的细节:原型网络为什么敢用均值当类中心?直觉上,嵌入空间里同一类样本会聚成一团,均值是这一团数据的稳健估计。1-shot 时没得选,均值就是那一个样本本身;5-shot 时均值能平滑掉个别样本的偏差。前提是编码器质量足够好,如果特征空间乱成一团,均值就成了几个不同类别的折中点,效果立刻崩。

2.2 元学习优化:学一个更容易适应新任务的初始化

另一类方法是元学习(meta-learning),最著名的代表是 MAML。它的想法不是学一个能直接分类的模型,而是学一套模型的初始化参数,保证在面对一个新任务时,只需要很少几步梯度更新,就能在新任务上达到不错的效果。

这个思路可以类比成新员工入职。普通培训是给员工灌输具体业务知识;MAML 则先训练员工拥有一种"通用工作方法",到了新岗位上,看两三个例子就能快速上手。技术上实现起来,外层循环负责在多个任务之间优化初始参数,内层循环用少量样本对模型做几次梯度下降,最后用查询集计算损失回传。

MAML 的问题在于二阶导数的计算开销大、训练稳定性难控制。后来出现的 Reptile 之类简化版本,去掉了二阶导数,实际用起来更省心,但理论上限略低。这类方法在标准评测集上效果好,但工程落地门槛高,我一般建议团队有一定算法储备再考虑。

2.3 数据增强路线:让有限样本在特征空间里"增殖"

第三条路线更直接:既然样本少,那就想办法造样本。传统的数据增强(翻转、旋转、裁剪、色彩抖动)是最廉价的手段,在小样本任务里几乎是标配,但增益有限。更激进的做法是在特征空间中操作——把两张图的嵌入向量做插值,生成一个"虚拟样本"的特征表示;或者用一个生成模型,根据现有样本合成更多图片来训练分类器。

这种方法的优点是可以和其他路线叠加使用,不是互斥关系。缺点是合成的样本不一定符合真实分布,质量差的时候不但没帮助,还会引入噪声。我在实际项目中通常把数据增强作为辅助,很少作为主方案。

2.4 三条路线怎么选

路线核心思想代表方法优点缺点
度量学习特征空间里比较远近孪生网络、原型网络、关系网络训练稳定、代码简单依赖特征质量,类别极相似时吃力
元学习优化学初始化或更新规则MAML、Reptile理论上限高,跨任务泛化好训练贵、调参难、工程成本高
数据增强合成或插值生成更多样本传统增强、特征插值、半监督通用可叠加合成样本质量不稳定,单独用增益有限

如果今天要快速解决业务问题,我会优先走度量学习;如果团队有充足的时间和算力去调元学习方法,再考虑 MAML 系。

3. 最容易翻车的环节:任务划分与信息泄露

3.1 Episode机制到底在模拟什么

小样本学习里一个容易被忽略的关键设计是 episode(回合)。传统分类训练是反复看一批批图片,而小样本学习训练时每次从训练集的类别池里随机抽 N 个类,每个类抽 K 张做支撑集、再抽若干张做查询集,组成一个"小任务"。模型在这个小任务上计算损失、更新参数,然后换下一组类别组合,再来一次。

这么做的目的是让模型在训练阶段就反复体验"小样本"状态。一个只在大量样本下训练过的模型,遇到少量样本时往往会过拟合、不知所措;而经过大量小任务训练的模型,会逐步学会"怎么从少量样本中提取有效信息"这个元技能。

3.2 三种典型的"作弊"操作,最后一种最隐蔽

小样本学习对数据划分极其敏感,我见过不少项目因为划分失误,报告了漂亮的成绩,现场却一塌糊涂。最常见的三种作弊方式:

第一,类别重叠。训练集、验证集、测试集里出现了同一个类别的样本。比如某类物体训练时见过,测试时又换了同类的其他图片来考,模型等于提前见过答案。这类错误通常来自脚本里随机切分图片时没有按类别分组。

第二,预训练数据泄露。很多人习惯加载 ImageNet 预训练模型来提取特征,却忘了检查测试类别是否也出现在预训练数据里。如果预训练模型已经见过测试类,特征里就包含了这些类别的信息,准确率虚高 10 到 20 个点都不奇怪。

第三,支撑集与查询集重叠。同一个图片文件因为重复采样,同时出现在支撑集和查询集里。测试时模型直接"抄了答案",比前两种更容易被忽略。

判断是否泄露有一个土办法:训练时记录验证集的准确率走势。如果验证准确率异常地高且早早就收敛,先停下来检查数据管线,而不是高兴。

3.3 正确的评估方法:重复采样与置信区间

小样本任务的评测天然带有随机性——随机抽哪 N 类、随机抽哪几张当支撑集,结果都会波动。正确做法是固定一个随机种子,在测试集上随机采样大量 episode(比如 1000 次),计算平均准确率和 95% 置信区间。只看一次采样的结果没有意义,那可能是运气好。

同样重要的是把准确率拆开看:1-shot 和 5-shot 分开报告。有时候一个方法 1-shot 不行但 5-shot 不错,另一个方法反过来,只有分开报告才能看清方法的适用边界。

4. 从零手写一个原型网络:完整代码与关键细节

4.1 为什么第一个项目先选原型网络

原型网络是我给小样本学习入门者的默认推荐,原因有三:一是原理直观,代码量少,两百行内能跑通;二是训练稳定,用标准的交叉熵损失配合主流优化器就能收敛;三是作为一个强基线,很多新方法都要拿它做对比,理解它等于理解了度量学习的骨架。

下面我按实际项目的代码结构逐步拆解,用的是 PyTorch,图像假设为 84×84 的 RGB 图。

4.2 编码器与距离度量

import torch import torch.nn as nn import torch.nn.functional as F class Encoder(nn.Module): def __init__(self, dim=64): super().__init__() self.conv1 = nn.Sequential( nn.Conv2d(3, dim, 3, padding=1), nn.BatchNorm2d(dim), nn.ReLU(), nn.MaxPool2d(2) ) self.conv2 = nn.Sequential( nn.Conv2d(dim, dim, 3, padding=1), nn.BatchNorm2d(dim), nn.ReLU(), nn.MaxPool2d(2) ) self.conv3 = nn.Sequential( nn.Conv2d(dim, dim, 3, padding=1), nn.BatchNorm2d(dim), nn.ReLU(), nn.MaxPool2d(2) ) self.conv4 = nn.Sequential( nn.Conv2d(dim, dim, 3, padding=1), nn.BatchNorm2d(dim), nn.ReLU(), nn.MaxPool2d(2) ) def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) x = self.conv4(x) return F.adaptive_avg_pool2d(x, (1, 1)).flatten(1)
def euclidean_dist(x, y): # x: [n_query, dim], y: [n_class, dim] n = x.size(0) m = y.size(0) x = x.unsqueeze(1).expand(n, m, -1) y = y.unsqueeze(0).expand(n, m, -1) return torch.pow(x - y, 2).sum(2)

编码器用了四层卷积加池化,最后接全局平均池化得到 64 维特征。这个特征维度是刻意压低的——样本本来就少,特征维度太高容易过拟合。距离度量直接用欧氏距离,注意没有做 L2 归一化,因为在小样本场景下,欧氏距离同时编码了方向和模长信息,效果往往比余弦相似度稳定。

4.3 Episode采样:每次训练都是一场小考试

def sample_episode(class_indices, class_list, n_way, n_shot, n_query): support_x, support_y, query_x, query_y = [], [], [], [] chosen_classes = torch.randperm(len(class_list))[:n_way] for c in range(n_way): class_id = class_list[chosen_classes[c]] idx = class_indices[class_id] perm = torch.randperm(len(idx)) shot_idx = perm[:n_shot] query_idx = perm[n_shot:n_shot + n_query] for i in shot_idx: support_x.append(sample_image(i)) support_y.append(c) for i in query_idx: query_x.append(sample_image(i)) query_y.append(c) return ( torch.stack(support_x), torch.tensor(support_y, dtype=torch.long), torch.stack(query_x), torch.tensor(query_y, dtype=torch.long), )

这段代码的核心是class_indices,它是一个把类别 ID 映射到该类别所有图片索引的字典,训练前构建好。每次 episode 先随机选 N 个类别,再从这些类别里分出一部分当支撑集、另一部分当查询集。必须确保一个图片索引一旦进了支撑集就不会再进查询集,否则就是前面说的信息泄露。

4.4 训练与评估循环

def compute_loss(model, support_x, support_y, query_x, query_y, n_way): support_emb = model(support_x) # [n_way * n_shot, dim] prototypes = torch.stack([ support_emb[support_y == c].mean(0) for c in range(n_way) ]) # [n_way, dim] query_emb = model(query_x) dists = euclidean_dist(query_emb, prototypes) # [n_query, n_way] log_p = F.log_softmax(-dists, dim=1) return F.nll_loss(log_p, query_y)

训练循环:

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(60): total_loss = 0.0 for _ in range(200): support_x, support_y, query_x, query_y = sample_episode( class_indices, train_classes, n_way=5, n_shot=1, n_query=15 ) loss = compute_loss(model, support_x, support_y, query_x, query_y, n_way=5) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch}, loss {total_loss / 200:.4f}")

评估时同样采样大量 episode,但只计算准确率,不更新参数:

def evaluate(model, class_indices, test_classes, n_way, n_shot, n_query, n_episodes=1000): accs = [] for _ in range(n_episodes): support_x, support_y, query_x, query_y = sample_episode( class_indices, test_classes, n_way, n_shot, n_query ) with torch.no_grad(): support_emb = model(support_x) prototypes = torch.stack([ support_emb[support_y == c].mean(0) for c in range(n_way) ]) query_emb = model(query_x) dists = euclidean_dist(query_emb, prototypes) pred = dists.min(1).indices accs.append((pred == query_y).float().mean().item()) return torch.tensor(accs).mean().item(), torch.tensor(accs).std().item()

4.5 关键超参与参数选择

参数常用值原因
特征维度64样本少,高维特征容易过拟合
n_query15查询样本太少会导致梯度波动大
优化器Adam lr=1e-3训练稳定,基本不用额外调参
epochs60-100小数据集上再多会过拟合
图像尺寸84×84小样本评测的常见标准

训练时 n_shot 的选择有讲究:如果业务最终是 5-shot,训练时最好也按 5-shot 来采样,保持训练和测试的任务结构一致。如果只有 1-shot 的资源,那就按 1-shot 训练,勉强用 5-shot 训练反而会偏离部署场景。

5. 真实场景下的小样本分类:常见的坑与应对

5.1 坑一:支撑集的分布漂移,训练与部署脱节

标准评测集里,支撑集图片和训练图片来自同一批干净数据,但真实场景不是这样。我接过一个项目:训练数据拍得整整齐齐,测试时客户用手机在产线上拍,角度歪、光线暗、还有运动模糊,支撑集里全是这种"脏"图。

遇到这种情况,单纯堆模型没用。我的做法是先把支撑集图片做质量筛选,太模糊的直接剔除,再进行简单的域适应微调——用支撑集图片对编码器做低学习率微调几步,让特征分布往部署域靠一靠。这和测试时训练的思路一致,即使只有几张图,也比完全不动强。

5.2 坑二:评测的类别数不代表业务的真实类别数

评测集通常用 5-way 或 10-way,但真实业务可能是 30 类、50 类甚至更多。way 数越大难度越高,5-way 上表现不错的方法到 20-way 可能明显下滑。这个差距容易被低估,我见过团队在 5-way 上验证了 80% 的准确率,上线 20 类任务后直接掉到 60%。

应对方式很简单:评估时按真实的类别数去配置 n_way。如果业务里有 30 个可能类别,就用 30-way 来测试。训练时也可以适当提高 n_way,让模型适应更复杂的区分任务。

5.3 坑三:一张错误标注,足以带偏整个原型

原型网络用均值表示类别中心,1-shot 时均值就是那个唯一的样本,如果支撑集里有一张标错类别或者本身有问题的图,整个类的原型直接偏掉。5-shot 时均值还能缓冲一些噪声,但一张极端错误图仍然可能拖动均值。

因此,支撑集的人工核验非常重要。投入模型之前,把支撑集图片逐张过一遍,删除模糊、遮挡、重复以及明显标错的图。这一步花不了多少时间,但对最终准确率的影响远大于换模型。

5.4 坑四:没跑最简单的基线就开始堆模型

我见过不少团队一上来就上 MAML,然后调参调到崩溃,最后发现冻结预训练特征加一个逻辑回归或 SVM,效果已经接近甚至超过精心调优的元学习模型。这就是典型的基线缺失。

预训练特征本身携带了丰富的通用视觉知识,在小样本条件下,冻结特征加简单分类器是一个极强且极稳定的起点。建议任何小样本项目的第一个实验都是这个,而不是直接用复杂方法。基线的具体做法是:把支撑集图片送入预训练编码器,得到特征向量,训练一个逻辑回归分类器,再用查询集特征做预测,整个链路半小时内能跑完。

5.5 坑五:业务要的不一定是 top-1 正确率

很多小样本模型的评分指标是 top-1 准确率,但真实业务里,错误分类的代价远高于"拒绝回答"。质检场景里,把一个不良品判别成良品,后果可能是整批货出问题;而模型说"这张图我不确定",只需要人工再看一眼。

因此在部署环节,我非常建议引入拒识机制:计算查询图片与最近原型之间的距离,当距离超过某个阈值时输出"未知",交给人来处理。这样 top-1 准确率可能有所下降,但误判带来的损失会小很多。

6. 这条赛道接下来的几个方向与落地建议

6.1 预训练模型的冲击:基准线抬高了

近几年图像-文本对齐预训练模型兴起之后,小样本分类的两极分化越来越明显。一边是传统小模型加元学习方法在标准评测集上廪战,另一边是直接用大规模预训练模型的视觉特征做最近邻或线性分类,zero-shot 和 few-shot 效果已经相当惊人。很多经典元学习方法在大模型特征面前反而显得笨重。

但这也带来了新的评测困境:预训练数据很可能已经包含了测试类别,公平对比越来越难。我个人的看法是,预训练特征会成为未来小样本任务的基础底座,而研究重点会逐步转移到"如何高效适配新类别"和"如何避免预训练数据泄露"上。

6.2 半监督与测试时自适应的兴起

另一个明确趋势是利用无标注数据。真实业务里,有标注的支撑样本很少,但同一场景下没标注的图片往往很多。半监督小样本学习把大量无标注数据引入训练过程,帮助模型更准地刻画类别边界。与之相关的 transduction 方法允许在测试阶段利用整个查询集的分布信息,效果通常比独立预测每张查询图更好。

这些方法在工程上并不复杂,很多只需要在原模型后加一个聚类或者分布对齐步骤,值得在项目里优先尝试。

6.3 给入行者的落地建议

如果现在要接一个小样本图像分类项目,我会按这个顺序推进:第一步,梳理数据,检查标注质量,划分好类别不重叠的训练、验证、测试集;第二步,跑一个"预训练特征加简单分类器"的基线,拿到一个诚实的参考点;第三步,按业务真实类别数配置评估协议,确保指标能反映上线效果;第四步,在基线上依次尝试原型网络、数据增强、关系网络,必要时再挑战元学习方法。

这套流程看起来平淡,但实际项目里最值钱的就是这些平淡的基建工作。模型选型永远排在数据质量之后。

最后再分享一个自己的习惯:每次拿到支撑集,我会用脚本把所有图片拼成一张大图,用文件管理器直接过一遍。模糊的、过曝的、标错的、重复的,一眼就能挑出来。很多小样本项目准确率上不去,问题不在模型,而在于那一两张拖后腿的支撑样本。把这一步做扎实,再去谈算法,你会发现事情突然就顺了很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询