简介这是一份面向 Python 学习者和深度学习初学者的完整大作业项目源码围绕基于卷积神经网络的手写数字识别任务展开覆盖数据加载、模型搭建、训练调参、效果可视化与结果分析等完整流程。代码采用模块化组织关键逻辑清晰尤其适合需要完成课程设计、期末大作业或入门 CNN 图像分类的读者参考。资源包为 zip 压缩格式共 15 个文件大小约 555KB包含 8 个 Python 脚本、3 张训练过程图片、1 个配置文件、1 个参数文件及辅助说明文件其中 Python 脚本涵盖网络结构定义、数据集预处理、模型训练、损失与准确率绘图、参数保存等环节方便读者直接运行并替换数据集扩展应用。目前已有 618 人学习下载。项目经严格调试评审分数达到 95 分以上具备较高的完成度和参考价值可直接用于复现实验、理解卷积神经网络核心原理也可作为后续优化和扩展的基础框架。1. 用 CNN 把 MNIST 跑进 99%这套手写数字识别源码到底解决什么问题期末大作业截止前三天最容易翻车的不是模型选型而是「代码在别人机器上能跑、到你手里全是红字」。这篇要聊的基于卷积神经网络手写数字识别项目源码95 分以上大作业本质是一个以 MNIST 手写数字识别为载体的完整 CNN 工程包有数据加载、有网络定义、有训练循环、有评估脚本还有可视化输出。它解决的不只是「识别准确率多少」这一个指标问题而是「从零到交一份能讲清楚、能复现、能答辩的深度学习项目」的整套流程。适合正在做 Python 课程设计、深度学习导论大作业或者第一次用 PyTorch 做图像分类任务的初学者。我按自己带项目时习惯的路径帮你拆开看先跑通再调优最后交付中间穿插参数选择和踩坑记录。2. 先跑通再谈优化从 zip 解压到第一个 device 判定的最小环境拿到这套 python 的基于卷积神经网络手写数字识别项目源码第一步不要在 IDE 里双击打开某个.py就开始读代码而是先把目录结构理顺。大作业源码的常见交付形态是main.py训练入口、model.py网络结构、train.py或utils.py数据加载与工具函数外加一个README.md。我第一次拿到类似项目时吃过亏直接双击main.py却发现没有装 PyTorch装完又缺 torchvision环境折腾两小时还没到训练。2.1 解压后的目录结构和三份关键文件先把 zip 解压到一个纯英文路径下比如D:\mnist_cnn_project不要放在带中文或空格的目录里否则后面torchvision.datasets下载数据时偶尔会因路径编码报错。解压后我一般会按文件职责快速分类重点找三样东西model.py或models/存放 CNN 定义。确认它用了几个卷积层、几个全连接层、有没有 Dropout。这决定你答辩时能讲多少深度。train.py或main.py训练循环、学习率、batch size、epoch 都在这里改。predict.py或evaluate.py加载训练好的权重对单张图片或测试集做预测。用编辑器打开model.py先看网络结构定义是否完整。大作业常见的 95 分以上项目网络不会是随便抄的 LeNet-5多少会加 BatchNorm 或 Dropout也可能在数据增强上做了文章。如果打开发现模型类里只有__init__没有forward这就是个坑——PyTorch 里模型前向传播全靠forward定义缺了它你调用model(x)会直接报错。2.2 用 PyTorch 跑通训练主循环的完整命令环境装好后先不要跑完整训练先用一个最小的脚本验证数据和模型能不能对上。我用的是 PyTorch 生态torch 2.x 配合 torchvision这是目前写 CNN 大作业最主流的组合。最小验证脚本长这样import torch import torchvision import torchvision.transforms as transforms # 只做 ToTensor先不引入 Normalize方便排查尺寸问题 transform transforms.Compose([ transforms.ToTensor(), ]) trainset torchvision.datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) trainloader torch.utils.data.DataLoader( trainset, batch_size64, shuffleTrue, num_workers2 ) # 这里直接 import 你解压出来的 model.py 里的模型类 from model import CNNNet model CNNNet() # 取一个 batch 做前向传播看形状是否吻合 images, labels next(iter(trainloader)) output model(images) print(输入尺寸:, images.shape) print(输出尺寸:, output.shape) print(标签范围:, labels.min().item(), labels.max().item())这段代码的作用有两点一是验证数据下载和解压是否正常。root./data表示数据集存在项目目录下的 data 文件夹第一次运行会自动从网络下载后续离线也能用。二是验证模型输出维度。MNIST 图片是 28×28 单通道经过卷积、池化、全连接后输出维度必须等于(batch_size, 10)。10 是数字 09 的类别数。如果输出维度是(batch_size, 84)之类说明全连接层的输入维度算错了后面训练必挂。运行方式在命令行里进到项目根目录执行python check_env.py这里我习惯把上面脚本命名为check_env.py而不是直接改main.py原因是不污染原始代码。如果打印出的output.shape是torch.Size([64, 10])说明模型结构没问题可以接着走训练。2.3 训练还是 CPU碰运气不如看这几行诊断代码很多同学拿到源码一运行就发现 CPU 训练慢到怀疑人生。先别急着换机器在训练脚本里设备判定通常长这样device torch.device(cuda if torch.cuda.is_available() else cpu) print(当前设备:, device)如果输出cpu而你确认自己电脑有 NVIDIA 显卡那大概率是 PyTorch 装成了 CPU 版本。解决方式不是重装整个 Anaconda而是用 pip 重装对应 CUDA 版本的 torch。注意 PyTorch 官方安装命令会因 CUDA 版本不同而变化我一般先运行nvidia-smi看驱动支持的 CUDA 版本再去官网选对应安装命令。但 CPU 也不是不能跑 MNIST 大作业。这个数据集很小6 万张训练图、28×28 单通道一个三层 CNN 在普通笔记本 CPU 上用 510 个 epoch 跑 1015 分钟是正常节奏。如果源码默认设置epochs50CPU 可能要 1 小时这时可以先调小到 5 个 epoch 验证流程最后再决定要不要全量训练。另一个思路是减少训练集样本数比如只取前 10000 张跑通但要小心后面说的过拟合问题。3. 网络设计不是越深越好99% 准确率的 CNN 到底该有几层手写数字识别为什么是 CNN 入门首选因为它足够简单简单到你可以用很浅的网络跑出很高的准确率。MNIST 的特点是图像尺寸小28×28、类别结构简单白色数字、黑色背景、边缘和形状信息明确。这种场景下盲目堆深度不是最优解。我见过有人为了追求高分把网络堆到 10 个卷积层结果训练集准确率 99.8%测试集反而只有 97%——典型的过拟合 训练不稳定。3.1 基线结构LeNet-5 变体为什么适合 MNISTLeNet-5 是 1998 年 Yann LeCun 为手写数字识别的经典结构到今天依然是 MNIST 任务的基线标杆。它的核心思想是「卷积提特征 全连接做分类」卷积层保留空间局部特征池化层降低分辨率全连接层把特征映射到类别概率。大作业里 95 分以上的项目网络通常不会照抄原始 LeNet-5而是做小改动比如把tanh激活换成ReLU收敛更快梯度消失风险更小。在卷积层后加BatchNorm2d稳定训练允许用稍大的学习率。最后一个卷积层后加Dropout缓解过拟合。输入尺寸从 28×28 开始经过第一层 3×3 卷积后尺寸变成 28×28padding1 保持分辨率池化后 14×14第二层卷积继续 3×3 padding1池化后 7×7。两次池化后一个 28×28 的图像变成 7×7 的特征图对 MNIST 来说信息量依然足够。3.2 把卷积核数量、池化策略和数据维度算清楚很多人栽在「全连接层输入维度算错」上。卷积核数量可以随便定但全连接层的in_features必须和展平后的特征图维度严格一致差一个数训练都跑不起来。以我常用的结构为例输入: (1, 28, 28) Conv1: 3x3, 32 个卷积核, padding1 - 输出 (32, 28, 28) ReLU MaxPool 2x2 - 输出 (32, 14, 14) Conv2: 3x3, 64 个卷积核, padding1 - 输出 (64, 14, 14) ReLU MaxPool 2x2 - 输出 (64, 7, 7) Flatten - 维度 64 * 7 * 7 3136 Linear(3136, 128) - ReLU - Dropout(0.5) Linear(128, 10)这里的关键参数有两个卷积核步长默认是 1padding 设 1 时卷积不会改变 H 和 WMaxPool 的 kernel2 stride2会把 H 和 W 各减半。28 → 14 → 7两次池化后变成 7×7全连接输入就是64×7×73136。如果你用了 3 次池化比如在第二个卷积层后还想再池化一次那么输出维度会变成 3×3 或 4×4取决于原始尺寸全连接输入维度也要跟着改成64×4×41024。3.3 在 nn.Sequential 里搭出可答辩的模型结构实际项目里模型定义会在model.py里用nn.Sequential串起来比较清晰。我自己答辩时会更倾向于用类方式定义因为可以在forward里自由插入Flatten操作逻辑更直观import torch.nn as nn class CNNNet(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(32), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(64), nn.MaxPool2d(kernel_size2, stride2), ) self.classifier nn.Sequential( nn.Dropout(p0.5), nn.Linear(64 * 7 * 7, 128), nn.ReLU(inplaceTrue), nn.Linear(128, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) # 展平等价于 Flatten x self.classifier(x) return x我在inplaceTrue上吃过一次亏有版本的 PyTorch 对inplaceTrue和某些算子组合会报梯度错误。如果训练时出现「one of the variables needed for gradient computation has been modified by an inplace operation」优先把inplaceTrue全部去掉再试。另外一个建议是view而不是reshapeview是零拷贝视图reshape可能触发数据拷贝虽然对小项目性能差异不明显但答辩时被问到「为什么用 view」能顺带讲清 PyTorch 内存布局这是加分项。4. 把训练调参变成可复现流程损失函数、优化器与数据增强的取舍网络结构定好后训练参数直接决定分数高低。MNIST 是十分类任务损失函数没有悬念用交叉熵nn.CrossEntropyLoss。优化器和学习率的组合则有很多讲头。大作业项目里训练参数我一般分成两类一类是「动一次就要重新验证」的关键参数学习率、batch size另一类是「基本不用动」的默认项权重初始化、损失函数。下面重点说前者。4.1 三个必调参数lr、batch_size 和 epochs 的合理区间学习率是深度学习中调节奏的旋钮。基于卷积神经网络手写数字识别的项目里lr0.001配 Adam 是主流起步配置。SGD 的话lr0.01比较常见但要注意动量momentum0.9)。如果训练脚本里写的学习率是 0.1 甚至 0.5而优化器是 Adamloss 大概率在前几个 step 就变成 NaN这是新手最容易踩的坑。batch_size 影响两个层面显存/内存占用和梯度稳定性。MNIST 单张图片只有 28×28显存压力不大batch_size64或128都合适。更大的 batch比如 512会让梯度方向更平滑但可能收敛到平坦的局部极小值准确率上限反而不如小 batch。我做实验对比过batch_size64lr0.001 Adam在 MNIST 上 10 个 epoch 就能到 99% 左右调到batch_size256后收敛稍快但最终准确率低了 0.2 到 0.3 个百分点。学术上有争论但工程上我认小 batch 的泛化优势。epochs 的合理区间要结合验证集来看。MNIST 任务在 5 个 epoch 时测试集准确率往往已经到 98%10 个 epoch 接近 99%20 个 epoch 后提升趋缓。如果训练脚本默认跑 50 个 epoch你不需要原样照跑先在train.py里找epochs变量改成 10记录最终准确率再决定要不要加量。多跑出来的 40 个 epoch 如果不能稳定提升 0.5 个点纯属浪费电。4.2 随机梯度下降还是 AdamMNIST 上我这样选答辩时老师大概率会问「为什么选这个优化器」。我一般这么回答Adam 有自适应学习率对学习率的初始选择不敏感收敛快适合小数据集快速出结果SGD Momentum 收敛稳定、泛化性略好但对学习率和 momentum 的配合更敏感需要调参经验。大作业场景选哪个都不算错但你得能说出自己的理由。如果你之前跑过一组对比比如「Adam 在 8 个 epoch 达到 99.2%SGD 需要 15 个 epoch 才到 99.0%」这就在答辩里成了数据支撑。注意如果你的数据集不是原始 MNIST 而是加了噪声或做了旋转增强的数据Adam 的优势会更明显因为增强后的数据分布变化更大自适应学习率更能跟上梯度起伏。4.3 数据增强RandomRotation 和 Normalize 的边界大作业想上 95 分单靠网络结构和调参不够数据增强是拉高分数最「便宜」的手段。MNIST 本身是干净数据你手写的数字往往歪歪扭扭、粗细不均、位置偏移。用原始 MNIST 训练出来的模型遇到自己手写的图片准确率会掉到 80% 以下这在第 5 章会重点讲。常见增强策略按收益排序RandomRotation(degrees10)轻微旋转让模型对角度偏移更鲁棒代价是训练变慢。RandomAffine(translate(0.1, 0.1))轻微平移模拟数字不在正中心的情况。ColorJitter和RandomErasing对 MNIST 意义不大灰度图没有丰富色彩信息。Normalize((0.1307,), (0.3081,))MNIST 的官方均值和标准差必须放在最后一步。注意一个边界数据增强不是越多越好。训练集使用增强后验证集和测试集仅做ToTensor()和Normalize()绝不能做旋转或平移。知乎上有个常见提问「为什么我的训练集准确率 99.8% 验证集只有 88%」一半的原因是把增强算子同时用在了验证集上验证集被刻意改坏模型评分的参照系失衡了。# 训练集增强 train_transform transforms.Compose([ transforms.RandomRotation(degrees8), transforms.RandomAffine(degrees0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)), ]) # 验证/测试集只做标准化 test_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)), ])RandomAffine里我特意把degrees0因为旋转已经由RandomRotation做了两个算子叠加会导致过度扭曲。translate的取值范围是相对图像尺寸的百分比(0.1, 0.1)表示水平和垂直方向最多偏移 10%。28×28 的图上移动 2.8 个像素视觉上很小但对模型泛化帮助明显。5. 手写数字识别的 5 个常见翻车现场与排查清单这个项目我帮人排查过许多次也自己重写过几版踩坑规律出奇一致。以下 5 条按出现频率排序你可以照着逐条核对。5.1 训练集 99% 验证集 60%过拟合的分水岭在哪现象训练 loss 掉到 0.02训练集准确率 99.8%但每次跑到验证集就只有 60% 多而且验证 loss 先降后升。原因这是教科书级过拟合。常见触发条件有三个模型容量过大全连接层堆到 1024 节点还加了两个隐层、训练集没有增强、Dropout 被去掉。三者凑一起模型直接背诵训练集。解决先加Dropout(p0.5)插在全连接层之间。如果你发现项目源码里没有 Dropout手动加上成本很低。再加数据增强用上一节那组 transform。这两步做完过拟合基本能压下来。如果还不够把全连接层第一个 Linear 的输出从 128 降到 64减少模型容量。5.2 损失下降到 0.01 但预测结果全是同一个数字现象训练 loss 很漂亮地下降但拿测试集算准确率只有 10% 左右打印预测标签发现全是 3 或者全是 5。原因这个翻车有几种可能性最常见的是分类问题被误建成了回归问题。检查损失函数如果写成了nn.MSELoss逻辑上模型输出会被迫逼近 label 的数值预测自然乱套。另一个原因是模型最后一层没有做log_softmax而损失函数还是CrossEntropyLoss——这不会报错因为 PyTorch 的CrossEntropyLoss内部自带 LogSoftmax但如果脚本里手动加了nn.LogSoftmax再丢进CrossEntropyLoss相当于做了两次 softmax数值上不会完全错但梯度信号会变弱。解决确认损失函数是nn.CrossEntropyLoss确认模型最后一层裸输出 logits不加 softmax 激活。预测时用于展示概率分布的话在torch.argmax(output, dim1)之前对output做torch.softmax但训练阶段不要动。5.3 自己手写的数字识别不对MNIST 测试集却表现完美现象MNIST 测试集准确率 99.2%但用画图工具写个数字 7模型预测成 2而且试几次都错。原因这是域偏移domain shift。MNIST 是 28×28 二值化、居中的手写数字。你自己写的图可能尺寸几百像素、抗锯齿灰度过渡、笔画位置偏上或偏下。模型没见过这种分布准确率暴跌是物理规律不是 bug。解决写个preprocess_my_image函数把任意图片统一处理成 MNIST 风格from PIL import Image import torchvision.transforms.functional as F def preprocess_my_image(img_path): img Image.open(img_path).convert(L) # 转灰度 img img.resize((28, 28), Image.Resampling.BILINEAR) # 反色MNIST 是白字黑底如果读入的图是黑字白底要反转 import numpy as np arr np.array(img) if arr.mean() 127: # 背景偏白 arr 255 - arr img Image.fromarray(arr) img F.to_tensor(img) img F.normalize(img, (0.1307,), (0.3081,)) return img.unsqueeze(0)这段代码的逻辑很直白统一尺寸、统一颜色方向、统一数值分布。如果处理后识别还是不对去数据增强里把旋转角度从 8 调到 15因为手写的数字歪斜程度通常比 MNIST 训练样本更夸张。这是我验证过最有效的修复方向比换网络结构管用。5.4 换机器跑不起来相对路径和随机种子的锅现象在自己电脑上训练好的模型把整个项目文件夹拷到另一台机器训练能跑但结果对不上或者加载权重时报错。原因大概率是两件事。第一项目代码里用了绝对路径比如C:\Users\你的名字\...换机器路径不存在。第二随机种子没有固定每次运行数据加载和权重初始化的随机顺序都不一样。解决在训练脚本最开头固定随机种子这一行代码是大作业项目的加分细节import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True set_seed(42)cudnn.deterministic True会让卷积计算在 GPU 上也严格可复现代价是可能降低一些计算性能。对 MNIST 这种小任务来说损失可忽略。至于路径问题把所有C:/...改成./data这种相对路径或者用os.path.join(os.path.dirname(os.path.abspath(__file__)), data)来定位换机器就不会再炸。5.5 准确率卡在 97% 上不去先看最后一层别急着加网络现象训练 20 个 epoch验证准确率一直在 97% 附近抖动涨不到 99%。原因97% 到 99% 之间不是网络深度不够往往是输入数据细节丢了。我遇到的最大概率原因是Normalize参数不对。很多人把Compose([ToTensor(), Normalize((0.5,), (0.5,))])当成万能公式但 MNIST 的均值和标准差是(0.1307,)和(0.3081,)用 0.5 会改变数据分布虽然不会让训练失败但会拖累收敛上限。解决先改Normalize参数。如果改完还是 97%再检查DataLoader的shuffleTrue是否设置。最后检查学习率调度器——有的项目会在训到一半把学习率降得太狠模型卡在局部极小值。去掉调度器用固定学习率跑 15 个 epoch 做个对比实验往往就有答案。6. 交付一份能拿高分的大作业验证脚本与答辩演示的落地技巧代码能跑出 99% 只是第一步。95 分以上大作业的评判逻辑通常是「功能完整 实验充分 表达清晰」。最后这一章讲怎么把训练好的模型变成交付物。6.1 可视化三件套loss 曲线、准确率曲线、样本预测图训练结束后把指标画成图这是答辩 PPT 里最好用的素材。主流做法是记录每个 epoch 的 loss 和准确率最后用matplotlib画在同一张图上。import matplotlib.pyplot as plt def plot_metrics(train_losses, val_losses, val_accs, epochs): plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.plot(range(1, epochs 1), train_losses, labelTrain Loss) plt.plot(range(1, epochs 1), val_losses, labelVal Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Loss Curve) plt.subplot(1, 2, 2) plt.plot(range(1, epochs 1), val_accs, labelVal Accuracy, colorgreen) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.title(Accuracy Curve) plt.tight_layout() plt.savefig(train_metrics.png, dpi200)注意dpi200保证输出图片在 PPT 上放大不糊。另外我习惯在画准确率曲线时只画验证集准确率不画训练集准确率因为两者接近 100% 时曲线容易纠缠不清答辩时反而显得不专业。样本预测图是另一个展示重点从测试集里随机抽 16 张图把模型预测和真实标签并排显示预测错误的用红色标注。这段代码不长但视觉效果很有冲击力能直接证明模型在「没见过」的数据上表现如何。6.2 混淆矩阵和错误样本分析答辩现场的无声加分项混淆矩阵是深度学习大作业里最容易拉开分差的部分。它能展示模型在哪些数字上混淆比如 4 和 9 容易分不清、3 和 5 偶尔混。写一个用sklearn.metrics.confusion_matrix生成矩阵再用seaborn.heatmap可视化的脚本10 分钟做完。答辩时主动说「我的模型在数字 4 上误判率最高因为 4 和 9 的上半部分结构相似」这句话能直接证明你是真的做过实验而不是交了一版抄来的代码。除了混淆矩阵我还建议导出一张「错误样本九宫格」从预测错误的测试图片里挑 9 张每张图片标题写成「真值: 4 / 预测: 9」。这个图放到项目 README 里比任何文字描述都有说服力。这里有个实用技巧MNIST 测试集本身就包含大量容易混淆的样本即使 99% 准确率6000 张里也有 60 张错误足够你挑出典型样例。6.3 模型保存与加载的规范做法大作业最后一步是把训练好的模型权重保存下来写成独立的推理脚本。这里我吃过亏训练时直接在torch.save(model.state_dict(), mnist_cnn.pth)推理时用model.load_state_dict加载没问题但如果你把整个模型torch.save(model, model.pth)换 PyTorch 版本后加载大概率报错。规范做法是只保存权重加载时先用同一个模型类构建实例再加载权重# 保存 torch.save(model.state_dict(), mnist_cnn.pth) # 加载 model CNNNet() model.load_state_dict(torch.load(mnist_cnn.pth, map_locationcpu)) model.eval()map_locationcpu这句很关键。它在没有 GPU 的机器上加载权重时把 CUDA 张量映射到 CPU避免报RuntimeError: Attempting to deserialize object on a CUDA device。之后用这个模型做推理记得调用model.eval()它会关掉 Dropout 和 BatchNorm 的训练行为让预测结果确定。还有一个很多人忽略的细节torch.load默认会把权重张量放在 CPU 上如果你想在 GPU 上推理可以用map_locationcuda:0。但对 MNIST 这个小模型来说CPU 推理速度已经足够快单张图片毫秒级出结果所以交付给老师时统一map_locationcpu最省事。我自己的习惯是交作业前把README.md里写清楚环境版本Python 3.x、PyTorch 2.x、torchvision、运行步骤先跑train.py再跑predict.py、预期结果测试集准确率多少然后把整个目录重新解压到一台干净机器上从零跑一遍。这一步能筛掉 80% 的「代码在老师电脑上跑不了」的意外。特别是环境版本版本不同可能导致torchvision的 API 行为差异写明白就是给老师也是给自己留条后路。这个习惯我保留到现在每次做完项目都会在交付前一晚全流程复跑一遍然后安心去睡觉。希望这篇拆解能让你少踩几个我当年踩过的坑把这个项目真正变成自己手里讲得清楚的作品。本文还有配套的精品资源点击获取