Python CNN手写数字识别实战:源码、数据集与报告全链路
简介这份资源是面向计算机相关专业学生与项目实战学习者的CNN卷积神经网络手写数字识别完整项目包可作为课程设计、期末大作业或毕业设计参考。内容包含可运行的Python源码、手写数字数据集以及配套实验报告文档覆盖模型训练、界面登录、图片识别等核心模块代码经导师指导并评审通过初学者也能按说明顺利跑通。压缩包共24个文件约31.45MB以py源码、docx实验报告、png效果图、md说明文档及数据集压缩包为主另含依赖清单与项目结构说明便于快速搭建环境并理解工程组织。目前已有89人学习关注。读者可获得从数据准备、模型搭建到训练评估的完整实现思路以及需求分析、系统设计、测试用例等文档模板既能直接用于作业提交也适合作为深度学习入门练手项目帮助理清CNN图像分类的落地流程与常见问题排查方向。1. 从一份能跑通的 CNN 手写数字识别源码说起很多人第一次接触深度学习都是从 MNIST 手写数字识别开始的。标题里这套「python 实现的 CNN 卷积神经网络手写数字识别项目源码数据集报告文档」本质上就是一条从零到一的完整链路Python 环境、卷积神经网络结构、MNIST 数据集、训练脚本、评估报告。它解决的不是「识别数字」这个玩具问题而是让你亲手把一张 28×28 的灰度图经过卷积、池化、全连接变成一个 0 到 9 的概率分布并且能复现、能调参、能写报告。这套东西适合谁适合刚学完 python 基础、想找一个能跑通又不至于劝退的深度学习入门项目的人也适合要交课程设计、需要源码加文档一起交付的学生还适合想快速验证自己环境配置对不对的工程师。我见过太多人卡在 python 安装、vscode python 环境配置、数据集下载这些前置环节上模型代码反而没写几行。所以这篇不聊虚的从环境到训练到排错把这条链路拆开讲清楚让你拿到源码能跑跑完知道每一步在干什么。2. 环境与数据把 python、依赖和 MNIST 一次装对2.1 python 安装与虚拟环境别在系统环境里裸奔python 安装教程网上一搜一大把但真正坑人的是版本和包管理。我一般建议用 python 3.8 到 3.10太新的版本某些深度学习库轮子还没跟上太老的又缺特性。装完 python 后第一件事不是 pip install而是建虚拟环境把项目依赖和系统环境隔离否则后面 torch 和 tensorflow 版本打架你会怀疑人生。# 创建虚拟环境目录名用 venv 是惯例 python -m venv venv # 激活虚拟环境Windows 用 venv\Scripts\activate source venv/bin/activate # 升级 pip避免装包时出现奇怪的解析错误 python -m pip install --upgrade pip # 安装核心依赖版本按你实际能装上的来 pip install torch torchvision numpy matplotlib逻辑说明venv把解释器和第三方库锁在项目目录里删掉即清理。torchvision自带 MNIST 下载和预处理工具省得自己写解析 IDX 文件的代码。参数上torch和torchvision版本要匹配比如 torch 2.x 配 torchvision 0.15 以上装之前可以去官网看对应关系。如果你用 conda把python -m venv换成conda create -n mnist python3.9即可后面激活命令换成conda activate mnist。提示vscode python 环境配置时按 CtrlShiftP 选「Python: Select Interpreter」指向 venv 里的 python否则终端和编辑器用的解释器不一致会出现「命令行能跑、编辑器报错」的玄学问题。2.2 MNIST 数据集下载、校验和预处理MNIST 是 60000 张训练图加 10000 张测试图每张 28×28 灰度标签 0 到 9。torchvision 的datasets.MNIST会自动下载但国内网络经常卡在下载那一步所以我会手动把四个压缩包放到./data/MNIST/raw/下再让代码去读。import torch from torchvision import datasets, transforms # 定义预处理转成张量并做归一化 transform transforms.Compose([ transforms.ToTensor(), # 把 PIL 图转成 [0,1] 的张量 transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值和标准差 ]) # 训练集downloadFalse 表示用本地已下好的文件 train_set datasets.MNIST(root./data, trainTrue, downloadFalse, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadFalse, transformtransform) # DataLoader 负责打乱和分批 train_loader torch.utils.data.DataLoader(train_set, batch_size64, shuffleTrue) test_loader torch.utils.data.DataLoader(test_set, batch_size1000, shuffleFalse)逻辑说明ToTensor把像素从 0-255 压到 0-1Normalize再减均值除标准差让输入分布接近标准正态收敛更快。参数(0.1307,)和(0.3081,)是 MNIST 训练集统计出来的固定值不要随便改。batch_size64是显存和梯度稳定性的折中显存小就降到 32想更快收敛可以试 128。shuffleTrue只在训练集开测试集必须关否则评估结果没有可比性。注意如果downloadFalse报「Dataset not found」说明文件没放对位置。四个文件名是train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz缺一不可。3. CNN 结构卷积、池化、全连接到底怎么搭3.1 卷积神经网络原理在 MNIST 上的最小映射卷积神经网络原理说起来复杂落到 MNIST 上其实就三件事卷积层提取局部特征池化层降维并保留主要信息全连接层做分类。一张 28×28 的图经过第一层卷积变成多个特征图每个特征图上的一个点对应原图一个小区域的响应。池化把 2×2 区域取最大尺寸减半参数量骤降。最后把特征图拉平接全连接输出 10 个类别的分数。常见做法是两层卷积加两层全连接。第一层卷积用 32 个 3×3 卷积核第二层用 64 个中间各接一个 2×2 最大池化。这样结构不深CPU 也能跑适合入门。有人问 cnn 和 rnn 的区别这里简单说CNN 擅长空间局部模式RNN 擅长序列时序手写数字是图像用 CNN 是正解。3.2 用 PyTorch 定义网络并跑通一次前向import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 第一层卷积输入 1 通道输出 32 通道卷积核 3x3 self.conv1 nn.Conv2d(1, 32, 3, 1) # 第二层卷积输入 32输出 64卷积核 3x3 self.conv2 nn.Conv2d(32, 64, 3, 1) # 随机丢弃防止过拟合 self.dropout1 nn.Dropout(0.25) self.dropout2 nn.Dropout(0.5) # 全连接经过两次池化后特征图是 5x564 通道 self.fc1 nn.Linear(64 * 5 * 5, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.conv1(x) # [B,1,28,28] - [B,32,26,26] x F.relu(x) x F.max_pool2d(x, 2) # - [B,32,13,13] x self.conv2(x) # - [B,64,11,11] x F.relu(x) x F.max_pool2d(x, 2) # - [B,64,5,5] x self.dropout1(x) x torch.flatten(x, 1) # 拉平成 [B, 64*5*5] x self.fc1(x) x F.relu(x) x self.dropout2(x) x self.fc2(x) return F.log_softmax(x, dim1) # 输出对数概率 model Net() print(model)逻辑说明Conv2d(1, 32, 3, 1)里四个参数分别是输入通道、输出通道、卷积核大小、步长。步长 1 且不加 padding 时28×28 卷完变 26×26池化后 13×13再卷变 11×11池化后 5×5所以全连接输入是 64×5×5。log_softmax配合后面的NLLLoss使用比先 softmax 再取 log 数值更稳。Dropout(0.25)和(0.5)是经验值训练集小或者过拟合明显时调大欠拟合时调小或去掉。提示如果你把卷积核改成 5×5或者加了 padding全连接的输入维度会变必须重新算否则运行时报维度不匹配。这是新手最常见的翻车点之一。3.3 训练循环损失、优化器和每一轮该看什么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) def train(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 F.nll_loss(output, target) # 负对数似然损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 if batch_idx % 100 0: print(fTrain Epoch: {epoch} [{batch_idx*len(data)}/{len(train_loader.dataset)}] f\tLoss: {loss.item():.6f}) for epoch in range(1, 6): train(epoch)逻辑说明optimizer.zero_grad()必须放在反向传播前否则梯度会累加。Adam学习率1e-3是常用起点收敛慢就降到1e-4震荡就再降。nll_loss对应模型输出的log_softmax两者是固定搭配。训练 5 轮在 MNIST 上通常能到 99% 左右测试准确率CPU 大概几分钟一轮GPU 几十秒。如果 loss 不降先检查数据有没有归一化、标签有没有对齐、学习率是不是太大。4. 评估、保存与报告让结果可复现、可交付4.1 测试集评估与混淆矩阵训练完不看测试集等于白跑。评估时要切到model.eval()并关掉梯度计算否则 dropout 和 batchnorm 行为不一致结果会偏低。def test(): model.eval() test_loss 0 correct 0 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) test_loss F.nll_loss(output, target, reductionsum).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader.dataset) acc 100. * correct / len(test_loader.dataset) print(f\nTest set: Average loss: {test_loss:.4f}, fAccuracy: {correct}/{len(test_loader.dataset)} ({acc:.2f}%)\n) test()逻辑说明reductionsum把整个测试集的 loss 加起来再除以样本数得到平均 loss。argmax(dim1)取概率最大的类别作为预测。torch.no_grad()省显存也提速。如果准确率明显低于训练集说明过拟合可以加 dropout、加数据增强或者减少全连接层神经元。报告文档里通常要放准确率曲线、损失曲线和几张错分样本图这些用 matplotlib 几行就能画。4.2 模型保存与加载别只存 state_dict 就完事# 保存只存参数体积小推荐 torch.save(model.state_dict(), mnist_cnn.pt) # 加载先建同结构模型再载入参数 model Net().to(device) model.load_state_dict(torch.load(mnist_cnn.pt, map_locationdevice)) model.eval()逻辑说明state_dict是字典只含权重和偏置不包含网络结构所以加载前必须先用Net()建好骨架。map_location在 CPU 上加载 GPU 训练的模型时必加否则报设备不匹配。如果你要交付源码加数据集加报告建议把训练好的权重、训练日志、评估脚本一起打包别人拿到后不用重训就能验证结果。注意报告文档里写准确率时要注明是第几轮、用什么优化器、batch size 多少。我见过有人把训练集准确率当测试集写答辩时被一问就露馅。5. 避坑与排查那些让新手卡半天的真实问题5.1 现象loss 一直是 2.3 左右不降原因模型输出没接log_softmax或者损失函数用错比如用CrossEntropyLoss又手动 softmax 了。解决确认nll_loss配log_softmax或者改用CrossEntropyLoss并去掉模型里的log_softmax二选一。5.2 现象报错「size mismatch for fc1.weight」原因卷积层或池化层改动后全连接输入维度没跟着改。解决在forward里flatten前打印x.shape把实际维度填进nn.Linear的第一个参数。5.3 现象测试准确率只有 10% 左右原因标签和输出没对齐或者数据没归一化或者shuffle开在了测试集上导致评估错乱。解决检查target是不是 0-9 的整数检查Normalize参数测试集shuffleFalse。5.4 现象GPU 显存够但训练极慢原因数据没to(device)模型在 GPU 数据在 CPU每步都在拷贝。解决确认data, target data.to(device), target.to(device)写在循环里且模型也.to(device)。5.5 现象vscode 里 import torch 报红但终端能跑原因编辑器选的解释器不是 venv 里的那个。解决CtrlShiftP 重新选解释器指向venv/bin/python或venv\Scripts\python.exe。6. 进阶技巧把 99% 再往上推一点入门跑通 99% 不难但想稳定到 99.5% 以上得动点真格。我一般会做三件事数据增强、学习率调度、模型集成。数据增强用transforms.RandomAffine做小角度旋转和平移模拟手写时的自然抖动注意 MNIST 是白字黑底旋转角度别超过 10 度否则数字容易糊。from torch.optim.lr_scheduler import StepLR # 每 2 个 epoch 学习率乘 0.5 scheduler StepLR(optimizer, step_size2, gamma0.5) for epoch in range(1, 11): train(epoch) test() scheduler.step() # 更新学习率逻辑说明StepLR在训练后期降低学习率让模型在局部最小值附近更精细地收敛。step_size2表示每两轮调一次gamma0.5是缩放系数。配合数据增强测试准确率通常能再涨 0.2 到 0.5 个百分点。如果想再稳一点可以训练 3 个不同初始化的模型预测时对 log 概率取平均这就是最简单的集成代价是训练时间翻三倍。验证方法上除了看整体准确率我习惯把错分样本单独存下来看。MNIST 里错得最多的往往是 4 和 9、3 和 5、7 和 1 这种形近字。把这些图放大看能判断是模型容量不够还是数据本身模糊。如果错分图人眼都难认那就不是模型的问题报告里如实写即可。最后说个习惯每次改完超参我都会把命令、参数、结果记在一个experiment.md里包括学习率、batch size、准确率、训练时长。这个习惯救过我很多次因为深度学习调参有时候就是玄学没有记录三天后你根本想不起来哪组参数跑出过最好结果。这套源码加数据集加报告的结构真正值钱的地方不是那几百行代码而是你能沿着它把「环境、数据、模型、评估、调参」整条链路走一遍后面换数据集、换任务套路是通的。希望帮到你。本文还有配套的精品资源点击获取