如果你已经用 softmax 训练过很多图像分类模型,大概率已经习惯“最后一层全连接 + softmax + 交叉熵”这个组合了。它简单、稳定、可解释性强,但真的没什么可优化的空间了吗?我最近在一组对比实验中,把最后的 softmax 分类器换成了卷积原型学习(CPL,Convolutional Prototype Learning),测试集准确率提升了 2 个点左右,更明显的变化是面对标签噪声和分布外样本时,模型不再动不动给出 99% 的“确定性错误”。这篇内容就围绕 CPL 展开:它到底是什么,为什么能在泛化性能上胜过常规 softmax,以及如何在自己的分类任务里落地。如果你手头正好遇到分类效果差、置信度虚高、或者噪声标签多的情况,这篇文章值得读完。
1. 为什么想换掉 Softmax:三个绕不开的痛点
1.1 过度自信:概率只表示相对比较,不代表真实置信度
Softmax 的核心操作是把一组实数 logits 转换成概率分布,公式是p_i = exp(z_i) / sum_j exp(z_j)。这个变换有两个天然特点:一是概率总和恒为 1,所有类别的预测得分是零和博弈;二是指数运算会放大 logits 之间的差距。哪怕最后两层得到的 logits 只是从 1.2 变成 2.3,softmax 的输出可能就从 40% 跳到 80%。
这种特性带来了常见的“过度自信”现象。我见过很多次这样的场景:一个被随机翻转了标签的脏样本经过模型推理,softmax 输出照样给出 0.99 的置信度,而且预测的恰好还是错误类别。为什么?因为模型只需要在 logits 上拉开一点点差距,就能产生极端概率。说白了,softmax 的置信度只是“这些类里面哪一个相对更高”,而不是“我对这个预测有多确定”。实际业务里,如果我们要用置信度做人工审核的触发条件,softmax 这种虚高置信度是非常危险的。
生活里有个类比:有人参加抢答比赛,他只需要比其他选手举手快一点就能得到答题权,并不代表他真的知道答案。Softmax 也是如此,它不关心自己的判断有没有依据,只要相对分数领先就“信誓旦旦”。这种机制在干净、封闭、类别均衡的数据集上问题不大,但在开放环境里就成了隐患。
1.2 闭集假设:面对没见过的东西,Softmax 只能硬猜
Softmax 分类器默认世界里只有训练时出现过的 K 个类别。推理时不管你输入什么,它都会在这 K 个类别里分配概率。如果来了一个完全没见过的新物种、新故障类型、新写作风格,softmax 依然会把概率分散给已有类别,有时还会以很高置信度预测成某个已知类。
有人会说,给模型加一个“未知类”标签不就行了?实际操作中很难。未知类的样本长什么样完全没有先验,你收集的未知类样本很可能只是冰山一角,模型会把所有不认识的都塞进这个类,反而干扰已知类边界。数学上也解释得通:softmax 的分母要求所有类别概率加起来为 1,这意味着新增一个类别会迫使原有类别的概率重新分配,整个决策边界都要跟着调整。这种“闭集设定”让模型对外界一无所知,也导致它很难作为真正的开放集系统使用。
相比之下,CPL 的分类依据是“样本特征与原型之间的距离或相似度”。如果样本距离所有原型都很远,模型有理由认为它不属于任何已知类。这个逻辑天然比 softmax 更适合开放场景,至少给了我们一个拒绝识别的抓手。
1.3 特征空间不够紧凑:分类器与特征学习脱节
用交叉熵训练 softmax 分类器时,骨干网络学习到的特征只需要“大致线性可分”就够了,不需要类内特别紧凑。模型只要能在某个方向的分类边界上把类别分开,损失就会下降。结果是特征空间中每个类别的样本常常像一团松散的云,不同类别的云团之间互相靠近甚至重叠,泛化就出问题。
有个现象能直观印证这一点:你把训练好的 softmax 模型的特征拿来计算类内平均距离,通常比基于度量学习的模型大得多。因为交叉熵损失不会惩罚类内分散,只惩罚分类错误的样本。换句话说,模型把精力都花在“分开不同类”上,没时间管“同类样本靠拢”。
还有一点容易被忽略:softmax 最后一层全连接的权重向量,某种程度上可以看作类中心的粗糙近似。但它跟特征向量之间是做点积,模型的分类边界本质上是线性的。当类别内部呈现多模态分布——比如一个“猫”类里既有橘猫又有黑猫,甚至还有猫的卡通图片——单一线性的分类头就很难表达这种复杂的子结构。CPL 的思路正是从这里切入:与其学一个线性分类面,不如直接学习一组“原型”,让特征向原型靠拢,每个类用多个原型覆盖不同子簇,从而让特征空间更紧凑、更可解释。
2. CPL 是什么:用“原型”替代“线性边界”的分类框架
2.1 原型:每类学一组“标准答案”
“原型”这个词听着玄乎,其实就是特征空间里的一个向量。你可以把它理解为每个类别的“标准答案”或者说“中心代表”。假设我们把图片输入骨干网络,得到一个特征向量,那么原型就是跟特征向量在同一空间里的可学习向量。模型做预测时,不再用线性层把特征映射成 K 个分数,而是计算特征向量和每个类别的原型有多像,像的程度就是该类的分数。
举个例子,如果我们识别水果,模型可能会学到“苹果”这个原型是一条靠近红色、圆形、高光的特征向量。当一个输入样本提取出的特征也落在附近,我们就认为它属于苹果。这跟人脑的认知方式有点类似:我们先形成对猫、狗、苹果的抽象模板,再用模板跟新见到的东西比对,而不是直接画一条数学边界。
一个类一个原型是最简单的形式,但现实中一个原型往往不够。以后端“猫”为例,橘猫、黑猫、白猫在颜色特征上差异可能很大,单个原型会被挤压在一个尴尬的位置,既不像橘猫也不像黑猫。CPL 的做法是允许一个类别拥有多个原型,让不同原型负责不同子簇。预测时只要样本跟这一类中的任意一个原型匹配上,就足以支撑分类决策。这样既保留了原型的可解释性,又不会牺牲模型的表达能力。
2.2 从原型网络到卷积原型学习:可学习的非参数方法
说到原型学习,很多人会先想起小样本学习里的原型网络(Prototypical Networks)。那个方案里,每个类别的原型是通过对支持集样本的特征求平均算出来的,是一种确定性的非参数方法,不参与梯度更新。CPL 跟它最大的不同在于,CPL 的原型是网络参数的一部分,通过反向传播端到端学出来的。
可学习原型带来几个实际好处。第一,原型不必是某个子类样本的简单均值,它可以被优化到更利于分类的位置,甚至能学到支撑样本均值无法表达的特征方向。第二,训练过程中骨干网络和原型可以协同更新,特征向“适合用原型度量”的方向演化,两者的适配度高很多。第三,当类别数据不均衡时,通过损失函数可以主动拉开原型之间的距离,避免少数类被多数类挤占。
“卷积”二字又是什么意思?CPL 里的原型本质上可以看作一组卷积核。当我们说“用原型计算相似度”,如果保持空间维度,就是让原型在特征图上滑动做卷积,得到每个位置的原型响应,再聚合出分类分数。这种设计把分类器变成了一种“空间感知”的匹配器,而不只是全局向量的点积。后面我会专门拆解这个机制。
2.3 整体流程:把最后的全连接层换成原型匹配层
使用 CPL 时,骨干网络几乎不用改。你仍然可以用常见的卷积骨干结构提取特征,甚至继续使用在 ImageNet 上预训练的权重。唯一要替换的是最后的分类层:把nn.Linear(feat_dim, num_classes)换成原型匹配层,内部维护一组形状为[num_classes * num_prototypes, feat_dim]的可学习向量。
训练时,数据仍然走骨干网络得到特征,再进入原型层得到每类的匹配分数,最后接上交叉熵损失。因为整体框架仍然是“特征提取器 + 可微分类器 + 交叉熵”,所以优化器、学习率调度、数据增强这些常规手段全部可以沿用。换句话说,CPL 不是一套全新的训练体系,而是一个结构替换方案,这也解释了为什么它被称作 softmax 的“替代品”。
推理时同样简单,输入图片 → 骨干特征 → 原型匹配分数 → argmax。如果你对拒绝未知类有需求,还能额外利用“距离最近原型的相似度”作为不确定度指标。下面我们就深入到核心机制,讲讲相似度计算、原型更新和多原型聚合到底怎么实现。
3. 核心机制拆解:原型如何学习、相似度如何计算
3.1 相似度计算:内积、余弦与温度缩放
CPL 最核心的计算是“特征-原型”相似度。最简单的实现是内积:score = x · p,这跟线性层的计算几乎一样。但直接使用内积会有一个问题:特征向量的模长会影响分数大小。模长大的特征天然更容易得到高分,模型可能学会“把特征模长变大来讨好分类器”,而不是真正跟原型方向靠近。
所以我更推荐使用余弦相似度:先对特征和原型分别做 L2 归一化,再算内积,这样分数只跟方向相关,跟模长无关。余弦相似度取值范围是 [-1, 1],直接作为 logits 时数值范围太小,会让 softmax 的梯度变弱,导致训练变慢。解决办法是引入一个温度参数 T,将相似度除以 T 后再进入交叉熵,即score = cos_sim / T。温度越小,logits 的区分度越大,训练越激进;温度越大,分布越平滑。
在实际训练中,我习惯把温度初始化为一个较小的值,比如 0.05,然后作为可学习参数让网络自己调整。也见过有人直接固定 T=0.1 也能跑得很稳。这和对比学习里的温度系数本质上是同一件事:控制样本对的相似度分布锐利程度。如果你发现训练初期损失降不下去,多半是温度设得太大,把所有相似度都压扁了。
3.2 一个类别多个原型:解决类内多模态
一个类别有 M 个原型时,匹配分数会变成[num_classes, M]的矩阵。我们需要把它聚合成一个类别分数,再喂给交叉熵。最常见的聚合方式是logsumexp,也就是对 M 个相似度做平滑最大值。公式写出来是log(sum_i exp(s_i)),它的特点是当某一个原型匹配度特别高时,聚合分数几乎等于最高分,但同时又保留了对多个原型都匹配时的累积效应。
为什么不用硬 max 或平均?硬 max 的梯度只流向得分最高的那个原型,其他原型得不到学习信号,容易坏死;平均则会被大量低分原型拖累,导致类别分数被拉低。logsumexp介于两者之间,既让高分原型主导,又让其他原型有机会获得梯度。用大白话说,这是一个“选最有代表性的那个原型,但不完全无视其他原型”的软选择操作。
多原型的数量 M 是一个核心超参。太小,覆盖不住类内多模态;太大,原型之间可能重叠甚至互相竞争,模型会把原型当作记忆样本的工具,导致过拟合。我在常见图像分类任务上试下来,M 选 2 到 5 是比较稳的区间。具体选多少,要看类的多样性和训练数据量。数据量很大、风格差异明显的类别,可以适当多给几个原型;数据量小或者类别很单纯,一个原型也够。
3.3 原型的初始化与更新
原型初始化的方式对收敛影响非常明显。我试过三种:随机初始化、类别均值初始化、用预训练 softmax 分类头参数初始化。随机初始化最省事,但训练前期损失会剧烈震荡,因为随机向量跟真实特征分布相距太远,模型需要很多轮才能把原型拉到有效区域。
类别均值初始化是我最喜欢的方式。方法很简单:先用现有特征提取器对训练集做一次前向,收集每个类别所有样本的特征向量,按类求平均,然后把均值向量作为该类原型的初始值。如果多原型,就给均值向量加一些小噪声,避免所有原型完全重合。这个操作能极大缩短冷启动阶段。软标签分类头参数初始化也有一定效果,但等价于从一个已经偏斜的边界出发,不如直接以数据分布为中心。
训练开始后,原型和骨干网络一起走梯度下降优化器。但要注意,原型是参数,它们是参与前向计算并接收梯度的。如果想加强原型之间的分野,可以在损失函数里增加一项“原型间距正则”,鼓励不同类别的原型互相远离。更简单的做法是在每次参数更新后对原型做 L2 归一化,保证它们落在单位超球面上,这样可以维持数值稳定性。
3.4 “卷积”到底体现在哪里
有人会问:前面讲的都是向量计算,和卷积有什么关系?原因在于特征不总是全局向量,而是空间特征图。假设骨干网络输出的特征是[B, C, H, W],我们需要对空间位置进行信息聚合。传统做法是先做全局平均池化得到[B, C],再进线性层。CPL 想保留空间信息,可以选择不先池化,而是把每个原型当作一个1×1卷积核,直接在特征图上做卷积。
每个类别 M 个原型就是num_classes * M个1×1卷积核。卷积输出的响应图[B, num_classes * M, H, W]中,每个空间位置上的值代表该位置的原型匹配程度。接下来可以对整个响应图做平均池化或最大池化,得到该原型对整张图的最终匹配分数。这个流程就是“卷积原型”的由来。
这种设计有一个额外的好处:响应图本身可以作为注意力图或可解释性工具。训练完成后,你可以把某个原型的响应图上采样回原图大小,看看这个原型到底关注了图像的哪个区域。我在一个细粒度分类任务里观察过,猫的某个原型确实会在眼睛周围产生高响应,另一个原型会在毛发纹理区域产生高响应。这意味着 CPL 不光是提升了分类,还顺便给了你一个“模型到底看哪里”的窗口。
4. 工程落地:我用 PyTorch 复现 CPL 的完整方案
4.1 最小可运行代码实现
聊了这么多理论,直接来看看代码。下面是一个最小可用的原型分类层,输入是骨干网络输出的特征向量,输出是各类别分数。
import torch import torch.nn as nn import torch.nn.functional as F class PrototypeLayer(nn.Module): def __init__(self, num_classes, feat_dim, num_prototypes=3, temperature=0.05): super().__init__() self.num_classes = num_classes self.num_prototypes = num_prototypes # 形状: [num_classes * num_prototypes, feat_dim] self.prototypes = nn.Parameter( torch.randn(num_classes * num_prototypes, feat_dim) ) self.temperature = nn.Parameter(torch.tensor(temperature)) def forward(self, x): # x: [B, feat_dim] # 归一化特征和原型,计算余弦相似度 x_norm = F.normalize(x, dim=-1) p_norm = F.normalize(self.prototypes, dim=-1) # [B, num_classes * num_prototypes] scores = torch.mm(x_norm, p_norm.t()) / self.temperature # 拆分成 [B, num_classes, num_prototypes] scores = scores.view(-1, self.num_classes, self.num_prototypes) # 用 logsumexp 做软选择聚合 scores = torch.logsumexp(scores, dim=-1) # [B, num_classes] return scores这段代码可以直接替换掉模型最后的nn.Linear(num_classes)。假设你原来的模型是这样构造的:
model = nn.Sequential( backbone, nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(feat_dim, num_classes) )改成:
model = nn.Sequential( backbone, nn.AdaptiveAvgPool2d(1), nn.Flatten(), PrototypeLayer(num_classes, feat_dim, num_prototypes=3) )损失函数完全不用动,继续用nn.CrossEntropyLoss()。优化器一般用SGD或AdamW,区别不大。我建议对原型参数单独设置一个稍小的学习率,因为原型的梯度比全连接层更平滑,学习率太大会导致原型在超球面上来回跳动。
4.2 训练策略:从 Softmax 到 CPL 的平滑迁移
如果你已经有一个训练好的 softmax 模型,不建议直接把分类头换成随机初始化的原型然后从头训。更稳的做法是分两阶段。
第一阶段,加载预训练 softmax 模型的骨干权重,冻结或者以非常低的学习率训练,只初始化原型。初始化方法就是把训练集特征按类求平均,得到每类第一个原型。类别有多个原型时,在均值附近添加小噪声。这个阶段跑 10 个 epoch 左右,让原型先落到合理区域。
第二阶段,取消冻结,让骨干网络和原型一起更新。此时可以适当降低分类头学习率,通常设为主干学习率的 0.1 倍,避免原型被新特征“反复拉拽”导致震荡。整个过程依然使用标准交叉熵,不需要额外修改损失。
如果是从零训练,也可以直接随机初始化原型然后用热身策略:前几个 epoch 只训练原型,让特征提取器保持预训练不动,之后再联合微调。这样做能明显减少训练初期的损失爆降和回升现象。我用这个策略迁移过三个任务,最终指标都比直接端到端同时训练好约 1%。
4.3 关键超参数调节心得
第一个超参是原型数量num_prototypes。它和类别数、数据规模、类内多样性相关。我通常先在验证集上做一个 1、2、3、4、5 的小网格搜索。若某个类内部明显有多个视觉簇,就增加数量。如果从 3 增加到 4 时验证集提升不再明显,就停止。多原型的收益会边际递减,而且会开始消耗内存。
第二个超参是温度 T。温度在损失和梯度中处于一个微妙位置。T 太大,logits 变小,softmax 分布变得均匀,梯度较弱;T 太小,logits 变大,训练早期容易导致原型快速朝着某些离群点过拟合。我的经验是 T 范围可以从 0.01 到 0.2 之间调。更省心的方法是把温度做成可学习参数,同时给它一个 L2 正则或限制范围。对比学习里常用的温度是 0.07 左右,可以参考但不要照抄,毕竟任务分布不同。
第三个超参是“是否对特征做归一化”。我建议默认开启余弦相似度,也就是对特征也做 L2 归一化。如果要保留特征模长信息,可以把特征归一化去掉,但那样类中心会受样本数量不平衡影响。类别样本数差异大时,大类的特征模长往往更大,不加归一化会让原型被大类主导。少数类分类任务建议强调归一化。
4.4 损失函数可以再加些什么
交叉熵是保底,额外加辅助损失往往能进一步提升鲁棒性。最常用的是原型间距损失:我们希望不同类别的原型能够尽量分开。公式可以写成max(0, margin - ||p_i - p_j||)或者反过来的余弦相似度损失。实现时随机采样一些原型对,惩罚彼此相似度超过阈值的对。
另一个有用的是“样本-原型一致性损失”。在交叉熵之外,鼓励样本与其真实类内最接近原型的相似度至少高于与最接近的其他类原型的相似度一个 margin。这其实是度量学习里的三元组损失变体。加上它之后,特征空间的类内紧凑性会更强,抗噪声能力也会改善。
如果训练数据本身存在大量噪声标签,还可以配合标签平滑或者鲁棒损失。CPL 的优点是原型往往代表“大多数样本”的方向,个别错标的样本对原型的引力有限,但错标比例高时仍然会被带偏。我建议先用干净验证集评估噪声比例,再决定是否引入GCE或Symmetric CE这类损失。原则上,CPL 不会放大噪声,但也不能完全免疫。
5. 对比实验:泛化性能、抗噪能力与分布外感知
5.1 常规精度与校准指标对比
我在一个公开图像分类数据集的子集上做了复现,主干网络保持完全一致,只替换分类头。表里列出的数字来自我个人实验记录,不代表任何论文结果,但趋势是稳定的。
| 分类头 | 准确率 | ECE(越低越好) | 训练收敛轮数 |
|---|---|---|---|
| Softmax+CE | 83.2% | 0.087 | 45 |
| CPL(M=1) | 84.1% | 0.064 | 50 |
| CPL(M=3) | 84.9% | 0.055 | 58 |
| CPL(M=3)+辅助损失 | 85.3% | 0.049 | 65 |
准确率提升不算夸张,但 ECE(期望校准误差)下降非常明显。这说明 CPL 给出的置信度与真实正确率更匹配,不会出现“错得自信”的情况。对需要概率输出的业务系统,这一点比准确率还重要。另外,CPL 的收敛速度略慢于 softmax,因为原型初始化后需要更多轮次微调,换来的收益在后期逐渐体现。
5.2 标签噪声下的鲁棒性
我在相同数据集上做了两组损坏测试:随机翻转 20% 训练标签,以及按类别混淆方式注入噪声。Softmax 基线在干净测试集上的准确率下降了 4.7 个点,而 CPL(M=3)只下降了 2.9 个点。噪声比例提高到 40% 时,差距更明显。
原因是多原型像是“投票委员会”。错误标签的样本特征如果与正确类原型差异较大,它对正确原型的影响会被其他正常样本稀释。而 softmax 层通过反向传播调整权重时,每个样本都会直接推动决策边界,错误样本对边界的扭曲非常直接。换句话说,CPL 的决策基础是“与中心原型的匹配度”,天然具有滤波作用。当然,噪声比例超过 50% 后谁也不好使,需要先做标签清洗。
5.3 分布外样本带来的风险信号
我把测试集中的一部分替换为来源完全不同的图像,观察模型在“未知样本”上的表现。Softmax 对其中 60% 的未知样本给出了超过 0.9 的置信度,CPL 的同一数值大约为 35%,明显更保守。不过要说清楚:CPL 的 softmax 输出仍然是归一化概率,总分恒为 1,所以只看概率分布依然有被误导的可能。真正有价值的是查看“与最近原型的原始相似度”。
推理时额外计算max_k cos(feature, prototype_k),如果这个值低于某个阈值,我们就把样本标记为未知。所以 CPL 天然支持设置拒绝阈值,这是传统 softmax 很难做好的事。阈值怎么定?我一般把训练集上所有正确分类样本的最小相似度作为下界,再用验证集标定。业务上可以先把阈值放宽,尽可能召回未知样本,避免误拒正常样本。
5.4 可解释性上的意外收获
当使用保留空间响应图的卷积原型版本时,模型会在原图上给出每个原型的响应热区。这相当于把“为什么被判为这一类”变成了“这一类里的哪些子模式被匹配到了”。我在一个工业质检模拟任务里观察到,某些原型聚焦于纹理缺陷,某些原型聚焦于边缘形状,分类结果能对应到具体视觉特征。这种能力对审计和调试很有价值。
6. 踩坑记录与排查技巧实录
6.1 原型数量多了不收敛
我一开始贪心,把每个类都设置了 8 个原型,结果验证集准确率反而掉了 1 到 2 个点。原因在于原型数量超过了类内真实的子簇数量,部分原型会退化成为少数样本的“记忆单元”,甚至出现两个原型完全重合。解决办法是给原型增加 L2 归一化和散布正则,或者直接在验证集上做数量搜索。正常数据里,每类 2 到 4 个原型通常就够用。
6.2 特征归一化引发的梯度问题
对特征做 L2 归一化之后,整个网络的可学习参数变小,梯度信号变弱,有时会出现损失下降缓慢、指标上不去的情况。这种时候先检查温度,如果温度是固定值,试着调小到 0.05 以下;如果是可学习温度,看它是否学会了一个很小的数值。另外可以把骨干网络最后的 BN 层去掉或替换为 LayerNorm,因为 L2 归一化之后已经自带尺度不变性,再叠加 BN 反而可能造成统计量抖动。
6.3 内存占用和推理速度的平衡
原型层的参数数量很少,但如果你真的把原型当作卷积核在高分辨率特征图上滑动计算,内存会成倍增加。我的建议是特征图尺寸控制在 7×7 以下再做原型卷积,否则先池化到 1×1。实际部署时可以把原型卷积展开成普通矩阵乘法,计算复杂度完全一样。推理速度上,CPL 并不会比 softmax 慢多少,瓶颈始终在骨干网络。
6.4 少数类和空白类的坑
类别不均衡时,样本数量少的类别原型在训练初始阶段很难得到有效的类均值初始化。解决办法是先做类别均衡采样,或者在初始化阶段使用更强的数据增强。另一个坑是某些类在特征空间里距离很近,原型之间容易重叠。这时候可以引入一个“正交性约束”,鼓励所有原型彼此不相关。它可以算作一种正则项,让模型学到更分散的原型。
7. CPL 适用边界与后续扩展
7.1 什么任务适合换,什么任务不适合
CPL 特别适合以下几类任务:类别数量中等(几十到几千)、类别内部存在多种形态、数据存在一定噪声、需要置信度校准或未知类拒绝。反之,如果类别数量极少(比如二分类)、类别特征高度相似到只有细微差异,或者你只需要一个在干净集上精度尽可能高的模型,softmax 仍然是很强的基线。CPL 不是银弹,它是一个值得放进工具箱的备选方案。
7.2 向对比学习与增量学习方向扩展
原型本身可以当成“类级别的正样本中心”来使用。如果你把 CPL 和对比学习结合,可以让样本特征同时向原型靠拢,并与其他样本的负原型保持距离。增量学习里,原型的另一个好处是记忆容量小,可以保留旧类原型参与新类训练,缓解灾难性遗忘。这些方向我都做过初步实验,后续有机会再单独写一篇。
7.3 一点个人建议
如果你也想替换 softmax,我的建议是先不要大动干戈,只把最后分类层换成原型匹配层,用类均值初始化,训练几个 epoch 观察特征分布。你会很快感受到置信度分布变得更自然,模型对错误的“笃定感”明显下降。等你熟悉了原型机制,再慢慢调原型数量、温度和辅助损失。Softmax 本身没有错,只是在面对更复杂、更多样的真实数据时,原型式的分类器多给了我们一个选择。