☰
CNN数字识别实战:从源码跑通到准确率提升的完整指南
2026/10/7 18:25:11 网站建设 项目流程

简介:这份源码资源面向希望用深度学习做数字识别的Python开发者与初学者,提供了一套基于卷积神经网络(CNN)的完整实现方案,可解决自定义图片数据难以直接用于模型训练的问题。包内共11个文件,以py脚本、pyc编译文件、png示例图片、txt说明、md文档及tar数据包为主,压缩包约252KB,涵盖数据预处理、模型训练与识别三大环节。其中预处理脚本可将图像转为MNIST格式数据集,支持二值化与灰度图处理;训练脚本支持GPU加速;识别功能通过特征模块中的identify函数调用训练好的模型完成。README与代码注释给出了清晰的使用指引,读者按步骤运行即可跑通从数据转换到数字识别的全流程,适合作为深度学习入门练手或课程设计参考。目前已有44人学习下载。

1. 从一份 CNN 数字识别源码说起:它到底能跑出什么结果

很多人第一次接触深度学习,都是从 MNIST 手写数字识别开始的。你拿到一份「基于 Python 的 CNN 数字识别系统」源码,最关心的其实不是它用了多少层卷积,而是:这东西能不能在我机器上跑起来、识别准不准、我能不能换成自己的图片测一测。数字识别看起来简单,但它把深度学习最核心的几件事全串起来了——数据加载、卷积特征提取、池化降维、全连接分类、损失反向传播、模型保存与推理。把这套流程吃透,你再去碰更复杂的图像任务,心里就有底了。

这份源码适合两类人:一类是刚学完 Python 基础、想找一个能跑通的最小深度学习项目练手的新手;另一类是想快速验证 CNN 效果、需要一个干净基线做对比的从业者。它解决的核心问题是:给你一个从训练到推理的完整闭环,而不是只丢给你一个模型定义文件。下面我按「环境怎么搭 → 数据怎么处理 → 网络怎么搭 → 怎么训练 → 怎么推理 → 坑在哪」的顺序,把这份源码拆开讲清楚。

2. 环境搭建与依赖安装:把 Python 和深度学习框架装对

2.1 选 Python 3.8 还是更新版本

源码类项目最怕的就是环境不兼容。我一般建议用 Python 3.8 到 3.10 之间的版本,太老的版本装不上新版框架,太新的版本又可能遇到某些库还没适配。如果你机器上还没有 Python,去官网下载安装包时记得勾选「Add Python to PATH」,否则后面在命令行里敲python会提示找不到命令。装完之后用下面两条命令确认版本和 pip 是否正常:

python --version pip --version

第一条输出类似Python 3.9.13,第二条输出 pip 的版本和对应的 Python 路径。如果 pip 版本太老,先升级一下:

python -m pip install --upgrade pip

这里用python -m pip而不是直接pip,是为了确保你升级的是当前这个 Python 解释器对应的 pip,避免多环境时升错地方。

2.2 安装 PyTorch 还是 TensorFlow

这份源码标题只说了 CNN,没指定框架。常见做法是二选一:PyTorch 或 TensorFlow/Keras。我一般会先看源码里 import 的是torch还是tensorflow,然后装对应的。以 PyTorch 为例,CPU 版本安装命令如下:

pip install torch torchvision

如果你有 NVIDIA 显卡并且想用 GPU 加速,去 PyTorch 官网根据 CUDA 版本生成对应的安装命令,不要直接抄别人的。装完之后验证一下:

import torch print(torch.__version__) print(torch.cuda.is_available())

第一行打印版本号,第二行返回True或False。返回False不代表装错了,只是说明当前用的是 CPU,对小规模 MNIST 来说 CPU 完全够用。另外源码里大概率还会用到numpy、matplotlib,一并装上:

pip install numpy matplotlib

numpy负责数组运算,matplotlib用来画训练曲线和预览图片。如果你在 VS Code 里跑,记得把解释器切到刚才装好库的那个 Python 环境,否则会出现「明明装了却 import 失败」的玄学问题。

2.3 目录结构与运行入口

拿到源码压缩包后先别急着跑,花两分钟看清楚目录。典型结构是:一个train.py负责训练,一个predict.py或inference.py负责推理,一个model.py定义网络,外加一个data文件夹放 MNIST 数据。先确认入口文件是哪个,再看它 import 了哪些本地模块。如果 import 报错说找不到某个模块,多半是工作目录不对——在命令行里cd到源码根目录再运行,而不是在别的路径下用绝对路径去调。

提示:不要一上来就改代码。先原封不动跑通一次,确认环境和数据都没问题,再去动超参数。这是排查问题时最重要的基线。

3. 数据加载与预处理:MNIST 读进来之后做了什么

3.1 MNIST 数据的来源与自动下载

MNIST 包含 6 万张训练图和 1 万张测试图,每张是 28×28 的灰度手写数字。用 PyTorch 的torchvision.datasets可以自动下载,不用你手动去找数据集:

from torchvision import datasets, transforms transform = transforms.Compose([ transforms.ToTensor(), # 把 PIL 图片转成张量,并归一化到 [0,1] transforms.Normalize((0.1307,), (0.3081,)) # 按 MNIST 全局均值和标准差做标准化 ]) train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

ToTensor()做了两件事:把像素值从 0-255 缩放到 0-1,并把维度从 HWC 转成 CHW,这是 PyTorch 卷积层要求的格式。Normalize里的 0.1307 和 0.3081 是 MNIST 训练集的全局均值和标准差,用它们标准化能让训练更稳。这两个数字是固定的,不用自己算。

3.2 DataLoader 的 batch_size 和 shuffle 怎么设

数据集对象只是描述了数据,真正喂给网络要靠 DataLoader:

from torch.utils.data import DataLoader train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

batch_size=64是常见起点,显存或内存不够就降到 32 或 16,想训练快一点可以升到 128,但太大可能影响收敛。训练集shuffle=True是为了打乱顺序,防止模型学到样本排列的规律;测试集设False,因为评估时顺序无所谓,而且方便复现结果。num_workers在 Windows 上建议保持默认 0,设大了反而容易报多进程相关的错。

3.3 数据增强要不要加

MNIST 本身比较干净,一般不需要复杂增强。但如果你想提升泛化能力,可以加一点随机旋转或平移:

transform = transforms.Compose([ transforms.RandomAffine(degrees=10, translate=(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])

degrees=10表示随机旋转正负 10 度,translate=(0.1,0.1)表示水平和垂直方向各随机平移 10%。注意增强只加在训练集上,测试集保持原始变换,否则评估结果会失真。新手常犯的错就是训练和测试用了同一套带增强的 transform,导致测试准确率虚高或波动大。

4. CNN 网络结构:卷积、池化、全连接怎么搭

4.1 一个能跑通的 CNN 定义

下面是一个经典的两层卷积结构,源码里大概率长这样:

import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 32, 3, 1) # 输入1通道,输出32通道,3x3卷积核,步长1 self.conv2 = nn.Conv2d(32, 64, 3, 1) # 输入32通道,输出64通道 self.fc1 = nn.Linear(9216, 128) # 64*12*12=9216,接128维全连接 self.fc2 = nn.Linear(128, 10) # 输出10类,对应数字0-9 def forward(self, x): x = F.relu(self.conv1(x)) # 第一次卷积+激活 x = F.max_pool2d(x, 2) # 2x2最大池化,尺寸减半 x = F.relu(self.conv2(x)) # 第二次卷积+激活 x = F.max_pool2d(x, 2) # 再次池化 x = torch.flatten(x, 1) # 展平成一维,保留batch维度 x = F.relu(self.fc1(x)) # 全连接+激活 x = self.fc2(x) # 输出层,不加softmax return x

Conv2d(1, 32, 3, 1)四个参数分别是输入通道、输出通道、卷积核大小、步长。第一层输入通道是 1,因为 MNIST 是灰度图;如果是彩色图就要改成 3。fc1的输入维度 9216 是算出来的:28×28 经过两次 3×3 卷积(每次减 2)和两次 2×2 池化(每次减半),最终是 64×12×12。这个数字必须和前面的输出对上,改网络结构时最容易在这里翻车。

4.2 为什么输出层不加 softmax

很多人会问:分类任务为什么最后一层不接 softmax?因为 PyTorch 的CrossEntropyLoss内部已经包含了log_softmax,如果你在模型里再加一个 softmax,等于做了两次,会导致梯度变小、训练变慢。所以训练时模型直接输出 logits,推理时想要概率再手动加 softmax。

4.3 参数量与显存估算

这个网络参数量大概在 120 万左右,其中绝大部分集中在fc1那一层(9216×128≈118 万)。MNIST 图片小,所以全连接层反而成了参数大户。如果你想减小模型,可以把fc1的 128 降到 64,或者用全局平均池化替代展平。显存方面,batch_size=64 时 CPU 内存占用不到 1GB,普通笔记本完全能跑。

5. 训练循环与参数调优:loss 不降怎么办

5.1 标准训练循环拆解

import torch 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(), lr=1e-3) criterion = nn.CrossEntropyLoss() for epoch in range(1, 6): 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 % 100 == 0: print(f'Epoch {epoch} [{batch_idx*len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.4f}')

optimizer.zero_grad()必须放在反向传播之前,否则梯度会累加。model.train()和后面的model.eval()切换很重要,涉及 Dropout 和 BatchNorm 的行为差异。学习率lr=1e-3是 Adam 的常用值,如果 loss 震荡厉害就降到 1e-4,收敛太慢就升到 3e-3 试试。

5.2 训练多久、准确率到多少算正常

MNIST 上这个网络通常 3 到 5 个 epoch 就能到 99% 左右的测试准确率。如果第 1 个 epoch 结束 loss 还在 2.3 附近不动,说明有问题——最常见的是数据没标准化、学习率太大、或者标签和输出对不上。正常情况第 1 个 epoch 结束 loss 应该降到 0.1 以下。

5.3 评估与保存模型

model.eval() correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) pred = output.argmax(dim=1) correct += pred.eq(target).sum().item() print(f'Test Accuracy: {100. * correct / len(test_loader.dataset):.2f}%') torch.save(model.state_dict(), 'mnist_cnn.pt')

torch.no_grad()关闭梯度计算,省内存也更快。保存用state_dict()而不是整个模型对象,这样加载时只要重新实例化网络结构再load_state_dict就行,兼容性更好。

6. 推理与踩坑排查:换自己的图片为什么识别错

6.1 用保存的模型做单张推理

from PIL import Image model = Net().to(device) model.load_state_dict(torch.load('mnist_cnn.pt', map_location=device)) model.eval() img = Image.open('my_digit.png').convert('L') # 转灰度 img = transform(img).unsqueeze(0).to(device) # 加batch维度 with torch.no_grad(): output = model(img) pred = output.argmax(dim=1).item() print(f'预测数字: {pred}')

convert('L')确保是单通道灰度图,unsqueeze(0)在第 0 维加一个 batch 维度,因为网络要求输入是[N, C, H, W]。这两步漏掉任何一个都会报维度错误。

6.2 避坑清单:五个血泪教训

现象一:训练 loss 一直不降。原因通常是数据没有做 Normalize,或者学习率设得过大导致震荡。解决方法是先确认 transform 里有没有 Normalize,再把学习率降到 1e-4 观察。

现象二:测试准确率很高,但自己拍的图片全识别错。原因是 MNIST 是黑底白字、数字居中且经过尺寸归一化,而你拍的照片往往是白底黑字、数字偏大或偏小。解决方法是先把图片二值化、反色、裁剪到数字区域,再缩放到 28×28。

现象三:RuntimeError: Given groups=1, weight of size...。这是卷积层输入通道数不对,多半是你把彩色图直接喂给了第一层输入通道为 1 的网络。解决方法是推理前convert('L')转灰度。

现象四:GPU 上训练正常,CPU 上加载模型报错。原因是保存时模型在 GPU 上,加载时没有指定map_location。解决方法是torch.load(..., map_location='cpu')。

现象五:每次运行结果都不一样。这是正常的,因为权重初始化和 shuffle 都有随机性。想复现就固定随机种子:

import torch, numpy as np, random torch.manual_seed(42) np.random.seed(42) random.seed(42)

7. 把准确率再往上推一点:几个我常用的技巧

训练到 99% 之后,很多人就停了。但如果你想把这份源码改造成自己的项目,有几个技巧值得试。第一个是学习率调度:前几个 epoch 用 1e-3,后面降到 1e-4,往往能再涨 0.1 到 0.2 个百分点。用torch.optim.lr_scheduler.StepLR几行就能加上:

scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.5) # 在每个 epoch 训练结束后调用 scheduler.step()

step_size=2表示每 2 个 epoch 调整一次,gamma=0.5表示学习率乘以 0.5。

第二个技巧是模型集成。训练 3 个结构相同但随机种子不同的模型,推理时把它们的输出概率平均,取最大值对应的类别。这个方法在 MNIST 上能把准确率推到 99.5% 以上,代价是推理时间翻三倍。对于数字识别这种轻量任务,完全可接受。

第三个技巧是错误样本分析。把测试集里预测错的图片挑出来可视化,你会发现大部分错误集中在书写潦草的 4 和 9、3 和 5 之间。针对这些类别做定向增强,比如对 4 和 9 加更多旋转样本,比盲目加层数有效得多。

技巧预期收益代价
学习率调度+0.1%~0.2%几乎无
模型集成(3个)+0.3%~0.5%推理时间×3
定向数据增强+0.2%~0.4%需分析错误样本
加深网络可能不涨反降训练变慢、易过拟合

最后说个我自己的习惯:每次改完超参数,我都会把训练日志和测试准确率记在一个表格里,而不是凭记忆。有一次我调了半天以为学习率有问题,翻记录才发现是某次改网络结构时把fc1的输入维度写错了,白白折腾一晚上。这种后悔药没处买,只能靠记录。希望这份拆解能帮你少走点弯路,把这份源码真正变成自己的东西。

本文还有配套的精品资源,点击获取

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

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

立即咨询