简介:基于Python深度学习实现MNIST手写数据集识别的完整工程,以rar压缩包形式提供,适合计算机、电子信息工程、数学等专业学生作为课程设计、期末大作业或毕业设计的参考资料。压缩包共18个文件,大小约19.77MB,包含5个Python源文件、5个编译生成的pyc缓存、3个JSON配置文件、2个图像数据文件(idx3-ubyte)、2个标签数据文件(idx1-ubyte)及1个pkl数据文件,覆盖网络结构定义、层实现、激活函数、训练与评估等关键模块。已有511人学习下载。整体代码和数据集配套完整,目录结构清晰,便于快速上手深度学习图像识别流程;读者可结合源码理解卷积网络处理MNIST数据的细节,并自行调试、添加功能以适配不同需求。
1. MNIST手写数据集识别:拿它起步Python深度学习,值不值
MNIST手写数字识别在深度学习领域当了二十多年的“入门第一关”。60000张28x28灰度图,一台没有独立显卡的笔记本也能在一小时内把准确率做到97%以上,这种即时的正反馈比任何理论都管用。很多做图像方向的开发者真正动手写第一个深度学习程序,选的就是它,因为任务简单到模型不是瓶颈,难点全在数据加载、预处理和训练参数上。这篇笔记要讲的是把“基于Python深度学习实现mnist手写数据集识别”这条链路完整跑通需要知道的事——数据从哪来、网络怎么搭、参数怎么调、踩过哪些坑,以及最后怎么拿自己写的字去验证模型。适合刚装好Python还没跑通第一个深度学习项目的入门者,也适合手里缺一份快速基线源码的CV从业者。
2. MNIST数据准备:torchvision下载失败怎么救,DataLoader参数怎么设
2.1 torchvision下载MNIST卡住或404:四份gz文件的本地化加载
在深度学习项目里,数据获取往往是被低估的一步。PyTorch官方提供的torchvision.datasets.MNIST接口设计得很省事,第一次运行会在root目录下自动创建MNIST/raw文件夹,然后从国外服务器把数据拉下来。这个流程在国内网络环境下经常翻车:长时间卡住、连接被重置,甚至直接报URLError。很多初学者以为是自己代码写错了,其实只是网络不通,白白在下载阶段消耗了大把耐心。
torchvision对MNIST数据源的设计其实很直接——四份gz压缩文件放到指定目录下,download=True就不会真正触发网络请求。我一般这样操作:先手动把四份gz文件下载好放进./data/MNIST/raw目录,然后代码里照常写download=True。torchvision检查到文件已存在,会跳过下载直接解压读取。关键点有两个:路径必须是MNIST/raw两级结构,文件名必须严格对齐——train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz。文件名错一个字符都会报FileNotFoundError,四个文件缺一个也会在解压时报错。
数据就位之后的加载代码很简单,但transform里面的两步操作直接决定训练效果:
# data_prepare.py import torchvision.transforms as transforms from torchvision.datasets import MNIST transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_data = MNIST( root='./data', train=True, transform=transform, download=True # 本地文件存在时不会真正走网络 ) test_data = MNIST( root='./data', train=False, transform=transform, download=True )这段代码里transform决定样本进入网络之前被怎样处理。ToTensor()把原始像素值从0到255的无符号整数缩放到0到1的浮点数,同时把图像形状从28x28变成1x28x28,多出的通道维度是卷积层输入的基本要求。Normalize接着用MNIST全量训练图像的均值0.1307和标准差0.3081做标准化,把输入分布压到接近零均值、单位方差。别小看这两步,直接拿0到255的原始整数进网络也能训练,但loss下降会明显变慢,最后准确率也到不了同样水平。
train=False则表示加载测试集,这一万张图在训练过程中绝不能混进训练数据,否则测试准确率会虚高,因为模型已经“见过”这些样本了。还有一个容易踩的细节:训练集和测试集的transform必须保持一致,不能训练时做Normalize而测试时忘了。一旦测试输入分布偏移,精度会莫名掉两三个点,不报错也不提示,排查起来非常折磨。这类问题用替换法检查最有效——把测试集transform改成和训练集完全一样,再跑一次对比精度。
下载完成后,建议用Python脚本确认四份数据文件完整性。压缩包下载中断时文件大小异常,torchvision解压时会报CRC错误,这个报错信息经常被忽略,大家光看到格式不对就怀疑代码。
import os raw_dir = './data/MNIST/raw' for fname in ['train-images-idx3-ubyte.gz', 'train-labels-idx1-ubyte.gz', 't10k-images-idx3-ubyte.gz', 't10k-labels-idx1-ubyte.gz']: path = os.path.join(raw_dir, fname) size = os.path.getsize(path) print(f"{fname}: {size} bytes") if size < 10000: print(" 文件过小,建议删除后重新下载")2.2 验证数据加载结果:batch形状、标签范围与matplotlib可视化
数据加载完成后,在训练之前花三十秒验证一次,往往能省掉后面数小时的排查。方法是从DataLoader里取一个batch,打印形状和标签范围,再用matplotlib画几张图肉眼确认内容对不对得上。
import matplotlib.pyplot as plt sample_images, sample_labels = next(iter(train_loader)) print("batch形状:", sample_images.shape) print("标签形状:", sample_labels.shape) print("标签范围:", sample_labels.min().item(), "-", sample_labels.max().item()) # 画出前八张图确认内容与标签对应 fig, axes = plt.subplots(2, 4, figsize=(6, 3)) for i, ax in enumerate(axes.flat): ax.imshow(sample_images[i][0], cmap='gray') ax.set_title(int(sample_labels[i])) ax.axis('off') plt.show()如果输出batch形状是torch.Size([64, 1, 28, 28]),说明batch_size为64、单通道灰度、宽高28的格式都没问题;标签范围应落在0到9之间。如果形状里的通道数不是1,或者标签范围出现10以上的值,多半是transform或加载参数写错了,趁早回头检查比后面黑盒排查高效得多。画出图像这一步也不要省,MNIST自带数据集的标签基本不会错,但如果换过数据源、做过裁剪或拼接操作,顺序错位很常见。
DataLoader的参数选择有讲究。batch_size=64在MNIST上是安全起步值,梯度更新稳定且单轮耗时短;调小到16会让loss曲线更震荡,有时能逃离较差的局部极小点但多数情况只是噪声变大;调到256以上每个epoch时间缩短,收敛精度通常略低,这是小批量梯度下降的固有特性。shuffle在训练集上必须开启,否则每个epoch模型都按固定顺序接触数字类别,学到的是顺序相关的假规律;测试集保持False即可。num_workers在Windows上建议设0,Linux上设2或4,Windows下非0值经常在运行中途报BrokenPipeError,这是教程评论区出现频率最高的报错之一。pin_memory=True对GPU训练有加速,纯CPU训练不影响,常规写上即可。
from torch.utils.data import DataLoader train_loader = DataLoader( train_data, batch_size=64, shuffle=True, num_workers=0 if os.name == 'nt' else 2, pin_memory=True ) test_loader = DataLoader( test_data, batch_size=128, shuffle=False, num_workers=0 if os.name == 'nt' else 2, pin_memory=True )还有一个经常出现的误区:训练和测试共用同一个DataLoader实例。这样测试阶段shuffle=True会随机打乱测试顺序,虽然不影响准确率统计,但每次排查预测错误样本时顺序都在变,很难对照原始图定位问题。更严重的是,如果代码里误把测试数据的梯度也回传,模型等于间接“看过”测试集,测试准确率虚高得毫无意义。训练和测试两个DataLoader分开定义,各自循环里保持正确的模式,是一个成本极低但能避免多个坑的好习惯。
3. 搭建识别网络:全连接为何不够,CNN如何选型与实现
3.1 全连接网络能跑但不够好:参数量与局部特征的差距
MNIST任务简单到两层全连接网络就能拿到90%以上准确率,这容易让人误以为卷积网络是多余的。实际上,全连接网络把每个像素都当成独立特征,完全没有利用数字图像中相邻像素之间的空间结构。数字“7”就是左上到右下的斜线,数字“0”就是一圈连续弧线,这种局部结构在全连接网络里必须靠大量冗余参数强行记忆,而不是像卷积那样通过权重共享天然提取。
这里有个具体的数值对比值得记住。输入784个像素、隐藏层256个神经元的全连接层,参数量为784x256+256,约20万;而一个卷积核为3x3、输出32个通道的卷积层,参数量只有3x3x32+32,约320个。即便搭建一个完整的CNN,总参数量也就在20万级别,和一层全连接隐藏层相当,但表达能力高出一截。这也是为什么MNIST教程最终都会导向CNN——同样的参数量,更好的特征提取方式,精度上不去才奇怪。
不使用更复杂网络的原因是MNIST本身太小。ResNet、VGG这类深度网络在这个任务上不仅训练更慢,还更容易过拟合,最终测试准确率未必比LeNet风格的小网络高。在简单任务上堆模型复杂度是入门阶段常见的错路,模型跑得慢不说,调参难度也大。先把小网络吃透,比直接上大模型更有价值。
3.2 用PyTorch实现LeNet风格CNN:结构、代码与参数量分析
MNIST最经典也最稳的卷积结构从LeNet-5演化而来。LeNet-5是上世纪90年代为银行支票手写数字识别设计的,放到今天依然适合28x28小灰度图。常见做法是两层卷积加两层全连接,中间穿插池化和Dropout。代码不长,但每一层的作用值得拆开讲。
import torch.nn as nn class MNISTCNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.dropout1 = nn.Dropout(0.25) self.fc1 = nn.Linear(64 * 7 * 7, 128) self.dropout2 = nn.Dropout(0.5) self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.pool(torch.relu(self.conv1(x))) # 输出 32x14x14 x = self.pool(torch.relu(self.conv2(x))) # 输出 64x7x7 x = x.view(x.size(0), -1) # 展平成 64x3136 x = self.dropout1(x) x = torch.relu(self.fc1(x)) x = self.dropout2(x) x = self.fc2(x) return xforward里每一行都对应一个明确的形状变化。conv1输入1x28x28,padding=1让卷积不改变空间尺寸,输出32通道的28x28特征图;池化把空间尺寸减半到14x14。conv2把通道从32变到64,再池化成7x7。view把每个样本展平成64x3136的向量,之后接两个全连接层,最后一层输出10个数值,对应0到9十个类别的得分。注意fc2后面没有接softmax——这是新手最容易混淆的地方:nn.CrossEntropyLoss在PyTorch内部自带softmax操作,手动加softmax反而让数值变换做了两遍,影响数值稳定性。所以标准写法就是输出原始得分,损失函数负责归一化。
Dropout是这套结构里控制过拟合的关键。两个Dropout分别放在展平后和第一个全连接层后,训练时随机把25%和50%的神经元输出置零,迫使网络不依赖少数神经元,效果相当于同时训练了大量共享权重的子网络。MNIST训练集和测试集分布非常接近,不加Dropout训练集能到100%而测试集只有98%左右;加上Dropout之后,测试集可以稳在99%以上。注意Dropout在推理阶段自动关闭,前提是代码里调了model.eval(),否则Dropout继续随机丢弃,推理结果每次都不稳定。
激活函数选ReLU而不是sigmoid或tanh,原因是ReLU在正区间的梯度恒为1,能有效缓解深层网络的梯度消失问题。MNIST只有两层卷积,梯度消失并不严重,但ReLU计算更快、收敛更快,是当代CNN的默认选择。如果换成sigmoid,训练曲线下降会明显慢一截,最终精度也会略低,所以没有特殊理由不要改。
另外这套网络没有加BatchNorm。MNIST数据分布本身很简单,训练批次64不算小,BatchNorm带来的分布稳定化收益不明显,反而额外增加参数量和推理阶段的计算。如果换到CIFAR或真实图像数据,BatchNorm基本就是标配,但在这个入门项目里不是必选项。这个取舍背后的思路是“最简单的能用的模型”:先跑通,再逐步加组件,而不是第一次就把所有组件堆上。
4. 训练循环的完整实现:损失函数、优化器与准确率提升路线
4.1 训练主循环的代码骨架与参数说明
数据和模型就位后,训练循环是整个项目的发动机。PyTorch的训练循环范式相对固定,核心步骤五步:前向传播算损失、梯度清零、反向传播、优化器更新、记录指标。MNIST一个epoch有60000/64约938个batch,CPU上跑一个epoch大约二三十秒,整个训练几十个epoch时间不算长,循环代码不需要做复杂优化,保持清晰可读更重要。
import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = MNISTCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total = 0.0, 0, 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) # 前向 loss = criterion(outputs, labels) optimizer.zero_grad() # 梯度清零 loss.backward() # 反向传播 optimizer.step() # 参数更新 total_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) return total_loss / total, correct / totaloptimizer.zero_grad()这行单独强调再多次都不为过。漏掉它,梯度会在每个batch累积,loss曲线剧烈震荡甚至发散到nan,很多新手排查半天才发现是三行顺序里的第一行丢了。loss.backward()计算梯度,optimizer.step()用梯度更新参数,这三行的顺序和调用方式固定下来后,几乎所有PyTorch项目都能复用。model.train()这行把模型切到训练模式,Dropout开始随机丢弃神经元;如果忘了调用,模型默认停在eval模式,Dropout不生效,训练效果和预期差异很大。
这里需要提一下设备管理。images、labels、model放到同一个device上,CPU训练什么都不用做;有GPU时,model先.to('cuda'),每个batch的tensor也要.to('cuda'),否则前向计算报device mismatch。代码里的.to(device)放在循环内部是合理的,加载器返回的tensor默认在CPU上,每batch搬到GPU开销很小。如果追求极致性能,可以配合pin_memory=True让搬运更快,但MNIST上感知不明显。
4.2 评估函数、epoch循环与学习率正则化的实际影响
训练主循环之外需要一个评估函数。评估和训练不同,不需要计算梯度,也不需要更新参数,只需要把batch里的图跑一次前向,统计预测正确的数量。这里的关键是用torch.no_grad()包住整个循环,避免PyTorch为推理构建计算图,浪费内存和算力。
def evaluate(model, loader, device): model.eval() correct, total = 0, 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) return correct / total for epoch in range(15): loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) test_acc = evaluate(model, test_loader, device) print(f"epoch {epoch+1:02d} loss {loss:.4f} train_acc {train_acc:.4f} test_acc {test_acc:.4f}")epoch循环里每轮在测试集上评估一次,观察两条曲线的走势。训练准确率稳步上升而测试准确率停滞甚至回落,就是过拟合的前兆,应该考虑减少epoch数量或增强Dropout。MNIST上跑到第10到15个epoch,测试准确率通常会稳定在98%到99%区间。如果第3个epoch还不到90%,优先回去检查数据预处理而不是调网络。
学习率是MNIST训练中影响最直接的超参数。Adam用lr=0.001是经过大量实验验证的默认值,不需要改。改成0.01,前几个epoch的loss会快速下降,随后在平台期震荡,最终测试精度到不了99%;改成0.0001,loss下降很慢,15个epoch可能只到95%。直觉是学习率大了不收敛,小了收敛慢,Adam默认值在这个任务上正好踩中平衡点。下表是经验值参考:
| 学习率 | 收敛速度 | 典型测试准确率(15个epoch) | | 0.01 | 快,但震荡 | 约97% | | 0.001 | 适中,稳定 | 约99% | | 0.0001 | 慢 | 约95%-96% |
L2正则化在PyTorch里对应优化器参数weight_decay。网上很多博客写着weight_decay=1e-4是标配,于是MNIST上也照抄,结果准确率反而更低。原因是MNIST数据量够大、任务简单,模型不容易真正过拟合,weight_decay限制模型容量反而坏事。在MNIST上weight_decay保持默认0,靠Dropout和epoch控制就足够。换到Fashion-MNIST或CIFAR这类更难的数据集,weight_decay的价值才体现出来。这个反直觉现象值得记一笔:简单任务上正则化不一定带来收益,参数不是越“正规”越好。
epoch数量同样不要贪多。MNIST上跑15到20个epoch,测试准确率饱和;继续训练,训练集准确率往100%冲,测试集反而可能出现毫厘级别的回落,这就是过拟合的早期信号。最稳的做法是保存测试准确率最高的模型权重,训练结束后用最优checkpoint推理,而不是默认用最后一个epoch的结果。在训练循环里加一个if test_acc > best_acc的判断保存最优权重,这个方法在任何数据集上都适用,值得养成习惯。
5. MNIST训练避坑指南:五个常见问题与排查方法
MNIST项目代码量不大,但运行时出的问题却不少。下面五个问题是我在实际带项目时见过频率最高的,每一条都按“现象 → 原因 → 解决”的结构写,方便直接对号入座。
5.1 loss不降反升或直接变成nan
现象:训练刚开始还好,几轮迭代后loss变成nan,或者从第一个epoch起就一直停在1.2附近打转。
原因:loss变成nan最常见的原因是梯度爆炸,触发点通常是学习率过大,或者模型输出没有做数值稳定处理。loss一直不降多数是数据预处理问题,最常见的是忘了归一化,输入像素值0到255全量送进网络。另一个高频因素是优化器的zero_grad漏写,导致梯度跨batch累积,loss曲线像心电图一样上下乱跳。
解决:先把学习率降到0.0001试探,如果loss恢复下降再逐步调回。检查数据有没有经过ToTensor()和Normalize,把训练样本的像素范围打印出来确认在0到1之间。最后检查训练循环里是否调用了optimizer.zero_grad()。三个地方都排查完仍无法解决,用单个batch测试——只取一个batch做前向和后向,确认loss能正常计算再跑全量。单batch测试是定位这类问题最有效的手段,能快速把问题范围缩小到模型还是数据。
5.2 训练集99.9%但测试集只有93%
现象:训练结束时训练集准确率接近100%,测试集却只有93%左右,差值超过五个点。
原因:典型过拟合。MNIST训练集和测试集来自同一分布,正常差值应控制在1到2个点内。差到5个点以上,通常是模型容量偏大、训练时忘记开启Dropout、或者epoch跑太久,模型把训练集的噪声当成有效特征学进去了。
解决:三步依次检查。确认forward里Dropout模块存在且训练时model.train()被调用过;把epoch数量减到10观察测试集准确率是否回升;把第一层卷积的通道数从32减到16降低模型容量。在MNIST上,前两步基本就能把差值拉回2个点以内,第三步是最后手段。如果减通道数之后测试准确率明显下降,说明之前是正则化不够而非容量过大,把通道数加回去并调整Dropout概率。
5.3 训练速度极慢,一个epoch要跑十几分钟
现象:代码正常跑但一个epoch耗时极长,整个实验完全做不动。
原因:三个常见方向。一是Windows上num_workers设成了非0,子进程数据加载冲突导致效率低下;二是CPU训练时没有启用底层数学库加速;三是循环里反复做tensor的CPU/GPU搬运,造成不必要的开销。
解决:Windows上把num_workers设0,通常立刻改善。确认PyTorch安装的是官方预编译版本,官方包默认链接MKL和OpenMP,CPU矩阵运算有加速;自己从源码编译如果没配好底层库,性能会有数量级差距。把设备变量固定下来,在循环外写device,模型和tensor统一用.to(device)转移,不要每轮重复创建device对象或反复搬运。这三个点都改完后,MNIST在普通笔记本上一个epoch的时间应该在30秒以内,如果还慢,检查后台是不是有程序占满CPU。
5.4 测试集准确率99%但推理自己写的数字全错
现象:测试集准确率很高,拿自己画的数字去推理,经常识别错,甚至完全不认识。
原因:大多数是预处理不一致。训练时图像经过ToTensor和Normalize,推理时单张图片直接转numpy数组或PIL图,没有做同样的处理,模型看到的输入分布和训练时对不上。另一个常见原因是颜色方向反了:MNIST是白底黑字,自己画的是黑底白字,颜色反转后网络看到的内容完全变样。
解决:推理代码里复用训练时的transform,确保图片先转28x28灰度图,再走ToTensor和Normalize。检查图片背景和前景的颜色关系,黑底白字先做像素反转(255减像素值)变成白底黑字再送入网络。单张图转成batch时用unsqueeze(0)调整形状,从28x28变成1x1x28x28。还有一个技巧:把预处理后的图片保存下来看一眼,确认形状完整、笔画粗细合适、数字居中——预处理后的图才是模型真正看到的内容,这一步很多人跳过,导致在错误前提下调模型参数。
5.5 同一份代码每次结果不同,无法复现实验
现象:同一份代码跑两次,测试准确率一次98.5%一次99.1%,结果对不上,怀疑代码有问题。
原因:训练本身是随机过程。参数初始化、数据打乱、Dropout的神经元丢弃都带随机性。不固定随机种子时,不同运行之间的差异可能超过一个百分点。这不代表代码有bug,但会让人在调参时无法判断效果变化来自参数还是随机波动。
解决:在脚本开头固定随机种子,覆盖Python内置random、NumPy和PyTorch三处。
import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)固定种子后同一份代码在同一环境下结果一致,适合调参阶段做对比实验。但要注意,固定种子看到的单次分数不完全等于真实水平,最终评估时应该放开随机种子跑5次取平均值,用均值判断模型能力。这里还有一个细节:在GPU上如果还想进一步提升复现性,可以设置torch.backends.cudnn.deterministic = True,但这个开关会让程序变慢,MNIST这种小模型通常不需要。
6. 模型保存、加载与自测:用自己的手写数字验证模型
6.1 保存/加载的正确姿势与跨设备注意事项
训练完成后,模型权重存在内存里,进程退出就没了。保存的目的是把训练好的权重写到磁盘,下次直接加载推理,不必重新训练。PyTorch推荐做法是保存state_dict——只存参数字典,不包含模型结构,格式清晰且兼容性好。加载前先定义同一结构的模型,再load参数。
# 保存 torch.save(model.state_dict(), 'mnist_cnn.pt') # 加载 model = MNISTCNN() model.load_state_dict(torch.load('mnist_cnn.pt', map_location='cpu')) model.eval()load_state_dict要求当前模型结构和保存时的完全一致,层名对不上会报Missing key。map_location='cpu'用于把GPU上保存的权重加载到CPU场景,不指定会报设备不匹配。加载后必须调用model.eval()切到推理模式,否则Dropout仍然随机丢弃神经元,同一次推理每次输出都可能不同。
6.2 自测手写数字:白底黑字、尺寸与归一化的坑
用自己的手写图片验证模型,是MNIST项目最后也是最有成就感的一步,预处理坑也最多。一张手机拍或画图软件写的图,和MNIST的28x28灰度格式差得很远。我一般走这条通道:转灰度图 → 反转颜色 → 缩放居中到28x28 → 转tensor并复用训练时的transform。
from PIL import Image, ImageOps def preprocess_image(path): img = Image.open(path).convert('L') # 转灰度 img = ImageOps.invert(img) # 黑底白字转白底黑字 img = img.resize((28, 28), Image.Resampling.LANCZOS) transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) tensor = transform(img).unsqueeze(0) # 加batch维度 return tensorImageOps.invert只在原图是黑底白字时使用,白底黑字不需要反转。resize用LANCZOS插值比默认的最近邻插值好很多,后者会把笔画缩出明显锯齿。unsqueeze(0)把1x28x28变成1x1x28x28,模型输入要求四维BxCxHxW,漏掉这行会报维度错误。推理出错时,先保存预处理后的图片看一下,很多“模型识别不准”其实是预处理把数字弄变形了——数字不居中、笔画太细、或者被缩放到只剩几个像素,这些都不是模型能解决的。
训练和推理全部跑通后,最值得做的验证不是反复看测试集准确率,而是拿一两张自己写的数字、打印体、甚至网上找的字体做一组对照。这个习惯能帮你建立对整套流程的直觉:知道数据从原始状态到模型输入之间发生了什么,也知道模型最终看到的是什么。我自己的体会是,MNIST的价值不在于把99%变成99.2%,而在于它足够小、足够干净,逼着人把数据、模型、训练、推理这条链路上每一个环节都理解到位。希望这篇里写的参数和坑,能帮你在同样的路上少花一些冤枉时间。
本文还有配套的精品资源,点击获取