1. 写在前面:先从“为什么需要它”说起
做深度学习的人,大多绕不开 PyTorch。我最初接触它是在两年前做某图像分类Demo,当时在 TensorFlow 和 PyTorch 之间犹豫了很久,最后因为社区活跃度和调试便捷性选了 PyTorch,事实证明这个选择帮我省下了大量排查代码问题的时间。
这篇博文适合两类人看:完全没有接触过深度学习框架的初学者,以及已经从其他框架迁移过来、想快速上手 PyTorch 的开发者。我会从安装环节讲起,一直到写一个完整的训练循环,中间会穿插大量我在实际项目中遇到的坑和处理方式,这些内容在官方文档里通常不会写太细。
先说结论:PyTorch 的核心价值在于“动态计算图”和“自动求导”,这让模型的调试和迭代变得异常直观。如果你曾经被静态图的编译过程折磨过,上手 PyTorch 之后会明显感觉到思路顺畅很多。这篇博文的目标是让你读完就能自己搭起一个可运行的训练环境,而不是停留在“看着教程能跑通,换台机器就懵”的状态。
2. 安装前的关键决策:环境、版本和硬件
2.1 为什么不要直接 pip install 到系统环境
很多初学者拿到安装命令,第一反应就是照着官方文档敲pip install torch,然后等进度条走完就以为大功告成了。我这里想先泼一点冷水:这个习惯在深度学习项目里会带来长期隐患。
深度学习项目依赖的包版本敏感度极高。今天装的 PyTorch 1.13 可能跑得好好的,明天另一个项目需要 PyTorch 2.1 的某个新特性,如果你两个项目共用一套环境,升级之后原来的代码大概率会出现各种不兼容。我做过的某跨平台系统项目里就出现过这种情况:一个老脚本依赖旧版 API,结果因为环境升级直接崩掉,排查花了大半天。
推荐的做法是建立独立的虚拟环境,这是所有深度学习项目的第一步。我一般用 conda,因为它能同时管理 Python 版本和包依赖,在切换不同 Python 版本时比 venv 更方便。具体来说:
conda create -n torch_env python=3.10 conda activate torch_env如果你不想装 Anaconda 全家桶,也可以用 Miniconda 或者 Python 自带的 venv,核心思路是一样的:给 PyTorch 一个干净、隔离的运行环境,避免版本冲突。
2.2 CPU 版还是 GPU 版:先看你的硬件
安装 PyTorch 最大的坑在于 GPU 版本和 CPU 版本的选择。很多人不知道自己电脑有没有独显,或者以为装个 GPU 版就万事大吉,结果代码报 “CUDA not available” 之后才回头排查。
判断是否需要 GPU 版很简单:如果你只是学习框架的基本用法、跑一些小的示例代码,CPU 版完全够用;如果你要训练真正的模型(哪怕是一个简单的卷积网络),GPU 的加速效果是数量级的差异。我实测过一个图像分类模型,CPU 上跑一个 epoch 大概需要 40 分钟,换成 GPU 之后只需要 2 分半钟,这个差距在实际项目里直接决定了你能否快速迭代。
怎么看自己的显卡:Windows 下打开任务管理器,看“性能”页签里有没有 GPU 项,注意区分“GPU 0”(可能是核显)和“GPU 1”(独立显卡);Linux 下执行nvidia-smi,如果提示命令不存在,说明没有装好 NVIDIA 驱动,或者根本没有 NVIDIA 显卡。
拿到显卡信息后,去 PyTorch 官网的 Get Started 页面,选择对应的操作系统、包管理工具(pip 还是 conda)和 CUDA 版本,页面会自动生成安装命令。需要注意的是,CUDA 版本要和你机器上装的驱动兼容,nvidia-smi显示的 “CUDA Version” 就是你的驱动支持的最高版本。
2.3 版本绑定:为什么不能闭眼装最新版
PyTorch 发布新版本很快,官方也在鼓励用户使用最新版本,但在生产环境或者学习阶段,我强烈建议选择稳定的次新版本。原因很简单:深度学习生态里的配套工具(比如某些数据增强库、第三方模型库)往往滞后于 PyTorch 的更新节奏,你装一个最新版,其他库可能还没适配,报错的时候原因是别人没跟上,而不是你的代码有问题。
以我常用的组合为例:Python 3.10 + PyTorch 2.1.x + CUDA 11.8,这个组合在绝大多数模型库里都能顺畅运行。如果你不确定选哪套组合,就选官方首页默认推荐的版本,同时锁定 Python 大版本,能规避 90% 以上的奇怪问题。
3. 安装实操:从命令到验证一次跑通
3.1 Linux / Windows 下的安装命令与区别
这里给出两条常见命令,一条是官网推荐的 pip 方式,一条是 conda 方式:
# pip 方式,以 CUDA 11.8 为例 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # conda 方式 conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia如果你确定不需要 GPU,直接:
pip install torch torchvision torchaudio注意 torchvision 和 torchaudio 是 PyTorch 生态的两个方向包:torchvision 处理图像相关的模型和数据集,torchaudio 处理音频。只装 torch 也行,但后面写实际项目时几乎都会用到 torchvision,所以我习惯一次装齐。
Windows 用户注意:如果你的 CUDA 版本是 12.x 系列,把上面的 cu118 换成 cu121 或 cu123 之类,具体可以用官网生成器查询。另外 Windows 下不要混用 conda 和 pip 装同一个包,可能会出现 DLL 加载错误,这个我在 4.3 节还会再提。
3.2 安装后的验证:不止是 import 一下
安装完成后,完整验证环境是否正常,至少要跑下面这段代码:
import torch print("PyTorch 版本:", torch.__version__) print("CUDA 是否可用:", torch.cuda.is_available()) if torch.cuda.is_available(): print("GPU 名称:", torch.cuda.get_device_name(0))如果你期望是 GPU 环境,但torch.cuda.is_available()返回False,排查顺序是这样的:先看nvidia-smi是否正常;正常的话,检查你安装的 PyTorch 对应 CUDA 版本和驱动支持的版本是否匹配;再检查你当前激活的虚拟环境是不是当初装 PyTorch 的那个环境。新手最常见的错误是在 base 环境里跑代码,但包装在另一个虚拟环境里,导入失败后误判为安装问题。
一个我常用的额外验证代码是真正在 GPU 上做一次小矩阵运算,确认计算不仅“可见”而且“可用”:
x = torch.rand(1000, 1000).cuda() y = torch.mm(x, x) print("GPU 计算正常:", y.sum().item())如果上面这段能跑出数字,说明环境真正可用,而不仅仅是检测到了显卡。
3.3 conda 和 pip 混用的一些经验
有一个经常被我忽略、直到踩坑才记住的规则:conda 和 pip 不要在同一环境里反复交替安装包。conda 会维护自己的包管理元数据,pip 装完的包 conda 不一定认识,反之亦然。这是某次我在一个 conda 环境里用 pip 安装了某个依赖库,conda 认为环境还是“干净”的,升级 conda 包时直接把 pip 安装的东西覆盖掉,代码瞬间崩溃。
如果需要装 PyTorch 生态之外的依赖库(比如 numpy 或 pandas),先用 conda 装;如果 conda 没有对应包(很多新库 conda 源更新很慢),再改用 pip。装完不要在同一个环境里继续折腾 conda 操作,专心用 pip 或者切换环境。
4. 张量与自动求导:PyTorch 的地基
4.1 张量的创建和基础操作
装好环境之后,第一个接触的概念一定是 Tensor(张量)。如果你学过 NumPy,可以把张量理解成 “带 GPU 加速、能自动求导的 NumPy 数组”。基本用法非常接近:torch.tensor()创建数组,ndim看维度,shape看形状,dtype看数据类型。
import torch a = torch.tensor([1, 2, 3]) print(a.shape, a.dtype) # torch.Size([3]) torch.int64 b = torch.zeros(2, 3) # 2行3列的全零张量 c = torch.randn(2, 3) # 标准正态分布随机数 d = torch.arange(10) # 0到9的等差数列需要留意torch.tensor()和torch.Tensor()之间的区别。前者根据传入数据推断类型,后者默认就是 32 位浮点。做深度学习模型时,数据一般是 32 位浮点,中间计算如果出现 64 位浮点会拖慢 GPU 计算,很多代码里的隐性问题就是这么来的。
张量在 CPU 和 GPU 之间的转移是一个高频操作,踩坑也集中在这里。.cuda()是把数据送到显存,.cpu()是拷回来。如果某个张量在 GPU 上,你直接对它做.numpy()会报错,必须.cpu().numpy()。同理,两个张量要参与运算,必须同在 CPU 或在同一块 GPU 上,这个限制在写多卡训练时尤其明显。
4.2 自动求导:核心魔法是怎么运作的
PyTorch 的自动求导机制,用一个词形容就是“记录一切”。当你给一个张量设置requires_grad=True,框架就开始记录所有对它的运算操作,形成一个计算图。在你调用.backward()的瞬间,它会沿着记录反向计算每个叶子节点的梯度,存到对应的.grad属性里。
x = torch.tensor(2.0, requires_grad=True) y = x ** 2 + 3 * x + 1 y.backward() print(x.grad) # 输出 7.0,因为 dy/dx = 2x + 3 = 2*2+3=7这个机制为什么重要?因为训练神经网络时,手动推导每一层的梯度公式几乎不可能(模型动辄几十上百层),自动求导直接解放了这部分工作。你只需要定义前向传播,反向求导全交给框架。
在实际项目中有两个高频坑:一是多个模块共用同一批参数,第一次backward()之后.grad会保留,第二次再计算时会累加,所以需要在每个优化步骤开始前手动调用optimizer.zero_grad(),这个做训练时最容易漏;二是在复杂的控制流(比如循环、条件判断)里计算图会变得庞大,用torch.no_grad()包住不需要求导的部分(比如推理阶段)可以大幅减少内存占用。
4.3 一个小练习:手动实现线性回归
光看不练很难真正理解张量和自动求导,这里给一个实用性很强的入门练习:用纯 PyTorch 实现一个最简单的线性回归。
假设数据满足 y = 2x + 1 的关系,我们加一点噪声模拟真实数据:
import torch torch.manual_seed(42) # 固定随机种子,保证可复现 x = torch.linspace(0, 1, 100) true_w, true_b = 2.0, 1.0 y = true_w * x + true_b + torch.randn_like(x) * 0.05 w = torch.randn(1, requires_grad=True) b = torch.zeros(1, requires_grad=True) learning_rate = 0.1 for epoch in range(200): y_pred = w * x + b loss = ((y_pred - y) ** 2).mean() # 均方误差损失 loss.backward() with torch.no_grad(): # 更新参数,不需要记录梯度 w -= learning_rate * w.grad b -= learning_rate * b.grad w.grad.zero_() b.grad.zero_() if (epoch + 1) % 20 == 0: print(f"Epoch {epoch + 1}, Loss: {loss.item():.4f}") print(f"学到的参数: w = {w.item():.3f}, b = {b.item():.3f}")这个练习做完,你应该能同时掌握三件事:张量的基本运算、requires_grad和backward()的用法、参数更新循环的基本结构。这三个能力是后面所有深度学习代码的基石。
5. 数据加载与模型构建:写一个像样的训练脚本
5.1 Dataset 和 DataLoader 的正确打开方式
实际项目的数据量动辄几万几十万张图片,不可能一次性把全部数据读进内存。PyTorch 提供了两个标准接口来解决这个问题:Dataset负责定义“如何取一条数据”,DataLoader负责“如何批量、乱序、并行地取数据”。
from torch.utils.data import Dataset, DataLoader from torchvision import transforms class MyDataset(Dataset): def __init__(self, image_paths, labels, transform=None): self.image_paths = image_paths self.labels = labels self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = load_image(self.image_paths[idx]) # 自行实现图片读取 if self.transform: image = self.transform(image) label = self.labels[idx] return image, label这里有几个关键细节值得展开说。
__getitem__里不要做太重的前处理。数据增强、归一化这些操作应该封装进transforms,不要在__getitem__里直接写。原因是 DataLoader 是多进程加载数据,__getitem__是每个 worker 都要执行的,太重会影响整体性能。
DataLoader 的几个参数直接影响训练效率:batch_size决定批量大小,shuffle=True在每个 epoch 开始时打乱数据(训练集必须打乱,防止模型学到数据的固定顺序;验证集不需要),num_workers控制子进程数量,drop_last=True可以在数据量不能被 batch_size 整除时丢弃最后不完整的 batch,防止 BatchNorm 层报错。
dataset = MyDataset(image_paths, labels, transform) dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4, drop_last=True)我在某跨平台系统项目里发现,设定 num_workers 后 Linux 上直接收益明显,Windows 上反而可能变慢甚至报错。Windows 的子进程机制和 Linux 不同,如果你在 Windows 上遇到 DataLoader 相关的报错,最省事的方案是把 num_workers 设为 0。
5.2 用 nn.Module 搭建一个卷积网络
PyTorch 推荐通过继承nn.Module来定义模型,两步走:在__init__里定义网络层结构,在forward里定义前向传播逻辑。
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2) self.fc = nn.Linear(32 * 8 * 8, 10) def forward(self, x): x = torch.relu(self.conv1(x)) x = self.pool(x) x = torch.relu(self.conv2(x)) x = self.pool(x) x = x.view(x.size(0), -1) x = self.fc(x) return x核心设计在于forward里用的是函数形式的torch.relu(),而不是nn.ReLU()实例。两种写法都能实现同样的功能,函数式写法不创建额外模块,代码更简洁,因为 ReLU 没有可学习参数,更适合这种场景。
nn.Linear的输入维度需要根据前一层输出算出来。上面这个例子中输入图片是 32x32,经过两次 MaxPool2d 后变成 8x8,所以全连接层输入是 32 * 8 * 8。算错这一步是最常见的报错之一,好在 PyTorch 的报错信息现在比较友好,会直接提示期望的维度大小。
5.3 损失函数、优化器与完整训练循环
模型搭好之后,训练循环本身其实是“四大步骤”的不断重复:前向传播算预测结果、计算损失、反向传播算梯度、更新参数。配合 4.2 节提到的zero_grad(),完整代码如下:
import torch.optim as optim model = SimpleCNN() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(10): running_loss = 0.0 for images, labels in dataloader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() # 清空上一轮梯度 outputs = model(images) # 前向传播 loss = criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新参数 running_loss += loss.item() print(f"Epoch {epoch + 1}, Loss: {running_loss / len(dataloader):.4f}")这里面值得深入说一下criterion的选择。CrossEntropyLoss是全连接分类任务最常用的损失函数,它内部已经包含了 softmax 计算,也就是说你在forward里不需要单独加nn.Softmax层。加了两层 softmax 会导致梯度计算异常,这是很多人踩过的隐蔽坑。
训练循环里有一个容易被忽视的重点:images, labels = images.cuda(), labels.cuda()这行。模型参数也要在训练前显式移到 GPU,调用model.cuda()。如果只把数据移动了,而模型还在 CPU,报错信息里会出现 “Expected all tensors to be on the same device” 之类的内容。遇到这个问题,优先检查是不是某个模块忘了调用.cuda()。
5.4 训练过程中的优雅处理:模型保存和日志记录
训练到一半崩溃是常态,所以保存检查点和日志记录必须从一开始就养成习惯。最简单的保存方式:
torch.save(model.state_dict(), "checkpoint.pth") torch.save({ "epoch": epoch, "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "loss": loss, }, "checkpoint_full.pth")只保存模型权重和保存完整训练状态的区别在于:完整状态可以在中断后无缝恢复,甚至可以从断点继续调优学习率。如果只是测试或者最终部署,只保存权重就够了;正式训练项目,我无条件建议保存完整状态。
日志记录方面,小项目直接 print 就能看;项目跑几天甚至几周时,建议至少把每个 epoch 的损失和准确率写进一个日志文件,或者接入可视化工具。我在某项目里吃过教训:没有日志、中断后凭感觉判断效果,最后浪费了一整轮实验时间。
6. 常见报错与性能排查:我踩过的那些坑
6.1 经典报错速查表
| 报错表现 | 原因 | 解决方案 |
|---|---|---|
CUDA not available | 驱动/版本不匹配,或 CPU 版 PyTorch | 检查 nvidia-smi,重装对应 CUDA 版 PyTorch |
Expected all tensors to be on the same device | 数据和模型不在同一设备 | 统一调用.cuda()或.to(device) |
size mismatch | 全连接层输入维度算错 | 打印中间层输出的 shape,逐一核对 |
tensor with no grad_fn | 参与了非叶子张量求梯度,或运算被 no_grad 包裹 | 检查requires_grad设置,是否误用no_grad |
RuntimeError: Found no NVIDIA driver | NVIDIA 驱动没装好 | 先安装匹配的驱动,再装 PyTorch |
BrokenPipeError | Windows 下 DataLoader 多进程问题 | 设置num_workers=0 |
这个表格基本覆盖了我入门阶段 80% 以上的报错场景。
6.2 训练速度慢:先分清瓶颈在哪
训练速度慢,很多人第一反应是“该换显卡了”,但其实很多时候问题出在代码层面。排查顺序一般是这样:
先看 CPU 是否打满。如果 CPU 占用率低,说明数据加载环节不能跟上 GPU 吃掉数据的速度,这时调大num_workers,增大batch_size,或者在数据读入后做缓存;如果 CPU 占用高但 GPU 利用率低(nvidia-smi里的 Volatile GPU-Util),说明瓶颈可能在数据预处理或频繁同步开销。
再看是否有频繁的.cpu().item()调用。每次把 GPU 张量转到 CPU 都是一次同步操作,如果在一个循环里反复做这种操作,会极大拖慢速度。正确做法是在 epoch 结束时汇总损失,而不是每个 batch 都同步一次。
最后检查全局锁和随机代码路径。有些第三方库会自带隐式同步,比如某些图像增强库在 GPU 版本和 CPU 版本之间反复横跳,这种情况需要仔细审查每个 batch 的数据流向。
6.3 可复现性:为什么同一个代码两次结果不一样
深度学习训练天然带有随机性:权重初始化是随机的,DataLoader 的数据顺序是随机的,GPU 上的并行运算也可能引入细微不一致。要保证结果可复现,需要设置多个随机种子:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic = True是让 cuDNN 使用确定性算法,cudnn.benchmark = False是关闭自动调优(自动调优会尝试不同卷积算法寻找最快方案,这会让每次运行的速度和结果有微小差异)。注意确定性模式会让性能略有下降,所以只在实验需要复现时开启,正式训练时反而建议关掉。
固定随机种子还有一个好处:同事或未来的你能在同一个环境下重现实验结论。我见过不少项目因为不固定种子,前后两次训练结果差距大到无法判断模型是否真的变好了。
7. 写在最后的实际体会
装 PyTorch 和跑通基本代码,难度并不高,文档也足够详细,但真正让你区别于初学者的,是那些文档没写、必须在实战里踩过才算数的经验。我自己从第一次接触 PyTorch 到现在,最大的感受是:遇到报错时别急着搜答案,先读懂报错信息本身,再理清数据从加载到进入计算图的完整流向,大部分问题自己能定位出来。
如果你还在装环境的阶段,按照这篇博文里的顺序来:先建虚拟环境、再检查显卡、装匹配的版本、跑一遍验证代码,整个过程十分钟内能完成。安装完成之后,动手把我前面给的两个小例子敲一遍(线性回归和卷积网络训练),这两段代码跑通,你就已经拥有了做深度学习实验的基本能力。
后续你想深入的方向无非是这些:模型怎么调参、学习率怎么调度、数据增强怎么做、分布式训练怎么启动,这些都是加分项。基础这个部分,读完这篇、跑完代码,稳了。
最后再分享一个小技巧:把torch.__version__、torch.cuda.is_available()和 GPU 型号这串信息存成一个小脚本放项目目录里,换环境、换机器时第一时间跑一下,能替你省掉很多“环境看起来正常但实际不正常”的排查时间。这个习惯我用了很久,推荐你试一试。