ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于PyTorch的CNN手写数字识别实战:从LeNet-5到自定义图片预测

基于PyTorch的CNN手写数字识别实战:从LeNet-5到自定义图片预测 简介面向深度学习初学者与图像识别入门者这是一份基于CNN卷积神经网络完成手写数字识别的完整实践包。压缩包共五个文件含可直接运行的Python训练脚本、Jupyter演示、实验报告PDF、说明文档与授权文件整体仅839KB轻量便于快速下载。资源围绕MNIST数据集展开覆盖数据预处理、CNN模型搭建、训练与测试全流程既适合课堂实验参考也可作为课程设计或期末报告的对照范本。实验报告以PDF形式梳理了CNN原理、网络结构、超参数调优与评估指标代码文件则可直接运行复现识别效果。目前已有4872人学习下载是快速上手深度学习图像分类任务的实用资料。1. 为什么手写数字识别是CNN的“最小可复现样本”手写数字识别是卷积神经网络CNN入门时最容易被低估的项目。MNIST数据集只有60000张训练图、10000张测试图每张28x28灰度图结构写对后训练几轮就能到99%准确率所以很多人拿它当“调库练习”。但真正的问题不在测试集而在模型离开MNIST之后手机随手拍的、带着背景和倾斜角的数字照片准确率可能直接跌破90%。这个项目恰好给出了一条完整闭环——数据预处理、CNN结构设计、训练脚本、验证流程和实验报告既适合初学者把每个模块为什么这么写弄清楚也适合熟悉PyTorch的人快速拿到一份可复现的基线工程。下面的内容全部围绕train.py和mnist test.ipynb展开可以抄完直接跑。2. MNIST预处理与CNN结构选型从28x28到特征图2.1 数据加载、归一化与独热编码少了哪一步都会丢准确率train.py里数据加载一般直接走框架封装而不是手读二进制。常见做法是用torchvision.datasets.MNIST先把像素值从0-255归一化到0-1再做标准化到均值0.1307、标准差0.3081。这两个数字是MNIST全量像素的统计量直接写在代码里能省去重复计算。归一化不是可选项它决定梯度下降在前几轮是平缓下降还是来回震荡。然后是标签处理。数字识别是10类分类标签0-9不能当作连续回归值需要转成one-hot编码。PyTorch的CrossEntropyLoss内部已经包含softmax和one-hot逻辑所以训练脚本里标签保持整数即可但如果你自己写损失函数漏掉这一步模型输出和标签的shape会直接对不上。另一个容易忽略的点是downloadTrue只在第一次联网拉数据离线环境要手动把MNIST文件放到./data目录否则会在DataLoader阶段卡住。下面这段代码是test.ipynb里最先出现的部分对应数据加载和预处理import torch from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader torch.utils.data.DataLoader(train_set, batch_size64, shuffleTrue) test_loader torch.utils.data.DataLoader(test_set, batch_size256, shuffleFalse)ToTensor()把PIL图像转成形状为(1, 28, 28)的张量并自动除以255Normalize参数是元组因为MNIST是单通道所以只给一个均值和一个标准差。训练集batch_size64是为了让梯度估计相对稳定测试集用256是为了减少评估耗时。注意测试集shuffleFalse否则每次评估的样本顺序不同误差曲线会多一层无意义的抖动。2.2 卷积层、池化层、全连接层的参数怎么配CNN的核心是“局部连接权重共享”。一张28x28图片直接接全连接层第一层参数就是28x28x输出维度很容易爆炸卷积层用一个滑窗扫过整张图不同位置共享同一组权重参数量只取决于卷积核尺寸和通道数同时能提取边缘、笔画方向这类局部特征。这里也要顺手解释一句很多初学者会把循环神经网络的思路搬过来但手写数字是静态图像没有明显时序依赖RNN的优势发挥不出来图神经网络更不谈MNIST没有天然图结构强行建图反而损失像素邻域信息。卷积核尺寸选3x3还是5x5MNIST数字笔画较细3x3足够捕获边缘第一层用5x5也能接住更大感受野但后续层一般固定3x3。卷积核数量从32开始每经过一次池化翻倍到64或128在MNIST上超过128后收益很小只会让参数量和训练时间上升。池化层用2x2最大池化、步长2把特征图尺寸缩小一半保留最显著响应同时给后续卷积更大的感受野。这张表是LeNet-5变体的结构参数也是这个项目里最常见的配置层输出尺寸卷积核/池化参数说明输入1x28x28-灰度图Conv132x28x283x3padding1提取边缘和笔画纹理ReLU32x28x28-引入非线性MaxPool32x14x142x2stride2降采样Conv264x14x143x3padding1组合局部特征ReLU64x14x14--MaxPool64x7x72x2降采样Flatten3136-拉直为一维FC1128-全连接Dropout128p0.5随机失活FC210-输出logits注意MaxPool层的尺寸计算28x28经过2x2池化且步长2输出14x14如果不整除边缘信息会被丢弃。所以上面两个卷积层都用了padding1保证卷积不改变空间尺寸让池化可控。2.3 train.py里落地的LeNet-5变体代码基于上面的表格用PyTorch实现网络结构。train.py常见的写法是把特征提取和分类器分开方便后续替换实验import torch.nn as nn class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(128, 10), ) def forward(self, x): x self.features(x) x self.classifier(x) return xfeatures部分完成两次卷积和两次池化28x28变成7x7特征图通道数从1变成64。classifier里先Flatten成3136维再经过128维全连接层Dropout(0.5)减小全连接层的过拟合风险最后输出10个实数logits。训练时配合交叉熵损失推理时想得到概率需要额外做softmax。关键参数说明padding1让3x3卷积保持输出尺寸不变MaxPool2d(2)默认stride等于kernel_size等价于MaxPool2d(2, 2)Dropout放在全连接层前卷积层后面不需要因为池化和权重共享本身已经降低过拟合压力。实际运行中这个模型10个epoch在测试集上能到99.2%左右但如果把padding去掉输出尺寸会变成26x26、12x12最后的Flatten维度就不是64*7*7代码会直接报错——这是新手最容易踩的坑。3. 训练过程是玄学损失函数、优化器与超参数调优3.1 为什么交叉熵损失Adam在MNIST上几乎是标配分类问题最常用的损失是交叉熵。它的梯度形式比均方误差更适配softmax输出输出层的梯度正比于预测概率与one-hot标签之差预测越错梯度越大不会像MSE那样在输出接近0或1时梯度消失。这也是手写数字识别里几乎都用CrossEntropyLoss的原因。优化器方面SGD和Adam选择不是拍脑袋。SGD配合动量收敛稳定但学习率敏感到需要手动调Adam自适应调整每个参数的学习率在MNIST这种小数据集上默认lr1e-3就能在5-10个epoch内到99%。从实验结果看Adam前期收敛快SGD后期测试准确率可能更高一点如果你要写在实验报告里最好两个都跑一遍对比损失曲线。3.2 学习率、batch size、dropout的配合关系超参数之间不是孤立的。学习率过大让损失在初期震荡过小则前几个epoch性能爬升极慢batch size影响梯度的噪声程度小batch噪声大反而可能逃离局部极小值。MNIST的建议区间是batch size 64-256Adam学习率1e-3到3e-3SGD学习率0.01到0.1。dropout只对全连接层使用设0.3-0.5都能用设太大会欠拟合训练集和测试集准确率同时上不去。给出一张可直接套用的超参数表超参数推荐值偏大/偏小的影响batch size64太小训练慢太大验证集loss曲线抖动learning rate1e-3过大会发散过小前期损失下降缓慢dropout0.5过小过拟合过大会欠拟合卷积核数量32-64过多参数量大MNIST收益有限epoch10-15少于5轮欠拟合超过20轮可能过拟合如果你发现训练集准确率高于测试集1%以上先检查dropout是否真的生效再检查测试集预处理是否和训练时一致——transforms.Normalize的均值和标准差写错是最常见的原因。3.3 训练循环代码与验证曲线解读train.py里的训练循环核心逻辑如下它包含前向传播、损失计算、反向传播、权重更新以及每个epoch结束后的验证import torch.optim as optim model CNN() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) epochs 10 for epoch in range(epochs): model.train() total_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1}/{epochs}, Loss: {total_loss/len(train_set):.4f}, Acc: {100*correct/total:.2f}%)optimizer.zero_grad()必须在每个batch开始前清空梯度否则PyTorch会把上一个batch的梯度累积起来loss.backward()计算当前batch的梯度optimizer.step()用Adam的规则更新权重。验证时model.eval()切换BatchNorm和Dropout模式再配合torch.no_grad()关闭梯度图既省显存也防止验证过程意外改变参数。torch.max(outputs.data, 1)返回每行最大值的索引作为预测类别。阅读验证曲线有几个经验训练损失下降但验证准确率停在某个值不动先调低学习率损失先降后升大概率过拟合增加dropout或提前停止验证准确率比训练低超过0.5%优先加数据增强而不是加深网络。这些现象在mnist test.ipynb里画出的acc/loss曲线上一眼就能看出。4. 测试集评估与自定义手写数字识别从准确率到泛化能力4.1 只看准确率会漏掉哪些问题MNIST测试集上的准确率很能打随便一个CNN都能到99%。但99%意味着每100张有1张错误如果识别银行卡号这类连续数字错误会被放大。评估时不能只看总准确率还要看每个类别的精确率和召回率。比如数字“9”容易被识别成“4”或“7”“0”被识别成“6”这些混淆集中在笔画相似的数字对上。常见做法是保存测试集的混淆矩阵看哪些类别在非对角线位置出现高值。建议至少记录下面四个指标指标计算公式在MNIST上的说明Accuracy正确样本/全部样本总体识别能力PrecisionTP/(FPTP)预测为该类的样本中正确的比例RecallTP/(FNTP)该类被正确找出的比例F12PrecisionRecall/(PrecisionRecall)平衡精确率和召回率用sklearn.metrics.classification_report可以直接导出这些值。还有一点容易被忽略测试集评估时要在模型收敛后做而不是训练过程中顺手打一行准确率否则模型还在更新参数结果会偏低。4.2 拍照→灰度→二值化→质心居中的预处理流水线模型是在28x28黑底白字上训练的而手机拍的数字通常是白底黑字、背景有杂色、尺寸不标准。直接缩放后丢给模型准确率会明显下降。我一般这样处理先转灰度图再用阈值二值化反色成白字黑底找到所有非零像素的边界框裁剪后等比缩放到20x20最后放在28x28画布中央。这套流程能消除大部分数据分布偏移。质心居中看着简单实际影响很大。MNIST数据集在发布前已经按质心对齐如果自定义图片里的数字偏离中心卷积核提取到的特征位置会和训练分布完全不同。计算非零像素的质心把图像平移到画布中心即可这一小步比多训练10个epoch都有效。反色步骤也不能省否则模型看到的数字区域恰好和训练集相反推理效果会完全崩掉。4.3 单张预测代码及参数说明下面是一个可直接放到predict.py里的预测函数输入图片路径输出预测类别和置信度from PIL import Image import torchvision.transforms.functional as TF import torch def preprocess_image(img_path): img Image.open(img_path).convert(L) img img.point(lambda p: 255 if p 127 else 0) img Image.eval(img, lambda p: 255 - p) bbox img.getbbox() if bbox: img img.crop(bbox) img.thumbnail((20, 20), Image.Resampling.LANCZOS) canvas Image.new(L, (28, 28), 0) canvas.paste(img, ((28 - img.width) // 2, (28 - img.height) // 2)) tensor TF.to_tensor(canvas) tensor TF.normalize(tensor, (0.1307,), (0.3081,)) return tensor.unsqueeze(0) model.eval() with torch.no_grad(): tensor preprocess_image(my_digit.png) logits model(tensor) prob torch.softmax(logits, dim1) pred torch.argmax(prob, dim1).item() print(pred, prob[0][pred].item())point(lambda p: 255 if p 127 else 0)做的是硬阈值二值化大于127的像素置白否则置黑Image.eval将黑白反转成MNIST风格的白字黑底。getbbox()返回非零区域边界框裁剪掉多余背景thumbnail((20, 20))等比缩放到不超过20x20避免拉伸变形canvas.paste把缩放的数字居中放到28x28黑底画布。TF.to_tensor把PIL图像转成0-1范围的张量再用训练时的均值和标准差标准化。最后unsqueeze(0)增加batch维度因为模型接受的是(batch, 1, 28, 28)形状的输入。提示如果输入照片是白纸黑字convert(L)之后再反色是必须的如果写数字时笔迹太细可以先做一次膨胀否则二值化后笔画容易断模型会把“8”识别成“3”。5. 图像增强与推理加速把实验报告写得更可信5.1 图像增强cnn算法在MNIST上的生效边界图像增强cnn算法在这个项目里能提升泛化能力但不能滥用。常用组合是随机旋转10度以内、随机平移2像素、偶尔加一点高斯噪声增强要加在训练集测试集和自定义图片必须保持和MNIST一致的预处理。旋转角度过大时数字“6”和“9”会互相翻转准确率反而下降所以MNIST上数据增强的边界比CIFAR更窄。如果只是想提升自拍图片的识别率优先解决质心对齐问题而不是盲目堆增强。5.2 用TensorBoard记录损失、准确率与卷积核训练循环里增加torch.utils.tensorboard.SummaryWriter每隔固定步数记录交叉熵损失和准确率。训练结束后用add_graph(model, dummy_input)保存网络结构再把第一层卷积权重model.features[0].weight用add_images可视化。你会看到卷积核收敛成边缘检测器比如横向笔画、纵向笔画、倾斜方向的响应。实验报告里放这两张图比只写一句“准确率99%”可信得多。5.3 实验报告里必须留的四个记录为了让实验报告能复现至少保留四项训练集和测试集的划分方式、随机种子、超参数表学习率、batch size、dropout、epoch、每次实验的测试准确率和损失曲线。最好再附三段错误样本截图说明失败集中在哪些数字对常见原因是笔画断裂或倾斜过大。把错误样本连同预处理后的图片放在报告中后续调参时能直接看出是数据问题还是模型问题。本文还有配套的精品资源点击获取
返回列表