PyTorch入门实战:从零构建你的第一个神经网络

发布时间:2026/10/9 6:39:32
PyTorch入门实战:从零构建你的第一个神经网络
如果你最近开始接触深度学习和PyTorch大概率会经历这样的画面照着教程敲完第一段代码满心期待看到准确率一路飙升结果不是报错就是训练了好几轮loss纹丝不动。更常见的是卡在安装阶段——conda命令找不到、torch装好了却用不了GPU、刚建好的环境跑两步又报依赖冲突。这些事我都经历过所以今天这篇就把“用PyTorch构建你的第一个神经网络”这件事从头到尾完整讲一遍。我按照自己带新人上手时的思路来组织内容先讲为什么第一步选PyTorch然后完整走一遍环境搭建接着把前馈神经网络的原理揉碎了说清楚最后给一段可以直接跑通的MNIST手写数字识别代码附带逐行解释。如果你是完全没接触过神经网络的初学者只有最基本的Python基础看完这篇文章你就有了一个能运行、能解释的起点如果你已经能把demo跑起来重点看最后的坑和扩展部分那些才是实战里真正会咬人的东西。1. 为什么第一站选PyTorch框架选择背后的学习曲线考量1.1 动态计算图让神经网络调试回归直觉PyTorch最大的特点是它的“动态计算图”圈内通常叫define-by-run。什么意思呢就是你的神经网络在跑前向传播的时候系统一边执行Python代码一边自动把计算过程记录下来形成计算图。后面做反向传播时autograd机制直接基于这张图来算梯度。这个设计对新手极其友好。你在写普通Python函数的时候想打印中间结果就print一下想打断点就打断点在PyTorch里完全一样。我见过很多从别的框架转过来的朋友最常说的一句话就是“在PyTorch里调bug终于不用靠猜了。”举个具体例子。你在处理MNIST图片时如果某个操作把张量形状搞错了你在模型的forward函数里直接打印x.shape运行代码就能看到问题出在第几层。这种调试体验非常接近你写普通Python程序的感觉。而在静态图框架里你得先理解“先建图、后执行”的模型调试时还要把整个计算流程串起来想对新手来说心智负担重不少。1.2 生态与资料学PyTorch能辐射到的工具半径选PyTorch还有一个很现实的原因生态。现在学术界和工业界的主流模型从BERT、GPT系列到YOLO大量官方实现和社区复现都优先用PyTorch。像Hugging Face的transformers、Ultralytics的YOLO这些库底层就是PyTorch。你把PyTorch学扎实了再去碰这些工具会轻松很多。如果用一句话总结我的建议不要把框架当成信仰Python基础 PyTorch 一点线性代数的直觉这个组合能让你在2025年的深度学习世界里做到“大部分开源项目拿下来就能跑”。TensorFlow没有不好在某些场景的部署链路上甚至更完备但就“构建你的第一个神经网络”这个诉求来说PyTorch是阻力最小的路径。2. 环境搭建的每一步与最常见翻车点2.1 用Anaconda把深度学习环境隔离在虚拟空间很多初学者直接往系统Python里装包装到后期通常是一片混乱。今天这个项目要numpy 1.23明天那个项目要numpy 2.0最后你只能看着依赖冲突的报错发呆。我的建议是从一开始就用Anaconda管理环境。具体操作分三步。第一步安装Anaconda。官网下载安装包一路下一步。这里有个很常见的坑在Windows上安装时如果你没把Anaconda加入PATH那么新开的命令行窗口里输入conda会提示“无法将‘conda’项识别为cmdlet、函数、脚本文件或可运行程序的名称”。解决办法很简单安装时勾选“Add Anaconda to my PATH”或者安装完后重启终端再不行就用开始菜单里的“Anaconda Prompt”来操作。第二步创建独立环境。打开终端执行conda create -n pytorch python3.10这条命令会创建一个名为pytorch、Python版本为3.10的独立环境。为什么单独建环境因为深度学习框架对Python版本、CUDA版本都比较敏感把它和日常项目隔离互不污染出了问题直接删掉重建也毫不心疼。第三步激活环境conda activate pytorch激活成功后命令行前面会出现(pytorch)前缀。之后你在这个环境里安装的一切Python包都只会作用于pytorch这个环境。2.2 CPU版还是GPU版一句话判断你装对没有这是整个安装流程里最容易被绕晕的地方。我先说一个很多人不知道的事实现在安装PyTorch日常用CPU训练完全能跑通第一个神经网络MNIST这种简单数据集在CPU上也就是几十秒一个epoch的事。所以如果你没有NVIDIA独立显卡或者装了半天GPU版本没成功直接用CPU版先跑完全不影响学习。安装命令分两种情况。如果你只想要CPU版pip install torch torchvision如果你有NVIDIA显卡想用GPU加速则需要在PyTorch官网选择对应的CUDA版本用类似下面这种命令安装pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118注意最后面的cu118代表CUDA 11.8cu121就是CUDA 12.1。选什么版本主要看你显卡驱动的支持范围。这里有个关键认知PyTorch的GPU版本身已经自带了CUDA运行库不要求你额外安装完整的CUDA Toolkit。你只需要保证显卡驱动不要太老就行。如果你不确定显卡驱动支持哪个CUDA版本打开命令行执行nvidia-smi右上角的CUDA Version就是驱动支持的上限。你选择的cu118、cu121只要不超过这个数字基本都能正常用。遇到最多的翻车现场是在Windows上用pip install torch装上了默认的CPU版然后自己以为在用GPU训练一看torch.cuda.is_available()返回False心态就崩了。所以装了第一件事先验证。2.3 验证环境时的“照妖镜”命令激活pytorch环境后执行下面这条命令python -c import torch; print(torch.__version__); print(torch.cuda.is_available())正常情况会输出类似torch.version比如2.3.1torch.cuda.is_available()如果是True说明GPU版本装好了如果返回False不要慌按这个顺序排查。先看版本号有没有带cu字样比如2.3.1cu118就是GPU版如果没有那就是装了CPU版用前面的--index-url命令重装。再看nvidia-smi能否正常显示显卡信息如果显示不出来说明驱动没装好。最后看你的显卡是不是NVIDIA的AMD显卡和Intel显卡目前即使能用也折腾建议先CPU。还有一种情况你明明安装了GPU版但import torch时报各种DLL加载失败多半是Visual C Redistributable没装去微软官网装一下就好。这块建议收藏一张表方便以后排查。现象可能原因处理方式conda命令无法识别未加入PATH或未重启终端使用Anaconda Prompt或重新配置PATHcuda.is_available()为False安装了CPU版torch用--index-url指定cu版本重装nvidia-smi无输出显卡驱动未安装安装对应驱动import torch报DLL错误缺少VC运行库安装Visual C Redistributable3. 前馈神经网络的骨架从784维输入到10维输出的旅程3.1 线性层、激活函数和参数量的粗略账先建立一点感觉。MNIST数据集里的每张图是28乘28像素的灰度图把它们拉平就是一个784维的向量。我们想做的事是让神经网络对这个784维向量做变换最终输出一个10维的向量分别代表数字0到9的得分。得分最高的那个数字就是模型的判断结果。最朴素的神经网络叫前馈神经网络也叫多层感知机英文缩写MLP。它的每一层做的操作其实只有两件事先做线性变换再套一个非线性激活函数。线性变换就是y Wx bW是权重矩阵b是偏置向量。权重矩阵负责把上一层的维度映射到当前层的维度。我以本文后面要用的网络为例输入784维中间过两个隐藏层分别128维和64维最后输出10维。每一层线性层都带一个ReLU激活函数。这个网络有多少参数算一下第一层784乘以128再加128个偏置第二层128乘以64再加64个偏置第三层64乘以10再加10个偏置。总共109386个参数。很多新手第一次知道一个不起眼的小网络有十万参数时都很惊讶但这恰恰是理解深度学习的关键起点所谓训练模型本质上就是在调整这十万个参数让它们组合出正确的映射关系。这里必须解释一个关键问题为什么不能只做线性变换非要加激活函数原因很数学。假设没有激活函数两层线性变换叠加起来W2(W1x b1) b2展开后还是W2W1x (W2b1 b2)本质上依然是一个线性变换。也就是说不管叠多少层没有非线性的多层网络和单层网络没有区别表达能力天花板很低。ReLU这种激活函数的作用就是往网络里引入非线性让它能逼近复杂的函数形状。3.2 损失函数与反向传播的直觉模型有了怎么判断它好不好这里需要一个衡量差距的尺子也就是损失函数。分类任务里最常用的是交叉熵损失PyTorch里对应nn.CrossEntropyLoss。它的思路是把模型输出的10个原始分数变成概率分布再计算真实标签对应的概率值的负对数。模型越自信且越正确损失越小模型越离谱损失越大。有了损失接下来的问题就是怎么让损失变小答案是反向传播加上梯度下降。反向传播利用微积分的链式法则从输出端反向计算损失对每个参数的偏导数。一个参数的偏导数大就说明它对损失的影响大而且还能告诉我们往哪个方向调整参数能降低损失。这里可以打个比方想象你晚上被蒙着眼睛丢在山上目标是最低点。看不见全局只能靠脚感受当前所在地的坡度。梯度就是那个坡度告诉你哪个方向最陡。你朝最陡的方向挪一小步再感受新位置的坡度再挪一小步不断重复最终就能走到山脚下。这就是梯度下降。PyTorch里的autograd包自动完成了所有偏导数的计算你只需要在损失上调用backward()梯度就会自动算出来并挂到每个参数上。3.3 为什么第一课选MNIST而不是CIFAR-10很多人会问为什么所有深度学习入门教程都拿MNIST说事不腻吗因为这个数据集实在是太适合教学了。28乘28的灰度图几乎没有什么冗余信息60万张的训练集也不算大CPU上几十秒就能训练完类别清晰图像大致处于居中位置模型只要学到基本笔画特征就能有不错的准确率。更重要的是及时反馈非常快第一次跑通就能看到准确率从随机猜的10%逐步升到90%以上。对比之下CIFAR-10虽然也是经典入门数据集但它是32乘32的彩色图三层MLP直接硬上准确率可能只有50%左右。新手容易一头雾水以为自己的代码有问题。要把效果做上去就得引入卷积神经网络而卷积核、池化、通道这些新概念一次全灌给初学者反而稀释了“第一个神经网络”的核心目标。所以我的建议很明确第一次训练网络用MNIST用全连接前馈网络。目标是走通“数据-模型-训练-评估”这条完整链路而不是追求高分。4. 逐行拆解第一个可运行的神经网络4.1 数据管线Dataset、Transform和DataLoader的分工PyTorch把数据读取拆成了三个环节很多新手看到一堆类和函数就晕其实分工很清晰。Dataset负责“数据从哪来、长什么样”Transform负责“读进来之后做什么预处理”DataLoader负责“怎么分批送到模型里”。先看数据准备代码from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_data datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform )datasets.MNIST会帮我们下载并解析MNIST数据。第一次运行时downloadTrue会从网上下载后面再跑就直接读本地缓存不用重复下载。这里有两个细节值得说。第一个是transforms.ToTensor()。它把PIL图片变成PyTorch的Tensor同时把像素值从0到255的整数缩放到0到1之间。这一步非常必要因为神经网络的数值运算在0到1量级下更稳定。第二个是Normalize用均值和标准差把数据归一化到接近零均值、单位方差。两个参数0.1307和0.3081是MNIST整个数据集的全局均值和标准差是提前算好的统计量。为什么要归一化因为输入特征的量级会影响梯度更新的效率零均值化之后网络收敛会快很多尤其是像我们这种从零开始训练的小网络效果差异很明显。然后看DataLoadertrain_loader DataLoader(train_data, batch_size64, shuffleTrue)这里的batch_size64表示每次取64张图打包成一个batch。为什么不一次性把6万张图全塞给模型一是内存吃不消二是深度学习里有一个共识用一小批数据算出来的梯度虽然带点噪声但这种噪声反而能帮助模型跳出局部最优泛化效果更好。shuffleTrue表示每个epoch开始前把数据重新打乱防止模型学到batch之间的固定顺序。比如如果原始数据里前两万张全是数字0不洗牌的话模型可能就只学会输出0了。测试集的DataLoader一般设置shuffleFalse因为评估时不需要打乱固定顺序也方便复现结果。4.2 模型定义nn.Module的__init__与forward各司其职在PyTorch里定义网络标准做法是继承nn.Module。整个类就两个关键部分__init__里定义有哪些层forward里定义数据如何流经这些层。import torch import torch.nn as nn class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(28 * 28, 128) self.fc2 nn.Linear(128, 64) self.fc3 nn.Linear(64, 10) def forward(self, x): x x.view(x.size(0), -1) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) x self.fc3(x) return xfc1、fc2、fc3就是三个线性层。nn.Linear第一个参数是输入维度第二个是输出维度。forward函数第一行x.view(x.size(0), -1)是把形状为(64, 1, 28, 28)的输入展平成(64, 784)-1表示自动推断这一维。如果不展平nn.Linear会把784维的权重和1乘28乘28的多维输入弄混直接报错。relu激活函数放在前两个线性层之后最后一个线性层fc3之后不接激活函数。这一点新手很容易误解为什么输出层不用softmax因为torch的nn.CrossEntropyLoss内部已经做了合适的处理你只要把原始分数也就是logits交给他它在计算损失时内部会先做softmax再算交叉熵。如果输出层手动加了softmax反而可能带来数值稳定性的问题。把这层逻辑想明白就不会对着别人的代码乱问了。4.3 训练循环前向、清零、反向、更新的固定节拍训练循环是深度学习代码里最核心、最固定的一段。先看代码再逐行讲model MLP() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) epochs 5 for epoch in range(epochs): total_loss 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() print(fEpoch {epoch1}/{epochs}, loss: {total_loss/len(train_loader):.4f})先看优化器。optim.Adam传入的是model.parameters()这会告诉优化器“你需要更新模型里的哪一批参数”。学习率lr0.001是Adam优化器的常见默认值对这个小模型来说是一个非常稳妥的起点。再看循环内部的四步顺序不能乱。第一步optimizer.zero_grad()清空梯度。这里必须解释一个PyTorch的“陷阱”默认情况下梯度是累积的也就是每调用一次backward()梯度会累加到之前的值上。如果你不清空下一轮迭代的梯度会跟上一轮混在一起loss表现会很诡异。所以每个batch开始前都必须把梯度清零这一步忘了你会看到loss一会儿涨一会儿跌完全无法收敛。第二步outputs model(images)这是前向传播数据从输入层流到输出层得到预测分数。第三步loss criterion(outputs, labels)计算当前batch的损失。第四步loss.backward()反向传播自动算出所有参数的梯度。第五步optimizer.step()优化器拿着刚算好的梯度让每个参数沿着梯度反方向更新一步。简单说前向传播负责预测反向传播负责告诉参数怎么走step负责真正迈出那一步。每个epoch结束时打印一下平均loss。新手最重要的观测指标就是这个loss每个epoch都在下降说明整个训练链路是通的。如果loss在某个区间震荡不降优先检查学习率是否太大如果loss干脆不动检查是不是忘了zero_grad或者数据预处理出了问题。4.4 测试评估为什么必须model.eval()和torch.no_grad()训练完之后怎么判断模型好坏在测试集上算准确率。这里有两条容易被忽略的规范correct 0 total 0 model.eval() with torch.no_grad(): for images, labels in test_loader: outputs model(images) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fTest accuracy: {correct/total:.4f})第一model.eval()这行必须加。它会告诉模型现在进入推理模式。我们当前这个MLP没有BatchNorm和Dropout层看不出来差别但在更复杂的网络里不切换模式会导致评估结果异常。养成一进测试就写model.eval()的习惯后面不吃亏。第二用with torch.no_grad()包住整个评估循环。它的作用是告诉autograd“这里不需要计算梯度”。推理阶段本来就不需要反向传播不开这个开关的话每算一次前向还会额外保存一些用于反向的中间变量既浪费内存又拖慢速度。两个技巧合起来虽然这个例子里体感不明显但模型一大差别立竿见影。准确率怎么算的outputs.argmax(dim1)表示对每个样本的10维输出取最大值的索引也就是模型预测的数字。然后和真实标签labels做比较用sum().item()统计这个batch里预测正确的数量累加起来除以总数就是准确率。在MNIST上我们随便一个MLP就能到97%以上。下面是把上面几段整合起来能直接运行并观察效果的完整代码import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_data datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_data datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_data, batch_size64, shuffleTrue) test_loader DataLoader(test_data, batch_size1000, shuffleFalse) class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(28 * 28, 128) self.fc2 nn.Linear(128, 64) self.fc3 nn.Linear(64, 10) def forward(self, x): x x.view(x.size(0), -1) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) x self.fc3(x) return x model MLP() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) epochs 5 for epoch in range(epochs): total_loss 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() print(fEpoch {epoch1}/{epochs}, loss: {total_loss/len(train_loader):.4f}) correct 0 total 0 model.eval() with torch.no_grad(): for images, labels in test_loader: outputs model(images) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fTest accuracy: {correct/total:.4f})你把这整段贴到.py文件里在激活了pytorch环境的前提下运行就能看到每个epoch的loss逐步下降最后输出测试准确率。5. 模型跑通之后保存、单图预测与下一步扩展5.1 保存与加载模型的最佳实践模型训练好了直接关了电脑就白练了所以要先学会保存。PyTorch保存模型有好几种方式我最推荐只保存state_dicttorch.save(model.state_dict(), mnist_mlp.pth)state_dict里存的是模型所有参数张量纯粹的数据不包含网络结构。加载的时候先重新定义一个结构相同的模型再把参数塞进去model MLP() model.load_state_dict(torch.load(mnist_mlp.pth, weights_onlyTrue)) model.eval()这里说两个细节。第一为什么不直接torch.save(model, model.pth)因为这种完整保存方式把网络结构和参数绑在一个文件里一旦你的源码结构改了旧文件很可能加载失败而且跨设备、跨Python版本时更容易出问题。state_dict只关心“参数值”和“参数名字”的对应移植性更好。第二新版PyTorch里torch.load默认会开启weights_onlyTrue旧版本建议显式传入这个参数这是官方在安全性上给出的越来越严格的要求。加载完记得补上model.eval()原因上一节已经说过。5.2 把任意手写图片喂给模型从PIL Image到4维Tensor训练和评估走完了下一步自然是想拿一张自己的手写数字图片让模型猜猜看。这里的代码不长但形状问题能绕晕新手from PIL import Image img Image.open(my_digit.png).convert(L) img img.resize((28, 28)) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) t transform(img) t t.unsqueeze(0)先说convert(L)它的作用是把图片转成灰度图因为MNIST就是灰度数据集。resize到28乘28保证输入尺寸和训练时一致。transform(img)把PIL图变成Tensor得到形状(1, 28, 28)这里的1是通道数。关键一步来了t.unsqueeze(0)会把这个形状变成(1, 1, 28, 28)在最前面增加一个维度。为什么必须加因为模型在训练时看到的输入总是带batch维度的也就是(N, 1, 28, 28)这种四维形状。单张图片本来就是(1, 28, 28)根本没有batch轴所以需要在第0维补一个1凑成四维。这算是新手最常见的形状错误看到模型报Expected 4D input先想到unsqueeze(0)就对了。预测代码model.eval() with torch.no_grad(): out model(t) pred out.argmax(dim1).item() print(pred)这里还有一个特别容易被忽略的细节输入图片的预处理必须跟训练时完全一致。很多人拿一张随手涂鸦的图丢给模型结果预测得乱七八糟不是模型废了而是图片背景是白的、数字是黑的跟MNIST里黑底白字的习惯正好相反。所以你自己画图时最好画成黑底白字或者先把颜色反相一下否则模型看到的是完全不同的数据分布效果当然差。5.3 下一步怎么走从MLP到CNN再到真实项目第一个MLP跑通了恭喜你已经跨过了最大的坎。接下来往哪里走我给一个明确的推荐顺序。第一把模型从MLP换成CNN。卷积网络专门为图像设计用Conv2d加MaxPool2d替换掉一部分Linear层你会发现MNIST准确率轻松超过99%。第二增加一个验证集从训练集里切出一部分数据来观察过拟合这是后面做任何真实项目都要有的基本功。第三动手调整超参数。不要只满足于跑通把hidden维度改成64、256激活函数从ReLU换成sigmoid学习率从0.001改成0.01再改成0.0001记录下准确率的变化。这种亲手做实验的感觉比看一万篇教程都管用。我个人带人学习的经验是不要急着追新模型。把MLP的loss曲线画出来看看它怎么下降训练集和测试集准确率差多少这些过程比“今天跑通了一个大模型”更能建立真正的直觉。最后说点实在的。我见过很多人在环境搭建上反复栽跟头后直接放弃真的可惜。遇到莫名其妙的报错记住一个排查原则先打印变量形状再看版本匹配最后才怀疑代码逻辑。另外第一个模型不要追求高精度MNIST上哪怕只有97%也说明你已经完整走通了“数据-模型-训练-评估”这整条链路后面换成CNN、RNN或者Transformer本质上都是在这条链路上做模块替换。真卡住了把报错信息连同PyTorch版本号一起丢到搜索引擎里找到答案的速度会快很多。