
如果让我给刚准备碰深度学习的同学推荐一个入门框架答案大概率是 PyTorch。这不是说它比其他框架绝对强而是它在“能跑通”和“能看懂”之间平衡得最舒服API 直观、出错信息友好、社区里中文资料多得惊人。这篇文章就用 PyTorch 带你把人生第一个神经网络跑起来选一个非常经典的任务——手写数字识别MNIST从环境搭建开始到训练出模型并算准确率结束。每个步骤我都会解释“为什么这么做”而不是丢一堆命令让你背。零基础、只写过点 Python 的同学完全能跟上如果你已经会一点 PyTorch也可以直接跳到训练循环和踩坑部分应该也能找到几个平时没注意的细节。1. 环境搭建一行命令装好 PyTorch 并验证 GPU1.1 创建虚拟环境用 conda 隔离一个干净的 PyTorch 空间我见过太多人栽在第一步直接在系统 Python 里 pip install torch装到一半发现和你正在用的另一个项目依赖冲突或者电脑里 Python 版本太老torch 装不上。所以我的固定操作是先用 Anaconda 建一个独立环境。conda create -n pytorch python3.9 -y conda activate pytorchPython 版本不用追新3.9 或 3.10 是最稳妥的选择。3.12、3.13 太新时个别依赖还没跟上容易碰到“装上了但 import 报错”的幺蛾子。激活之后后面所有 pip 命令都会装进这个干净环境装坏了大不了删掉重建完全不影响系统其他项目。如果你不想用 Anaconda用 Python 自带的 venv 也行但 conda 在切 Python 版本和解决某些二进制依赖时更省心。新手阶段直接用 conda别在这个事上浪费时间。1.2 安装命令怎么选CPU 还是 GPUCUDA 版本怎么匹配PyTorch 的安装命令不像普通包那样无脑pip install torch就行。它区分 CPU 版和 GPU 版GPU 版还要对应不同的 CUDA 版本。我第一次装的时候也被这块绕晕了现在说清几个关键点。先打开 PyTorch 官网的安装选择器按你的系统选择会给你一段命令。常用场景大概是下面几种场景安装命令只跑 CPU先把流程跑通pip install torch torchvision torchaudio本机是 NVIDIA 显卡驱动支持 CUDA 11.8pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118本机是 NVIDIA 显卡驱动支持 CUDA 12.1 或更高pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121很多人看到“CUDA 版本”就头疼我教一个最简单的判断方法在终端输入nvidia-smi看右上角。nvidia-smi输出的右上角会写CUDA Version: 12.4之类。这是你的显卡驱动支持的最高 CUDA 版本。它和 torch 安装命令里的 cu118、cu121 不需要完全一样只要“驱动支持的版本 ≥ torch 需要的版本”就行。比如驱动是 12.4装 cu121 或 cu118 都完全没问题。如果驱动太老比如只支持 10.x那才需要换更老版本的命令。还有一个细节如果官网的下载链接速度很慢可以换国内镜像源但对 GPU 版我不建议随便换源因为那些 whl 文件动辄 2GB 起步镜像源同步有时会滞后。CPU 版用清华镜像就很舒服pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple1.3 验证安装两行代码确认 GPU 真的能用装完以后别急着写模型先做一次体检。在终端进入 Python运行下面这段import torch print(torch.__version__) print(torch.cuda.is_available())如果你装的是 GPU 版且一切正常第二行会输出True。如果输出False先不要怀疑显卡大多数情况是 PYPI 默认装成了 CPU 版或者 CUDA 版本确实不匹配。可以用pip list看 torch 后面有没有cu118、cu121这样的后缀有后缀说明装的是 GPU 版问题可能出在驱动上。再顺手验证一下矩阵运算有没有真正跑到 GPU 上x torch.rand(3, 3) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0)) x_gpu x.cuda() print(x_gpu.device)x_gpu.device会显示cuda:0到这一步环境就彻底打通了。如果只是为了跑通第一个神经网络CPU 版也完全够用后面等真要训练大模型时再上 GPU 也不迟。2. 神经网络思路为什么这样设计一个分类器2.1 神经网络在做什么一个 784 维的“打分器”先不急着上代码否则你大概率会对着一堆nn.Linear发懵。手写数字识别这个任务输入是一张 28x28 的灰度图对计算机来说就是 784 个数字每个数字代表一个像素的亮度0 到 255 之间。一个全连接神经网络本质上是一台“打分机器”。我们把 784 个亮度值喂进去经过层层计算最后输出 10 个分数分别表示“这张图是数字 0 的可能性”“是数字 1 的可能性”……哪个分数最高模型就认为这张图是哪个数字。训练的过程就是不断调整网络内部的参数权重和偏置让正确答案的分数越来越高、错误答案的分数越来越低。这个过程很像小时候学认数字一开始看什么都像 1写错了被老师纠正下次看到某个写法就更容易猜对。网络内部没有人在“看”它只是在反复调整几千上万个参数罢了。2.2 为什么全连接网络适合当第一个模型第一节课我坚持用全连接网络而不是直接用卷积神经网络。原因是全连接网络把最本质的概念全部暴露给你了输入怎么进入网络、矩阵乘法怎么连接层与层、激活函数为什么存在、梯度从哪里来。这些都是深度学习大厦的地基。它的结构很简单输入层 784 个神经元接一个 128 个神经元的隐藏层再接一个 64 个神经元的隐藏层最后接 10 个神经元的输出层。隐藏层数量不是越深越好MNIST 是个比较简单的数据集三层全连接就能跑出 97% 左右的准确率。隐藏层神经元选 128、64 算比较保守的方案你可以自己改大改小感受差异。全连接网络有一个比较大的缺点参数多、对图片空间结构利用得不好。但作为“第一个模型”它跑得快、好调试、原理透明比一上来就上 ResNet、Transformer 友好太多。等你理解了这些基础再去碰卷积、循环、注意力那些概念会轻松很多。2.3 两个必须搞懂的概念激活函数与损失函数隐藏层里的激活函数我用的是 ReLU公式就是max(0, x)。它的作用通俗说就是给网络引入“非线性”。如果所有层都只是线性变换那不管堆多少层整个网络最终等价于一层线性变换表达能力非常有限。ReLU 计算极快梯度消失问题也比 Sigmoid 轻得多是当前全连接网络和卷积网络里最主流的默认选择。输出层背后接的是损失函数。这里用的是CrossEntropyLoss也就是交叉熵损失。PyTorch 的这个函数内部会先把网络的输出logits做一次 softmax转成概率分布再和真实标签计算交叉熵。所以你不需要在模型最后一层手动加 softmax直接输出 10 个原始分数就行。顺便说一句不同任务配不同的输出层和损失函数多分类任务用CrossEntropyLoss二分类也可以用BCEWithLogitsLoss回归任务比如预测房价输出层只有一个神经元损失用MSELoss。文章后面所有代码都围绕多分类展开。3. 数据准备MNIST 加 DataLoader 一次讲透3.1 transforms 预处理从像素到训练数据的必要转换MNIST 是 torchvision 里自带的数据集第一次运行代码会自动下载。如果你在的图像识别领域待一段时间这个数据集会反复出现先认识它不亏。from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform)这里两个操作千万别跳过它们直接影响模型能不能收敛。ToTensor()把 PIL 图片转换成 PyTorch 张量像素值从 0~255 缩放到 0~1同时把形状从(28, 28)变成(1, 28, 28)多出来的那个维度是通道数。Normalize((0.1307,), (0.3081,))是标准化用 MNIST 数据集全体像素的均值和标准差做归一化让数据变成均值为 0、方差接近 1 的分布。为什么要这么做想象一下如果输入数值大部分在 0~255 之间一层层乘下来数值会变得很大很小梯度很容易爆炸或消失。标准化以后每个特征的范围比较统一训练会稳定很多。如果你自己收集数据均值标准差要自己算不要拿别人的直接套。如果下载 MNIST 时网络特别慢可以把root指向一个网络快一点的目录或者提前把数据集文件放到root下的MNIST/raw目录里。数据集本身不大耐心等一会也能下完。3.2 DataLoader批次、打乱、顺序的讲究原始数据集拿到手还要套一个 DataLoader它负责在训练时按批次取数据。from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse)batch_size64的意思是每次取 64 张图一起喂给模型。为什么不一次把所有 60000 张图全丢进去内存和显存撑不住而且一次只看 64 张图模型更新一次参数后面每一批还能继续学这种“小步快跑”的方式效果通常更好。为什么不一次只喂 1 张单张计算会浪费 GPU 并行能力训练速度慢。64 是个比较通用的默认值你可以试试 32 或 128感受一下训练速度和精度的微妙平衡。shuffleTrue是每个 epoch 开始前把数据打乱。这很重要因为如果数据集里前 6000 张全是数字 0你不打乱模型连续几批看到的都是同一种数字梯度计算就偏向某一类模型容易学偏。测试集不需要打乱因为它只是用来评估不参与训练。你可以先看一眼取出来的数据长什么样images, labels next(iter(train_loader)) print(images.shape) # torch.Size([64, 1, 28, 28]) print(labels.shape) # torch.Size([64])第一行的 4 个维度分别是batch 大小、通道数、图片高、图片宽。后面的模型里你会频繁和这个形状打交道。3.3 看一眼你的数据可视化习惯从第一天养成训练前强烈建议写几行代码把你取出来的 batch 画出来看看。不是走形式而是防止数据集本身出了问题你还在傻傻调模型。我用的是 matplotlibimport matplotlib.pyplot as plt grid torchvision.utils.make_grid(images, nrow8) plt.imshow(grid.permute(1, 2, 0).numpy().squeeze(), cmapgray) plt.axis(off) plt.show()把一张 batch 拼成网格打印出来第一件事确认图片是不是清晰可辨的 0~9第二件事确认标签有没有明显错位。我见过有人折腾半天 loss 不降最后发现是数据读出来全是噪声图这种问题看一眼图立刻就能定位。4. 训练循环损失、优化器、反向传播逐行拆解4.1 用 nn.Module 定义三层网络并计算参数量模型定义是核心中的核心。PyTorch 里所有自定义网络都要继承nn.Module这个父类帮你管理参数、切换训练/评估模式、保存加载模型省掉无数手工活。import torch import torch.nn as nn import torch.nn.functional as F class Net(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 F.relu(self.fc1(x)) x F.relu(self.fc2(x)) x self.fc3(x) return x model Net() print(model)super().__init__()这一行必须有否则注册不了子模块。forward里第一行x.view(x.size(0), -1)是把(64, 1, 28, 28)的张量展平成(64, 784)也就是去掉通道和二维结构把每张图拉成一个 784 维的向量。这里我建议用view但如果你在某次操作后发现报“内存不连续”的错误可以换成x.reshape(x.size(0), -1)效果基本一样兼容性更好。许多人第一次学都会好奇网络里到底有多少参数可以用一行代码算出来total_params sum(p.numel() for p in model.parameters()) print(total_params)这个三层网络的总参数量大概是 109386。拆开算第一层 784x128128100480第二层 128x64648256第三层 64x1010650。这个数在今天的深度学习里已经算非常迷你了但它足以在 MNIST 上逼近 98% 的准确率。这也侧面说明问题难度决定模型复杂度不是网络越大越好。训练前最好固定随机种子方便复现结果torch.manual_seed(42)在 CPU 上只设这一个就够如果你后面用 GPU 还想完全复现还得设置torch.cuda.manual_seed_all(42)以及numpy.random.seed(42)不过第一个项目不用纠结到这个程度。4.2 训练循环为什么每个 batch 都要 zero_grad定义好模型接着就是整个项目最核心的训练循环。我先把完整的代码摆出来再逐行讲。device torch.device(cuda if torch.cuda.is_available() else cpu) model Net().to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) num_epochs 5 for epoch in range(num_epochs): total_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) avg_loss total_loss / len(train_loader.dataset) print(fEpoch {epoch 1}/{num_epochs}, avg_loss {avg_loss:.4f})先看优化器。我选了Adam而不是SGD因为 Adam 自带自适应学习率默认参数基本不用调对新手极其友好。SGD 并不是不好但它对手动调学习率和动量的要求更高容易让新手劝退。学习率设 0.001 是 Adam 最常用的起始值如果你发现 loss 不降第一反应就是往小调试着 0.0003 或 0.0001。循环内部每 batch 四步走顺序千万别乱optimizer.zero_grad()把上一步计算的梯度清零。outputs model(images)前向传播得到预测分数。loss.backward()基于当前 loss 反向传播自动计算所有参数的梯度。optimizer.step()用算好的梯度更新参数。为什么每轮都要zero_gradPyTorch 默认会在backward()时把梯度累加起来不清零的话第二次 backward 的梯度会叠加到第一次上参数更新就乱了。这是新手最常见的问题之一漏掉这行loss 会像震荡的股票一样反复横跳。loss.item()是把 loss 张量里的标量取出来变成普通 Python 数。如果不做这一步你会保留一堆计算图引用内存越占越多跑几个 epoch 就可能 OOM。累加时乘images.size(0)是因为每个 batch 的样本数可能不整除最后用样本总数求平均更准。说实话第一次在你屏幕上刷出Epoch 1/5, avg_loss 0.32...这种数字时多少会有点兴奋因为你已经完成了一个神经网络的训练流程。4.3 评估模型no_grad 和准确率计算的细节训练完不能只看 loss要看看模型在没见过的测试集上表现如何。准确率是最直观的指标。def evaluate(model, loader): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return correct / total test_acc evaluate(model, test_loader) print(fTest accuracy: {test_acc:.4f})代码里有两个很关键的习惯。第一个是model.eval()它把模型切到评估模式。我们当前网络没有 BatchNorm 和 Dropout所以影响不大但强烈建议从一开始就养成这个习惯否则以后模型里有 Dropout 时评估时会随机丢弃神经元结果忽高忽低。第二个是with torch.no_grad():明确告诉 PyTorch 不需要计算梯度。评估阶段只做前向推理不需要梯度保留计算图只会白白占用内存、拖慢速度。torch.max(outputs, 1)返回两个值每行最大值和它所在的下标。我们只关心下标也就是模型预测的类别所以用_丢弃最大值部分。(predicted labels)得到一个布尔张量.sum()统计相等数量。按这个流程CPU 上训练 5 个 epoch大概一两分钟测试集准确率能到 97%~98%。我第一次跑出来看到这个数字还挺意外原来一个几百 KB 的神经网络就能把识别数字这件事做得这么好。最后把模型存下来训练成果不能白费torch.save(model.state_dict(), mnist_fc.pth)加载时要注意必须先实例化同结构的网络再往里填参数model Net() model.load_state_dict(torch.load(mnist_fc.pth)) model.eval()只保存state_dict而不是保存整个模型对象是更稳妥的官方推荐做法。以后代码升级、模型结构微调加载起来都灵活。5. 避坑指南新手最常遇到的 5 类问题及排查方法5.1 报错 shape mismatch八成是忘了展平新手遇到最多的错误就是形状对不上典型的提示长这样RuntimeError: mat1 and mat2 shapes cannot be multiplied (64x1x28x28 and 784x128)这个错误翻译成人话是模型第一层想接收一个形状为 784 的向量结果你递进来一个 28x28 的矩阵前面还挂着个1维通道。解决方法就是上一节代码里那个forward开头的展平操作x x.view(x.size(0), -1)排查这类问题我发现最笨也最有效的方法是打印每一步的形状print(x.shape)放在每个线性层之前和之后各打一次错误位置一目了然。不要靠猜直接看数字。5.2 Loss 变 NaN 或卡住不降按这个顺序排查loss 变成nan非常吓人但原因其实就那么几个。我自己的排查顺序是第一检查学习率是不是太大。学习率太高会导致梯度更新幅度过大参数直接飞走loss 变成 NaN。用torch.optim.Adam(model.parameters(), lr0.001)时我没遇到过但如果你自己乱调成 0.1很可能当场翻车。处理方法是调低到 0.0001 试一下很多问题立刻消失。第二检查输入数据有没有 NaN。如果你的数据集是自己爬的或者手工拼的很可能混入空值。可以用torch.isnan(x).any()扫一遍。数据集的毛病模型调参是补不回来的。第三检查有没有做归一化。像素值 0~255 直接输入网络数值波动剧烈网络很容易学不动。回到 transforms 那一节把Normalize加上loss 曲线会顺滑得多。如果 loss 不是 NaN 而是“不降”也就是每个 epoch 结束打印的数值几乎不变多半也是学习率太低或者模型结构有问题。换个更大的学习率、加一层隐藏层、换个激活函数一个个变量去试不要同时改两处否则你根本不知道是谁救了你。5.3 GPU 不可用或显存不足别急着怀疑显卡torch.cuda.is_available()输出False这个问题的排查前面说过先确认安装命令里的 index-url 带上了cu118或cu121再确认驱动版本别太老。有些带图形界面的启动器会自作聪明帮你内置一个 torch 环境那个环境里写死的版本经常和你的显卡不匹配显示“不支持设备”基本都是这个原因。我的建议是绕过这类启动器自己建 conda 环境老老实实从头装出了问题好排查。另一种常见报错是RuntimeError: CUDA out of memory.显存不够最直接的办法是把batch_size从 64 调小到 32 或 16。如果还不行可以试试在代码里加一句torch.cuda.empty_cache()它会把 PyTorch 缓存但没有实际使用的显存释放掉。注意这只释放缓存不是帮你把模型变小。深层网络训练时如果每个 batch 都申请新显存又不释放累计起来也会爆养成及时释放不需要的大张量的习惯也挺重要。5.4 过拟合信号训练集满分但测试集掉链子很多人练着练着发现训练集准确率已经 99%测试集却只有 90%这就是过拟合的典型信号。模型不是学会了规律而是把训练集背下来了。全连接网络在 MNIST 上不太会严重过拟合但到了你自己的小数据集上这个问题会很早出现。基础的应对方法有四招增加训练数据做数据增强、降低模型复杂度减少隐藏层神经元数量、在隐藏层后加 Dropout、训练中提前停止。对我来说最有效的是在每轮 epoch 训练完后同时打印训练集和测试集的准确率观察两条曲线的差距。如果它们越拉越大过拟合开始了如果两者都低是欠拟合如果两者都高说明当前设置比较健康。这里给新手一个忠告不要一上来就在网络结构上拼命堆层数、加神经元。一个能解释清楚的简单模型远比一个说不出为什么跑得好的复杂模型更有价值。先把数据、归一化、学习率这些基本功练扎实再慢慢往结构里加东西这是我的经验也是我带过的人普遍走得比较顺的路径。pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple上面这是 CPU 版的备用安装方式适合网络不畅的情况。整个流程走下来你会发现“构建第一个神经网络”这件事真正的门槛不在模型代码上而在环境、数据和一堆细节习惯。把这些基本功打牢后面换 CNN、RNN、Transformer都是在这个框架上换积木而已。