MNIST手写数字识别:SVM、决策树、KNN与朴素贝叶斯分类器对比实践

发布时间:2026/9/12 22:19:01
MNIST手写数字识别:SVM、决策树、KNN与朴素贝叶斯分类器对比实践
简介一份基于Python 3.6的MNIST手写数字识别完整实现核心采用SVM、决策树、KNN与朴素贝叶斯四种经典机器学习算法分别完成手写数字分类任务并输出准确率对比结果。项目面向人工智能、计算机、自动化等专业的在校学生与机器学习入门者适合作为课程设计、毕业设计、算法实验或项目初期演示素材。压缩包内共19个文件其中包含4个Python源程序、8张准确率与分类结果图、标准MNIST数据集文件、训练好的模型文件pkl以及决策树可视化与README说明文档整体大小约11MB代码、数据与结果图分目录存放便于按模块学习与复现。目前已有538人学习下载代码经测试可正常运行答辩评审平均分达96分。读者可获得完整可运行的源码、可视化评估结果与数据预处理流程既能直观理解四种算法在手写识别任务上的差异也可在现有框架上扩展其他分类器或进行调参实战。1. 为什么拿 MNIST 练手写数字识别而不是直接上深度学习MNIST 手写数字识别是机器学习里最“老牌”的基准题28×28 灰度图10 个类别6 万训练样本加 1 万测试样本。很多人一上来就搭 CNN但 CNN 在 CPU 上训练慢、调参链路长反而不适合理解分类器之间的差异。这里拿 SVM、决策树、KNN、朴素贝叶斯四种经典方法在同一份数据集上做横向对比反而能清楚看到“模型复杂度—准确率—推理速度”的取舍。这份资源把四套实现、原始数据集、结果图和 README 都打包好了代码基于 Python 3.6 和 scikit-learn跑通不需要 GPU。适合正在做机器学习课程设计、毕设开题或者想快速验证分类器效果的人。直接跑代码能复现准确率对比改参数也能观察不同分类器的敏感点。下面从数据读取开始逐步拆到模型保存和误判检查。2. 数据集二进制格式解析idx3-ubyte 与 idx1-ubyte 怎么读MNIST 原始文件不是图片而是 IDX 二进制格式。Dataset 目录下四个文件分别是训练图像、训练标签、测试图像、测试标签。用numpy.fromfile或struct解析即可不依赖第三方图像库。import numpy as np import struct def load_mnist_images(path): with open(path, rb) as f: magic, num, rows, cols struct.unpack(IIII, f.read(16)) data np.fromfile(f, dtypenp.uint8).reshape(num, rows, cols) return data def load_mnist_labels(path): with open(path, rb) as f: magic, num struct.unpack(II, f.read(8)) labels np.fromfile(f, dtypenp.uint8) return labelsstruct.unpack(IIII, f.read(16))读前 16 字节魔数、样本数、行数、列数。注意是大端序因为 IDX 格式规定高位在前。图像数据每个像素是 0–255 的 uint8需要归一化到 [0,1] 或 [0,255] 供后续模型使用。标签直接是 uint8 数组第 0 维与图像一一对应。读出来后做两份数据预处理X_train load_mnist_images(Dataset/train-images.idx3-ubyte) y_train load_mnist_labels(Dataset/train-labels.idx1-ubyte) X_test load_mnist_images(Dataset/t10k-images.idx3-ubyte) y_test load_mnist_labels(Dataset/t10k-labels.idx1-ubyte) X_train X_train.reshape(len(X_train), -1) # (60000, 784) X_test X_test.reshape(len(X_test), -1) # (10000, 784) X_train X_train / 255.0 X_test X_test / 255.0这里把 28×28 展平成 784 维向量。SVM 和朴素贝叶斯对特征尺度敏感灰度值归一化能明显缩短训练收敛时间。KNN 如果直接算欧氏距离非归一化会导致距离被大数值像素主导。原始二进制解析比torchvision.datasets.MNIST更可控——torchvision 内置下载经常因为源站 404 失败而这份资源自带四个原始文件离线就能用。解析完可以顺手检查一下标签分布确认数据没读错unique, counts np.unique(y_train, return_countsTrue) print(dict(zip(unique, counts)))如果某个数字样本数明显异常说明文件偏移或 reshape 有问题。正常时每个类别约 5000–7000 个样本。3. 四种分类器的实现与调参SVM、决策树、KNN、朴素贝叶斯3.1 SVMRBF 核是 MNIST 上的稳妥选择SVM 在 MNIST 这种中等规模数据集上效果很好但全量 6 万样本训练 RBF-SVM 会比较慢代码里常用SVC(kernelrbf, C1.0, gammascale)。from sklearn.svm import SVC svm_model SVC(kernelrbf, C1.0, gammascale, probabilityFalse) svm_model.fit(X_train[:10000], y_train[:10000]) # 可先用子集验证 acc_svm svm_model.score(X_test, y_test) print(SVM accuracy:, acc_svm)C是误分类惩罚系数越大越容易过拟合gamma控制 RBF 核的影响半径scale会根据特征数量自动取1 / (n_features * X.var())。全量训练时建议用decision_function_shapeovr腾出更多计算内存。如果不降采样6 万样本的核矩阵计算量偏大实际调参时可先用 1 万样本跑通流程再逐步加数据。3.2 决策树先限制深度再谈精度决策树不需要特征缩放但 MNIST 像素级特征会让它长出很深的树容易过拟合。代码里DecisionTreeClassifier应设max_depth、min_samples_split和criterion。from sklearn.tree import DecisionTreeClassifier dt_model DecisionTreeClassifier(criteriongini, max_depth12, min_samples_split4, random_state42) dt_model.fit(X_train, y_train) acc_dt dt_model.score(X_test, y_test) print(DecisionTree accuracy:, acc_dt)max_depth12是经验值MNIST 上超过 15 层几乎一定会过拟合训练集准确率接近 1.0测试集却停滞在 0.85 左右。min_samples_split4让每个叶子至少包含 4 个样本削弱噪点影响。决策树在这里准确率是最低的但它把每个像素比较过程暴露得最清楚方便观察哪些位置的数字结构最稳定。树的可视化可以用sklearn.tree.export_graphviz导出 DOT 文件配合 Graphviz 转成图片。资源里的JueCetree.dot就是这个产物。导出命令from sklearn.tree import export_graphviz export_graphviz(dt_model, out_fileJueCetree.dot, feature_names[fpixel_{i} for i in range(784)], class_names[str(i) for i in range(10)], filledTrue, roundedTrue)导出的 DOT 文件可以用dot -Tpng JueCetree.dot -o tree.png转图。注意决策树 12 层时节点已经很多导出图片会是宽幅长图建议只看前几层节点。3.3 KNNk 值、距离度量与搜索算法KNN 是惰性学习训练阶段只是存数据预测时才计算距离。代码里常见参数是n_neighbors3、weightsdistance、algorithmauto。from sklearn.neighbors import KNeighborsClassifier knn_model KNeighborsClassifier(n_neighbors3, weightsdistance, algorithmauto, p2) knn_model.fit(X_train[:20000], y_train[:20000]) acc_knn knn_model.score(X_test, y_test) print(KNN accuracy:, acc_knn)n_neighbors3的准确率通常高于 5 或 7因为数字边界像素噪声较大邻居越少越能保留局部结构。weightsdistance让近邻权重更高比默认的uniform更适合高维稀疏像素。p2是欧氏距离如果改p1曼哈顿距离准确率会小幅下降但预测更快。KNN 的坑是内存和耗时6 万样本 784 维全量预测一次要跑几分钟。实际使用可以用algorithmkd_tree降维度量次数但 MNIST 特征维度较高kd-tree 在高维下退化严重auto会自己选择暴力计算。需要更快速度的话可以只取全部训练样本的 2 万个子集作为最终模型。3.4 朴素贝叶斯高斯分布假设下的基线准确率朴素贝叶斯假设特征条件独立对像素数据来说成立性很弱但作为基线很有价值。MNIST 像素是 0–255 整数高斯朴素贝叶斯最常用。from sklearn.naive_bayes import GaussianNB gnb_model GaussianNB() gnb_model.fit(X_train, y_train) acc_gnb gnb_model.score(X_test, y_test) print(GaussianNB accuracy:, acc_gnb)高斯朴素贝叶斯在 MNIST 上大约能到 0.55–0.60低于决策树。因为每个像素独立建模数字的空间结构被完全打散。如果想提高贝叶斯效果可以用BernoulliNB做二值化但准确率提升有限。这里保留它主要是给整个对比实验一个“无脑下限”验证数据集和评估流程是否正确。四种模型的准确率、训练时间和推理速度放在一张表里模型核心参数测试集准确率参考训练耗时预测速度SVMkernelrbf, C1.00.97–0.98长全量中决策树max_depth120.84–0.87短极快KNNn_neighbors30.96–0.97无慢高斯朴素贝叶斯默认0.55–0.60快快SVM 和 KNN 在 MNIST 上都能逼近卷积网络的下限但代价是推理阶段的存储或计算。决策树用精度换可解释性而朴素贝叶斯适合当流程验证的第一版。4. 准确率对比与结果可视化为什么贝叶斯是下限SVM 是上限模型训完不能只看 print 的数字要统一画成对比图方便写进实验报告或答辩 PPT。资源里res文件夹的SVM.png、KNN.png、DT-acc.png、bayes-acc.png就是每轮训练的准确率曲线。补一张综合柱状图更直观。import matplotlib.pyplot as plt models [SVM, DecisionTree, KNN, Bayes] accs [acc_svm, acc_dt, acc_knn, acc_gnb] plt.figure(figsize(8, 5)) bars plt.bar(models, accs, color[#4C72B0, #55A868, #C44E52, #8172B2]) plt.ylim(0, 1.05) plt.ylabel(Test Accuracy) plt.title(MNIST Classification Accuracy Comparison) for bar, acc in zip(bars, accs): plt.text(bar.get_x() bar.get_width()/2, bar.get_height() 0.01, f{acc:.4f}, hacenter, vabottom) plt.tight_layout() plt.savefig(res/compare-acc.png, dpi150)柱状图上每个模型顶部标注准确率能直接看出决策树和贝叶斯明显低于 SVM 和 KNN。如果发现 SVM 准确率反而不如 KNN先检查C是否太小或者训练数据有没有归一化。KNN 如果准确率掉了两个点检查n_neighbors是不是被改成 10 以上。这里也建议把训练过程中每步的 acc 存成列表单独画一条随样本量增长的变化曲线。代码里bayes-acc.png和DT-acc.png就是这么来的。对 KNN 来说每次预测一个测试样本就要计算它与全部训练样本的距离所以测试过程也会产生逐批准确率可以画成散点图观察误判分布。实验报告里通常要解释四个模型准确率差距的来源SVM 用核函数把低维像素空间映射到高维能学到数字笔画边界KNN 直接依赖距离度量对噪声敏感但样本足够时依然强决策树只能做坐标轴的垂直划分MNIST 里同一数字的不同写法在像素空间并不是坐标对齐的所以效率低朴素贝叶斯把像素完全割裂丢失了空间关系准确率最低。这样的解释既能体现你对模型原理的理解也能对应上代码中每个参数的作用。5. 模型持久化与误判样本排查让手写数字识别更实用模型训练好后用joblib.dump保存到model.pkl下次直接加载不用重新训练。资源里已经带了model.pkl但你自己重训后应该覆盖它。import joblib # 训练完成后保存例如保存训练效果最好的 SVM joblib.dump(svm_model, model.pkl) # 加载并使用 loaded_model joblib.load(model.pkl) sample X_test[0].reshape(1, -1) pred loaded_model.predict(sample) print(predicted:, pred[0], true:, y_test[0])保存模型时如果同时存了 KNNmodel.pkl会包含整个训练集体积会到几十 MB。对 SVM 来说只保存支持向量和系数体积小很多。如果model.pkl加载后预测报维度错误确认保存前输入是不是(n_samples, 784)的二维数组。实用性进阶是排查误判样本。从测试集里找出预测错误的前几个手写数字画出来看看模型到底被什么样的笔画带偏了。import numpy as np import matplotlib.pyplot as plt pred loaded_model.predict(X_test) wrong_idx np.where(pred ! y_test)[0] plt.figure(figsize(8, 2)) for i, idx in enumerate(wrong_idx[:8]): plt.subplot(2, 4, i 1) plt.imshow(X_test[idx].reshape(28, 28), cmapgray) plt.title(ftrue{y_test[idx]}\npred{pred[idx]}) plt.axis(off) plt.tight_layout() plt.savefig(wrong-cases.png, dpi120)手写数字里最容易误判的组合是 4 和 9、3 和 5、7 和 2因为这些数字在收笔或封口位置的像素分布非常接近。看一下误判图如果错误集中在某一种形变写法上可以考虑在训练集里增广这类样本轻微旋转、平移。如果误判很平均多半是模型容量不够SVM 就继续调C和gammaKNN 就换距离权重。最后一个替代方案如果以后想拿新数据测试不想再解析idx3-ubyte可以用numpy.save把读好的样本和标签存成.npy后续np.load直接进内存避免每次都写二进制解析函数。这份资源的完整代码路径是Code目录下的SVM.py、KNN.py、Bayes.py、tree.py跑的时候记得把当前目录切到项目根目录保证相对路径能定位到Dataset。本文还有配套的精品资源点击获取