ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

K-NN、多类感知器与SVM:手写数字识别分类器对比实验

K-NN、多类感知器与SVM:手写数字识别分类器对比实验

1. 为什么还要拿传统分类器做手写数字识别

1.1 这个比较实验到底想回答什么问题

深度学习席卷图像识别之后,很多朋友一上来就喜欢堆卷积网络,好像手写数字识别这类任务不挂个神经网络就不过瘾。但真实场景里未必总有GPU,也未必有十万级标注样本。某些课程设计、工业质检的边端盒子、以及快速跑通的算法原型,都会用到传统的机器学习分类器。K-NN、多类感知器、SVM正好是三种最有代表性的分类器:一个完全靠样本记忆,一个用线性边界做多类切分,一个靠核函数把数据映射到高维再切开。把它们放在同一个数据集上做横向比较,比单纯看别人博客里的准确率数字要可靠得多。

我这次实验的核心问题有三个:第一,在统一预处理和统一评估口径下,这三种分类器到底能把手写数字识别推到什么水平;第二,它们的训练成本、推理速度、内存占用分别是什么量级,为什么权威资料里K-NN的准确率有时看着很高,实际部署却很少被采用;第三,从错误样本来看,它们失败的模式是否一样,哪些数字组合对哪个模型最难。搞清楚这三点,其实也就回答了"我的场景资源有限,到底该选谁"。

1.2 统一数据集与评估口径的重要性

做比较研究的第一个陷阱是不统一。有人用完整训练集,有人只抽样了2万张,有人直接拿原始像素喂模型,有人先做了一遍很重的数据增强,最后放在一起比较,结论完全没有意义。我这次固定在一个28x28灰度手写数字数据集上,共7万张样本,按60000训练/10000测试划分,保持分布一致。数字类别为0到9,每个类别在训练集里的样本量都接近6000张,类别相对均衡。

评估指标方面,我同时记录宏观准确率(所有样本里预测正确的比例)和每类别的精确率、召回率、F1值,因为只看总准确率很容易掩盖某个数字被严重混淆的问题。另外,我还单独测了推理阶段的平均延迟:把测试集逐批送入模型,统计每百张图片的耗时。延迟测试在CPU环境下进行,线程数固定,避免后台任务抖动影响结论。这种"口径统一"的做法说起来简单,但很多人做对比实验时恰恰会忽略,导致最后得出的结论和实际表现南辕北辙。

接下来聊聊三种模型的原理与参数设定,因为这一步直接决定了实验结果的上下限。

2. 三种分类器的原理差异与关键参数设定

2.1 K-NN:不训练就是训练

K-NN没有显式的训练过程,它只是把训练集样本连同标签存下来,预测时计算输入样本与所有存储样本的距离,找出最近的K个邻居,用多数投票决定类别。距离度量我用的是欧氏距离,这也是手写数字这类低维稠密特征下最稳定的选择。K值从1、3、5、7、9做了一轮网格搜索,最终K=3在验证集上效果最好;继续加大K会导致边界过于平滑,把一些本身就模糊的数字给"平均"掉了。

需要特别注意的是,K-NN对数据尺度非常敏感。像素值如果不做归一化,距离计算会被数值大的特征主导;我统一把像素范围缩放到[0,1]。另一个容易踩的坑是"维数灾难"。原始784维像素直接算欧氏距离,不仅慢,而且大量维度包含冗余信息,距离反而变得不敏感。所以K-NN在实验管线里搭配了PCA降维,这个后面会详细说。

K-NN还有一个常被忽略的属性:它本质上没有"模型容量"这个概念,模型复杂度完全由训练数据决定。数据量翻倍,预测时需要的计算量也翻倍;数据里混入异常样本,K=1时一个离群点就可能让预测崩盘。因此,K-NN在对比实验里最适合当作基线,它告诉你的不是"最优解在哪里",而是"最朴素的记忆法能做到什么程度"。

2.2 多类感知器:从线性边界到多类决策

标题里的"多类感知器"指的是不借助神经网络隐藏层的传统感知器扩展形式,本质是多类线性分类器。它学习W矩阵和偏置b,输入特征经过线性变换后得到每个类别的得分,最终用argmax取最大得分作为预测结果。在机器学习库里,对应实现既可以是一层感知器,也可以是带多类支持的多层感知器的简化版本。我这里用的是一个单隐藏层的感知器模型,隐藏层单元数设成了128,激活函数用ReLU。这样它在"线性分类器"基础上具备了一定的非线性表达能力,又比完整的大规模深度网络轻得多。

多类感知器训练时的核心参数是学习率和迭代代数。我用的是Adam优化器,初始学习率0.001,训练代数50轮,批量大小64。训练过程中每轮结束在验证集上计算一次准确率,如果连续3轮不升就提前停止。这样既保证收敛,又不会在噪声上过拟合。感知器的训练成本在三者中算中等:参数只有784x128加上128x10,约11万个参数,CPU训练也只要几分钟。

多说一句,很多人会把多类感知器和"线性SVM"混淆。感知器优化的是分类错误驱动的损失或者交叉熵,SVM优化的是最大间隔。同样一条决策边界,SVM通常要求"边界离两类数据足够远",而感知器只要分对就行,鲁棒性差距在这种对比里会反映在验证集和测试集的准确率差上。

2.3 SVM:核函数带来的升维利器

SVM我选的是RBF核,惩罚系数C和核系数gamma通过网格搜索确定。实验范围是C取{0.1,1,10,100},gamma取{0.001,0.0001}。最终C=1、gamma=0.0001在验证集上取得了最好结果。这里解释一下gamma的含义:它控制单个训练样本影响范围的大小。gamma越大,决策边界越贴近样本点,容易过拟合;gamma太小,边界又过于平滑,分不清相近数字。

SVM原始求解目标是找到几何间隔最大的超平面,面对手写数字这种本身线性不可分的问题,光靠原始像素空间很难切干净。RBF核把784维像素特征隐式映射到更高维空间,让原本缠在一起的点更容易被超平面切开。代价是训练复杂度高,尤其在样本量6万、特征几百维的情况下,直接训练耗时较长。

在对比实验中,SVM通常拿到最高准确率,但训练时间比K-NN"存储式"要多很多,比单隐藏层感知器也要慢。后面第四节的结果也验证了这一点。另外,SVM的调参远比另外两者敏感,C和gamma一旦搭配不当,验证集准确率能直接从97%掉到92%,这也是我在调参部分重点强调的原因。

3. 预处理、降维与实验环境

3.1 像素归一化与基础特征

把原始像素值除以255缩放到[0,1]只是第一步。我额外做了一点形态学上的处理:对图像做中心化(减去均值),但这里注意,均值是从训练集算出来的,验证集和测试集不能重新计算各自的均值,否则就引入了数据泄漏。更严谨的做法是将归一化器的拟合操作放在训练集上,测试集只调用变换功能,不参与任何统计量的计算。

我没有做复杂的数据增强,比如随机旋转、平移、弹性畸变,因为这次比较的重点是分类器能力而不是数据增强策略。如果每个模型用的训练数据都做了不同增强,公平性就没了。想对比增强带来的收益,可以单独开一组实验,而不是混在分类器对比里。这一点我在实验记录里专门标注了,避免后面读代码的人误以为"忘记做增强"。

3.2 PCA降维的必要性

原始特征784维对K-NN和SVM都是负担。K-NN在784维下算欧氏距离,不仅慢,而且高维空间里距离区分度变差;SVM则因为核矩阵的规模问题训练成本直线上升。我做了PCA降维实验,把维度压缩到30和60两种配置分别测试。

PCA在验证集上的结果说明了一个很有趣的现象:K-NN在30维时的准确率反而比784维全特征更高,因为降维相当于去掉了大量噪声和冗余;SVM在60维时准确率保持96%以上,训练时间比784维下降了一个数量级;感知器对维度变化不那么敏感,30维时稍微下降,60维基本持平。

最终我采用的降维维度是60维,累计解释方差约90%。这个选择在三个分类器之间是一个比较均衡的折中,既不因为过度压缩损失太多信息,又能显著缓解计算压力。降维还有一个额外好处:模型体积变小,K-NN存储训练样本的内存占用从784维下的约180MB降到了60维下的不到15MB,这对部署在边缘设备上的场景相当关键。

3.3 实验环境与可复现性

硬件是常见的8核CPU笔记本,内存16GB,无独立GPU。软件栈是Python 3.10、scikit-learn以及必要的数值计算库,随机种子固定为42。划分数据、训练、验证、测试的整个过程都固定种子,保证结果可复现。我在代码里把归一化、降维、模型训练三个环节串成了流水线,这样交叉验证时不会因为步骤错位而引入泄漏。

关于可复现性再说一个心得:固定随机种子不是万能的,不同库版本、不同并行线程数都有可能造成微小波动。我建议在实验记录里写下完整的依赖版本号,否则别人复制你的代码时常会困惑"为什么复现不了你的数字"。另外,SVM训练时如果开了多线程,每次运行的支持向量集合可能略有差异,导致推理结果在小数位后几位漂移。遇到这种情况不用慌,把线程数固定下来,或者只在关键实验中串行执行,结果就稳定了。

4. 对比结果:准确率、混淆矩阵与推理效率

4.1 总体准确率对比

三种分类器在相同预处理、相同PCA降维和相同数据集划分下的结果如下表所示。需要说明的是,表格里的准确率是测试集最终结果,不是验证集:

分类器测试集准确率训练耗时单张推理耗时
K-NN (k=3, PCA=60)97.2%约1分钟(建索引)约0.8ms
多类感知器 (128隐藏单元)96.8%约5分钟约0.02ms
SVM (RBF, C=1, gamma=1e-4)97.8%约25分钟约0.15ms

SVM在准确率上略胜半筹,K-NN紧随其后,感知器最低。但差距并没有想象中那么大,三者都在96%以上。如果只看这个数字,很多人可能觉得"都很强啊,随便选一个就行"。但实际差距藏在推理时间、模型体积和边缘场景下的稳定性里。K-NN的单张0.8ms看着不高,但这是建立在测试集只有1万张、训练集只有6万张的基础上的;如果训练集涨到100万张,这个数字会线性涨到10ms以上,线上接口完全扛不住。

4.2 错误样本的解剖

从混淆矩阵看,三个分类器的困难类别出奇一致:最容易分错的是"4"和"9","7"和"2","3"和"5"。这跟人类观察者写字潦草时的混淆模式类似。有意思的是,K-NN在"0"和"6"这对组合上有时候误判,而SVM几乎不犯这类错误。原因在于RBF核能捕捉到局部结构差异,对笔画的封闭性更敏感;K-NN只看全局距离,很难把"0"底部多出的一个小尾巴和"6"区分开。

多类感知器的错误分布则更"集中",几乎都落在那些靠近决策边界的模糊样本上。我把测试集中预测错误的样本挑出来看,大部分在图像上就是人眼都难以辨认的"涂鸦级"数字。这说明在数据集本身噪声较高时,传统分类器的准确率存在明显的天花板,不是模型能力不足,而是标注歧义太多。这个问题即便换成现代深度模型也会出现,只是比例更低。

4.3 训练成本与推理延迟的取舍

训练成本方面,K-NN建索引很快,但推理时要计算与全部训练样本的距离,测试集1万张跑完约8秒;SVM训练最慢,25分钟主要是网格搜索阶段拉高了总时长,最终模型只保留了支持向量,推理时只用和少数支持向量计算核函数,所以单张大约0.15ms;多类感知器一旦训练完,推理就是一次矩阵乘法和argmax,单张0.02ms,是三者中部署最轻量的。

模型体积方面,K-NN需要保存全部6万张样本,假设每张60维浮点,约几十MB;SVM保存支持向量和权重,大约十几MB;感知器只有权重矩阵和偏置,不到2MB。对于嵌入式设备来说,后面两者的体积明显更友好。我在实验里把三个模型都序列化保存后记录磁盘占用,感知器文件最小,只有1.8MB,SVM是7.6MB,K-NN因为要存样本和索引,达到了23MB。

5. 从实验到选型:不同场景下到底怎么选

5.1 K-NN的价值不在线上推理

K-NN最大的问题不是准确率,而是推理阶段的高复杂度。每来一个新样本都要和所有旧样本比对,如果训练集涨到100万,延迟会线性增长。因此,在需要频繁预测、低延迟的线上场景,K-NN很少作为主力模型。那它适合哪里?小样本场景和冷启动。如果手里只有几百上千张带标签数据,K-NN不需要训练,直接就能用,而且效果往往不错;另外一个适合场景是作为基线模型,任何新方案上线前,先用K-NN跑出一个参考准确率,看新方案到底进步了多少。

我在这次实验里体会最深的是,K-NN虽然算法朴素,但它对数据质量的反映非常真实。训练集里一旦混入错标样本,K-NN的准确率会立刻掉下去,而SVM和感知器因为有正则化或间隔约束,反而能扛住一部分噪声。从这个角度看,K-NN无意中成了"数据质量探测器",这一点是很多介绍K-NN的资料里不会提的。

5.2 感知器和SVM怎么选

多类感知器的优势在推理速度和模型体积。如果产品要跑在单片机或低端边缘设备上,矩阵乘法的计算量完全可控,非常合适。缺点是它的决策边界不如SVM"稳",在小样本下容易受个别噪点影响,需要更多的正则化投入。我在实验里尝试过减少隐藏层单元到64,准确率只下降了0.3个百分点,但推理速度和模型体积进一步缩小,这对部署场景是一个值得考虑的方向。

SVM适合准确率优先且推理频率不极端的场景。RBF核给了它强大的非线性能力,在小样本数据集上经常比神经网络表现更好。但要注意,SVM对超参数敏感,C和gamma需要仔细调,正则化不足时会过拟合。我在实验里把C调到10时,训练准确率上升但测试准确率下降,这就是典型的过拟合信号。如果你不想花太多时间调参,一个经验值是优先固定gamma在1/n_features附近,再调C,通常能快速找到一个可用的组合。

5.3 和深度模型的界限在哪里

这次实验里的三种分类器准确率都在97%左右,而一个简单的卷积神经网络在相同数据上通常能做到99%以上。差距来自特征表达层次:传统分类器依赖人工设计的特征或者原始像素,卷积网络则自动学习从边缘、纹理到数字结构的层次化特征。不过深度模型需要稳定的算力环境做训练,数据量少时容易过拟合,部署框架也更重。

我的建议是:如果项目目标是学习分类器原理或者资源受限到只有几百KB内存,传统分类器完全够用;如果目标是工业级准确率,且对延迟、内存的容忍度较高,可以上轻量卷积网络;如果两者之间,先用SVM或感知器快速验证可行性,往往比一上来就调深度学习网络更务实。现实中很多手写识别项目的第一版就是传统分类器,等数据积累到一定规模、明确性能瓶颈在特征表达之后,再迁移到深度模型,这种渐进式升级的路径比一步到位要稳妥得多。

6. 我踩过的坑:数据泄漏、维度灾难与调参止损

6.1 归一化与PCA合流时的泄漏

我在第一版实验里犯过经典错误:先对整个数据集做归一化,再切训练集和测试集,切完再做PCA,也是对整个数据拟合。表面看结果很好,但测试集的信息已经通过均值、方差和主成分方向泄漏进了模型。正确顺序必须是:先切分数据,归一化器的拟合只作用在训练集上,PCA的拟合也只作用在训练集上,然后变换训练测试集。修复这个Bug之后,准确率反而下降了零点几个百分点,这才是真实水平。

这种泄漏非常隐蔽,因为结果不是灾难性的崩坏,只是"虚高"了一点点。很多人看到准确率很高就不会去深挖,等部署到真实环境才发现模型没有实验里那么强。排查泄漏的方法也简单:把测试准确率和训练准确率对比,如果训练准确率接近99%而测试只有94%,除了过拟合,还应该怀疑是不是预处理阶段偷看了测试集。

6.2 K-NN在784维下的距离失真

我最早跑K-NN时直接用原始像素,结果准确率只有94%左右,而且推理慢得离谱。后来降到60维,准确率升到97.2%,时间缩短了一个量级。这个现象的核心原因是高维空间中所有点之间的距离趋于接近,近邻的"近"变得没有意义。所以不要迷信"原始信息完整",对K-NN和SVM这类基于距离或核的方法,降维是必要的,不是可选项。

我做的另一组对照是只归一化不降维,让K-NN和SVM都在784维上跑,结果SVM准确率略有下降但还在可接受范围,K-NN则明显掉点。这个对照让我彻底理解了为什么资料里总说K-NN受维度灾难影响更严重:它没有隐式的特征加权机制,所有维度在距离计算里等权参与,冗余维度越多,噪声干扰越大。

6.3 感知器与SVM的标签编码问题

还有一个小坑:多类感知器的输出得分是每个类别一个值,类别用整数编码,从0到9。如果训练时标签从1到10编码,推理时预测结果会整体偏移。这种错误非常隐蔽,不打印混淆矩阵根本发现不了。我的建议是训练和推理统一用同一个标签编码器对象,千万别在两个阶段分别创建新的编码器。SVM也有类似问题,它的内部实现会自动处理类别标签,但如果你手动把标签做了映射,推理时忘了还原,输出就会全部错位。

为了排查这类问题,我在实验流程里加了一道"快速自检":每次训练完模型后,先拿50条训练样本预测一遍,如果准确率低于99%,直接判定代码有Bug,而不是怀疑模型没收敛。这一步帮我抓住了好几个标签错位和特征顺序颠倒的问题。

6.4 网格搜索的时间止损策略

SVM的网格搜索如果采用穷举4x4组合,每次训练都要在6万样本上跑,最慢的一组可能要一个小时。我后来在搜索前先随机采样2万训练数据做一轮粗筛,把明显不行的参数组合排除,再回到全量数据精调。这个策略节省了大约60%的时间,而且精度损失很小。

更经济的方式是使用随机搜索而不是网格搜索,尤其在超参数维度较多的时候。我在C和gamma的二维空间里先随机采样20组组合,看准确率的大致分布,锁定一个高概率区域后再加密搜索。这两种策略结合,把SVM调参的整体耗时从最初的40多分钟压到了15分钟以内。如果你用的数据集比手写数字大得多,这个技巧会省下更多时间。

如果让我再做一次这个实验,我会把重点从"谁的准确率最高"转向"谁在特定约束下最合适"。准确率只是一个数字,真正的工程考量还包括数据收集成本、推理速度、模型体积、调参难度和可解释性。希望这些实验数据和踩坑记录能帮你少走一些弯路。下次你的项目如果再遇到"手写数字识别"或类似的图像分类任务,可以先问自己一句:我真的需要一块显卡吗?也许一个SVM或者一个轻量感知器,已经能解决大部分问题了。

RELATED READING

延伸阅读

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