☰
手写数字识别完整实战:从KNN/SVM到CNN的答辩指南
2026/10/17 7:22:00 网站建设 项目流程

每年临近期末,都会有人来问同一个问题:数字识别到底怎么做?这个题目看起来太简单了——数据集现成、代码到处都有、个把小时就能跑出百分之九十几的准确率。可真正做起来,有人卡在环境配置,有人卡在模型选型,还有人跑完了代码却讲不清原理,答辩时一问就露馅。

这篇文章把基于机器学习和深度学习的手写数字识别从“能跑”做到“能答辩”的完整链路讲清楚。我会先用传统机器学习算法(KNN、SVM)打基线,再上CNN做主力模型,最后聊实验对比、错误分析和交付细节。全程给出的都是这几年反复被验证过的实操方案,适合正在做课程设计或毕业设计的同学,也适合想快速上手一个完整视觉识别项目的初学者参考。

1. 数字识别在课设毕设里的定位:先搞清楚裁判怎么打分

1.1 从MNIST说开去:这个数据集为什么经久不衰

手写数字识别几乎等于MNIST数据集的代名词。60000张28x28灰度训练图,10000张测试图,数字0到9共10类,每张图本质上就是一个784维的向量。MNIST之所以成为入门首选,原因非常务实:

  • 数据量适中,CPU训练就能在几分钟内跑完一个传统模型,GPU反而是锦上添花。
  • 任务直观,随便在纸上画个数字,人都能瞬间判断,模型效果容易感知。
  • 难度梯度合理,简单KNN能到96%以上,深度一点的CNN能到99%以上,方法之间的差距非常明显。

但MNIST的问题也要心里有数:它是个相当“干净”的数据集,图片基本居中,笔画粗细规范,背景没有噪声。所以在MNIST上拿到99%不代表你的模型真的理解了手写数字,换用真实手写图片表现会明显下降。这也是我后面建议做自建数据测试的原因——提前把这个短板点出来,答辩时反而能成为加分项。

1.2 课程设计和毕业设计的评分侧重点完全不同

课程设计通常考察“完整流程”:数据处理、模型训练、评估、简单界面,能跑通并且逻辑自洽就能拿差不多的分数。但毕业设计要求“对比与改进”:不能只跑一个模型,需要比较多个方案,解释优劣,最好还能指出原方案的问题并做针对性改进。

这个差异直接决定你的实验设计。如果报告里只写“我用了CNN,准确率99%”,课设能过,但毕设会显得单薄;如果写成“从KNN、SVM到CNN的精度与训练成本对比,并针对误分类做了数据增强实验”,深度立刻就出来了。后面所有章节的布局,都是围绕这个评分逻辑展开的。

1.3 准确率目标设到多少才算拿得出手

以同类项目常见的典型结果来看:KNN基线在96%左右,SVM配合RBF核能到98%-99%,简易CNN在99%以上,加上数据增强可以逼近99.5%。我建议把目标拆成三个梯度:

  • 基线不低于95%,用于证明你的评估链路没问题;
  • 主力模型不低于98%,这是“能拿上台面”的门槛;
  • 冲刺目标99%以上,这个数字在报告里才有真正亮点。

这样设计的目的是让每个结果都有对比对象。老师问“你的模型为什么好”,你不需要空口解释,直接拿出一张完整的实验对比表,说服力比任何修辞都强。

2. 环境、数据集与基线:动手前先把这三件事钉死

2.1 框架选型:PyTorch、TensorFlow还是Scikit-learn

如果只做传统算法,Scikit-learn完全够用,KNN、SVM、交叉验证都是现成的。如果要做深度学习,我建议PyTorch。理由很实际:调试体验好,中途打印张量形状、检查梯度都很方便;生态成熟,MNIST相关教程和踩坑记录非常多,碰见问题至少查得到;周围同学和老师用得也多,交流成本低。

TensorFlow也能做,但版本变更带来的API差异会浪费不少时间,尤其是从Keras到2.x再到各种兼容模式,很容易让人在环境问题上栽跟头。另外提醒一句:这个项目没有GPU也能完整跑下来,MNIST单张图只有28x28,简易CNN在CPU上训练一个epoch大约几十秒到一分钟,做对比实验完全可以接受。

安装环境时建议新建一个干净的虚拟环境,装好Python 3.9+、torch、torchvision、numpy、matplotlib、scikit-learn。我见过太多次因为环境混乱,最后分不清是代码的bug还是依赖冲突导致的错误。

2.2 数据集加载与预处理细节

torchvision可以直接下载MNIST,最容易被忽略的是归一化参数。标准写法是:

from torchvision import datasets, transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_data = datasets.MNIST(root='./data', train=True, download=True, transform=transform) test_data = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

这里的(0.1307, 0.3081)是MNIST全集的均值和标准差。为什么不能只用简单的除以255?因为神经网络对输入尺度很敏感,虽然0到1的范围看起来也合理,但使用统计得到的均值和标准差做标准化,梯度更新会更稳,loss下降更平滑。而对sklearn里的传统模型,我通常直接用除以255,因为KNN、SVM这类方法不太受尺度影响,但统一处理总归更规整。

2.3 训练集、验证集、测试集三分法:最容易翻车的地方

MNIST官方把60000张作为训练集、10000张作为测试集。很多人直接拿60000张训练,然后用那10000张反复调参——这相当于把测试集当验证集用,调出来的结果是“信息泄漏”的,最后报告里的数字偏乐观,模型真实泛化能力容易被高估。

正确做法是从训练集中再抽5000到10000张作为验证集,调参只看验证集的结果,测试集留到最后算一次。这里有个小建议:写一个固定划分函数,设置随机种子,保证每次跑的验证集一致。这样不同模型、不同参数之间的对比才有可比性。MNIST的类别分布本身很均匀,直接随机采样即可,不需要做复杂的分层处理。

2.4 写CNN之前的全流程最小验证

在正式写CNN之前,先跑一个简单模型把数据加载、预处理、评估这条链路全部打通。我一般用逻辑回归做这一步:

from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score X_train_flat = train_images.reshape(-1, 784) / 255.0 X_test_flat = test_images.reshape(-1, 784) / 255.0 lr = LogisticRegression(max_iter=200) lr.fit(X_train_flat, y_train) print("LR baseline:", accuracy_score(y_test, lr.predict(X_test_flat))) # 典型结果约 0.92

为什么要多这一步?因为很多翻车事故——label对不上、特征维度算错、归一化混用——都会在逻辑回归上提前暴露。折腾半小时确认链路没问题,再进入后面的正题,值。

3. 传统机器学习线:从KNN到SVM的精度爬坡

3.1 为什么强烈建议先做传统方法,而不是直接上CNN

直接写CNN确实更快看到高准确率,但课设和毕设通常要求体现对不同算法的理解与比较。传统方法的价值有三层:

第一,理解分类的本质。KNN和SVM都在做同一个事情:在特征空间里找决策边界。理解了它们的边界思路,再回头看CNN的特征提取会更有全局感。第二,给CNN一个参照系。你说CNN比SVM好1.5%,没有对照就不知道这1.5%的分量;有了传统方法铺底,这个数字才有意义。第三,实验表格需要打底。KNN、SVM代码量小,几分钟出一个结果,作为实验对比表的第一行和第二行极其合适。

3.2 KNN:用手写距离就能拼出96%准确率

KNN的思路非常直观:把所有训练图片看成784维空间里的点,预测新图片时,找它空间距离最近的k个点,投票决定类别。距离一般用欧氏距离。在MNIST原始像素上,k=3时准确率能达到96%左右;k越大边界越平滑,但也容易把细微的区分信息抹掉;k=1最容易过拟合。

实操中还有一个参数值得调整:weights。默认是uniform,即所有近邻一票一权;改成distance,让距离近的点话语权更大,通常能提高一点准确率。

from sklearn.neighbors import KNeighborsClassifier knn = KNeighborsClassifier(n_neighbors=3, weights='distance') knn.fit(X_train_flat, y_train) y_pred_knn = knn.predict(X_test_flat) print("KNN:", accuracy_score(y_test, y_pred_knn)) # 典型结果约 0.96

注意KNN没有显式的训练过程,预测时要把所有训练样本都算一遍。6万样本、每张784维,计算量不小。如果机器一般,可以先抽1万训练样本来跑,准确率会低0.5%左右,实验表里注明即可。

3.3 SVM+RBF核:传统方法的精度天花板

SVM的核心思想是找一个间隔最大的超平面把类别分开。MNIST是10分类问题,sklearn默认用one-vs-rest策略处理多分类。RBF核的作用是把原始像素空间映射到更高维,让原本线性不可分的数据变得可分,相当于在空间中把样本“顶”出一些高维隆起,从而切出复杂的边界。

常用参数就两个:C和gamma。C是正则化强度,越大越容忍复杂边界,但也越容易过拟合;gamma控制RBF核的宽度,越大越关注局部细节。在MNIST上我常用C=10、gamma=0.01,抽样1万训练集时准确率在98%以上。

from sklearn.svm import SVC svm = SVC(C=10, gamma=0.01, kernel='rbf') svm.fit(X_train_sub, y_train_sub) print("SVM:", accuracy_score(y_test, svm.predict(X_test_flat))) # 典型结果约 0.98

SVC训练的时间复杂度比较高,6万全量样本可能要跑较长时间。做毕设时我的建议是:先用1万抽样把流程跑通,报告里注明样本规模,最后如果要刷新最终数值再上全量。

3.4 特征工程:原始像素够用吗,HOG值不值得做

MNIST训练集足够大且图片较干净,直接使用784维原始像素做SVM已经能接近99%。HOG(方向梯度直方图)通过统计图像局部区域的梯度方向分布来描述形状,对光照和轻微位移更鲁棒,理论上更适合手写字符。

但实际对比下来,HOG+SVM在MNIST上比原始像素的SVM提升很小,大约0.2到0.5个百分点,而特征提取本身会多写不少代码。我的结论是:以原始像素为主,HOG作为拓展实验在报告中提一句即可。省下来的时间留给CNN和数据增强,性价比高得多。

4. 深度学习主线:CNN从结构设计到训练踩坑实录

4.1 为什么CNN比全连接网络更适合图像

如果把28x28图片拉成784维向量直接喂给全连接网络,也能得到不错的结果,但问题在于:数字可能出现在不同位置,同一数字有不同的笔画粗细和倾斜角度。全连接网络对图像中每个位置的权重是完全独立的,它需要自己从数据里“死记硬背”每种位置的变化。

CNN通过卷积核做局部特征提取,一个卷积核学到的边缘、弧线、交叉点模式可以在全图复用,这就是参数共享;再加上池化层做下采样,空间上的轻微偏移被容忍。合在一起的效果是:CNN对数字的平移和轻微形变更鲁棒,而且需要的数据量更少,不容易把模型撑爆。

4.2 一个简单但有效的基础结构

不需要一上来就残差网络或者MobileNet。MNIST是灰度小图,一个结构清晰的五层CNN足够:

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.25), nn.Linear(128, 10), ) def forward(self, x): return self.classifier(self.features(x))

第一层从1通道变成32通道,小卷积核3x3尽量保留空间信息;经过两次2x2最大池化,特征图尺寸从28变成14再变成7。最后展平为6477=3136维,送入两层全连接分类。整个模型参数量约42万,CPU训练每轮几十秒,验证准确率能到99%以上。对比经典LeNet-5,思路类似但更简化,作为课设代码也更好讲。

4.3 训练超参:一套可以直接抄的稳定配置

超参不需要多高级,关键是稳定、可复现。我日常用这一套:

  • 优化器:Adam,学习率0.001
  • batch size:128
  • 训练轮数:10到15轮
  • 损失函数:CrossEntropyLoss
  • 设备:有GPU用GPU,没有就CPU
import torch from torch.utils.data import DataLoader train_loader = DataLoader(train_data, batch_size=128, shuffle=True) valid_loader = DataLoader(valid_data, batch_size=256, shuffle=False) model = SimpleCNN() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(10): model.train() for images, labels in train_loader: optimizer.zero_grad() loss = criterion(model(images), labels) loss.backward() optimizer.step() # epoch结束后在验证集上算一次准确率

这里有两个细节容易被忽略。第一,batch_size不要设太大,MNIST是小图,128是比较稳的值;如果GPU显存有限,64也可以。第二,shuffle必须打开,否则每个epoch见到的样本顺序不变,梯度更新会引入不必要的周期性偏差。

4.4 训练中常见的三个异常与排查思路

只贴正确代码远远不够,训练时最容易遇到三类问题。

第一个:loss一开始就很大而且不下降。优先检查数据有没有做ToTensor归一化,再检查学习率。0.001对Adam是常规值,如果你换成SGD,往往需要0.01甚至0.1,不然学不动。

第二个:训练准确率很高但验证准确率上不去。这是典型的过拟合信号。先看训练集和验证集的准确率差距有多大,如果差了两三个点以上,就补Dropout、减少模型层数,或者引入数据增强。MNIST本身比较简单,层数越多反而越容易过拟合,不是越深越好。

第三个:loss曲线出现NaN。多半是学习率过大或输入有极端值。MNIST像素范围很温和,基本可以锁定学习率的锅,把它减小一个数量级再试。

我强烈建议把每一轮的训练loss和验证loss画在同一张图里。两条曲线距离逐渐拉大,就是过拟合的直观信号;验证loss不降反升,说明该早停了。这张图放进报告里,比任何文字描述都有说服力。

5. 让项目从七十分到九十分:对比实验、数据增强与错误分析

5.1 一张实验对比表顶十段描述性文字

实验表格要按“证据链”的逻辑设计,既有方法演进,也有成本指标:

模型参数量训练时间(CPU,抽样1万数据)测试准确率
逻辑回归约7850约30秒92.0%
KNN(k=3,distance)不可训练参数约2分钟95.8%
SVM(C=10,gamma=0.01)不可数约5分钟98.3%
简单CNN约42万约3分钟/轮99.2%
CNN + 数据增强约42万约3分钟/轮99.5%

这个排列回答了四个问题:方法怎么演进、模型复杂度多大、训练成本多高、最终效果如何。答辩时老师盯着表格问“为什么CNN比SVM多花时间还值得”,你就可以顺着说准确率提升了一个多百分点,而且参数量不大,模型结构简单,部署成本很低。

5.2 数据增强:收益最大的“免费午餐”

数据增强的本质是用有限的真实数据造出更多样的训练样本,让模型见过更多版本的数字。MNIST上最有价值的三个操作是随机旋转、平移和缩放:

from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomAffine(degrees=10, translate=(0.1, 0.1), scale=(0.9, 1.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])

degrees=10表示最大旋转10度,translate表示水平和垂直平移比例,scale表示缩放比例。加这组增强后准确率一般能提升0.2到0.4个百分点,看起来不大,但模型对真实手写图片的鲁棒性会明显变好。

这里要特别提醒:旋转角度别设太大。数字6旋转过头就像9,0和6的区分也会变难。手写数字的方向本身就是语义的一部分,增强不是越猛越好。

5.3 错误分析:你的模型到底认错了谁

准确率99%的模型,剩下的1%错在哪里,是论文和报告里最容易做出深度的地方。用混淆矩阵统计即可:

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm = confusion_matrix(y_true, y_pred) ConfusionMatrixDisplay(cm).plot()

MNIST上最常见的混淆对是:4和9、3和8、7和2。原因很直观:手写体里3的弧线如果稍微闭合一点就像8,4和9在笔画的连接方式上容易互相“骗过”,7带个横杠、2的弯钩大小稍变就能混淆。

做错误分析时,把错例图片挑出来打印成网格图,这是报告里很有分量的一页。针对错例可以做“定向增强”:比如在数据增强时额外生成一些3和8的旋转形变样本。准确率可能只提升0.1%,但“根据错误分析改进模型”这个思路本身,比结果更让答辩老师认可。

5.4 把模型变成能演示的东西

多数课设和毕设要求交一个可运行的系统。最简单的方案是用Gradio搭Web界面,或者用tkinter做桌面窗口。核心逻辑都一样:加载训练好的模型,读入用户画的数字图片,走同一条预处理链路,输出预测结果和置信度。

def predict(model, image_path, transform): img = Image.open(image_path).convert('L').resize((28, 28)) x = transform(img).unsqueeze(0) with torch.no_grad(): probs = torch.softmax(model(x), dim=1) pred = probs.argmax(1).item() conf = probs.max(1).item() return pred, conf

最容易出坑的地方是:训练时用了ToTensor和Normalize,预测时也必须用一模一样的transform,很多人漏掉Normalize导致结果错得离谱。另一个坑是resize,手绘图实际尺寸可能是200x200,必须先统一缩放到28x28,同时转成灰度图再喂进模型,千万别直接把彩色图丢进去。

6. 交付与答辩:代码组织、实验记录和问答准备

6.1 目录结构:让老师一眼觉得你有工程素养

不需要上Docker那套,但项目目录至少要清晰。推荐这样组织:

mnist_project/ ├── data/ # 数据集(运行脚本自动下载) ├── models/ # 模型定义和训练好的权重 │ ├── simple_cnn.py │ └── best_model.pt ├── utils/ # 数据处理和可视化工具 │ ├── dataset.py │ └── visualize.py ├── train.py # 训练入口 ├── eval.py # 评估入口 ├── demo.py # 演示界面 ├── requirements.txt └── README.md

运行入口收敛到train.py和eval.py,一个文件负责单一职责。每个函数的docstring写清楚输入输出,这些细节很加分。尤其是答辩老师现场想跑你代码的时候,好的目录结构和注释能帮你减少大量解释成本。

6.2 README和实验记录:性价比最高的投入

README不需要长篇大论,写清楚四件事:如何安装环境、如何运行训练和评估、实验结果表格、目录结构说明。实验记录则单独建一个markdown文件,记录每次实验的日期、模型、超参、验证集准确率、测试集准确率:

日期模型学习率数据增强验证准确率测试准确率
03-12KNN k=3-无95.7%95.8%
03-13SVM-无98.1%98.3%
03-14CNN v11e-3无99.0%99.2%
03-15CNN v21e-3旋转+平移+缩放99.4%99.5%

这张表不只是写报告的素材,更是答辩时“你做了哪些探索”最有力的证据。每次实验一句话说明和一张结果截图,攒下来最后整理会很轻松。

6.3 答辩高频提问与准备思路

下面几个问题几乎必问,提前准备:

第一个,“为什么选这个模型?”别只答“因为它准确率高”。更好的思路是:先说MNIST是小尺寸灰度图,参数量不需要很大;再对比全连接网络参数爆炸、共享性差,CNN局部连接、参数共享、平移鲁棒,所以选CNN在结构上更匹配这个任务。

第二个,“你的模型还能怎么改进?”从数据层面说:增强更多样化,比如加入弹性变换;从模型层面说:加BatchNorm、残差连接,或尝试更深的结构;从方法论层面说:做模型集成。说出一两条并解释原理即可,别贪多。

第三个,“传统机器学习和深度学习有什么区别?”要讲透一点:传统方法依赖人工确定的特征,比如原始像素或HOG,SVM、KNN直接在特征空间找决策边界;深度学习自动从数据中学习特征的层级表示,CNN的卷积核本身就是特征提取器,不需要手工设计特征。最好用表格对比辅助说明。

第四个,“验证准确率和测试准确率为什么有差距?”这说明你理解评估原理:验证集来自训练集划分,用于调参;测试集是完全没有参与过训练和调参的独立样本,能更诚实反映泛化能力。这一点能答好,印象分会提升不少。

最后聊一个很多人忽略的经验:做这个题目,最容易踩的坑不是模型不会训练,而是“从头到尾只跑通一个模型,却讲不出为什么”。所以哪怕你的CNN准确率已经到99%,也建议回头把KNN和SVM的对比补上,把失败实验记录下来。答辩时老师很少纠结你比SOTA差多少,更在意你有没有真正理解自己在做什么。如果时间充裕,可以顺手用手机拍一张手写数字测试一下模型的泛化效果——这种“功课外的功课”,往往就是课设和毕设拉开档次的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询