资讯详情

Python深度学习实践:手写数字识别从零到训练跑通

📅 2026/10/21 7:15:37 | 华诺云谱 👁 阅读
Python深度学习实践:手写数字识别从零到训练跑通
写这篇“Python 深度学习实践指南一”的时候我特意把标题里的“一”当成一个承诺来对待第一篇不搞花活只解决一个根本问题——一个手上有Python基础、但还没真正跑通过深度学习项目的人怎么从零把一个能看的模型训练出来。我见过太多这样卡住的情况看了一堆理论知道神经网络、反向传播这些概念但真打开编辑器却不知道该装什么环境、用哪个框架、数据怎么塞进去、模型Step到底怎么走。资料倒是多可要么是一堆抽象公式要么是只给你抄的完整代码缺一步“为什么这样写”。这篇指南就是冲着这个空缺去的。我会选一个覆盖面最全的经典任务——手写数字图像分类从环境准备、数据加载、网络搭建到训练循环、评估调参一步一步过每一段都会把背后的逻辑讲清楚。内容适合两类人刚学完Python语法、想往深度学习方向走的人以及有了一点机器学习基础、但没动手写过完整训练流程的人。CPU机器完全能跑不需要先买显卡跑通一个epoch也就几十秒。1. 先想清楚要跑通什么第一个项目的目标设定1.1 为什么第一个项目选图像分类而不是其他方向很多人入门深度学习的第一个纠结就是不知道该拿什么练手。有人想直接上NLP做聊天机器人有人想碰目标检测做自动驾驶结果往往是在依赖安装和数据预处理阶段就耗光了热情模型影子都没见到。我给你的建议非常直接从图像分类里最简单、最标准、数据量又最友好的那一种开始。现在深度学习几乎所有关键流程——数据加载、批量训练、梯度回传、模型保存与加载、评估指标——在这类任务上都能完整走一遍但复杂度停留在“入门友好”的档位。以手写数字识别为例它有四个让人舒服的特点数据规模合理训练集几万张单张图片极小内存压力几乎为零类别固定清晰0到9共十类直观不绕输入只有灰度单通道不需要处理彩色图像的空洞、通道对齐等问题公开的标准数据集预置在主流框架里几行代码就能拿下来不需要自己满世界找数据。这个任务虽然经典但并不是“玩具”。它是卷积神经网络、池化、全连接层这些核心组件的天然教学载体。你今天在这个项目里打通的所有函数调用习惯、训练认知后面迁移到彩色图片分类、目标检测、文本分类逻辑都是一脉相承的。1.2 环境方案的取舍框架、解释器与依赖安装动手第一步先解决跑代码的“厨房”问题。我实测下来一个对新手最不折腾的组合是Python 3.10或者3.11 PyTorch CPU环境完全够用。框架选择上现在主流的开源深度学习框架在能力上已经没有本质差距真正影响新手体验的是两点API设计是否符合直觉、社区教程是否容易理解。从实践感受看PyTorch的调试体验对新手更友好TensorFlow则多了“会话”、“图模式”等历史包袱学习成本偏高。所以我个人建议新入门的人直接选PyTorch等原理彻底吃透了再说要不要接触其他生态。环境安装有个关键教训不要用系统自带全局Python直接装深度学习库包冲突会让你怀疑人生。正确做法是建一个独立虚拟环境。我通常用命令行方式python -m venv dl_env source dl_env/bin/activate # Windows下是 dl_env\Scripts\activate pip install torch torchvision装完可以用一行代码验证是否成功import torch print(torch.__version__) print(torch.cuda.is_available()) # 输出False也没关系CPU环境足够跑本项目GPU和CPU的选择这里我想多说一句。深度学习的GPU加速就像给汽车换引擎但前提是你得先会开车。手写数字识别数据量小、网络结构轻CPU训练一个完整epoch只需要几十秒多等一点完全不影响学习节奏。等后面你做更大规模的模型服务器和GPU配置到了代码里只需要把张量挪到GPU并写个设备判断迁移成本极低。所以第一步真不用被显卡焦虑绑架。2. 数据准备喂给模型的原料处理2.1 数据集的下载与目录组织逻辑任何深度学习项目的第一步永远不是“写模型”而是“看数据”。把数据拿到手、弄明白它长什么样是所有后续工作的地基。用PyTorch内置数据集接口下载手写数字数据两行代码就能搞定。这里有一个值得注意的点它返回的是已经包装好的、带标签格式的数据对象不需要我们手工去解压文件和解析标签框架已经帮我们完成了大半工作。from torchvision import datasets, transforms train_data datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransforms.ToTensor()) test_data datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransforms.ToTensor()) print(len(train_data), len(test_data))下载完成后我强烈建议你停下来三分钟亲手看看这些数据长什么样。可以用matplotlib把几张图打印出来import matplotlib.pyplot as plt img, label train_data[0] plt.imshow(img.squeeze(), cmapgray) plt.title(flabel: {label}) plt.show()我见过很多同学跳过这一步直接开写模型最后模型准确率低第一反应是调网络结构实际上问题出在他根本不了解自己的数据——有些样本本身就很歪有些标签写得不标准。看一眼数据你会对模型的输入输出有非常直观的感受。这里再补一个工程上的认识为什么下载的数据要分“训练集”和“测试集”因为模型在训练集上表现的优化目标不代表泛化到没见过的数据上依然优秀。测试集相当于“考场”只有考场成绩才是模型真实水平的估值。而后面我们还会把训练集再分一部分出来做“验证集”作用是训练过程中实时监控模型有没有走偏避免到考场上才发现学砸了。2.2 数据预处理与批量加载Transform和DataLoader详解下载下来的原始数据是PIL图像对象值为0到255的整数。但网络训练真正需要的是浮点类型的张量并且数值范围最好控制在0到1之间。这一步就叫数据预处理在PyTorch里通过transforms管道完成。最基本的转换是ToTensor()它帮我们做了三件小事把PIL图像转成张量、把高度宽度通道的排列整理成符合模型输入要求的顺序、把像素值从0到255缩放到0到1。但如果只做这一步你会发现训练收敛速度慢、网络更容易振荡。原因在于虽然像素被缩放到0到1但0和1的分布依然偏斜我们更希望输入服从均值接近0、标准差接近1的正态分布形式。所以常规做法是在ToTensor之后再接一个Normalizetransform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])这个均值和标准差是预先统计出来的固定值。Normalize不会改变图像内容信息只是把坐标原点挪一挪、把尺度归一化让模型在数值上更容易优化。数据本身处理完后还要解决一个实际问题几万张图片不可能一次性全部塞进内存参与计算工程上采用“小批量”循环喂给模型。PyTorch里负责这个机制的核心组件是DataLoader。from torch.utils.data import DataLoader train_loader DataLoader(train_data, batch_size64, shuffleTrue) test_loader DataLoader(test_data, batch_size64, shuffleFalse) images, labels next(iter(train_loader)) print(images.shape) # 输出 torch.Size([64, 1, 28, 28]) print(labels.shape) # 输出 torch.Size([64])batch_size64意思是每轮循环取64张图作为一个批次每个批次里模型做一次参数更新。shuffleTrue的作用是每个epoch尽量打乱样本顺序避免模型学到数据原有的排列规律泛化能力会更好。这里有一个很多新手容易忽略的背景知识batch size其实是一个重要的超参数它会影响训练的稳定性和速度。batch越大梯度估计越准但对显存或内存要求更高batch越小更新越频繁噪声越大。64这个值对这类任务是经过验证的稳妥起点你不用一开始就纠结调参先跑通流程再说。2.3 数据增强给模型多找点“堂兄堂妹”如果你只想跑通第一个项目数据增强这节可以先跳过但如果你想看一眼真正的工业级实践这部分才是分水岭。数据增强的思路非常朴素手写数字样本只有几万张模型很容易把训练集背下来换一批新写法就识别不准。那我们就主动给样本做“微整形”旋转几度、平移几个像素、加一点扰动人为扩充数据规模。模型因此见到更多样式的变化泛化能力自然更稳。train_transform transforms.Compose([ transforms.RandomRotation(10), transforms.RandomAffine(0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])但注意这个增强变换只能用在训练集。测试集负责给模型“打分”必须保持原始状态否则考试题都被改过了成绩还有什么说服力。所以完整做法是训练集和测试集分别用不同的transform。我给大家一个经验判断标准入门阶段不用花太多时间堆增强策略先加最便宜的RandomRotation和translate就够了。增强策略的收益在数据量足够大时才会充分涌现小规模数据集上过度增强反而可能把模型搞“糊涂”。这个项目里我先讲清楚原理真正跑的时候用基础版transform就行。3. 模型搭建与训练循环从空壳到活体3.1 网络结构设计的原则不必从零发明很多初学者一上来就被“网络结构设计”吓住以为要灵光乍现发明个新架构。实际上最稳妥的学习路径是先用一个公开、成熟、简单的结构跑通全流程以后再谈创新。对28×28灰度图像分类一个经典的轻量卷积网络结构是这样的第一层卷积输入1通道输出8通道卷积核3×3配合激活函数第一层池化2×2最大池化把特征图尺寸减半第二层卷积8通道变16通道仍是3×3卷积核第二层池化再减半特征图变成7×7×16展平后过全连接层输出10类得分。用PyTorch实现代码可以写得很紧凑import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 8, kernel_size3, padding1) self.conv2 nn.Conv2d(8, 16, kernel_size3, padding1) self.fc nn.Linear(16 * 7 * 7, 10) def forward(self, x): x F.relu(self.conv1(x)) x F.max_pool2d(x, 2) x F.relu(self.conv2(x)) x F.max_pool2d(x, 2) x x.view(x.size(0), -1) return self.fc(x)这个结构里有一个新手最容易懵的地方为什么最后是全连接层而且输入维度是16×7×7因为经过两次池化减半后28×28变成14×14再变成7×7卷积输出通道是16展平后的总特征数量就是16×7×7784。这个数字是结构推导出来的而不是拍脑袋定的。为什么用卷积而不是直接全连接一个关键原因是参数量的巨大差异。直接用全连接层处理28×28输入光第一层就要784×10约八千个参数而卷积层通过权值共享既大幅减少参数量又能自动捕捉平移不变性的局部特征。这也是图像任务能跑更深模型的根本原因。3.2 损失函数与优化器选择梯度下降的“方向盘”模型结构定了但模型里的参数初始是随机的。它凭什么知道往哪个方向调参数靠的是两样东西损失函数和优化器。损失函数衡量“预测得有多差”。对多分类任务标准选择是交叉熵损失PyTorch里对应nn.CrossEntropyLoss()。这里有个非常容易踩坑的细节CrossEntropyLoss内部已经默认包含了Softmax计算。也就是说你的模型最后输出10个原始得分就行不需要在forward里再手写一个softmax层否则等于把概率做了两遍标准化训练会出问题。优化器则决定“怎么调参数”。傻瓜版本是随机乱调专业版本是沿着梯度的反方向走因为梯度指示了参数朝哪个方向更新能让损失下降。PyTorch里最常用的是Adam和SGDimport torch.optim as optim model SimpleCNN() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001)Adam对新手极其友好它自动调整每个参数的学习率所以即使lr设置不太合适通常也能收敛。SGD则更“朴素”适合调明白原理以后再去挑战它。我的建议很直接第一个项目用Adam把学习率设为0.001这是无数项目验证过的稳妥起点。3.3 完整训练循环的实现细节三个容易写错的地方训练循环是整个项目最核心的代码段。它看着简单但新手在这段代码里踩的坑比前面所有步骤加起来还多。先给一个完整的、能直接跑通的版本def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss 0 for images, labels in loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) return total_loss / len(loader.dataset) def evaluate(model, loader, criterion): model.eval() total_loss 0 correct 0 with torch.no_grad(): for images, labels in loader: outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() return total_loss / len(loader.dataset), correct / len(loader.dataset) num_epochs 5 for epoch in range(num_epochs): train_loss train_one_epoch(model, train_loader, criterion, optimizer) val_loss, val_acc evaluate(model, test_loader, criterion) print(fEpoch {epoch1}/{num_epochs}, train_loss: {train_loss:.4f}, val_loss: {val_loss:.4f}, val_acc: {val_acc:.4f})这段代码里有三个地方新手几乎一定会写错第一个是optimizer.zero_grad()的位置。很多人忘了写导致梯度在每个batch里不断累加模型参数更新越来越混乱。正确写法是每个batch开始前把上一batch的梯度清零。为什么必须清零因为PyTorch的梯度是“累积”的它不会自动覆盖不清零就相当于把上次的方向和这次的方向加在一起走。第二个是model.train()和model.eval()的切换。这两个方法影响的是dropout、batch normalization这类层的行为。虽然我们这个简单网络里没有这些层但养成“训练前调train、评估前调eval”的习惯非常重要否则后面换了带BN的网络你会莫名其妙发现验证结果和训练结果对不上。第三个是evaluate里的torch.no_grad()。评估阶段我们并不需要计算梯度no_grad()能关闭自动求导机制既省内存又加速。这里要注意的是outputs.argmax(dim1)就是我们预测的类别下标与真实标签做比较才得到准确率。跑完5个epoch正常情况下最后测试集准确率应该能到98%以上。如果达不到先别急着调网络回到上一节把数据处理、batch、学习率都检查一遍九成问题都在那。4. 评估、可视化与调参别只盯着损失函数4.1 用准确率和混淆矩阵一起判断模型好坏训练完后拿一个数字衡量模型表现是最直观的准确率就是“预测正确的样本数除以总样本数”。但如果你只用准确率一个指标很容易被表面数据骗过去。举个我在实际项目中遇过的例子某个二分类任务里90%的样本都是A类模型什么都不学全部猜A准确率已经90%。看起来性能爆表但B类的正确率是0。这种场景下准确率完全掩盖了灾难性的模型行为。所以更靠谱的做法是加一个混淆矩阵可视化直观看到每一类的预测情况。比如数字7的真实样本里有多少被正确识别为7又有多少被误判成了1或9。这种细颗粒度的信息能帮你判断模型到底在哪一类上“犯糊涂”。import numpy as np from sklearn.metrics import confusion_matrix all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: preds model(images).argmax(dim1) all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)看到混淆矩阵之后你可能会发现数字9经常被误判成4数字5偶尔被判成3。这类问题光靠调lr已经解决不了通常需要回到数据层面去观察是不是某些样本本身书写风格太接近。4.2 损失曲线的读法训练、验证两条线的关系在调参这件事上我强烈建议你养成记录每个epoch的损失值的习惯训练完后把曲线画出来plt.plot(train_losses, labeltrain_loss) plt.plot(val_losses, labelval_loss) plt.legend() plt.show()两条曲线的走向能透露出非常多信息。训练损失不断下降而验证损失也同步下降说明模型在学习训练损失很低但验证损失开始反弹说明模型开始“死记硬背”训练集这就是过拟合的信号。一旦看到这个苗头常见的对策是增强数据、使用dropout、简化网络结构、提前停止训练。反过来如果训练损失和验证损失一起停在比较高的位置不肯降那更多是欠拟合说明模型的表达能力或者训练策略存在问题这时可以考虑加深网络、增大学习率或减少正则化约束。曲线图还有一个容易被忽略的作用它可以帮你判断优化器的工作状态。比如训练损失出现剧烈震荡大概率是学习率设大了又比如训练损失下降非常慢可能是数据标准化没做好。损失曲线就是你调试模型的“仪表盘”会比你看散落的数值有效得多。4.3 训练异常排查速查表我把实际调试中大概率遇见的几种异常情况整理成一个速查表供大家逐个排查现象优先排查方向解释损失值输出为nan学习率过大 / 数据存在异常值梯度爆炸导致数值溢出先调低lr到0.0001以下再试训练损失几乎不下降数据未标准化 / lr过小 / 网络结构错误检查transform里是否忘了Normalize验证集准确率高但测试集低数据切分不合理 / 测试集和训练集分布不一致检查是否对测试集也做了数据增强训练损失下降但验证波动大batch size太小 / 验证集数量少适当增大batch size增加验证集样本量模型快速“记住”训练集过拟合增加数据增强、加dropout、减少epoch数量RuntimeError: shape mismatch展平后的维度与全连接输入不一致检查view前的特征图尺寸对照网络结构推导记住一个原则遇到异常先读报错信息大部分定位线索都在里面。许多人一看到报错就慌了下意识想改网络结构实际上仔细读一遍英文提示方法选项通常已经给出。5. 踩坑记录与经验补充连续训练与模型保存的工程细节5.1 随机种子为什么每次跑结果不一样同样是那份代码不同人电脑上跑出的准确率可能差零点几个百分点这是正常的。因为模型参数初始化、数据shuffle都是随机的。为了实验结果可复现最好在训练开头固定随机种子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) set_seed(42)这能让你的训练流程在相同代码下每次跑出几乎一致的结果方便排查是“代码改动”还是“随机波动”导致的效果变化。这个习惯在发论文、做工业项目对比实验时更是基本要求。5.2 模型保存与加载别只保存weights就完了训练好模型之后保存方式也有讲究。常见的两种做法是torch.save(model.state_dict(), model_weights.pth) # 以及完整保存 torch.save({ state_dict: model.state_dict(), optimizer_state: optimizer.state_dict(), epoch: epoch, }, checkpoint.pth)state_dict保存的是参数字典占用空间小、加载灵活。checkpoint则把优化器状态、当前epoch都包进去方便断点续训。第一次跑项目第一种方式就够了。加载模型时的新手陷阱是忘了先实例化模型结构直接用load_state_dict然后报一堆键不匹配错误。正确顺序是model SimpleCNN() model.load_state_dict(torch.load(model_weights.pth)) model.eval()最后那个model.eval()在这里很关键。保存后如果你要继续做推理或评估别忘了切到评估模式。否则以后模型带了dropout层推理结果会莫名其妙变差。5.3 断点续训和日志记录让训练过程可回放跑第一个项目时你可能觉得日志不重要但一旦开始调参你很快就会发现“我上次那个参数组合到底结果是多少”的记忆已经在三天后模糊了。所以从第一版就养成记录的习惯每个epoch的损失、准确率、超参数配置用CSV或者简单txt记录都行。断点续训的逻辑也不复杂就是在每个epoch结束或训练中断时保存一个checkpoint下次训练通过加载checkpoint恢复模型和优化器状态接着跑。这个技巧在你面对大模型、长训练时间时几乎是必须的CPU训练尤其建议每个epoch存一次。说点个人体会跑通第一个深度学习项目最奇妙的瞬间不是看到准确率跳上去的那一刻而是某一天你把训练速度从CPU换到GPU、把网络从几层加到几十层突然意识到之前那些“高大上”的概念原来只是因为自己亲手写过一遍就完全没有了神秘感。第一次写训练循环的时候我也犯过不调zero_grad导致损失原地打转的错误第一次看混淆矩阵时我也惊讶于那些手写连体数字能把模型骗成什么样。这些坑实在太多了但踩过之后你对框架、对深度学习的理解深度跟纯看教程完全不是一回事。如果你跑完这个指南有疑问或者卡在某个具体报错上欢迎在评论区把日志贴出来我会尽量帮忙看。下一篇我会在这个基础上往前走一步继续聊模型训练效率、学习率调度、更复杂的网络结构这些进阶话题到时候见。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑