想象你第一次来到郊外观星,夜空中散落着三片隐约可见的星群:一片呈勺状,一片呈十字,一片呈弯弧。你只认识其中几颗最亮的星,却要判断一颗新出现的星星属于哪一片星群。直觉做法是:先找到每片星群的大致中心——那些亮星围绕的“共同重心”,再看新星离哪个中心最近。这就是 Prototypical Networks(原型网络)的核心思想:用极少量的样本来计算每个类别的“星光重心”(即原型向量),然后按距离把新样本归入最近的星座。这篇教程没有复杂的数学推导,而是从星空直觉出发,带你一步步认识原型网络如何完成小样本分类,并理解它的公式、代码与实战价值。
掌握原型网络,你便打通了小样本分类的经典基线,也为后续理解 MAML、CNAPS 等更复杂的元学习算法铺好了跳板。它是 AI 工程师进阶元学习领域的必经一站。
目录
可视化演示
展示时域信号波形与其频域频谱的对应关系,通过调节频率和噪声观察信号分解过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
时域与频域分析 青色曲线为时域信号波形 橙色柱为频域频谱分量 X(k) = Σ x(n)·e^(-j2πkn/N) FFT将信号从时域转换到频域。
核心思想:每个类别都有“重心”
回到观星的场景。你之所以能判断新星星属于哪片星群,不是因为记住了每一颗已知星的位置,而是因为每片星群都有一个“视觉中心”——所有星星环绕着它聚拢。只要这片星群没有被完全打散,中心就具有代表性,新星的归属也就有了依据。这个“视觉中心”,就是类别的原型(prototype)。
在原型网络中,每个类别的原型不是凭空给出的,而是通过对该类别所有已知样本(称为支撑集,support set)的特征向量取平均得到的。为了让“平均”有意义,我们需要先把原始样本送进一个神经网络,将其映射到一个嵌入空间(embedding space)。在这个空间里,属于同一类别的样本彼此靠近,就像同一个星座的星星天然聚在一起。支撑集的平均位置就构成了类别原型。
形式化地说,假设支撑集里类别 $k$ 的样本集合为 $S_k$,经过嵌入函数 $f_\phi$ 后,该类的原型向量定义为:
一句话总结:原型网络之所以“能打”,是因为它把复杂的分类任务化简为“找重心 + 比远近”,就像观星者只需记住每片星群的大致中心,而不是每一颗星的精确位置。
- 文本中的符号约定$x_i$ 是第 $i$ 个原始样本,$y_i$ 是其类别标签;$S_k$ 表示所有属于类别 $k$ 的支撑样本组成的集合。
- 嵌入函数是什么$f_\phi$ 是一个带参数 $\phi$ 的神经网络,把原始样本 $x_i$ 变成向量 $f_\phi(x_i)$,同一类样本做相同映射后更接近。
- 求和是在做什么把所有支撑样本的嵌入向量逐位相加,等价于把所有“星星坐标”按维度累加。
- 平均的意义用集合大小 $|S_k|$ 求平均,得到类别 $k$ 的中心 $c_k$,即原型向量。
模型结构:观星者的三件套
观星者通常随身携带三样东西:望远镜、星图和量角器。望远镜负责看清星星(提取特征),星图负责标记已知星群(保存支撑集),量角器负责测量角度和距离(判断远近)。原型网络的结构恰好对应这三个组件。
第一个组件是特征提取网络,即嵌入函数 $f_\phi$。它扮演望远镜,把原始图像或特征向量映射到嵌入空间。第二个组件是原型计算模块,它不训练参数,而是做一次简单的算术:把同一类的支撑样本嵌入取平均,得出每个类的原型。第三个组件是距离分类头,它对查询样本与各个原型求距离,再将负距离转换为类别置信度。
整个推理流程可以表示为一个概率公式:给定查询样本 $x$,它属于类别 $k$ 的概率由查询嵌入 $f_\phi(x)$ 与原型 $c_k$ 的距离决定:
这套结构和观星工具一一对应:望远镜负责看清特征,星图负责存样本,量角器负责算距离。三件套缺一不可。
- 负距离的作用距离 $d$ 越大说明越不相似,加负号变成越大越相似的得分。
- 指数核化对得分取指数,把“差距”放大成非负权重,同时保证计算稳定。
- 分母归一化对全部 $K$ 个类别的指数求和,把所有类的分数压缩成总和为 1 的概率分布。
原型向量:支撑集求均值
你可能会问:为什么要用“平均”来代表一个类别?难道一个类别内部没有差异吗?当然有,但小样本场景下我们没有太多信息,平均是一种最稳的估计——就像做饭时不知道每个人口味,最安全的做法是取“大众口味”的平均值。原型向量就是类别信息在嵌入空间中的“大众口味”。
从数学上看,原型向量是对支撑集嵌入的类内平均。这个操作有两个好处:第一,它不引入额外可学习参数,完全由支撑集直接计算,因此面对新类别时无需微调模型;第二,它天然支持任意数量的类别,只需要对每个类分别求均值即可。值得注意的是,虽然“平均”听起来简单,但真正重要的是嵌入函数 $f_\phi$。如果嵌入函数训练得好,同类样本的嵌入自然聚拢,平均值就极具代表性;如果嵌入函数混乱,平均值也会失去意义。所以原型网络的全部学习压力都集中在嵌入网络上——这就像观星者不仅要有一张好星图,更关键的是有一台能把星星“看清”的好望远镜。
在实际训练中,支撑集通常由 $N$ 个类别、每类 $K$ 个样本组成,即所谓的 $N\text{-way } K\text{-shot}$ 设定。$K$ 越小,计算出的原型噪声越大,但原型网络的价值恰恰是在 $K=1$ 或 $K=5$ 时依然保持可用。原型向量的计算完全透明、可解释,这是它优于许多黑箱模型的重要特质。
距离度量:直线还是方向?
有了原型之后,剩下的问题就是“怎么算距离”。现实中,判断两颗星星是否属于同一片星群,既可以用它们之间的直线距离(欧氏距离),也可以用它们之间的方向夹角(余弦距离)。两种度量方式对应两种不同的几何直觉。
欧氏距离关注的是绝对位置差:
它计算查询样本嵌入与原型向量在嵌入空间中的直线长度。余弦距离关注的是方向一致性:
它先算两个向量夹角的余弦值(角度越小,余弦越接近 1),再用 1 减去它得到“距离”。
在实践中,原论文作者发现欧氏距离在小样本分类任务上普遍优于余弦距离。原因是欧氏距离对向量长度更敏感,与“类别中心”的几何结构更匹配:如果把嵌入网络看作一个线性判别模型,欧氏距离下的原型分类恰好等价于高斯判别分析,具有更扎实的理论支持。而余弦距离强行忽略了模长信息,反而丢失了一部分判别能力。当然,在某些语义场景下,余弦距离仍可作为备选。
- 欧氏距离的计算对两个向量逐位相减后取平方和,再开根号,得到实际直线长度。
- 余弦相似度两向量的内积除以各自的模长乘积,结果落在 $[-1, 1]$,越大表示方向越一致。
- 转成距离用 1 减去余弦相似度,相似度越高,距离越接近 0。
训练策略:用“情景”练就观星术
原型网络不是在普通数据集上一次性训练,而是采用Episode 元训练策略。观星者不是靠死记硬背某一片夜空来学习,而是反复在随机挑选的若干片星域中练习“找重心”的能力;每次练习都面对一组全新的星群组合。这就逼迫他学会通用的方法,而不是记住特定星空。
每个 episode 的具体流程是:先从全部类别中随机抽取 $N$ 个类别,再从这 $N$ 个类中各自抽取支撑集($K$ 个样本)和查询集($Q$ 个样本)。原型网络用支撑集计算原型,再对查询集样本进行分类,最后用分类误差更新嵌入网络的参数。这样一个 episode 就是一个完整的“小规模多分类任务”,模型的每一次更新都发生在任务层面,而不是样本层面。
训练所用的损失函数是查询集上的负对数似然:
通过大量随机组合的 episode 训练,模型学会了在不同类别组合下都能快速找到“重心”并正确分类。这种“见多识广”恰如观星者阅遍无数星域,最终形成了通用的星群识别能力。
- 单样本对数项对查询样本 $x_i$ 的真实类别概率取对数,概率越大,$\log$ 值越接近 0。
- 负号与平均取负号使损失为正,再对全部 $|Q|$ 个查询样本求平均,得到该 episode 的整体损失。
Matching 对比:一对一与一对多
在元学习的星空中,原型网络并不孤独。它的近亲 Matching Networks(匹配网络) 同样支持小样本分类,但两者的机制有微妙差异。打个比方:Matching Networks 像是观星者对星空图中的每一颗已知星都单独比对,新星与每颗星分别测距离,再按加权投票决定归属;原型网络则是先把同类星星汇总成一个“代表星星”,再只与代表比远近。
Matching 网络通过注意力机制计算查询样本与所有支撑样本的加权求和,相当于保留每个支撑样本的独立影响。而原型网络先做一次均值聚合,把支撑集压缩成类别原型,再去算距离。这意味着原型网络的计算量更小——查询样本只需和 $N$ 个原型比较,而 Matching 网络需要和全部支撑样本逐个比较。当支撑集很大时,原型网络的推理速度优势非常明显。
但两者并非谁绝对更好。在分布较为复杂、同类样本内部呈多峰分布时,Matching 网络保留逐样本信息的做法可能更有弹性;而在支撑集规模有限、类别边界清晰时,原型网络的均值归纳更稳健。实际选型时,通常把原型网络作为首选基线,因为它是当之无愧的“最简可行方案”,而且解释性更强——原型本身就是一种可解释的类别表征。
代码实践:从星空到 PyTorch
理论落地向来是新手最关心的环节。原型网络的代码并不复杂,核心步骤只有数据组织、模型定义、训练循环和评估。我们先在合成高斯点云数据上模拟“观星”任务:4 片星群(4 个类),每片星群取 5 个已知点(5-shot),其余点作为查询集。嵌入网络用简单的多层感知机即可。
训练阶段的核心是 sample_episode 函数:它从数据集中随机挑类、随机挑支撑集和查询集。模型先用支撑集算出原型,再用欧氏距离分类查询集,最后用交叉熵损失反向传播更新嵌入网络。评估阶段与训练几乎一致,只需在未见过的类别组合上计算准确率即可。整套逻辑与前面讲的“观星术”完全吻合——随机星域 + 找重心 + 比远近。
Python 代码部分将在第 5 节给出完整实现。你可以在本地运行,观察不同 N-way K-shot 配置对准确率的影响,这能帮助你建立起对小样本分类任务最直观的体感。
局限展望:星空亦有边界
原型网络虽然优雅,但并不是万能的。它最大的局限在于数据稀缺:当支撑集每类只有 1 个样本时,原型的方差会非常大,就像只凭一两颗星星很难确定星群重心。另一个问题是分布漂移:如果测试时新类别的数据分布与训练时差异很大,原型的均值估计就会失真,分类效果大打折扣——这好比你在北半球练熟了看星的技巧,突然被扔到南半球,熟悉的星群全变了样。
为了应对这些局限,研究者提出了若干改进方向:一是多原型方法,允许一个类别拥有多个原型,以刻画类内的多峰分布;二是半监督扩展,利用无标注数据来修正原型位置;三是与度量学习结合,通过更强大的嵌入函数让同类样本在嵌入空间中更加紧凑。这些方向都是在原型网络这个坚实基线之上的自然延伸。
理解原型网络的边界,与理解它的优点同样重要。就像一位成熟的观星者,既知道如何用星群重心快速判断归属,也知道何时该拿出星图辅助、何时该借助更精密的仪器。
AI 应用场景
罕见病影像识别
阶段:小样本分类落地
解决的问题:罕见病在临床上样本极其稀缺,难以为每个病种准备充足的训练数据。传统监督学习在样本不足时严重过拟合,模型难以泛化。原型网络通过在大量已知病种上训练嵌入网络,使新病种只需要几张标注影像即可快速建立分类原型,完成识别。
具体案例:某医院积累了 2000 张常见皮肤病的皮肤镜影像,但新引入的一种罕见皮肤病的标注图像只有 8 张。医生使用原型网络在 2000 张常见病影像上元训练嵌入网络,再加入 8 张罕见病图像作为支撑集,模型立刻可以对新患者影像进行分类,无需重新训练。原型向量的可视化还帮助医生验证了模型关注区域的合理性。
新品类商品识别
阶段:增量学习与上线部署
解决的问题:电商平台每周都会上架全新品类的商品,传统分类器需要重新收集大量数据、重训模型,周期长且成本高。原型网络的“无需微调即支持新类”特性,让运营人员只需为新品类上传少量样例,就能在嵌入空间中立即生成原型并开始分类。
具体案例:某电商平台在鞋服分类系统中部署原型网络。当出现“洞洞鞋”这一新品类时,运营人员上传 5 张不同角度的商品图,系统自动计算新类别原型,并即刻对所有新商品图完成分类。整个过程不到 10 分钟,而旧方案需要一周的数据标注与模型重训周期。
Python 演示
"""Prototypical Networks 原型网络:合成小样本分类"""import torchimport torch.nn as nnimport torch.optim as optimimport numpy as np class EmbeddingNet(nn.Module): """把原始 2D 坐标映射到嵌入空间的编码器""" def __init__(self, input_dim=2, hidden_dim=64, output_dim=8): super(EmbeddingNet, self).__init__() self.encoder = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, output_dim) ) def forward(self, x): return self.encoder(x) def compute_prototypes(support_emb, support_y, num_classes): """对每个类别求支撑集嵌入的均值,得到原型向量""" prototypes = [] for cls in range(num_classes): mask = support_y == cls class_emb = support_emb[mask] prototypes.append(class_emb.mean(dim=0)) return torch.stack(prototypes) def euclidean_distance(x, y): """批量计算查询嵌入与所有原型之间的欧氏距离""" x2 = (x ** 2).sum(dim=1, keepdim=True) y2 = (y ** 2).sum(dim=1, keepdim=True) cross = 2.0 * torch.matmul(x, y.t()) return torch.sqrt(x2 + y2.t() - cross + 1e-8) def train_one_episode(model, optimizer, support_x, support_y, query_x, query_y, num_classes): """单个 episode 训练:嵌入->原型->距离->损失->反向传播""" model.train() support_emb = model(support_x) query_emb = model(query_x) prototypes = compute_prototypes(support_emb, support_y, num_classes) dists = euclidean_distance(query_emb, prototypes) logits = -dists loss = nn.functional.cross_entropy(logits, query_y) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item() def sample_episode(features, labels, num_classes, support_size, query_size): """从数据集随机采样一个 episode:选类、抽支撑集和查询集""" classes = np.random.choice(np.unique(labels), num_classes, replace=False) support_x, support_y, query_x, query_y = [], [], [], [] for new_cls, old_cls in enumerate(classes): idx = np.where(labels == old_cls)[0] np.random.shuffle(idx) support_idx = idx[:support_size] query_idx = idx[support_size:support_size + query_size] for i in support_idx: support_x.append(features[i]) support_y.append(new_cls) for i in query_idx: query_x.append(features[i]) query_y.append(new_cls) return (torch.tensor(np.stack(support_x), dtype=torch.float32), torch.tensor(support_y, dtype=torch.long), torch.tensor(np.stack(query_x), dtype=torch.float32), torch.tensor(query_y, dtype=torch.long)) def main(): """在 4 类高斯点云合成数据上训练原型网络""" np.random.seed(42) torch.manual_seed(42) num_classes, support_size, query_size = 4, 5, 15 means = np.array([[0, 0], [4, 0], [0, 4], [4, 4]]) features, labels = [], [] for cls, mean in enumerate(means): points = np.random.randn(200, 2) * 0.8 + mean features.append(points) labels.extend([cls] * 200) features = np.concatenate(features) labels = np.array(labels) model = EmbeddingNet() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(200): s_x, s_y, q_x, q_y = sample_episode( features, labels, num_classes, support_size, query_size) loss = train_one_episode(model, optimizer, s_x, s_y, q_x, q_y, num_classes) if (epoch + 1) % 50 == 0: print("epoch %d | loss %.4f" % (epoch + 1, loss)) print("训练完成") if __name__ == "__main__": main()
class EmbeddingNet(nn.Module)定义嵌入网络,把原始 2D 坐标映射到 8 维嵌入空间,网络参数就是全部的“可学习参数”。self.encoder = nn.Sequential(...)三层线性层加 ReLU 激活,构成一个简单的特征提取器。def compute_prototypes(...)遍历每个类别,用布尔掩码筛出该类支撑样本,然后mean(dim=0)求原型。prototypes.append(class_emb.mean(dim=0))这是原型计算的核心:类内嵌入的均值。def euclidean_distance(x, y)用平方展开方式计算两两欧氏距离,避免显式写循环。logits = -dists把距离取负号,距离越近得分越高,作为 logits。loss = nn.functional.cross_entropy(logits, query_y)对查询集计算交叉熵损失,驱动嵌入网络更新。def sample_episode(...)随机选类、随机抽支撑集与查询集,构造一个元学习任务。classes = np.random.choice(np.unique(labels), num_classes, replace=False)从数据集中不重复抽取 4 个类别。for epoch in range(200)训练 200 个 episode,每个 episode 都面对不同的类别组合。
常见误区
误区原型就是支撑集样本直接求平均,跟嵌入网络没关系;只要有了支撑集,不做任何训练也能分类。网上很多简化教程让初学者以为原型网络“零训练”。
正解原型向量虽然是支撑集均值,但均值计算建立在嵌入网络已经训练好的前提上。嵌入网络决定了样本在空间中的分布方式——如果嵌入很差,同类样本的均值可能落在其他类附近,分类自然一塌糊涂。原型网络的本质是“训练一个优秀的嵌入网络”,均值只是最后临门一脚。就像星群中心只有在星星已经按星座聚集时才有意义,如果满天星星乱序排列,求出的“中心”没有任何代表性。
为何常见:因为原型向量的计算式子太过简单,初学者容易忽略背后的嵌入训练,误以为完全不需要学习过程。
误区反正都是度量距离,欧氏距离和余弦距离在原型网络里可以任意替换,效果差别不大。有些同学在实现时随手换成 cosine 距离,以为只是换一行代码的事。
正解原论文的实验表明,在小样本分类基准上,欧氏距离显著优于余弦距离。原因在于欧氏距离保留了向量的模长信息,在训练良好的嵌入空间中,样本距离原型的“绝对距离”具有判别力;而余弦距离只比较方向,丢弃了模长这一维宝贵的信息。换距离度量相当于换了整个分类空间的几何结构,不是简单替换。
为何常见:大量自监督学习(如 SimCSE)和对比学习模型都偏爱余弦相似度,容易让初学者形成惯性思维,误以为所有向量任务都该用余弦距离。
误区直觉上总认为加大支撑集规模(比如从 5-shot 加到 50-shot),原型网络的分类准确率就会无限提高。
正解支撑集越大,原型的方差确实会降低,但最终效果还受嵌入空间质量的约束。如果嵌入网络的表达能力不足,或者不同类别的分布本身高度重叠,即便支撑集再大,原型的区分度也不会提升。此外,随着支撑集规模增大,episode 内数据分布的多样性增加,模型反而可能面临“元训练与元测试不匹配”的问题。原型网络的设计初衷就是小样本场景,盲目扩大支撑集偏离了它的适用区间。
为何常见:传统监督学习的经验告诉我们“数据越多越好”,这种思维惯性与元学习任务的目标存在根本冲突。
面试题
Q1. 原型网络中原型向量的定义是什么?
- A.某个支撑样本的原始输入向量
- B.类别内所有支撑样本嵌入的平均向量← 正确答案
- C.查询样本对应的嵌入向量
- D.神经网络第一个隐藏层的输出
答案:B。原型向量由支撑集样本经嵌入网络映射后取均值得到(B 正确)。A 丢掉嵌入映射,C 是查询嵌入,D 是网络中间特征,均非类别原型。
Q2. 原型网络进行小样本分类时,新样本的类别如何确定?
- A.随机选择一个类别
- B.找到距离最近的那个类别的原型← 正确答案
- C.与所有支撑样本逐一比较取投票
- D.直接输出预设类别
答案:B。原型网络将查询样本嵌入,再与各类原型计算距离,归入距离最小的类别。C 是一对一比对,属于 Matching Networks 的做法。
Q3. 在 N-way K-shot 设定中,“N-way”和“K-shot”分别指什么?
- A.N个类别、每类K个支撑样本← 正确答案
- B.N个支撑样本、K个类别
- C.N个查询样本、K个测试集
- D.N个训练集、K个验证集
答案:A。N-way 代表每次任务使用 N 个类别,K-shot 代表每个类别提供 K 个支撑样本。这是小样本学习任务的标准描述。
Q4. 原型网络的训练为什么要用 Episode 方式?
- A.为了避免过拟合单个样本
- B.让模型学会面对新类别组合快速分类← 正确答案
- C.减少数据加载的内存占用
- D.因为交叉熵只能用于 episode
答案:B。Episode 元训练随机组合类别与样本,模拟测试时“见到新类别”的场景,迫使模型学会通用的原型归纳与距离判断能力,而不是记忆特定类别。
Q5. 欧氏距离与余弦距离在原型网络中的区别是什么?
- A.欧氏距离关注方向,余弦距离关注长度
- B.欧氏距离关注绝对位置,余弦距离关注方向夹角← 正确答案
- C.两者数学上完全等价
- D.余弦距离计算更慢
答案:B。欧氏距离计算空间中两点直线长度,对向量模长敏感;余弦距离只关注向量的方向夹角。原型网络的实验表明欧氏距离通常更优。
Q6. 原型网络与 Matching Networks 最本质的差异是什么?
- A.原型网络不用嵌入网络
- B.原型网络先把同类样本聚合成原型,再算距离← 正确答案
- C.Matching 不需要支撑集
- D.两者没有任何区别
答案:B。Matching Networks 对每个支撑样本单独计算注意力权重,原型网络则先用均值聚合出类别原型,再以“查询到原型”的距离分类,计算量更小、归纳更强。
Q7. 为什么原论文中欧氏距离比余弦距离效果更好?
- A.欧氏距离可微而余弦距离不可微
- B.欧氏距离等价于高斯判别模型,数学解释更清晰← 正确答案
- C.余弦距离需要额外归一化网络参数
- D.欧氏距离在反向传播时梯度更大
答案:B。在欧氏距离下,带线性嵌入的原型网络可以等价于特定协方差结构的高斯判别分析,具备理论保证;余弦距离丢弃了模长信息,削弱了嵌入空间的表征能力,因此实践中通常略逊一筹。
Q8. 原型向量在推理阶段有哪些用途?
- A.表示类别在嵌入空间的中心位置← 正确答案
- B.计算查询样本到每个类别的距离← 正确答案
- C.作为分类决策的直接依据← 正确答案
- D.存储所有支撑样本的原始像素
答案:ABC。原型向量是类别的中心表征(A),被用于计算查询样本与各类的距离(B),并进一步决定分类结果(C)。D 描述的存储原始支撑样本并非原型向量的用途。
Q9. 原型网络在哪些情况下容易出现性能下降?
- A.支撑集每类只有 1 个样本,原型方差大← 正确答案
- B.训练分布与测试分布差异显著← 正确答案
- C.类别样本内部呈现多峰分布← 正确答案
- D.查询集样本数量非常庞大
答案:ABC。单样本导致原型不稳定(A),分布漂移让均值估计失真(B),多峰分布下单一原型难以代表整个类别(C)。D 查询集数量本身不影响原型质量,只影响推理时间。
Q10. 以下哪些是原型网络的合理改进方向?
- A.每类学习多个原型以刻画多峰分布← 正确答案
- B.引入无标注数据修正原型位置← 正确答案
- C.训练更强的嵌入网络使同类更紧凑← 正确答案
- D.增加全连接层直接输出类别编号
答案:ABC。多原型、半监督修正、更好嵌入网络都是原型网络的经典扩展方向。D 的全连接输出类别编号违背了小样本“类别不可预知”的核心假设。