ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

半小时从零手写PyTorch神经网络:MNIST手写数字识别实战

半小时从零手写PyTorch神经网络:MNIST手写数字识别实战 很多同学想入门深度学习但面对PyTorch、神经网络这些概念总觉得门槛太高被复杂的数学公式和抽象的网络结构劝退。其实从零开始手写一个能跑通的模型是理解这一切的最佳路径。本文将带你用半小时从零开始手写一个完整的神经网络并用PyTorch框架将其实现和训练。我们会从最基础的概念讲起逐行拆解代码确保你能理解每一行在做什么最终跑通一个识别手写数字的模型。无论你是编程新手还是有一定基础想转AI的开发者这篇实战指南都能让你获得“从无到有”的成就感。1. 背景与核心概念为什么是PyTorch和神经网络在开始敲代码之前我们需要先统一“语言”理解几个核心概念这能让你后面的学习事半功倍。1.1 神经网络是什么你可以把神经网络想象成一个非常复杂的“函数拟合器”。它的核心任务是学习从输入数据比如一张图片的像素到输出结果比如图片中的数字是“5”之间的映射关系。通俗理解假设你教一个小孩认猫。你给他看很多猫的图片输入并告诉他“这是猫”输出。经过多次学习小孩大脑里形成了一个判断“猫”的模型。神经网络的学习过程与此类似通过大量“图片-标签”数据自动调整内部参数最终学会识别。专业定义神经网络是一种受人脑神经元连接方式启发的计算模型由大量相互连接的节点神经元组成。每个连接都有权重学习过程就是通过算法如反向传播不断调整这些权重以最小化预测输出和真实输出之间的误差。1.2 PyTorch 是什么为什么选它PyTorch 是一个开源的深度学习框架由 Facebook现 Meta的 AI 研究团队主导开发。在 2024 年它和 TensorFlow 依然是业界最主流的两大框架。核心优势动态计算图。这是 PyTorch 早期“出圈”并深受研究人员喜爱的主要原因。它的计算图是动态构建的这意味着你可以在程序运行时随意改变图的形状和大小就像写普通的 Python 代码一样灵活。这对于调试和研究新模型结构非常友好。Pythonic 的设计PyTorch 的 API 设计非常贴近 Python 和 NumPy 的使用习惯学习曲线相对平缓。如果你熟悉 Python上手会很快。强大的生态拥有 TorchVision计算机视觉、TorchText自然语言处理、TorchAudio音频处理等官方库以及海量的社区模型和教程。对于初学者而言PyTorch 的直观性和易调试性让它成为入门深度学习的绝佳选择。1.3 我们即将构建的模型全连接神经网络FNN我们将构建一个最经典、结构最清晰的神经网络——全连接神经网络也叫多层感知机。结构它由输入层、若干隐藏层和输出层组成相邻层之间的每个神经元都两两相连。任务我们使用经典的MNIST 手写数字数据集。该数据集包含 70,000 张 28x28 像素的灰度手写数字图片0-9。我们的任务就是训练一个模型输入一张 28x28 的图片输出它最可能是哪个数字0-9之间的一个。为什么从这个开始它结构简单避免了卷积、循环等复杂操作能让我们聚焦于神经网络最核心的前向传播、损失计算、反向传播、参数更新这一完整流程。2. 环境准备与版本说明“工欲善其事必先利其器”。让我们先把环境搭建好。2.1 基础环境操作系统Windows 10/11, macOS, 或 Linux (如 Ubuntu) 均可。本文命令以 Windows/macOS 的通用命令为例。编程语言Python 3.8 或以上版本。这是 PyTorch 官方支持的主流版本。包管理工具强烈推荐使用Anaconda或Miniconda来创建独立的 Python 环境避免包冲突。2.2 创建并激活 Conda 环境打开你的终端Windows 用 Anaconda Prompt 或 PowerShellmacOS/Linux 用 Terminal。# 创建一个名为 pytorch_tutorial 的新环境并指定 Python 版本为 3.9 conda create -n pytorch_tutorial python3.9 # 激活这个环境 conda activate pytorch_tutorial激活后你的命令行提示符前面通常会显示(pytorch_tutorial)表示你已进入该环境。2.3 安装 PyTorch访问 PyTorch 官网 首页会有一个安装命令生成器。你需要根据你的系统、包管理工具、Python版本以及是否有CUDAGPU加速来选择合适的命令。无GPU仅CPU选择 CUDA 为 “None”。这是最简单的方式适合所有电脑但训练速度较慢。有NVIDIA GPU请先确认你的显卡支持 CUDA并安装对应版本的 NVIDIA 驱动和 CUDA Toolkit。然后在官网选择匹配的 CUDA 版本如 11.8, 12.1。本文以CPU 版本为例安装命令如下安装时请以官网最新命令为准# 使用 pip 安装 PyTorch (CPU版本) 及 torchvision用于数据集和图像变换 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu安装完成后可以验证一下# 在 Python 交互环境或新建一个 test.py 文件中运行 import torch print(torch.__version__) # 输出 PyTorch 版本如 2.2.0 print(torch.cuda.is_available()) # 输出 False (CPU版本) 或 True (GPU版本)2.4 项目结构创建一个项目文件夹例如pytorch_first_model内部结构建议如下pytorch_first_model/ ├── model.py # 我们的神经网络模型定义 ├── train.py # 训练脚本 ├── utils.py # 工具函数如可视化 └── README.md # 项目说明我们将按照这个结构来编写代码。3. 核心原理拆解前向传播与反向传播在写代码前必须理解神经网络工作的两个核心过程。3.1 前向传播前向传播就是数据从输入层流向输出层的过程。对于一层全连接层其计算可以表示为输出 激活函数(输入 × 权重 偏置)线性变换z xW b。输入数据x乘以权重矩阵W再加上偏置向量b。激活函数a σ(z)。对线性变换的结果z应用一个非线性函数σ如 ReLU, Sigmoid。这是神经网络能够学习非线性关系的关键。没有它多层网络将退化为单层线性模型。3.2 损失函数模型预测值ŷ和真实标签y之间有多大的差距损失函数就是用来量化这个差距的。对于我们的多分类任务最常用的是交叉熵损失。3.3 反向传播与梯度下降这是神经网络学习的“引擎”。计算梯度通过链式求导法则从损失函数开始反向逐层计算损失相对于每一个权重W和偏置b的梯度导数。梯度指明了参数调整的方向和幅度。参数更新使用优化器如 SGD, Adam沿着梯度的反方向因为要最小化损失微调所有参数。更新公式SGD为例W W - 学习率 × 梯度PyTorch 的 Autograd自动求导系统帮我们自动完成了反向传播中复杂的梯度计算我们只需要关注前向传播和损失计算。4. 手写代码逐行构建我们的神经网络现在我们开始把理论变成代码。请跟随步骤在对应的文件中编写。4.1 定义神经网络模型 (model.py)我们创建一个具有两个隐藏层的全连接网络。# model.py import torch import torch.nn as nn import torch.nn.functional as F class SimpleNN(nn.Module): # 必须继承 nn.Module 一个简单的全连接神经网络用于MNIST手写数字识别。 网络结构784(输入) - 128 - 64 - 10(输出) def __init__(self): super(SimpleNN, self).__init__() # 调用父类初始化 # 定义网络层 # nn.Linear(in_features, out_features) 定义一个全连接层 self.fc1 nn.Linear(28 * 28, 128) # 输入层到第一隐藏层 self.fc2 nn.Linear(128, 64) # 第一隐藏层到第二隐藏层 self.fc3 nn.Linear(64, 10) # 第二隐藏层到输出层 # 注意我们通常不在__init__中定义激活函数而是在forward中调用F.relu等 def forward(self, x): 定义前向传播过程。 x: 输入张量形状为 (batch_size, 1, 28, 28) 或 (batch_size, 28*28) # 步骤1: 将图片数据展平成一维向量 (batch_size, 28*28) # view() 方法用于改变张量形状-1表示自动推断该维度大小 x x.view(-1, 28 * 28) # 步骤2: 第一层线性变换 - ReLU激活 x self.fc1(x) # 线性变换: (batch_size, 128) x F.relu(x) # ReLU激活: 将负值置为0保持正值不变 # 步骤3: 第二层线性变换 - ReLU激活 x self.fc2(x) # (batch_size, 64) x F.relu(x) # 步骤4: 输出层线性变换 (不需要激活函数因为后面会用CrossEntropyLoss) # CrossEntropyLoss内部已经包含了Softmax操作 x self.fc3(x) # (batch_size, 10) return x # 可以写一个简单的测试确保模型能跑通 if __name__ __main__: # 实例化模型 model SimpleNN() print(model) # 创建一个随机输入 (模拟一个批次有4张图片) dummy_input torch.randn(4, 1, 28, 28) output model(dummy_input) print(f输入形状: {dummy_input.shape}) print(f输出形状: {output.shape}) # 应该是 (4, 10)逐行解释nn.Module: PyTorch中所有神经网络的基类。nn.Linear: 实现y xA^T b的线性层。forward: 你必须重写这个方法它定义了数据如何流过网络。PyTorch会在调用model(x)时自动执行forward。x.view(-1, 28*28): 将每张28x28的图片拉平成784维的向量。-1代表自动计算该维度通常是批次大小batch_size。F.relu: PyTorch函数式接口中的ReLU激活函数。4.2 编写训练脚本 (train.py)这是整个流程的核心包含了数据加载、训练循环和验证。# train.py import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader from model import SimpleNN # 导入我们定义的模型 import matplotlib.pyplot as plt def train(): # 超参数设置 batch_size 64 learning_rate 0.01 epochs 10 # 1. 数据准备 # 定义数据预处理转换将PIL图像转换为Tensor并归一化到[0,1]范围 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) # MNIST数据集的均值和标准差 ]) # 下载并加载训练集和测试集 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 创建数据加载器自动分批次、打乱数据 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse) # 2. 初始化模型、损失函数、优化器 device torch.device(cuda if torch.cuda.is_available() else cpu) print(f使用设备: {device}) model SimpleNN().to(device) # 将模型移动到设备CPU/GPU criterion nn.CrossEntropyLoss() # 交叉熵损失函数适用于多分类 optimizer optim.SGD(model.parameters(), lrlearning_rate) # 随机梯度下降优化器 # 用于记录训练过程 train_losses [] train_accuracies [] # 3. 训练循环 for epoch in range(epochs): model.train() # 将模型设置为训练模式影响Dropout、BatchNorm等层 running_loss 0.0 correct 0 total 0 # 遍历训练数据加载器中的每一个批次 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) # 清零梯度这是非常重要的一步否则梯度会累加 optimizer.zero_grad() # 前向传播 output model(data) # 调用模型的forward方法 loss criterion(output, target) # 反向传播 loss.backward() # Autograd自动计算所有参数的梯度 # 参数更新 optimizer.step() # 根据梯度更新参数 # 统计信息 running_loss loss.item() _, predicted torch.max(output.data, 1) # 获取预测类别最大值的索引 total target.size(0) correct (predicted target).sum().item() # 每处理100个批次打印一次进度 if batch_idx % 100 99: print(fEpoch [{epoch1}/{epochs}], Step [{batch_idx1}/{len(train_loader)}], Loss: {loss.item():.4f}) # 计算本轮epoch的平均损失和准确率 epoch_loss running_loss / len(train_loader) epoch_acc 100. * correct / total train_losses.append(epoch_loss) train_accuracies.append(epoch_acc) print(fEpoch [{epoch1}/{epochs}] 训练结束 - 平均损失: {epoch_loss:.4f}, 准确率: {epoch_acc:.2f}%) # 4. 在测试集上评估模型 model.eval() # 将模型设置为评估模式 test_correct 0 test_total 0 # 在评估时不计算梯度以节省内存和计算 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) _, predicted torch.max(output.data, 1) test_total target.size(0) test_correct (predicted target).sum().item() test_accuracy 100. * test_correct / test_total print(f\n 最终测试结果 ) print(f测试集准确率: {test_accuracy:.2f}%) # 5. 保存训练好的模型 torch.save(model.state_dict(), mnist_simple_nn.pth) print(模型已保存为 mnist_simple_nn.pth) # 6. (可选) 绘制训练曲线 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(train_losses, labelTraining Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss over Epochs) plt.legend() plt.subplot(1, 2, 2) plt.plot(train_accuracies, labelTraining Accuracy) plt.xlabel(Epoch) plt.ylabel(Accuracy (%)) plt.title(Training Accuracy over Epochs) plt.legend() plt.tight_layout() plt.savefig(training_curve.png) plt.show() if __name__ __main__: train()关键代码块解释transforms: 定义数据预处理流程ToTensor将图像转为PyTorch张量并归一化到[0,1]Normalize进行标准化。DataLoader: 负责批量加载数据、打乱顺序、多进程读取等是高效训练的关键。model.to(device): 将模型参数和缓冲区移动到指定的设备CPU或GPU。optimizer.zero_grad():至关重要在每次反向传播前必须将梯度清零否则梯度会在不同批次间累积。loss.backward(): 触发自动求导计算图中所有requires_gradTrue的张量的梯度。optimizer.step(): 根据计算出的梯度更新模型参数。model.train()/model.eval(): 切换模型模式。某些层如Dropout, BatchNorm在训练和评估时行为不同。torch.no_grad(): 上下文管理器在其内部关闭梯度计算用于推理阶段以提升速度和节省内存。torch.save(model.state_dict(), ...): 保存模型的参数字典这是保存和加载模型的推荐方式。4.3 运行与验证在终端中确保你位于项目目录pytorch_first_model下并且 Conda 环境已激活然后运行python train.py你会看到类似以下的输出数据下载和训练过程开始使用设备: cpu Epoch [1/10], Step [100/938], Loss: 2.2957 Epoch [1/10], Step [200/938], Loss: 2.2678 ... Epoch [10/10] 训练结束 - 平均损失: 0.1501, 准确率: 95.62% 最终测试结果 测试集准确率: 96.05%恭喜你的第一个神经网络模型已经训练完成并在测试集上达到了约96%的准确率。同时当前目录下会生成模型文件mnist_simple_nn.pth和训练曲线图training_curve.png。5. 核心概念与代码深度解析跑通代码只是第一步理解背后的“为什么”才能举一反三。5.1 张量PyTorch 的数据基石PyTorch 中所有数据的基本单位是张量你可以把它理解为多维数组。torch.Tensor和torch.tensor(): 前者是类后者是创建张量的函数。torch.tensor(data)会根据数据创建新的张量。形状x.shape或x.size()。理解形状是调试神经网络的关键。例如(64, 1, 28, 28)表示一个批次有64张图片每张图片1个颜色通道灰度高28像素宽28像素。设备张量可以存在于CPU内存或GPU显存中。.to(device)方法用于在设备间移动数据。5.2 Autograd自动求导的魔法PyTorch 的torch.Tensor有一个属性.requires_grad如果设置为TruePyTorch 就会开始跟踪在其上的所有操作。当你调用.backward()时它会自动计算所有梯度并累积到每个张量的.grad属性中。# 一个简单的Autograd例子 x torch.tensor([1., 2., 3.], requires_gradTrue) y x * 2 # y 2x z y.mean() # z mean(y) print(x) # tensor([1., 2., 3.], requires_gradTrue) print(y) # tensor([2., 4., 6.], grad_fnMulBackward0) print(z) # tensor(4., grad_fnMeanBackward0) z.backward() # 计算梯度 dz/dx print(x.grad) # tensor([0.6667, 0.6667, 0.6667]) # dz/dx 2/35.3 优化器如何更新参数我们使用了optim.SGD。优化器的核心作用是根据梯度更新参数。除了SGD还有optim.Adam: 目前最流行的优化器自适应调整学习率通常收敛更快更稳。optim.RMSprop: 常用于RNN。optim.Adagrad/optim.Adadelta: 自适应学习率优化器。你可以尝试在代码中将optim.SGD替换为optim.Adam并调整学习率如lr0.001观察训练效果的变化。5.4 损失函数衡量“错误”的尺子nn.CrossEntropyLoss()内部做了两件事对网络的原始输出logits应用LogSoftmax。计算NLLLoss负对数似然损失。 因此我们的网络输出层不需要再添加Softmax激活函数。在推理时如果需要概率可以对输出调用F.softmax(output, dim1)。6. 常见问题与排查思路在实践过程中你几乎一定会遇到下面这些问题。问题现象可能原因解决思路RuntimeError: Expected all tensors to be on the same device数据和模型不在同一个设备CPU/GPU上。确保在训练循环开始前将model和每一批data,target都通过.to(device)移动到同一设备。RuntimeError: size mismatch, m1: [a x b], m2: [c x d]全连接层nn.Linear的输入维度与权重维度不匹配。检查forward函数中数据展平后的形状x.view(-1, 28*28)是否正确以及各层nn.Linear定义的in_features和out_features是否连贯。UserWarning: volatile was removed...或Warning: use oftorch.set_grad_enabled使用了旧版本的API或上下文管理器。使用with torch.no_grad():替代旧的volatile变量使用model.eval()和torch.no_grad()组合进行推理。训练损失不下降准确率随机~10%学习率可能设置不当或网络结构有问题或数据没有正确归一化。1. 尝试降低学习率如从0.01调到0.001。2. 检查数据预处理流程特别是Normalize的参数是否正确。3. 简化网络如减少层数确保前向传播能正确执行。GPU内存溢出 (CUDA out of memory)批次大小 (batch_size) 设置过大或模型参数量过大。1. 减小batch_size。2. 在数据加载时使用pin_memoryTrue并配合num_workers提升数据加载效率。3. 检查是否有不必要的大张量常驻GPU内存。ImportError: No module named torchPyTorch 没有安装或不在当前Python环境中。1. 确认已激活正确的 Conda 环境 (conda activate pytorch_tutorial)。2. 在环境中重新安装 PyTorch (pip install torch ...)。7. 最佳实践与进阶方向掌握了基础之后遵循一些最佳实践能让你的项目更健壮并指引你走向更深的领域。7.1 工程化最佳实践版本控制使用 Git 管理你的代码特别是model.py和train.py。将data/文件夹和模型文件.pth加入.gitignore。配置管理将超参数如batch_size,learning_rate,epochs集中放在一个配置文件如config.yaml或通过命令行参数argparse库传入而不是硬编码在脚本中。日志记录使用 Python 的logging模块替代print可以更方便地控制日志级别、输出到文件等。模型保存与加载# 保存推荐只保存状态字典 torch.save(model.state_dict(), model.pth) # 加载先实例化模型结构再加载参数 model SimpleNN() model.load_state_dict(torch.load(model.pth)) model.eval() # 别忘了切换到评估模式数据增强对于图像任务在训练时加入随机变换如旋转、裁剪、翻转可以提升模型的泛化能力。这可以在transforms.Compose中为训练集添加。train_transform transforms.Compose([ transforms.RandomRotation(10), # 随机旋转10度 transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])7.2 模型优化方向调整网络结构增加/减少隐藏层神经元数量。添加Dropout 层(nn.Dropout(p0.5)) 来防止过拟合。在forward中在激活函数后添加x F.dropout(x, p0.5, trainingself.training)。尝试不同的激活函数如LeakyReLU,ELU。调整超参数学习率是最重要的超参数。可以尝试学习率衰减策略 (optim.lr_scheduler.StepLR)。优化器将SGD换成Adam通常能获得更好的收敛效果。批次大小影响训练速度和模型泛化能力常见值有 32, 64, 128。监控与可视化使用TensorBoard或Weights Biases等工具实时监控损失、准确率曲线甚至可视化模型计算图和参数分布。7.3 下一步学习路线成功运行第一个模型后你可以沿着以下路径深入计算机视觉学习卷积神经网络。这是处理图像的主流网络。尝试用nn.Conv2d重构一个 CNN 来提升 MNIST 的准确率轻松达到99%。接着挑战更复杂的数据集如 CIFAR-10。自然语言处理学习循环神经网络和Transformer。从文本分类、情感分析开始。模型部署学习如何使用TorchScript、ONNX或PyTorch Mobile将训练好的模型部署到服务器、Web端或移动端。深入研究框架理解DataLoader的多进程原理、自定义数据集类、混合精度训练、分布式训练等高级特性。通过这个半小时的实战你已经亲手搭建、训练并评估了一个真正的神经网络模型。你不仅看到了代码如何运行更理解了每一行代码背后的意图。记住深度学习的学习是一个“实践-理论-再实践”的循环。接下来就基于这个简单的项目去尝试修改网络结构、调整参数、解决你遇到的新问题吧。
返回列表