29  经典分类器

识别篇走到这里,我们其实一直在做分类,只是形式越来越自由。条码识别本质上是”查表”——符号系统把答案预先编进了黑白条空,算法只需解码;OCR 把范围放宽到几十个字符类别,但字符集是已知且封闭的。本章面对的是通用分类问题本身:给定一个目标,判断它属于事先定义的哪一类——药片上的污点是黑点、纤维还是碎屑团?这类问题没有编码规则可依赖,只能从样本中学习统计规律。在深度学习席卷视觉领域之前,K 近邻(k-nearest neighbors, KNN)多层感知机(multilayer perceptron, MLP)支持向量机(support vector machine, SVM)是模式识别的”三大件”;时至今日,在特征维度低、样本量小、需要可解释性的工业任务里,它们依然是性价比之王——训练以秒计,预测以微秒计,不需要 GPU,错判时还能查出原因。本章用一个异物分选任务贯穿三种分类器:在亮背景上区分三类暗色目标——圆点(dot)、细长纤维(fiber)与不规则团簇(cluster),如 图 29.1 前三行所示;第四行那些”胖胶囊”是故意制造的边界样本,我们留到后面制造麻烦。

图 29.1: 三分类任务的样本图。前三行依次为圆点、纤维、团簇三类(每类 60 个样本中的前 10 个,96×96 灰度图,叠加 \(\sigma=5\) 高斯噪声);第四行是 10 个故意落在圆点与纤维之间的”胖胶囊”边界样本。

29.1 特征空间与可分性

分类器不直接吃图像。第一步是把每幅图变成一个特征向量(feature vector):阈值分割得到目标区域后,用 章节 23 介绍过的区域特征把它压缩成 4 个数——面积(area)、各向异性(anisometry)、圆形度(circularity)与紧凑度(compactness)。每个样本于是成为四维特征空间(feature space)中的一个点,“分类”就变成了在这个空间里划分领地的几何问题。

真实产线的异物长什么样?图 29.2 取自奥普特 Smart3 深度学习示例方案的一帧——一支半透明软管表面散落着多颗暗色异物。我们用与上文完全相同的流程处理它:在异物聚集区划定 ROI、阈值分割(管体亮、异物近黑)、blob 分析滤除印刷网点,再对每颗异物提取同一套 4 维区域特征。分割出的 6 颗异物及其真实测量值如下:

异物 面积 各向异性 圆形度 紧凑度 形态
P1 1143 1.67 0.500 1.40 块状碎屑(团簇形)
P2 902 1.64 0.478 2.14 块状碎屑(团簇形)
P3 1180 2.29 0.358 2.21 块状碎屑(团簇形)
P4 562 1.14 0.753 1.09 紧凑暗点(圆点形)
P5 294 1.33 0.571 1.20 紧凑暗点(圆点形)
P6 322 3.28 0.280 1.73 细长碎屑(纤维形)

这组真实测量印证了合成任务的设定并非凭空构造:P4、P5 是紧凑暗点(圆形度 0.57~0.75、各向异性近 1),对应”圆点”;P1~P3 是面积更大、边缘破碎的块状碎屑(圆形度 0.36~0.50),对应”团簇”;P6 是一条细长碎屑(各向异性 3.28、圆形度仅 0.28),形态偏向”纤维”。真实异物确实分布在圆点—纤维—团簇这同一条形态谱上,本章的特征设计因此有据可依。

图 29.2: 真实异物的特征提取(奥普特 Smart3 深度学习示例样图,1280×1024 灰度)。左:半透明软管异物聚集区灰度图;右:阈值(管体亮、异物近黑)+ blob 分析分割出的 6 颗异物,外接框按重心横坐标从左到右编号 P1~P6。真实异物覆盖紧凑圆点(P4/P5)、细长碎屑(P6)、块状碎屑团(P1~P3)三种形态,与合成的三类目标一一对应。

但正因为它”真实”,分类器的训练与评估反而无法建立在它之上,原因有三:其一,单帧仅 6 颗异物、且无逐颗类别真值标注,凑不出每类 40/20 的训练—测试切分;其二,真实异物的形态谱是连续的——P6 的各向异性 3.28 正贴着我们后面特意合成的”胖胶囊”边界样本区间(3.33~5.16)的下沿,类与类之间没有干净的间隔,无法用来公平地比较三种分类器的边界行为;其三,受控合成集能精确摆布可分性、样本量与边界难度,正是后续实验所需的”实验台”。因此本章的分工是:真实样图负责验证特征提取与形态谱的真实性,分类器的训练与对照实验则在可标注、可切分、可调难度的受控合成集上进行(合成样本见 图 29.1)。

图 29.3 把这套受控合成数据集投影到各向异性 × 圆形度平面上。三类目标的格局一目了然:纤维(十字)孤悬右下——各向异性 7.15~18.0、圆形度仅 0.07~0.15,与谁都不挨着;而圆点(实心圆盘)与团簇(空心方框)却在左上角挤成一团——圆点的各向异性 1.00~1.48 与团簇的 1.09~2.17 几乎重合,圆形度区间(0.50~0.79 对 0.41~0.68)也大面积交叠。仅凭这两维,没有任何分类器能把它们分开。救兵在另外两维:圆点面积 54~340,团簇面积 373~807,一刀两断;紧凑度(0.90~1.09 对 1.17~1.66)同样泾渭分明。灰色菱形是那 10 个边界胶囊,各向异性 3.33~5.16,恰好架在圆点与纤维之间的真空地带。

图 29.3: 特征空间散点图(横轴各向异性、纵轴圆形度)。实心圆盘=圆点,十字=纤维,空心方框=团簇,灰色菱形=边界胶囊。纤维独占右下;圆点与团簇在此二维投影中重叠,需靠面积与紧凑度才能分开。

这张图给出本章第一条、也可能是最重要的一条结论:可分性(separability)首先取决于特征选择,其次才轮到分类器。特征选对了,三类样本在四维空间里彼此远离,连最朴素的分类器都能全对;特征选错了——比如只给前两维——再精巧的模型也无米下锅。工程上排查分类效果差的问题,应当先画特征散点图检查可分性,而不是急着换模型调参数。

特征工程与端到端学习的分工:深度网络把”提特征”也交给学习,代价是需要大量标注样本;经典分类器把特征交给领域知识(形状、纹理、颜色统计),学习只负责低维空间里的划界。工业任务常常样本少而机理清楚,恰好落在后者的舒适区。

29.2 三种分类器的几何

三种分类器都在特征空间里学一个从向量到类别的映射 \(f:\mathbb{R}^4 \to \{\text{dot},\text{fiber},\text{cluster}\}\),但它们的”世界观”截然不同。

KNN 不学习,只记忆。训练阶段仅仅把全部样本存起来(实践中建一棵 kd 树加速检索);预测时找出与查询点最近的 \(K\) 个训练样本,让它们投票。它的决策完全是局部的:边界由零星几个样本的位置决定,样本带噪声,边界就跟着斑驳起伏。优点同样源于此——无训练成本、增删样本即时生效、每个判决都能指出”是哪几个邻居投的票”。不同特征量纲悬殊(面积以百计、圆形度不足 1),距离计算前必须做归一化,否则面积一维独霸距离。

MLP 学一个平滑函数。输入层 4 个节点对应特征,隐层做非线性变换,输出层 3 个节点经归一化后给出各类的软分数(soft score),取最大者为判决。训练用反向传播迭代拟合,是三者中唯一需要”慢慢学”的。它的决策是全局的:所有训练样本共同塑造一个连续曲面,个别噪声样本被平均掉,边界因此平滑。代价是训练有随机性、隐层节点数要人为指定,而且软分数只是拟合的副产品,并非校准过的概率——这一点后面有实例。

SVM 只关心边界附近。它寻找把两类分开且间隔(margin)最大的超平面,超平面只由贴着间隔的少数支持向量(support vector)决定,远处的样本再多也不参与;配合核技巧(kernel trick)——本章用径向基函数(radial basis function, RBF)核——可以在隐式的高维空间里划线性边界,回到原空间就成了弯曲边界。决策函数形如

\[ f(\mathbf{x}) = \operatorname{sign}\Big(\sum_{i} \alpha_i y_i\, k(\mathbf{x}_i, \mathbf{x}) + b\Big), \]

求和只跑遍支持向量。多分类由多个两类判决组合而成。

\(K\) 的选择是 KNN 唯一的超参数:\(K=1\) 边界最碎、对噪声最敏感;\(K\) 增大边界变平滑,但小类会被大类”淹没”。本章用 \(K=3\)——既能投票(避免单个噪声邻居一票定音),又不至于在 40 个样本/类的规模上抹平局部结构。

三种世界观投射到平面上就是 图 29.4。我们把面积与紧凑度固定在训练集均值(301.5 与 2.15),在各向异性 × 圆形度平面上以 72×72 网格逐点询问三个训练好的模型。KNN(左)的上半区一片斑驳——固定的面积值恰好落在圆点与团簇之间,这片区域距离两类样本都不近,判决在邻居的拉锯中来回翻转,右侧还散落着孤立的”飞地”;MLP(中)给出教科书般干净的平滑边界;SVM(右)的边界由几段近似直线的弧拼接而成——每一段都是某几个支持向量的 RBF 影响范围的交界。三幅图分类同一份数据、准确率不相上下,但边界形态忠实暴露了各自的归纳偏置(inductive bias):局部记忆、全局拟合、最大间隔。

图 29.4: 三种分类器在各向异性 × 圆形度平面上的决策区域(左:KNN;中:MLP;右:SVM。另两维固定为训练集均值;浅灰=圆点、中灰=纤维、深灰=团簇;白块/黑块/白色空心框为三类训练样本)。KNN 斑驳、MLP 平滑、SVM 分段弯曲——三种归纳偏置的直观写照。

29.3 实验:准确率与样本量

每类 60 个样本按 40/20 切分训练与测试,再把训练集砍到每类 5 个、甚至每类 2 个,考察样本量的影响。全部结果如下(测试集恒为 3×20):

分类器 训练 120(40/类) 训练 15(5/类) 训练 6(2/类)
KNN(\(K=3\),z-score,kd 树) 100% 98.3%1 100%
MLP(4-6-3,500 次迭代) 100% 100% 100%
SVM(NU-SVC,RBF 核) 100% 100% 100%

诚实的结论是:没有谁崩溃。每类只剩 2 个训练样本时,三个分类器仍然几乎全对——因为特征空间里三类本来就离得远,2 个点也足以标定各自的领地。这正是上一节论断的反面印证:可分性好的任务对分类器选择和样本量都不敏感;分类器之间的差异,要到边界地带和真正的小样本难题上才会显形。指望用一个”易分”的验收数据集比出三种分类器的高下,是产线选型时常见的方法论错误。

表中结果有一处须如实交代:KNN 的准确率跨运行不稳定——SDK 的 kd 树构建存在非确定性,同一份数据重跑,KNN 的判定会出现偶发翻转——实测 5 次重跑准确率在 91.7%~100% 之间波动,最多一次翻转 5 个样本,且并不固定出现在哪个训练规模档。MLP 与 SVM 的结果则完全可复现。

29.4 模糊地带:置信度的三种语义

现在请出第四行那 10 个胖胶囊。它们的各向异性 3.33~5.16,落在圆点(≤1.48)与纤维(≥7.15)之间的无人区,没有标准答案——我们感兴趣的不是”对错”,而是三个用同一份数据训练出来的分类器会怎么判、以及它们各自如何表达”有多确定”。

结果出现了真实的三方分歧:KNN 把 8 个判为纤维、2 个判为圆点;MLP 把 10 个全部判为圆点;SVM 把 9 个判为纤维、1 个判为团簇。同样的训练集,同样的查询点,三种归纳偏置给出三种答案:KNN 看局部——胶囊离纤维样本群更近,邻居投票就倒向纤维;MLP 的全局平滑曲面把圆点的领地一路外推过了无人区;SVM 的间隔中线把多数胶囊划给纤维一侧,而最接近圆形的那个(各向异性 3.33)竟被推给了第三方团簇。分类器在边界处的分歧,暴露的不是谁对谁错,而是它们各自的归纳偏置——训练数据没有覆盖的地方,模型只能按自己的世界观脑补。

更有工程价值的是三者表达置信度的方式。KNN 给出邻居的票数与相似度:8 个判纤维的样本是 3 票全票,而某个判圆点的样本输出”dot 得票 0.667(相似度 0.613)、fiber 得票 0.333(相似度 0.635)“——票数与相似度甚至互相矛盾,犹豫写在脸上。MLP 给出软分数:近半数胶囊给出 dot=1.000 的满分,其余多在 0.95 以上(最低 0.703)——再次提醒,软分数是曲面外推的副产品,高分不等于高可靠,距离训练数据越远越要打折扣。SVM 在本 SDK 中最干脆:只给硬标签,没有分数、没有间隔距离,连”犹豫”的渠道都没有。

拒识(rejection)是工业分类的标配需求:与其错判流出,不如挑出存疑样本转人工复核。实现拒识的前提是分类器输出可比较的置信度并支持阈值化——这与 章节 28 中用 minScore 卡掉低分字符是同一个机制。

这就引出选型铁律:需要拒识或人工复核兜底的产线,必须选置信度输出可用的分类器。本 SDK 的 SVM 输出硬标签,意味着它在这条产线上无法支持”低置信度送人工”的流程——无论它的准确率多漂亮。

29.5 SciVision 实现

三个分类器分别由 SciSvKNNClassifierSciSvMLPClassifierSciSvSVMClassifier 提供,调用骨架一致:初始化 → 逐样本 AddSampleTrainClassify

// KNN:无训练过程,Train 只是建索引
SCIMV::SciSvKNNClassifier knn;
knn.InitializeModel(4, featNames);            // 特征维数 + 特征名数组
knn.AddSample(featVec, className);            // 逐样本加入(4 维 SciVarArray + 类名)
knn.Train(SCI_KNN_Z_SCORE, SCI_KNN_KDTREE);   // z-score 归一化, kd 树索引
knn.Classify(3, SCI_KNN_MAX_COUNT, fv,        // K=3, 多数票
             &cls, &score, &names, &votes, &sims);  // 输出邻居票数与相似度

// MLP:4-6-3 全连接网络
SCIMV::SciSvMLPClassifier mlp;
mlp.LoadModel(path);                          // 必须先调用!文件不存在也行(见下)
mlp.InitializeModel(0, 4, 6, 3);              // 模式 0=特征分类; 输入/隐层/输出节点数
mlp.SetNames(featNames, classNames);
mlp.Train(500, 1e-6f, 0.0f, 1, 0);            // 最大迭代, 误差容限, 无正则, 标准化
mlp.Classify(fv, &cls, &score, &names, &scores);    // 输出全类软分数

// SVM:NU-SVC + RBF 核
SCIMV::SciSvSVMClassifier svm;
svm.LoadModel(path);                          // 同样必须先调用
svm.InitializeModel(featNames, 4, SCI_SVM_NU_SVC, 0.01, SCI_SVM_RBF, 1.0, 1.0, 0.0);
svm.Train(1000, 1e-6f, 1);                    // min-max 标准化
svm.Classify(fv, &cls, &names);               // 只有硬标签,无分数/间隔输出

API 本身都能跑通,但这组模块存在本书遇到的最严重的一批 SDK 缺陷——多条跨模块的进程级状态污染,全部经过反复实测定位,原样记录:

  • ManualThreshold 之后 KNN Train 必崩溃(0xC0000005):同一进程里只要调用过阈值分割,后续 KNN 训练就访问违例。这迫使本章示例拆成两次进程运行:demo.exe extract 负责合成数据、阈值分割、特征提取并写出 features.csvdemo.exe classify 另起进程读 CSV 完成训练与评估。特征与分类之间用纯文本文件交接,是当前唯一稳妥的隔离方式。
  • MLP 与 SVM 都训练过之后,再训练 KNN 得到静默坏模型:不报错、不崩溃,但团簇类测试 0/20 全错。规避方法是按”KNN 块 → MLP 块 → SVM 块”组织代码,一个分类器的全部训练与预测做完再碰下一个。
  • 约 100 次 KNN Classify 之后,同进程的下一次 KNN Train 崩溃或产出坏模型(实测 10 次安全、100 次崩溃):每个块内必须先把所有模型训练完,再开始批量预测。
  • KNN Train 的 treeType=1(暴力搜索)训练后 Classify 崩溃——只能用 kd 树(0);而 kd 树又有跨运行非确定性(上节 ±1 翻转的根源)。
  • MLP/SVM 的 InitializeModel 之前必须先调一次 LoadModel,否则报 122706011/122707003——传一个不存在的文件路径也行,返回”已新建空模型”码即可继续。
  • KNN 归一化模式 0/1 下 Classify 返回的类名字符串偶发乱码——用 z-score(模式 2)未观察到此问题。

这份清单的方法论意义不亚于其内容:商用库的稳定性边界不会写在手册里,只能用系统化的实验探明——固定其余变量、逐一引入模块、用探针程序定位崩溃阈值,与 章节 5 用黄金实验反推接口真实行为是同一套思路。把绕行方案连同来龙去脉写进代码注释(见 code/classical_classifiers/main.cpp 文件头),后来者才不必再踩一遍。

工业案例:人造革纹理分选的特征之争

某人造革产线需按纹理粗细把成品分为三个等级。初版方案把整幅原始图像缩放后直接喂给 MLP,“端到端”的准确率勉强过八成,且对光照漂移极其敏感,每次换灯重训一次。复盘后改走经典路线:每幅图先计算 GLCM 的能量、对比度、相关性、同质性四个纹理特征(见 章节 25),再用 KNN 在这个四维空间里分类。准确率反超原方案十多个百分点,对光照漂移也稳定——纹理统计量对整体亮度变化天然不敏感。更受质检员欢迎的是可解释性:每个错判样本都能调出它的 3 个近邻图像人工比对,边界等级的革面”像谁”一目了然。教训:小数据的工业任务里,好特征 + 简单分类器通常胜过原始数据 + 复杂模型

29.6 小结

  • 可分性先于分类器:分类问题的成败大半在特征选择阶段就已决定。本章任务里圆点与团簇在各向异性 × 圆形度投影中完全重叠,靠面积与紧凑度才分开——排查分类问题先画特征散点图。
  • 三种归纳偏置:KNN 局部记忆(边界斑驳、判决可溯源到邻居)、MLP 全局平滑函数(软分数、需迭代训练)、SVM 最大间隔 + 核技巧(边界只由支持向量决定)。易分任务上三者难分高下——每类 2 个训练样本都能近乎全对;差异在边界地带才显形。
  • 置信度语义各不相同:邻居票数 + 相似度、软分数、硬标签——10 个边界胶囊让三个 100% 准确率的分类器给出 8:2、10:0、9:1 三种判决。需要拒识与人工复核的产线,必须选置信度输出可用、可阈值化的分类器。
  • 商用库的稳定性边界要用实验探明:本章 SDK 的分类器模块存在跨模块内存破坏,规避方案(特征提取与分类分两个进程、先全部训练再批量预测)来自系统化的崩溃定位实验,而非手册。
  • 更系统的分类器理论(贝叶斯决策、特征选择与新颖性检测)可阅读 Steger 等人著作的分类章节 (Steger, Ulrich, 和 Wiedemann 2018),以及模式识别的两部标准教材 (Duda, Hart, 和 Stork 2001; Bishop 2006)。本章三种分类器各自的奠基文献都值得回到原文:KNN 的渐近误差界由 Cover 与 Hart 给出 (Cover 和 Hart 1967),MLP 训练所依赖的反向传播由 Rumelhart、Hinton 与 Williams 确立 (Rumelhart, Hinton, 和 Williams 1986),SVM(含最大间隔与核技巧)则源自 Cortes 与 Vapnik 的支持向量网络 (Cortes 和 Vapnik 1995)

识别篇至此收束。从条码的确定性解码,到 OCR 的封闭集字符分类,再到本章面向开放特征空间的统计决策——三章走完了”从查表到学习”的谱系:编码规则能管住的交给解码器,规则管不住的交给特征与分类器,而分类器管不住的边界地带,留给置信度与人工复核。


  1. 单次代表值;实测 5 次重跑范围 91.7%~100%,非确定性不限于此训练规模档↩︎