PyTorch实战:MNIST手写数字识别从训练到部署全流程
简介这是一份面向Python初学者与深度学习入门者的手写数字识别实战资源围绕卷积神经网络CNN识别手写数字这一经典计算机视觉任务展开帮助读者理解图像特征提取与分类的完整流程。压缩包共13个文件约6.49MB以10张jpg示例图片、1个py主程序、1个hdf5模型权重文件为主另含少量系统隐藏文件图片可用于直观查看待识别样本与预测效果py脚本承载模型构建与训练逻辑hdf5则保存已训练好的LeNet-5网络参数便于直接加载推理或继续微调。资源涉及MNIST数据集的加载与预处理、卷积层与池化层的搭建、模型编译训练评估及预测等环节并留有数据增强、正则化、Dropout、学习率调度等优化思路供深入探索。目前已有289人学习下载适合希望以最小成本跑通手写数字识别全流程、积累CNN实战经验的读者参考。1. 手写数字识别 Python 实战从 MNIST 到能跑通的推理脚本很多人第一次接触深度学习都是从 MNIST 手写数字识别开始的。它数据集小、任务清晰、CPU 也能跑看起来是个完美的入门项目。但真正动手时你会发现卡住新手的从来不是模型结构而是环境配置、数据格式、张量维度、训练不收敛这些琐碎问题。这篇笔记就围绕 handwrite_recognition_python 这条线把从环境搭建、数据加载、模型定义、训练调参到推理部署的完整路径讲清楚重点放在能复现、能排错、能改参数上。适合刚配好 Python 环境想跑第一个深度学习项目的人也适合想把手写数字识别做成一个小工具、需要知道边界在哪的开发者。下面所有代码都基于 PyTorch因为它在调试友好度和社区资料上对新手最友好。2. 环境与数据把 MNIST 手写数字识别的地基打稳2.1 Python 环境配置与 PyTorch 安装的取舍手写数字识别对算力要求极低CPU 版本足够跑通全流程但如果你后续想扩展到更复杂的模型建议直接装 CUDA 版本。常见做法是用 conda 建独立环境避免和系统 Python 冲突。下面这套命令在 Windows、Linux、macOS 上都能用只是 CUDA 版本号需要按显卡驱动调整。# 创建独立环境Python 版本选 3.9 或 3.10 兼容性最好 conda create -n mnist python3.10 -y conda activate mnist # 安装 PyTorchCPU 版本最省事先跑通再说 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 如果需要 GPU换成对应 CUDA 版本的命令例如 CUDA 11.8 # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 验证安装 python -c import torch; print(torch.__version__, torch.cuda.is_available())这段命令的逻辑是先隔离环境再装框架最后验证。参数上python3.10是经验值3.12 在某些 torchvision 版本上会有兼容问题--index-url指定官方 wheel 源比默认源快很多。验证时如果输出True说明 GPU 可用输出False也不影响 MNIST 训练只是速度慢一点。如果你用 VSCode 或 PyCharm记得把解释器切到这个 conda 环境否则会出现「明明装了却 import 失败」的玄学问题。2.2 MNIST 数据集的加载与预处理参数MNIST 包含 60000 张训练图和 10000 张测试图每张是 28x28 的灰度图。torchvision 已经内置了下载和加载接口但预处理这一步不能省。下面是最小可用的数据加载代码。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 定义预处理转张量 归一化 transform transforms.Compose([ transforms.ToTensor(), # 把 PIL 图像转成 [0,1] 的张量 transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值和标准差 ]) # 下载并加载训练集和测试集 train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 包装成 DataLoader train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers2) test_loader DataLoader(test_set, batch_size1000, shuffleFalse, num_workers2) print(len(train_set), len(test_set))逻辑说明ToTensor把像素值从 0-255 压到 0-1Normalize再减均值除标准差让输入分布接近标准正态这是训练稳定的关键。参数上batch_size64是 MNIST 上的常用值太小会导致训练抖动太大显存和内存占用上升但收益递减shuffleTrue只在训练集开测试集必须关掉否则评估结果没有可比性num_workers在 Windows 上建议设 0否则容易报多进程相关的错。归一化用的0.1307和0.3081是 MNIST 训练集的统计值直接抄即可自己重算反而容易出错。提示第一次运行会自动下载约 10MB 的数据到./data目录如果下载卡住可以手动下载后放到对应目录文件名保持MNIST/raw结构。3. 模型定义与训练一个能收敛的 CNN 长什么样3.1 卷积网络结构的选择与维度推导MNIST 用全连接网络也能到 97% 左右但 CNN 能轻松到 99% 以上而且参数量更少。下面这个结构是我反复用过、稳定收敛的版本两个卷积块加两个全连接层。import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 32, 3, padding1) # 输入1通道输出32通道3x3卷积 self.conv2 nn.Conv2d(32, 64, 3, padding1) # 32进64出 self.pool nn.MaxPool2d(2, 2) # 2x2池化尺寸减半 self.fc1 nn.Linear(64 * 7 * 7, 128) # 两次池化后 28-14-7 self.fc2 nn.Linear(128, 10) # 10个类别 self.drop nn.Dropout(0.25) def forward(self, x): x self.pool(F.relu(self.conv1(x))) # [B,32,14,14] x self.pool(F.relu(self.conv2(x))) # [B,64,7,7] x x.view(x.size(0), -1) # 展平成 [B, 64*7*7] x F.relu(self.fc1(x)) x self.drop(x) x self.fc2(x) return x model Net() print(sum(p.numel() for p in model.parameters()))逻辑说明padding1保证卷积后尺寸不变池化负责降维view把特征图拉平送进全连接。参数上64*7*7这个数字必须和前面的结构严格对应改卷积层或池化层时这里要同步改否则会报维度不匹配。Dropout(0.25)放在全连接层之间是为了抑制过拟合MNIST 上不用也能跑但用了泛化更稳。整个模型参数量在 40 万左右CPU 训练一轮几秒钟。3.2 训练循环、损失函数与优化器参数训练循环是新手最容易写错的地方核心就三件事梯度清零、反向传播、参数更新。下面是一个带测试评估的完整循环。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model Net().to(device) optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() def train_epoch(epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() # 清空上一轮梯度 output model(data) # 前向 loss criterion(output, target) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 if batch_idx % 200 0: print(fEpoch {epoch} [{batch_idx*len(data)}/{len(train_set)}] loss{loss.item():.4f}) def test(): model.eval() correct, total_loss 0, 0 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) total_loss criterion(output, target).item() pred output.argmax(dim1) correct pred.eq(target).sum().item() acc 100. * correct / len(test_set) print(fTest loss{total_loss/len(test_loader):.4f} acc{acc:.2f}%) return acc for epoch in range(1, 6): train_epoch(epoch) test()逻辑说明model.train()和model.eval()会切换 Dropout 和 BatchNorm 的行为漏写会导致评估结果偏低。zero_grad必须在backward之前否则梯度会累加。参数上Adam的lr1e-3是默认推荐值MNIST 上 5 轮就能到 99% 左右如果换成 SGD学习率要调到 0.01 并加动量。argmax(dim1)取每行最大值的索引作为预测类别eq做逐元素比较后求和得到正确数。测试时用torch.no_grad()关闭梯度计算省显存也更快。注意如果 loss 一直不降先检查数据是否归一化、标签是否对得上、学习率是否过大。这三项能解决八成不收敛问题。4. 推理与部署把训练好的模型变成能用的工具4.1 模型保存、加载与单张图片推理训练完不保存等于白跑。PyTorch 推荐保存state_dict而不是整个模型这样加载时更灵活。# 保存 torch.save(model.state_dict(), mnist_cnn.pth) # 加载 model Net().to(device) model.load_state_dict(torch.load(mnist_cnn.pth, map_locationdevice)) model.eval() # 单张图片推理 from PIL import Image def predict(image_path): img Image.open(image_path).convert(L) # 转灰度 img img.resize((28, 28)) # 统一尺寸 img transform(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): output model(img) prob torch.softmax(output, dim1) pred output.argmax(dim1).item() return pred, prob[0][pred].item() print(predict(test_digit.png))逻辑说明convert(L)保证输入是单通道resize保证尺寸一致unsqueeze(0)补上 batch 维度因为模型期望输入是[B,1,28,28]。softmax把输出转成概率方便看置信度。参数上map_location在 CPU 加载 GPU 训练的模型时必填否则会报设备不匹配。实际使用中手写图片往往有背景噪声建议先做二值化和居中裁剪否则准确率会明显下降。4.2 用 Gradio 快速搭一个手写识别界面如果想让别人也能用Gradio 是最省事的方案几行代码就能出一个网页界面。import gradio as gr import numpy as np def recognize(sketch): if sketch is None: return 请先写一个数字 img Image.fromarray(sketch).convert(L).resize((28, 28)) img transform(img).unsqueeze(0).to(device) with torch.no_grad(): output model(img) pred output.argmax(dim1).item() prob torch.softmax(output, dim1)[0][pred].item() return f预测结果{pred}置信度{prob:.2%} demo gr.Interface(fnrecognize, inputsgr.Sketchpad(), outputstext, title手写数字识别) demo.launch()逻辑说明Sketchpad返回的是 numpy 数组需要转成 PIL 图像再走和训练一致的预处理。参数上resize((28,28))必须和训练时一致否则分布对不上。这个界面适合演示和自测但不适合高并发场景生产环境还是建议封装成 API 服务。5. 避坑与排查手写数字识别最常见的 5 个翻车现场5.1 现象训练 loss 正常下降但测试准确率始终在 10% 左右原因通常是标签和输出维度对不上或者评估时忘了model.eval()导致 Dropout 仍在随机丢弃。解决方法是先打印一个 batch 的target看取值范围确认是 0-9再检查评估函数里有没有model.eval()和torch.no_grad()。如果用的是自己写的 Dataset重点检查__getitem__返回的标签是不是从 0 开始。5.2 现象报错RuntimeError: Given groups1, weight of size [32,1,3,3], expected input[64,3,28,28] to have 1 channels这是输入通道数不匹配。MNIST 是灰度图通道数为 1但如果你用Image.open没加convert(L)或者用了彩色图就会变成 3 通道。解决办法是在预处理里强制转灰度或者把模型第一层改成nn.Conv2d(3, 32, 3, padding1)。更稳妥的做法是在 Dataset 里就统一转灰度。5.3 现象训练速度极慢CPU 占用很高但 GPU 利用率接近 0常见原因是num_workers设得太大导致进程调度开销或者数据没放到 GPU 上。先检查data.to(device)和target.to(device)是否都写了再尝试把num_workers降到 0 或 2。Windows 上多进程 DataLoader 容易出问题直接设 0 最稳。另外如果 batch_size 太小GPU 也跑不满可以适当加大到 128 或 256。5.4 现象自己手写的数字识别总是错但测试集准确率很高这是典型的分布偏移。MNIST 的图片是居中、二值化、黑底白字的而你用画板写的数字往往有抗锯齿、灰边、位置偏移。解决办法是在推理前做和训练一致的预处理转灰度、二值化、裁剪到数字边界、缩放到 28x28、再归一化。如果还不行可以用 MNIST 的训练集做数据增强加入旋转、平移、缩放让模型更鲁棒。5.5 现象保存模型后再加载预测结果全乱多半是保存和加载的结构不一致。torch.save(model.state_dict())只存参数加载时必须先实例化同样的网络结构再load_state_dict。如果改了网络层名或层数就会报 key 不匹配。解决方法是把模型定义单独放在一个文件里训练和推理都 import 同一个类。另外map_location在跨设备加载时一定要写。6. 进阶技巧把 MNIST 准确率推到 99.5% 以上的几个实操手段想让模型从 99% 再往上走靠的不是换更深的网络而是把训练细节做扎实。第一个手段是学习率调度用StepLR或CosineAnnealingLR在训练后期降低学习率让模型收敛到更平坦的极小值。第二个手段是数据增强RandomRotation(10)和RandomAffine(0, translate(0.1,0.1))能显著提升对手写风格变化的鲁棒性。第三个手段是集成训练 3 到 5 个不同初始化的模型推理时对 softmax 输出取平均准确率通常能再涨 0.2 到 0.3 个百分点。from torch.optim.lr_scheduler import StepLR from torchvision import transforms # 带增强的预处理 train_transform transforms.Compose([ transforms.RandomRotation(10), transforms.RandomAffine(0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 学习率调度 scheduler StepLR(optimizer, step_size2, gamma0.5) for epoch in range(1, 11): train_epoch(epoch) test() scheduler.step() # 每 2 轮学习率乘 0.5参数上step_size2表示每两轮调一次gamma0.5表示学习率减半这两个值在 MNIST 上比较温和不会导致训练崩溃。数据增强只加在训练集测试集保持原样否则评估结果不可比。集成的时候注意每个模型要用不同的随机种子否则多样性不够提升有限。还有一个容易被忽略的点是权重初始化。默认初始化在 MNIST 上够用但如果你把网络加深用 Kaiming 初始化能让训练更稳。另外把输入像素从 0-1 再减均值除标准差这一步很多人图省事跳过结果就是收敛慢、准确率低一截。我自己的习惯是任何图像任务先把归一化做对再谈模型结构。这个项目跑通之后建议你试着把同样的流程套到 FashionMNIST 或 EMNIST 上看看哪些参数需要改、哪些坑会重复出现。希望帮到你。本文还有配套的精品资源点击获取