尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PyTorch MNIST手写数字识别:从环境搭建到模型训练的完整实践指南

PyTorch MNIST手写数字识别:从环境搭建到模型训练的完整实践指南 1. 项目概述为什么从MNIST开始你的PyTorch之旅如果你刚接触深度学习面对一堆陌生的库和复杂的概念感觉无从下手那从MNIST手写数字识别项目开始绝对是个明智的选择。这几乎是每个深度学习工程师和研究者都走过的“新手村”。它就像学编程时的“Hello World”看似简单却包含了构建一个完整神经网络模型所需的所有核心环节数据加载与预处理、模型定义、损失函数选择、优化器配置、训练循环、评估测试。我当年也是从这个项目入手才真正理解了张量Tensor流动和梯度下降是怎么回事。MNIST数据集本身也极具代表性。它包含了6万张训练图片和1万张测试图片每张都是28x28像素的灰度手写数字0-9。数据干净、规整没有现实世界中复杂的噪声和干扰让你可以专注于理解模型本身的工作原理而不是花费大量时间在数据清洗上。用PyTorch来实现它你能直观地感受到这个框架的优雅和灵活——动态计算图让调试变得异常方便模块化的设计让搭建网络像搭积木一样简单。接下来我会手把手带你走完整个流程不仅告诉你怎么做更会解释每一个步骤背后的“为什么”并分享一些我早期踩过的坑和总结的技巧。2. 环境搭建与核心工具链解析在写第一行代码之前一个稳定、清晰的环境是高效学习的基础。很多人卡在第一步不是因为PyTorch难而是环境没配好。2.1 Anaconda不可或缺的Python环境管理器强烈建议使用Anaconda来管理你的Python环境。深度学习项目常常依赖特定版本的库直接安装在系统Python里很容易引发版本冲突。Anaconda可以为你创建独立的虚拟环境每个项目互不干扰。安装好Anaconda后打开终端Windows用Anaconda PromptMac/Linux用终端我们创建一个专用于本项目的环境conda create -n pytorch-mnist python3.9这里指定Python 3.9因为它是一个在稳定性和库兼容性之间取得很好平衡的版本。新版本如3.11、3.12有时会遇到一些第三方库尚未适配的小问题。激活环境conda activate pytorch-mnist你会看到命令行提示符前面变成了(pytorch-mnist)这表示你已经在这个独立的环境中工作了。2.2 PyTorch安装CPU与GPU版本的选择这是关键一步。PyTorch官网pytorch.org提供了安装命令生成器。你需要根据自己电脑的配置来选择。如果你没有NVIDIA独立显卡或者显卡不支持CUDA直接选择CPU版本。这完全不影响你学习核心概念MNIST数据集很小用CPU训练也很快。pip install torch torchvision torchaudio如果你有NVIDIA显卡并想使用GPU加速你需要先确认显卡支持CUDA。然后去官网选择对应的CUDA版本比如11.8或12.1。安装命令类似pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118注意CUDA版本、PyTorch版本、显卡驱动版本三者需要兼容。如果遇到问题最稳妥的方法是去PyTorch官网查看官方推荐的版本组合。我曾因为CUDA版本和PyTorch版本不匹配折腾了大半天。安装完成后在Python交互环境中输入以下命令验证安装并检查GPU是否可用import torch print(torch.__version__) # 打印PyTorch版本 print(torch.cuda.is_available()) # 打印True则表示GPU可用如果最后一行输出True恭喜你可以享受GPU带来的数十倍训练加速了。2.3 辅助工具Jupyter Notebook还是PyCharm对于学习和演示Jupyter Notebook非常直观可以分段运行代码并即时看到结果。你可以通过pip install jupyter在刚才的环境里安装然后用jupyter notebook命令启动。但对于将来开发更复杂的项目我推荐使用PyCharm、VS Code这类集成开发环境IDE。它们提供强大的代码补全、调试和版本管理功能。将PyCharm的解释器设置到我们刚创建的pytorch-mnistconda环境就可以开始编码了。3. 数据加载与预处理模型训练的“第一公里”模型的好坏一半取决于数据。PyTorch提供了torchvision库让加载MNIST这样的标准数据集变得极其简单。3.1 使用torchvision下载与加载MNISTimport torch from torchvision import datasets, transforms # 定义数据预处理转换管道 transform transforms.Compose([ transforms.ToTensor(), # 将PIL图像或NumPy数组转换为PyTorch张量 transforms.Normalize((0.1307,), (0.3081,)) # 对张量进行标准化 ]) # 下载并加载训练集和测试集 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform)root’./data’指定数据集下载到当前目录的data文件夹下。如果已经下载过下次运行会直接读取无需重复下载。downloadTrue确保如果本地没有数据会自动从网络下载。有时会因为网络问题下载失败如果遇到可以尝试手动下载MNIST数据集四个.gz文件并放到./data/MNIST/raw/目录下。transformtransform这是关键它定义了数据加载到内存后在送入模型前需要经过哪些处理。3.2 深入理解预处理ToTensor和Normalizetransforms.ToTensor()做了两件重要的事第一把图像数据从原始的[0, 255]的整数范围转换到[0.0, 1.0]的浮点数范围除以255。第二它改变了数据的维度shape。原始图像是(H, W)即高度和宽度ToTensor会将其变为(C, H, W)即通道、高度、宽度。MNIST是灰度图所以通道数C1。这个格式正是PyTorch卷积层所期望的输入格式[batch_size, channels, height, width]。transforms.Normalize((0.1307,), (0.3081,))是标准化操作。这里的两个参数(0.1307,)和(0.3081,)分别是MNIST训练集的均值和标准差。标准化公式是input (input - mean) / std。它的目的是将数据分布调整到以0为中心、标准差为1的标准正态分布附近。这样做可以加速模型的收敛让优化器如SGD工作得更稳定。这两个值是MNIST数据集固有的统计值直接使用即可。3.3 创建数据加载器批量处理与随机打散我们很少一次性把整个数据集6万张图扔进模型。而是采用“小批量梯度下降”每次取一小批batch数据来计算损失和梯度。from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse)batch_size批大小。这是一个超参数。太小如8则训练不稳定噪声大太大如全部数据则内存可能不够且更新慢。64是一个常用的起始值。shuffleTrue仅在训练集上使用。每个训练周期epoch开始前打乱数据的顺序防止模型学习到数据顺序带来的偏见有助于提升泛化能力。shuffleFalse在测试集上我们不需要打乱按顺序评估即可。你可以遍历train_loader来看看数据的结构# 获取一个批次的数据 images, labels next(iter(train_loader)) print(images.shape) # 输出torch.Size([64, 1, 28, 28]) print(labels.shape) # 输出torch.Size([64])这里images是一个四维张量[64, 1, 28, 28]代表64张图每张图1个通道28像素高28像素宽。labels是一个一维张量包含了64个对应的真实数字标签0-9。4. 构建你的第一个神经网络模型数据准备好了接下来就是搭建模型。我们将构建一个经典的卷积神经网络CNN它比全连接网络更适合图像任务。4.1 模型定义继承nn.Module类在PyTorch中所有模型都继承自torch.nn.Module类。你需要定义两个核心方法__init__和forward。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(in_channels1, out_channels32, kernel_size3, padding1) # 第二个卷积层输入32输出64卷积核3x3 self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) # 最大池化层窗口2x2 self.pool nn.MaxPool2d(kernel_size2, stride2) # Dropout层随机丢弃50%的神经元防止过拟合 self.dropout1 nn.Dropout2d(0.25) self.dropout2 nn.Dropout(0.5) # 全连接层。经过两次池化28x28 - 14x14 - 7x7通道数为64 # 所以输入特征数是 64 * 7 * 7 self.fc1 nn.Linear(64 * 7 * 7, 128) # 第一个全连接层 self.fc2 nn.Linear(128, 10) # 输出层10个类别0-9 def forward(self, x): # 卷积 - 激活 - 池化 x self.pool(F.relu(self.conv1(x))) x self.dropout1(x) x self.pool(F.relu(self.conv2(x))) x self.dropout1(x) # 将多维特征图“展平”成一维向量以便输入全连接层 x x.view(-1, 64 * 7 * 7) x F.relu(self.fc1(x)) x self.dropout2(x) x self.fc2(x) # 输出层不需要激活函数后面会接Softmax或CrossEntropyLoss return x # 实例化模型 model Net() print(model)4.2 关键组件原理解析卷积层Conv2d可以理解为一个小型特征探测器滤波器在图像上滑动。kernel_size3表示使用3x3的滤波器。padding1表示在图像边缘填充一圈0使得输出特征图的空间尺寸高宽不变28x28。out_channels32表示使用32个不同的滤波器提取32种特征。激活函数ReLU全称Rectified Linear Unit。公式是f(x) max(0, x)。它给网络引入了非线性使得网络能够学习复杂的模式。没有它多层网络就等价于一个单层线性网络。池化层MaxPool2d用于降维减少计算量同时增加特征的平移不变性。kernel_size2, stride2表示在2x2的窗口内取最大值并且窗口移动步长为2这样高和宽都减半28-14-7。Dropout一种正则化技术。在训练时随机将一部分神经元的输出置为0“丢弃”迫使网络不过度依赖某些局部特征从而减轻过拟合。Dropout2d用于卷积层后的特征图Dropout用于全连接层后。全连接层Linear在卷积层提取出高级抽象特征后全连接层负责将这些特征组合起来完成最终的分类任务。view(-1, 64*7*7)操作将[batch_size, 64, 7, 7]的张量重塑为[batch_size, 3136]-1表示自动推导batch_size。输出层最后一个全连接层输出10个值对应10个数字类别的“得分”logits。我们通常不在这里加Softmax因为PyTorch的交叉熵损失函数nn.CrossEntropyLoss内部已经包含了Softmax计算并且数值上更稳定。4.3 模型可视化与参数统计理解模型结构后可以将其移动到GPU如果可用以加速计算device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) print(f‘Using device: {device}’)可以使用torchsummary库来可视化模型结构和参数数量pip install torchsummaryfrom torchsummary import summary summary(model, input_size(1, 28, 28)) # 输入尺寸通道高宽这会打印出每一层的输出形状和可训练参数数量帮助你确认网络结构是否符合预期。5. 配置损失函数与优化器模型定义了从输入到输出的映射函数我们需要定义如何衡量预测结果的好坏损失函数以及如何根据这个“不好”的程度来更新模型参数优化器。5.1 损失函数交叉熵损失对于多分类问题交叉熵损失Cross-Entropy Loss是标准选择。criterion nn.CrossEntropyLoss()它做了两件事首先对模型的原始输出logits应用Softmax将其转换为概率分布每个类别的概率和为1。然后计算这个预测概率分布与真实标签one-hot形式之间的交叉熵。值越小说明预测越准确。5.2 优化器Adam与SGD的选择优化器决定了参数更新的策略。这里介绍两个最常用的SGD随机梯度下降最基础但依然强大的优化器。它沿着损失函数的负梯度方向更新参数。optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9)lr学习率。这是最重要的超参数之一控制每次参数更新的步长。太大可能导致震荡不收敛太小则收敛缓慢。0.01是一个常见的起点。momentum动量。可以理解为“惯性”帮助优化器在正确的方向上加速并抑制震荡。0.9是一个常用值。Adam自适应矩估计。它会为每个参数计算自适应的学习率通常收敛更快对初始学习率不那么敏感是很多场景下的默认选择。optimizer torch.optim.Adam(model.parameters(), lr0.001)Adam的默认学习率lr0.001在大多数情况下效果不错。实操心得对于MNIST这种相对简单的任务SGD和Adam都能达到很好的效果。我个人的习惯是如果追求极致的精度在更复杂的数据集上可能会花时间精细调参SGD。而对于快速原型开发和大多数任务Adam是更省心的选择。本项目我们可以先用Adam。6. 核心训练循环与模型评估这是整个项目的引擎也是最体现PyTorch灵活性的地方。6.1 训练循环代码逐行解析def train(model, device, train_loader, optimizer, criterion, epoch): model.train() # 将模型设置为训练模式启用Dropout等 train_loss 0 correct 0 total 0 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) # 将数据移至GPU/CPU optimizer.zero_grad() # **关键** 清空上一轮计算的梯度 output model(data) # 前向传播得到预测输出 loss criterion(output, target) # 计算损失 loss.backward() # 反向传播计算损失关于各参数的梯度 optimizer.step() # 优化器更新参数根据梯度调整参数值 train_loss loss.item() # 累加损失值 _, predicted output.max(1) # 获取预测类别最大值的索引 total target.size(0) correct predicted.eq(target).sum().item() # 统计预测正确的数量 # 每处理100个batch打印一次进度 if batch_idx % 100 0: print(f‘Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}’) # 计算整个epoch的平均损失和准确率 avg_loss train_loss / len(train_loader) accuracy 100. * correct / total print(f‘\nTraining set: Average loss: {avg_loss:.4f}, Accuracy: {correct}/{total} ({accuracy:.2f}%)\n’) return avg_loss, accuracy关键步骤解读model.train()切换模型状态。这会启用训练时特有的模块如Dropout和BatchNorm本例未使用BN。optimizer.zero_grad()极易忘记但至关重要PyTorch的梯度是累加的。如果不清零下一次.backward()时梯度会与上一次的叠加导致更新错误。loss.backward()自动微分引擎开始工作从最终的损失值loss开始沿着计算图反向传播计算出图中每个可训练参数requires_gradTrue的梯度。这些梯度存储在参数的.grad属性中。optimizer.step()优化器根据参数的.grad和自身算法如Adam的动量和自适应学习率来更新所有参数的值。6.2 测试/评估函数评估时我们不需要计算梯度和更新参数。def test(model, device, test_loader, criterion): model.eval() # 将模型设置为评估模式关闭Dropout等 test_loss 0 correct 0 total 0 with torch.no_grad(): # **关键** 禁用梯度计算节省内存和计算 for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) test_loss criterion(output, target).item() # 累加损失 _, predicted output.max(1) total target.size(0) correct predicted.eq(target).sum().item() avg_loss test_loss / len(test_loader) accuracy 100. * correct / total print(f‘Test set: Average loss: {avg_loss:.4f}, Accuracy: {correct}/{total} ({accuracy:.2f}%)\n’) return avg_loss, accuracymodel.eval()切换模型状态。这会禁用Dropout和BatchNorm的随机性使用训练好的固定参数进行前向传播。with torch.no_grad():在这个上下文管理器内所有计算都不会构建计算图不保存中间变量用于反向传播可以大幅提升计算速度、减少内存占用。6.3 启动训练与保存最佳模型现在将训练和测试循环组织起来并运行多个epoch整个训练数据集完整遍历一次称为一个epoch。num_epochs 10 best_acc 0.0 train_losses, train_accs, test_losses, test_accs [], [], [], [] for epoch in range(1, num_epochs 1): train_loss, train_acc train(model, device, train_loader, optimizer, criterion, epoch) test_loss, test_acc test(model, device, test_loader, criterion) # 记录历史数据可用于绘图 train_losses.append(train_loss) train_accs.append(train_acc) test_losses.append(test_loss) test_accs.append(test_acc) # 保存测试集上准确率最高的模型 if test_acc best_acc: best_acc test_acc torch.save(model.state_dict(), ‘mnist_cnn_best.pth’) print(f‘ Saved best model with test accuracy: {best_acc:.2f}%’) print(f‘Best test accuracy: {best_acc:.2f}%’)通常训练10-15个epoch后MNIST上的准确率就能达到99%以上。model.state_dict()保存的是模型的所有参数权重和偏置不包含模型结构本身。加载时需先实例化相同的Net()再调用model.load_state_dict(torch.load(‘mnist_cnn_best.pth’))。7. 可视化与调试深入理解模型行为训练完成后只看准确率数字是不够的。可视化能帮你更直观地理解模型学到了什么以及在哪里犯了错。7.1 绘制训练曲线使用Matplotlib绘制损失和准确率随epoch的变化曲线。import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(train_losses, label‘Train Loss’) plt.plot(test_losses, label‘Test Loss’) plt.xlabel(‘Epoch’) plt.ylabel(‘Loss’) plt.legend() plt.title(‘Training and Test Loss’) plt.subplot(1, 2, 2) plt.plot(train_accs, label‘Train Acc’) plt.plot(test_accs, label‘Test Acc’) plt.xlabel(‘Epoch’) plt.ylabel(‘Accuracy (%)’) plt.legend() plt.title(‘Training and Test Accuracy’) plt.tight_layout() plt.show()理想的曲线应该是训练损失稳步下降测试损失先降后升可能出现过拟合训练准确率稳步上升测试准确率同步上升并最终趋于稳定。如果训练损失不降或测试准确率远低于训练准确率就需要调整模型或超参数了。7.2 查看错误样本分析模型在哪些样本上预测错误能提供宝贵的改进线索。model.eval() mistakes [] with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) _, pred output.max(1) # 找出预测错误的索引 wrong_idx (pred ! target).nonzero(as_tupleTrue)[0] for idx in wrong_idx[:10]: # 只看前10个错误样本 mistakes.append({ ‘image’: data[idx].cpu().squeeze(), # 移除通道维度变回(28,28) ‘true’: target[idx].cpu().item(), ‘pred’: pred[idx].cpu().item() }) if len(mistakes) 10: break # 可视化错误样本 fig, axes plt.subplots(2, 5, figsize(12, 5)) for i, ax in enumerate(axes.flat): if i len(mistakes): ax.imshow(mistakes[i][‘image’], cmap‘gray’) ax.set_title(f‘True: {mistakes[i][“true”]}, Pred: {mistakes[i][“pred”]}’) ax.axis(‘off’) plt.show()你可能会发现模型容易将“4”和“9”、“5”和“6”、“7”和“1”混淆。这些数字在书写上本身就有相似性。这可以引导你思考是否需要更复杂的数据增强如旋转、缩放来让模型更鲁棒或者网络结构是否需要调整以捕捉更细微的特征7.3 查看卷积核特征图进阶对于卷积神经网络我们可以可视化第一层卷积核学到的特征看看它关注图像的哪些部分。# 获取第一层卷积层的权重 weights model.conv1.weight.data.cpu() fig, axes plt.subplots(4, 8, figsize(12, 6)) # 我们第一层有32个卷积核 for i, ax in enumerate(axes.flat): if i 32: # 每个卷积核是[out_channels, in_channels, kH, kW]我们取第一个通道 ax.imshow(weights[i, 0], cmap‘gray’) ax.set_title(f‘Kernel {i}’) ax.axis(‘off’) plt.tight_layout() plt.show()你可能会看到一些核学习到了边缘检测器如水平、垂直、斜边一些则可能是斑点检测器。这直观地展示了CNN底层是在学习基础的视觉特征。8. 常见问题排查与性能调优指南在实际操作中你几乎一定会遇到下面这些问题。这里是我总结的排查清单和调优思路。8.1 准确率始终上不去低于95%检查数据预处理确认Normalize的均值和标准差是否正确。错误的值会导致输入数据分布异常。MNIST使用(0.1307,), (0.3081,)。检查模型模式训练时是否调用了model.train()测试时是否调用了model.eval()和torch.no_grad()Dropout在测试时若不关闭会引入随机性导致性能下降。检查优化器学习率lr是否合适尝试一个数量级的变化如从0.001调到0.01或0.0001。也可以使用学习率调度器torch.optim.lr_scheduler.StepLR每隔一定epoch衰减学习率。检查损失函数确认criterion是否是nn.CrossEntropyLoss()并且标签target是0-9的整数而不是one-hot编码。模型容量不足对于MNIST两层卷积的网络足够。但如果你的网络层数过少或通道数过小比如只有4个通道可能无法学习足够特征。可以适当增加conv1和conv2的out_channels如32-64, 64-128。过拟合严重如果训练准确率远高于测试准确率例如99.5% vs 98%说明过拟合。可以增强正则化增大Dropout比例如从0.25调到0.5、在网络中加入BatchNorm2d层、或者使用更简单的模型。8.2 训练过程损失为NaN或突然变得巨大梯度爆炸这是最常见的原因。现象是损失突然变成nan或一个非常大的数。解决方案使用梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。在loss.backward()之后optimizer.step()之前加入这行代码它会将所有参数的梯度范数限制在max_norm以内。检查学习率学习率过大是导致梯度爆炸的主因立即调小。检查数据数据中是否有异常值如非法的像素值确保数据经过ToTensor归一化到了[0,1]。损失函数或模型输出有误检查模型最后一层是否错误地添加了激活函数如Softmax。对于CrossEntropyLoss输入应该是原始的logits。8.3 GPU内存溢出CUDA out of memory减小batch_size这是最直接有效的方法。将batch_size从64降到32或16。使用更小的模型减少卷积层的通道数或全连接层的神经元数。检查是否有张量长期驻留GPU在训练循环中确保不需要的中间变量及时释放。使用del variable和torch.cuda.empty_cache()需谨慎通常不是必须。混合精度训练这是一个高级技巧使用torch.cuda.amp自动混合精度可以显著减少GPU内存占用并加速训练。但对于入门项目优先考虑前两种方法。8.4 模型保存与加载的正确姿势保存torch.save(model.state_dict(), ‘model.pth’)。只保存参数轻量且通用。加载model Net() # 必须使用完全相同的模型类定义 model.load_state_dict(torch.load(‘model.pth’)) model.to(device) model.eval() # 如果是用于推理记得切换到eval模式常见错误加载时模型结构如层名、参数形状与保存时不一致会导致错误。确保用于加载的Net类定义与保存时一模一样。走完以上所有步骤你不仅完成了一个MNIST手写数字识别项目更重要的是你掌握了使用PyTorch进行深度学习开发的完整工作流和核心思维方式。接下来你可以尝试挑战更复杂的数据集如CIFAR-10或者修改网络结构添加更多层、使用ResNet块甚至尝试不同的任务如图像生成这些都将建立在你从本项目打下的坚实基础上。记住理解每一步背后的“为什么”比单纯复制代码跑出结果重要得多。
返回列表