ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MNIST手写数字识别:逻辑回归、SVM、KNN实战与调参

MNIST手写数字识别:逻辑回归、SVM、KNN实战与调参 简介一套基于机器学习方法完成MNIST手写数字识别的完整项目包含SVM、决策树、KNN、朴素贝叶斯四种经典算法的Python实现与准确率对比实验。代码基于Python 3.6编写数据、脚本和结果分目录存放适合计算机相关专业学生用于课程设计、毕业设计或机器学习入门实践。资源共19个文件以Python程序、图片结果、MNIST原始数据集及模型文件为主其中idx格式为手写数字图像与标签数据png为各类算法识别效果和准确率曲线py为可运行代码压缩包仅11.04MB轻量易用目前已有539人学习下载。下载后可直接对照README了解项目结构复现四种模型并进行结果比较还可在此基础上修改特征或调参用于其他分类任务或作为项目初期演示。整体逻辑清晰、代码经过运行验证是一份兼顾教学与实用价值的机器学习参考资料。1. MNIST 手写数字识别为什么它仍是机器学习入门的第一道关很多人觉得 MNIST 已经被玩烂了一上来就想去啃 ImageNet、目标检测或者大模型。但实际带过项目就知道MNIST 手写数字识别是少数能把“机器学习应用流程”完整走一遍的数据集数据加载、预处理、特征工程、模型选型、评估调参、误差分析每一步都有明确的量化指标。它只有 10 个类别、28×28 像素单张图 784 维用一台普通笔记本跑逻辑回归或者支持向量机几分钟就能出结果适合新手建立完整认知也适合熟手快速验证一个新的特征工程思路。这篇笔记我会直接给出可复现的源代码和数据集处理细节看完你能在本地把逻辑回归、SVM、KNN 三个模型跑通并知道参数怎么调、坑在哪里。2. 先把数据和标签管明白MNIST 数据集下载与预处理2.1 MNIST 数据集的三种获取方式内置接口、原始文件、镜像源MNIST 数据集本身不大训练集 60000 张、测试集 10000 张每张图是 28×28 的灰度图。常见做法有三种机器学习框架内置接口、官网原始文件、国内镜像。我一般建议新手先走框架内置接口代码最少但要注意版本兼容。# 方式一PyTorch 内置接口最省事但可能遇到下载 404 from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform )这段代码首先定义了一个 transform 流水线ToTensor()把 PIL 图像转成张量并把像素值从 0-255 缩放到 0-1Normalize((0.1307,), (0.3081,))用 MNIST 全集的均值和标准差做标准化。注意这里的均值和标准差是官方统计好的常数不是你自己算的。trainTrue表示加载训练集downloadTrue表示如果本地没有就自动下载。跑第二次时数据已在本地不会重复下载。如果你遇到torchvision下载 MNIST 时 404这是近期很常见的一个坑就改用下面的原始文件方式。# 方式二手动下载原始文件并解析不依赖框架版本 import gzip import numpy as np def load_mnist_from_files(image_path, label_path): with gzip.open(label_path, rb) as f: labels np.frombuffer(f.read(), dtypenp.uint8, offset8) with gzip.open(image_path, rb) as f: images np.frombuffer(f.read(), dtypenp.uint8, offset16) images images.reshape(-1, 28 * 28) return images, labels train_images, train_labels load_mnist_from_files( data/train-images-idx3-ubyte.gz, data/train-labels-idx1-ubyte.gz ) test_images, test_labels load_mnist_from_files( data/t10k-images-idx3-ubyte.gz, data/t10k-labels-idx1-ubyte.gz )这里要说明一下 MNIST 原始文件的格式标签文件的前 8 个字节是文件头前 4 字节是魔法数后 4 字节是样本数图像文件的前 16 个字节是文件头额外包含行数和列数。所以offset8和offset16就是在跳过这些头部信息。np.frombuffer直接把字节流转成 numpy 数组效率比逐字节读取高得多。2.2 归一化与数据划分为什么测试集不能碰训练流程MNIST 的像素值范围是 0-255如果不做归一化直接喂给逻辑回归或 SVM会导致梯度下降收敛很慢而且某些特征的数值范围过大会主导距离计算。正常流程是先把像素除以 255 缩放到 [0,1]再从训练集里切出一部分做验证集用于调参。测试集只能最后用一次用多了就会“泄漏”导致你对泛化能力的估计过于乐观。# 划分训练集/验证集并做归一化 from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( train_images, train_labels, test_size0.1, random_state42 ) X_train X_train / 255.0 X_val X_val / 255.0 X_test test_images / 255.0这里test_size0.1表示从 60000 张训练图中留出 6000 张做验证集剩下 54000 张用来训练模型。random_state42是为了保证每次运行划分结果一致方便复现。注意这里归一化用的是简单除法不是标准化。对于像素这种有明确物理范围的特征min-max 归一化通常就够用了。2.3 可视化检查先看图像再谈模型我见过不少新手拿到数据就直接开训跑了半天发现准确率不对劲最后检查才发现是数据加载出了问题。所以强烈建议先画出几张图像看一眼。import matplotlib.pyplot as plt fig, axes plt.subplots(2, 5, figsize(10, 4)) for i, ax in enumerate(axes.flat): ax.imshow(train_images[i].reshape(28, 28), cmapgray) ax.set_title(fLabel: {train_labels[i]}) ax.axis(off) plt.tight_layout() plt.show()这段代码把前 10 张训练图像以 2 行 5 列的形式画出来每张图上标注对应的标签。运行之后你应该看到清晰的手写数字图像是白底黑字或反色取决于显示方式。如果图像全是噪声或者数字倒置说明文件解析有问题趁早停下来排查别白白浪费训练时间。3. 特征工程把 784 维像素变成模型能学的特征3.1 为什么原始像素在 MNIST 上够用但换个数据集就不行机器学习中的数据处理是什么这是新手最常问的问题之一。具体到 MNIST原始像素是 784 维的灰度值逻辑回归和 SVM 直接吃原始像素也能达到 92% 和 94% 左右的准确率。这是因为手写数字本身是规整的结构化图像数字和背景的对比度清晰像素级别的模式差异足够大。但如果你拿的是真实场景拍摄的图片比如街景数字、车牌或者票据光照、角度、遮挡都会让像素值变得不稳定这时候原始像素就不行了需要更鲁棒的特征或者深度学习模型自动提取特征。3.2 主成分分析降维从 784 维到保留 90% 方差原始像素虽然能用但 784 维对某些模型比如 RBF 核的 SVM来说计算开销不小。常见做法是先做 PCA 降维把数据压缩到几十维同时保留大部分信息。MNIST 的像素之间相关性很强一个数字的笔画是连续的相邻像素高度相关所以降维空间很大。from sklearn.decomposition import PCA pca PCA(n_components0.95, random_state42) X_train_pca pca.fit_transform(X_train) X_val_pca pca.transform(X_val) X_test_pca pca.transform(X_test) print(f原始特征维度: {X_train.shape[1]}) print(fPCA 保留下来的维度: {X_train_pca.shape[1]})n_components0.95表示保留 95% 的方差MNIST 上通常能把 784 维压到 100 维左右。fit_transform在训练集上拟合 PCA 参数并同时完成变换而transform只做变换不再重新拟合——测试集和验证集绝对不能参与 PCA 的拟合否则又是数据泄漏。这里有一个实际收益如果后面要跑 RBF 核的 SVMPCA 降维后训练时间可以缩短几倍而准确率损失不到 0.5 个百分点。对于调参密集的场景先用 PCA 降维能省下大量时间。3.3 像素特征和 HOG 特征的对比什么时候需要手工特征在深度学习普及之前HOG方向梯度直方图是手写数字识别的主流特征之一。HOG 统计图像局部区域的梯度方向分布对光照变化和轻微形变更鲁棒。但 MNIST 本身是干净的二值化图像HOG 带来的提升有限反而增加计算复杂度。我在实际项目中得到的经验是MNIST 上用原始像素 SVM 已经能到 97% 以上HOG 特征最多再提升 0.2 个百分点且调参成本更高。如果你的场景是自然图像HOG 值得试如果只是复现 MNIST 流程别在特征工程上过度投入把精力放在后面模型的调参上更划算。# HOG 特征提取示例了解即可MNIST 上不必优先使用 from skimage.feature import hog from skimage import exposure def extract_hog_features(images): features [] for img in images: fd hog(img.reshape(28, 28), orientations9, pixels_per_cell(4, 4), cells_per_block(2, 2), visualizeFalse) features.append(fd) return np.array(features)这段代码对每张图计算 HOG 特征。orientations9表示把梯度方向分成 9 个区间pixels_per_cell(4, 4)表示每个 cell 是 4×4 像素cells_per_block(2, 2)表示每个 block 包含 2×2 个 cell。这些参数控制特征的粒度。如果你只是想跑通 MNIST这段代码可以先不看知道有这个选项就行。4. 三个经典模型的对比实现逻辑回归、SVM 与 KNN4.1 逻辑回归先跑一个 baseline 再说逻辑回归虽然名字里有“回归”但它是分类模型。在 MNIST 上用它做多分类实际上是 Softmax 回归——把线性输出经过 softmax 变成 10 个类别的概率分布。它是最简单的 baseline训练速度快能帮你验证数据加载和特征工程的正确性。from sklearn.linear_model import LogisticRegression lr LogisticRegression( max_iter1000, solverlbfgs, C1.0, random_state42 ) lr.fit(X_train_pca, y_train) val_acc lr.score(X_val_pca, y_val) print(f逻辑回归验证集准确率: {val_acc:.4f})max_iter1000是最大迭代次数lbfgs适合中小数据集收敛快。C1.0是正则化强度的倒数C 越小正则化越强C 越大越容易过拟合。如果你用的是原始 784 维特征训练时间会长一些但也能跑完。逻辑回归在 MNIST 上的验证集准确率通常在 92%-95% 之间取决于是否降维和调参。这个结果就是你的 baseline后面所有模型都要跟它比。4.2 支持向量机RBF 核是 MNIST 的主力SVM 在 MNIST 上表现比逻辑回归好不少RBF 核能把像素空间映射到高维空间捕捉非线性边界。但 RBF 核 SVM 有两个关键参数C 和 gamma。C 控制误分类的惩罚力度gamma 控制 RBF 核的宽度——gamma 越大决策边界越复杂越容易过拟合。from sklearn.svm import SVC svm SVC( kernelrbf, C10.0, gamma0.01, random_state42 ) svm.fit(X_train_pca, y_train) val_acc_svm svm.score(X_val_pca, y_val) print(fSVM 验证集准确率: {val_acc_svm:.4f})kernelrbf是高斯径向基核C10.0表示对误分类的惩罚较大模型更倾向于正确分类所有训练样本。gamma0.01是核函数的系数这个值在降维后的数据上通常取 0.001 到 0.1 之间。如果 gamma 太大比如 1.0模型会过拟合到训练集上验证集准确率反而下降。SVM 在 PCA 降维后的数据上验证集准确率能达到 96%-97%。4.3 KNN不训练也能到 97% 的“懒模型”KNN 是另一种思路——它根本没有训练过程只是把训练数据存下来预测时计算新样本和所有训练样本的距离取最近 K 个邻居投票。MNIST 上 KNN 的表现意外地好因为数字图像之间的距离度量欧氏距离在有归一化的情况下已经能反映视觉相似度。但它的缺点是预测时要遍历全部训练样本60000 张图逐张计算距离速度很慢。from sklearn.neighbors import KNeighborsClassifier knn KNeighborsClassifier( n_neighbors5, weightsdistance, p2 ) knn.fit(X_train_pca, y_train) val_acc_knn knn.score(X_val_pca, y_val) print(fKNN 验证集准确率: {val_acc_knn:.4f})n_neighbors5表示取最近的 5 个邻居投票。weightsdistance表示距离越近的样本投票权越大如果用默认的uniform所有邻居权重相同在 MNIST 上准确率会低一些。p2表示使用欧氏距离L2 范数。KNN 在降维后的 MNIST 上验证集准确率也能到 96% 左右但预测一万张测试图可能要等几十秒到几分钟这是它最大的短板。模型特征验证集准确率参考训练时间预测速度逻辑回归PCA(95%)92%-94%秒级极快SVM RBFPCA(95%)96%-97.5%分钟级快KNNPCA(95%)95%-96%无仅存储很慢以上数据是基于我本地跑过的参考值具体数字会因 sklearn 版本、PCA 保留方差比例和参数设置浮动。你的复现只要趋势一致就说明流程是对的。5. MNIST 识别避坑手册五个让新手反复翻车的细节5.1 torchvision 下载 MNIST 时报 404 错误现象执行datasets.MNIST(downloadTrue)时控制台报 HTTP 404下载失败。原因PyTorch 官方代码里引用的 MNIST 下载地址是在yann.lecun.com的旧路径源站偶尔会调整文件路径或响应头导致新版 torchvision 下载时 404。这个是 2024 年前后高频出现的问题跟你的网络环境关系不大。解决不要死磕内置下载接口。手动从镜像下载train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz四个文件放到./data目录下然后用 2.1 节原始文件解析的方式读取。这个方案不依赖任何框架版本一次下载永久使用。提示下载后记得检查文件大小。四个文件分别约为 9.9MB、28KB、1.6MB、4.4KB如果大小差太多大概率是下载到了错误页面或断点续传损坏。5.2 图像和标签错位损失一直在降但准确率上不去现象训练时 loss 下降正常但验证集准确率一直徘徊在 10% 左右相当于随机猜测。原因这是数据加载时图像和标签没有对齐。常见于自己解析原始文件后对图像做了 reshape 或 shuffle 但标签没有同步变换。解决在训练前打印前 10 张图像和标签肉眼确认对应关系参考 2.3 节的可视化代码。另外在划分训练/验证集时train_test_split默认会同步划分特征和标签只要不手动打乱索引就行。如果你用了shuffle或者np.random.permutation务必保证对图像和标签使用同一套索引。5.3 归一化放在划分之前整个流程的正确姿势现象模型在训练集上准确率很高但测试集上惨不忍睹且无论怎么调参都拉不回来。原因你在划分训练/测试集之前就对全量数据做了归一化或 PCA导致测试集的信息泄漏到了训练集。测试集本来就是用来模拟“未来数据”的如果它参与了归一化参数的拟合测试结果就失去了意义。解决严格遵循这个顺序先划分再在训练集上fit_transform然后对验证集和测试集只做transform。我在 3.2 节的代码里已经按这个顺序写了照着做就不会出错。5.4 直接拿 784 维原始像素跑 RBF 核 SVM时间翻几倍现象SVM 训练跑了十几分钟还没结束验证集准确率也没有显著提升。原因RBF 核 SVM 的计算复杂度大约是 O(n²) 到 O(n³)样本数相关的同时特征维度越高核矩阵计算越慢。784 维对一个 54000 样本的训练集来说是很大的负担。解决先做 PCA 降维如保留 95% 方差再喂给 SVM。我实测降维到 100 维左右训练时间能缩短 5-10 倍准确率损失不到 0.5 个百分点。对于调参实验降维几乎是必须的否则你可能等不起一组交叉验证。5.5 把 KNN 当默认选择感受一下 KNN 的预测延迟现象KNN 训练存储瞬间完成但预测测试集的 10000 张图时要等好几分钟有些同学误以为程序卡死了。原因KNN 预测时要计算待测样本与所有训练样本的距离。训练集 54000 张图降维后 100 维每张测试图都要做 54000 次距离计算总量巨大。解决KNN 可以作为理解“距离度量”的入门模型但实际项目里很少用它做大规模预测。MNIST 上优先选 SVM 或逻辑回归。如果你必须用 KNN可以尝试减小训练集样本量比如只用 10000 张或者用 KD-Tree 加速sklearn 的algorithmkd_tree但准确率会有一定牺牲。6. 把模型搬到自己的场景混淆矩阵、交叉验证与超参搜索模型跑通之后你可能会想MNIST 这种玩具数据集上 97% 有什么意义真正的价值在于把整套流程迁移到自己的数据上。我建议你做完三个阶段再收手——混淆矩阵看错分模式、交叉验证稳定评估、网格搜索找最优参数。先看混淆矩阵。准确率会把很多信息掩盖掉比如数字 4 和 9 之间的混淆、7 和 1 之间的混淆这些都是手写识别的经典难点。打印混淆矩阵能精确告诉你模型到底错在哪些类。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay y_pred svm.predict(X_val_pca) cm confusion_matrix(y_val, y_pred) disp ConfusionMatrixDisplay(cm) disp.plot() plt.show()confusion_matrix的每一行代表真实类别每一列代表预测类别。对角线上的值越大越好。如果某个非对角元素特别大比如第 4 行第 9 列的值很高说明模型经常把 4 认成 9这时候可以考虑增加这类样本的权重或者换特征方向强化区分。再做交叉验证。单次划分训练/验证集会受随机性影响换一个random_state结果就变了。用cross_val_score做 5 折交叉验证把 5 次的均值作为模型能力的估计更可靠。from sklearn.model_selection import cross_val_score scores cross_val_score(svm, X_train_pca, y_train, cv5, scoringaccuracy) print(f5 折交叉验证准确率: {scores.mean():.4f} ± {scores.std():.4f})cv5表示把训练集分成 5 份每次用 4 份训练 1 份验证轮转 5 次。scoringaccuracy指定评估指标。如果 5 次结果的方差很大比如标准差超过 0.5%说明模型对数据划分敏感可能是特征不稳定或样本量不足。最后做超参搜索。SVM 的C和gamma最值得调逻辑回归的C也值得调。用GridSearchCV在参数网格上搜索比你手动试快得多。from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1] } grid GridSearchCV( SVC(kernelrbf, random_state42), param_grid, cv3, scoringaccuracy, n_jobs-1 ) grid.fit(X_train_pca, y_train) print(f最优参数: {grid.best_params_}) print(f最优交叉验证准确率: {grid.best_score_:.4f})param_grid定义了 4 个 C 值和 3 个 gamma 值共 12 组参数组合。cv3表示每组参数跑 3 折交叉验证总耗时是 36 次训练。n_jobs-1让 sklearn 用满 CPU 所有核心并行计算。如果不想等太久可以先用 PCA 降维再搜参我通常就是这么做的。我自己的习惯是先用逻辑回归跑通全流程再换 SVM 并调参最后看看混淆矩阵里错得最多的类别能不能通过数据增强改善——比如给数字图像做轻微旋转或缩放模拟真实手写的变化。MNIST 上 98% 是一个门槛过了之后你会发现迁移到自己的数据集时这套“先 baseline、再调参、再看错分、再增强”的流程同样适用。每次做完一个项目我都会把这个流程从头再走一遍总能在细节里掏出点新东西。这个数据集虽然小但它是检验你机器学习基本功是否扎实的试金石希望帮到你。本文还有配套的精品资源点击获取
返回列表