1. 从零上手 PyTorch:先搞清楚它到底在解决什么问题
很多人第一次接触 PyTorch,是被“深度学习框架”这四个字吓住的,觉得一定要先啃完线性代数、微积分、概率论才能动手。其实不是。PyTorch 最核心的价值,是把“定义计算”和“自动求导”这两件事变得像写普通 Python 一样自然。你不需要先成为数学家,才能跑通第一个模型。
我刚开始用的时候,最大的困惑不是 API 怎么调,而是“为什么要有框架”。后来想明白了:如果只用 NumPy 手写神经网络,前向传播还好说,反向传播的梯度推导能把人逼疯,而且每改一次网络结构,梯度公式就得重推一遍。PyTorch 的autograd机制把这件事彻底自动化了——你只管定义前向计算,反向梯度它帮你算。这就是它最根本的吸引力。
这篇文章适合三类人:一是刚学完 Python 基础、想入门深度学习的同学;二是用过其他框架、想快速迁移到 PyTorch 的开发者;三是跑过别人的 demo,但自己从头搭网络时总是报错、不知道从哪下手的人。我会从环境搭建讲到张量操作、自动求导、模型定义、训练循环,再到实际踩过的坑,尽量把每个“为什么”讲清楚,而不是只丢一堆代码。
需要先说明一点:PyTorch 的版本迭代很快,API 在不同版本间会有细微差异。我下面讲的内容基于近几个稳定版本的通用的做法,如果你用的是很老的版本,个别接口可能对不上,建议先升级到较新的稳定版。另外,本文不涉及任何需要特殊网络环境才能完成的操作,所有安装和依赖都走常规的包管理渠道即可。
2. 环境搭建:别一上来就装 CUDA 版本
2.1 先确认你的硬件和系统底子
装 PyTorch 之前,先花两分钟确认三件事:操作系统、Python 版本、有没有独立显卡。这三件事直接决定你该装哪个版本的包。
操作系统方面,Windows、macOS、Linux 都支持。Python 版本建议 3.9 到 3.11 之间,太老的版本很多新特性用不了,太新的版本有时候第三方库还没跟上。显卡方面,如果你有支持 CUDA 的独立显卡,可以装 GPU 版本;如果没有,或者只是先学着玩,装 CPU 版本完全够用。
我见过太多新手一上来就折腾 GPU 版本,结果显卡驱动、CUDA 版本、cuDNN 版本三者对不上,卡在安装环节一整天,学习热情直接浇灭。所以我的建议很明确:第一次装,先装 CPU 版本,把流程跑通,再考虑 GPU。
2.2 CPU 版本的安装其实一行命令
CPU 版本的安装非常干净,不依赖任何显卡驱动。用 pip 的话,直接:
pip install torch torchvision torchaudio如果你用 conda,也可以:
conda install pytorch torchvision torchaudio -c pytorch装完之后验证一下:
import torch print(torch.__version__) print(torch.cuda.is_available())CPU 版本下torch.cuda.is_available()会返回False,这是正常的,不用慌。只要import torch不报错,版本号能打印出来,环境就没问题。
2.3 GPU 版本安装前必须核对的三件事
如果你确实要用 GPU,安装前必须核对:显卡驱动版本、CUDA 运行时版本、PyTorch 对应的 CUDA 编译版本。这三者不是随便搭配的。
一个常见的误区是:以为装了最新版 CUDA 就一定最好。实际上 PyTorch 官方发布的包是针对特定 CUDA 版本编译的,你本机的驱动要能支持这个 CUDA 版本才行。驱动版本太低,装了也用不了。
我的经验做法是:先去 PyTorch 官网的安装选择器里,根据你的系统选好配置,它会直接给你一条对应的安装命令。不要自己凭记忆拼命令,版本号错一位就可能装成 CPU 版或者装不上。
装完 GPU 版本后,验证要更仔细:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果is_available()返回True,并且能打印出显卡型号,才算真正成功。如果返回False,八成是驱动和 CUDA 版本不匹配,这时候别急着重装,先查驱动版本。
提示:安装过程中如果遇到下载慢的问题,可以配置国内镜像源来加速,这是常规的包管理操作,和特殊网络工具无关。
2.4 虚拟环境:强烈建议,不是可选项
不管你是学 PyTorch 还是做其他 Python 项目,我都强烈建议用虚拟环境。原因很简单:不同项目依赖的库版本可能冲突,全局安装迟早会把环境搞乱。
用 venv 的话:
python -m venv pytorch_env # Windows pytorch_env\Scripts\activate # macOS / Linux source pytorch_env/bin/activate激活之后再装 PyTorch,所有依赖都隔离在这个环境里。哪天搞坏了,删掉重建就行,不影响系统 Python。这个习惯看起来麻烦,实际上能帮你省下大量排查环境冲突的时间。
3. 张量:PyTorch 里一切数据的基本形态
3.1 张量和 NumPy 数组到底差在哪
如果你用过 NumPy,那张量(Tensor)理解起来会很快——它俩长得很像,都是多维数组。但张量多了两个关键能力:一是可以在 GPU 上运行,二是支持自动求导。
创建张量的方式很多,最常用的几种:
import torch # 从列表创建 a = torch.tensor([1, 2, 3]) # 全零、全一 b = torch.zeros(2, 3) c = torch.ones(2, 3) # 随机初始化 d = torch.randn(2, 3) # 从 NumPy 转换 import numpy as np e = torch.from_numpy(np.array([1, 2, 3]))这里有个细节值得说:torch.tensor()和torch.Tensor()不是一回事。前者会根据输入数据推断类型,后者默认创建浮点型。新手经常在这上面踩坑,明明传进去的是整数,结果类型不对导致后续报错。养成习惯,创建时明确指定dtype,比如torch.tensor([1, 2, 3], dtype=torch.float32)。
3.2 形状操作是最高频的操作
实际写模型时,张量形状对不上是最常见的报错来源。所以形状相关的操作必须熟练。
常用的几个:
view()和reshape():改变形状。区别在于view()要求内存连续,reshape()更宽容。squeeze()和unsqueeze():去掉或增加维度。permute()和transpose():交换维度顺序。cat()和stack():拼接张量。
举个实际场景:一张 RGB 图片读进来是(H, W, C)格式,但 PyTorch 的卷积层要求(C, H, W),这时候就要用permute调整维度顺序。再比如单张图片要送进网络,需要从(C, H, W)变成(1, C, H, W),加一个 batch 维度,用unsqueeze(0)。
我踩过的一个坑是:用view()调整形状时,如果张量在内存里不连续,会直接报错。解决办法是先.contiguous()再view(),或者干脆用reshape()。这个坑在涉及transpose或permute之后特别容易遇到。
3.3 设备迁移:CPU 和 GPU 之间的搬运
张量默认在 CPU 上。要用 GPU,得显式搬过去:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") a = torch.randn(3, 3).to(device)这里有个高频错误:模型在 GPU 上,数据在 CPU 上,一运算就报错。所以训练前一定要确保模型和数据在同一个设备上。我通常会在代码开头定义好device,然后模型.to(device),每个 batch 的数据也.to(device),形成固定习惯。
还有一个容易忽略的点:从 GPU 取回数据到 CPU 时,如果这个张量需要梯度,不能直接转 NumPy,得先.detach().cpu().numpy()。这个链条我建议直接背下来,因为调试时经常要用。
3.4 广播机制:方便但也是坑
张量运算支持广播(broadcasting),就是形状不同的张量在某些条件下可以自动对齐。比如(3, 1)和(1, 4)相加会得到(3, 4)。
广播让代码简洁,但也容易埋雷。有时候你以为形状对上了,其实是广播悄悄帮你扩展了,结果算出来的东西根本不是你要的。我的建议是:在关键运算前,打印一下张量形状,确认符合预期再往下走。这个习惯能帮你省下大量调试时间。
4. 自动求导:PyTorch 最值得理解透的机制
4.1 为什么需要自动求导
训练神经网络本质上是“前向算损失,反向算梯度,然后更新参数”。反向传播要算损失对每个参数的偏导数。如果手动推导,网络一复杂就没法维护。自动求导就是把这个过程自动化。
PyTorch 的做法是:记录你所有对张量的操作,构建一张计算图,反向传播时沿着图链式求导。你只需要在前向计算时正常写代码,反向时调一句.backward()。
4.2 requires_grad 和计算图的构建
要让一个张量参与求导,得设置requires_grad=True:
x = torch.tensor([2.0], requires_grad=True) y = x ** 2 + 3 * x y.backward() print(x.grad) # 输出 2*x + 3 = 7这里y是由x计算出来的,PyTorch 自动记录了y = x² + 3x这个关系。调backward()后,x.grad就是导数。
关键理解点:计算图是在前向计算时动态构建的。这也是 PyTorch 被称为“动态图”框架的原因。每次前向都会重新建图,所以你可以用普通的 Python 控制流(if、for)来写模型,非常灵活。
4.3 梯度累积和清零:新手最容易忘的一步
这是 PyTorch 训练里最经典的坑:梯度会累积。
每次调backward(),梯度是累加到.grad上的,不是覆盖。所以一个训练循环里,必须在反向传播前把上一轮的梯度清零:
optimizer.zero_grad() loss.backward() optimizer.step()顺序不能乱。如果忘了zero_grad(),梯度会越加越大,模型直接训崩。我见过不少人调了半天超参数,最后发现是忘了清零。
反过来说,梯度累积这个特性也能被利用。如果你想用更大的等效 batch size,但显存不够,可以攒几个 batch 的梯度再更新一次,这就是“梯度累积”技巧。原理就是故意不清零,攒够了再step()。
4.4 detach 和 no_grad:什么时候该切断梯度
有些场景我们不希望梯度继续传下去。比如:
- 计算评估指标时,不需要梯度。
- 从网络中间取特征做别的事,不想影响原网络。
这时候用torch.no_grad()上下文管理器:
with torch.no_grad(): output = model(input)它比.detach()更彻底,整个块内的操作都不建图,省显存也省时间。验证和测试阶段一定要包在no_grad()里,否则显存会白白被计算图占满。
.detach()则是针对单个张量,把它从计算图里摘出来,后续操作不再记录梯度。两者用途不同,别混用。
5. 搭一个完整模型:从定义到训练跑通
5.1 nn.Module 的固定套路
PyTorch 定义模型的标准方式是继承nn.Module,在__init__里定义层,在forward里定义计算流程:
import torch.nn as nn class SimpleNet(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 128) self.fc2 = nn.Linear(128, 10) self.relu = nn.ReLU() def forward(self, x): x = self.relu(self.fc1(x)) x = self.fc2(x) return x两个必须记住的点:一是__init__里必须先调super().__init__(),否则会报错;二是层必须定义为实例属性,不能定义成局部变量,否则参数不会被注册,优化器找不到它们。
5.2 损失函数和优化器的选择逻辑
损失函数看任务类型:分类用交叉熵nn.CrossEntropyLoss,回归用均方误差nn.MSELoss。注意CrossEntropyLoss内部已经包含了 softmax,所以网络最后一层不要再加 softmax,否则重复了。
优化器最常用的是 Adam 和 SGD。Adam 对学习率不那么敏感,适合快速起步;SGD 配合动量在调好的情况下泛化可能更好,但需要更细致地调学习率。我的建议是:先用 Adam 跑通,再考虑换 SGD 调优。
criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)5.3 训练循环的骨架
一个标准训练循环长这样:
for epoch in range(num_epochs): model.train() for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step()model.train()和model.eval()的切换很重要。有些层(比如 Dropout、BatchNorm)在训练和推理时行为不同,必须正确切换模式。验证时:
model.eval() with torch.no_grad(): for inputs, labels in val_loader: ...我踩过的坑是:验证完忘了切回model.train(),结果下一轮训练时 Dropout 没生效,训练效果异常。所以养成习惯,训练前train(),验证前eval(),成对出现。
5.4 保存和加载模型
保存有两种方式:保存整个模型,或只保存参数。推荐只保存参数(state_dict),因为保存整个模型会把类定义也序列化进去,换环境容易出问题。
torch.save(model.state_dict(), "model.pth") # 加载 model = SimpleNet() model.load_state_dict(torch.load("model.pth")) model.to(device)加载时要注意:先实例化模型结构,再load_state_dict,最后搬到设备上。顺序错了可能报错或者参数没加载上。
6. 那些让我卡了半天的报错和排查思路
6.1 形状不匹配:从报错信息里读出关键信息
最常见的报错就是形状不匹配。PyTorch 的报错信息其实挺详细,会告诉你期望的形状和实际的形状。关键是别慌,先看两个数字差在哪。
排查思路:在报错的那一层前面打印输入形状,确认是不是自己以为的形状。十有八九是某个view或permute写错了,或者 batch 维度没处理好。
6.2 设备不一致:模型和数据分居两地
报错信息里出现Expected all tensors to be on the same device,就是设备和数据不在同一处。检查模型.to(device)了没,数据.to(device)了没。特别是自己新加的张量(比如手动构造的 mask),很容易忘了搬。
6.3 显存不够:先降 batch size,再查泄漏
显存不够(OOM)时,第一反应是降 batch size。如果降了还不行,就要查是不是验证阶段没包no_grad(),导致计算图一直累积。另一个常见原因是把带梯度的张量存进了列表,整个训练过程都在累积图。排查时可以用工具看显存占用,定位是哪一步涨上去的。
6.4 损失不下降:从数据和标签查起
模型训不动,先别急着调网络结构。按这个顺序查:数据有没有正确加载、标签有没有对错位、学习率是不是太大或太小、损失函数和任务是否匹配。我遇到过标签和输入没对齐的情况,模型怎么训都不收敛,查了半天才发现是数据加载环节的问题。
7. 我个人的几条实操心得
第一,先把 CPU 版本跑通,再上 GPU。这个顺序能帮你排除掉大量环境问题,把精力集中在代码逻辑上。
第二,每个关键步骤都打印形状和类型。调试深度学习代码,形状和类型对不上占了报错的一大半,提前打印比事后猜快得多。
第三,训练循环的骨架固定下来,别每次重写。zero_grad、backward、step这三步的顺序,train和eval的切换,写成模板,减少低级错误。
第四,小数据先过拟合。拿几十条数据,看模型能不能把训练集拟合到接近零损失。如果能,说明模型和训练流程没问题,再上大数据;如果不能,问题出在代码而不是数据量。
第五,善用官方文档和示例。PyTorch 的官方教程质量很高,遇到不确定的 API,直接查文档比搜零散的博客靠谱。版本对应关系也以官方为准。
这套流程走下来,从环境到训练再到排错,基本能覆盖入门阶段会遇到的大部分情况。剩下的就是多写、多跑、多踩坑,经验这东西,看再多不如自己动手跑一遍。