☰
PyTorch入门实战:从零构建你的第一个神经网络
2026/10/9 6:39:19 网站建设 项目流程

如果你最近开始接触深度学习和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 python=3.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.8,cu121就是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.1
  • torch.cuda.is_available(),如果是True,说明GPU版本装好了

如果返回False,不要慌,按这个顺序排查。先看版本号有没有带+cu字样,比如2.3.1+cu118就是GPU版;如果没有,那就是装了CPU版,用前面的--index-url命令重装。再看nvidia-smi能否正常显示显卡信息,如果显示不出来,说明驱动没装好。最后看你的显卡是不是NVIDIA的,AMD显卡和Intel显卡目前即使能用也折腾,建议先CPU。

还有一种情况,你明明安装了GPU版,但import torch时报各种DLL加载失败,多半是Visual C++ Redistributable没装,去微软官网装一下就好。这块建议收藏一张表,方便以后排查。

现象可能原因处理方式
conda命令无法识别未加入PATH或未重启终端使用Anaconda Prompt或重新配置PATH
cuda.is_available()为False安装了CPU版torch用--index-url指定cu版本重装
nvidia-smi无输出显卡驱动未安装安装对应驱动
import torch报DLL错误缺少VC运行库安装Visual C++ Redistributable

3. 前馈神经网络的骨架:从784维输入到10维输出的旅程

3.1 线性层、激活函数和参数量的粗略账

先建立一点感觉。MNIST数据集里的每张图是28乘28像素的灰度图,把它们拉平就是一个784维的向量。我们想做的事,是让神经网络对这个784维向量做变换,最终输出一个10维的向量,分别代表数字0到9的得分。得分最高的那个数字,就是模型的判断结果。

最朴素的神经网络叫前馈神经网络,也叫多层感知机,英文缩写MLP。它的每一层做的操作其实只有两件事:先做线性变换,再套一个非线性激活函数。线性变换就是y = Wx + b,W是权重矩阵,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', train=True, download=True, transform=transform )

datasets.MNIST会帮我们下载并解析MNIST数据。第一次运行时download=True会从网上下载,后面再跑就直接读本地缓存,不用重复下载。这里有两个细节值得说。

第一个是transforms.ToTensor()。它把PIL图片变成PyTorch的Tensor,同时把像素值从0到255的整数缩放到0到1之间。这一步非常必要,因为神经网络的数值运算在0到1量级下更稳定。第二个是Normalize,用均值和标准差把数据归一化到接近零均值、单位方差。两个参数0.1307和0.3081是MNIST整个数据集的全局均值和标准差,是提前算好的统计量。为什么要归一化?因为输入特征的量级会影响梯度更新的效率,零均值化之后网络收敛会快很多,尤其是像我们这种从零开始训练的小网络,效果差异很明显。

然后看DataLoader:

train_loader = DataLoader(train_data, batch_size=64, shuffle=True)

这里的batch_size=64表示每次取64张图打包成一个batch。为什么不一次性把6万张图全塞给模型?一是内存吃不消,二是深度学习里有一个共识:用一小批数据算出来的梯度虽然带点噪声,但这种噪声反而能帮助模型跳出局部最优,泛化效果更好。shuffle=True表示每个epoch开始前把数据重新打乱,防止模型学到batch之间的固定顺序。比如如果原始数据里前两万张全是数字0,不洗牌的话模型可能就只学会输出0了。测试集的DataLoader一般设置shuffle=False,因为评估时不需要打乱,固定顺序也方便复现结果。

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 x

fc1、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(), lr=0.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(f'Epoch {epoch+1}/{epochs}, loss: {total_loss/len(train_loader):.4f}')

先看优化器。optim.Adam传入的是model.parameters(),这会告诉优化器“你需要更新模型里的哪一批参数”。学习率lr=0.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(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f'Test accuracy: {correct/total:.4f}')

第一,model.eval()这行必须加。它会告诉模型现在进入推理模式。我们当前这个MLP没有BatchNorm和Dropout层,看不出来差别,但在更复杂的网络里,不切换模式会导致评估结果异常。养成一进测试就写model.eval()的习惯,后面不吃亏。

第二,用with torch.no_grad()包住整个评估循环。它的作用是告诉autograd“这里不需要计算梯度”。推理阶段本来就不需要反向传播,不开这个开关的话,每算一次前向还会额外保存一些用于反向的中间变量,既浪费内存又拖慢速度。两个技巧合起来,虽然这个例子里体感不明显,但模型一大,差别立竿见影。

准确率怎么算的?outputs.argmax(dim=1)表示对每个样本的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', train=True, download=True, transform=transform) test_data = datasets.MNIST(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_data, batch_size=64, shuffle=True) test_loader = DataLoader(test_data, batch_size=1000, shuffle=False) 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(), lr=0.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(f'Epoch {epoch+1}/{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(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f'Test accuracy: {correct/total:.4f}')

你把这整段贴到.py文件里,在激活了pytorch环境的前提下运行,就能看到每个epoch的loss逐步下降,最后输出测试准确率。

5. 模型跑通之后:保存、单图预测与下一步扩展

5.1 保存与加载模型的最佳实践

模型训练好了,直接关了电脑就白练了,所以要先学会保存。PyTorch保存模型有好几种方式,我最推荐只保存state_dict:

torch.save(model.state_dict(), 'mnist_mlp.pth')

state_dict里存的是模型所有参数张量,纯粹的数据,不包含网络结构。加载的时候先重新定义一个结构相同的模型,再把参数塞进去:

model = MLP() model.load_state_dict(torch.load('mnist_mlp.pth', weights_only=True)) model.eval()

这里说两个细节。第一,为什么不直接torch.save(model, 'model.pth')?因为这种完整保存方式把网络结构和参数绑在一个文件里,一旦你的源码结构改了,旧文件很可能加载失败,而且跨设备、跨Python版本时更容易出问题。state_dict只关心“参数值”和“参数名字”的对应,移植性更好。第二,新版PyTorch里torch.load默认会开启weights_only=True,旧版本建议显式传入这个参数,这是官方在安全性上给出的越来越严格的要求。加载完记得补上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(dim=1).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版本号一起丢到搜索引擎里,找到答案的速度会快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询