深度学习
PyTorch 入门:张量、自动求导(autograd)与标准训练循环
进阶 25 分钟 适合: 开发者 / 研究者
本课要点
- ▸张量(tensor)就是能放到 GPU 上、能自动求导的 NumPy 数组,写法几乎一样。
- ▸给张量设 requires_grad=True,PyTorch 会在前向计算时记录一张计算图;调用 loss.backward() 就用链式法则把梯度一路算回参数,存进 .grad。
- ▸PyTorch 的计算图是动态的:每次前向都重新建图,所以可以直接用 Python 的 if 和 for 写模型。
- ▸标准训练循环五步:前向、算损失、optimizer.zero_grad()、loss.backward()、optimizer.step()。梯度默认会累加,所以每步要清零。
- ▸评估时用 model.eval() 切换 Dropout、BatchNorm 的行为,再用 torch.no_grad() 关掉梯度记录,省内存也更快。
一句话回答
PyTorch 是主流的深度学习框架之一,本站大多数代码练习(nanoGPT、TRL、micrograd 的对照实现等)都用它。学会它只需要抓住两点:张量会自动记录计算过程,loss.backward() 能自动算出所有参数的梯度;以及一个五步的标准训练循环。本课属于 PyTorch 入门与自动求导 单元,前置是 Python 与 NumPy。
第一步:张量与设备
张量(tensor)的用法和 NumPy 数组几乎一样:有形状、数据类型,支持广播和矩阵运算。区别在两点:它可以放到 GPU 上计算,并且能自动求导(官方教程)。
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
a = torch.randn(3, 4, device=device)
b = torch.randn(4, 2, device=device)
c = a @ b # 矩阵乘法,形状 (3, 2)
参与同一次运算的张量必须在同一个设备上,否则会报错。这是新手最常见的错误之一。安装方法按 官方安装页 选择系统和 CUDA 版本,不要随手 pip install 一个不匹配的版本。
第二步:autograd 是怎么算梯度的
训练神经网络就是反复“算梯度、改参数”。手推每一层的导数既慢又容易错,自动微分(automatic differentiation)把它交给程序:前向计算时记下每一步运算,再沿相反方向用链式法则逐步求导。这种“反向模式”自动微分正是神经网络里 反向传播 的一般形式(Baydin 等,2015)。
在 PyTorch 里只要把需要求导的张量标成 requires_grad=True:
import torch
x = torch.tensor(2.0, requires_grad=True)
w = torch.tensor(3.0, requires_grad=True)
y = w * x ** 2 + 1 # y = 3·4 + 1 = 13
y.backward()
print(x.grad, w.grad) # dy/dx = 2wx = 12,dy/dw = x² = 4
print(y.grad_fn) # <AddBackward0 ...>:y 是由加法得到的
官方教程讲了几个要点(autograd 教程、Autograd mechanics):
- 前向时,autograd 把运算记成一张有向无环图(DAG),每个结果张量的
grad_fn指向生成它的运算。 backward()从结果出发,沿图用链式法则算到叶子张量(你自己创建的参数),梯度存进叶子的.grad。- 图是动态的:每次
backward()之后,下一次前向会从头重建计算图。所以模型里可以直接写 Python 的if、for,每一步走的分支不同也没关系。PyTorch 论文把这种风格称为“命令式”:模型就是一段普通的 Python 程序(Paszke 等,2019)。 - 梯度默认会累加,不会自动清零:
w = torch.tensor(1.0, requires_grad=True)
for i in range(2):
(w * 3).backward()
print(w.grad) # 第一次 3,第二次 6:累加了
想看清 autograd 在底层做了什么,最好的办法是跟着 Karpathy 的 micrograd 自己写一个:它的自动求导引擎约 100 行,神经网络库约 50 行,接口模仿 PyTorch。
第三步:标准训练循环
把上一课用 NumPy 手写梯度的线性回归改成 PyTorch,梯度那两行就消失了:
import torch
from torch import nn
torch.manual_seed(0)
X = torch.rand(200, 1)
y = 3 * X + 2 + 0.1 * torch.randn(200, 1)
model = nn.Linear(1, 1)
loss_fn = nn.MSELoss()
opt = torch.optim.SGD(model.parameters(), lr=0.5)
for step in range(500):
pred = model(X) # 1. 前向
loss = loss_fn(pred, y) # 2. 算损失
opt.zero_grad() # 3. 清空上一步的梯度
loss.backward() # 4. 反向求梯度
opt.step() # 5. 按梯度更新参数
print(model.weight.item(), model.bias.item()) # 接近 3 和 2
这五步就是 PyTorch 官方教程里的优化循环。教程特别说明:梯度默认会累加,为了避免重复计算,每次迭代都要显式清零(Optimizing Model Parameters)。大模型的预训练和微调,本质上也是这五步,只是模型更大、数据更多、跑在多张 GPU 上。
第四步:完整一点的写法(数据加载、Adam 与评估)
真实项目里,数据要分批(mini-batch)送进模型,优化器多用 Adam。Adam 根据梯度的一阶矩和二阶矩估计自适应地调整每个参数的步长(Kingma & Ba,2014)。
import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader
device = "cuda" if torch.cuda.is_available() else "cpu"
X = torch.randn(1000, 20)
y = (X[:, 0] + X[:, 1] > 0).long() # 一个简单的二分类任务
loader = DataLoader(TensorDataset(X, y), batch_size=64, shuffle=True)
model = nn.Sequential(
nn.Linear(20, 64), nn.ReLU(), nn.Dropout(0.1), nn.Linear(64, 2)
).to(device)
loss_fn = nn.CrossEntropyLoss()
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(5):
model.train() # 训练模式:Dropout 生效
for xb, yb in loader:
xb, yb = xb.to(device), yb.to(device)
loss = loss_fn(model(xb), yb)
opt.zero_grad()
loss.backward()
opt.step()
model.eval() # 评估模式:Dropout 关闭
with torch.no_grad(): # 不记录计算图
acc = (model(X.to(device)).argmax(1) == y.to(device)).float().mean().item()
print(epoch, round(acc, 3))
几个要点:
Dataset和DataLoader:Dataset负责“第 i 个样本是什么”,DataLoader负责分批、打乱和并行读取(数据加载教程)。model.train()/model.eval():Dropout 和 BatchNorm 在训练和推理时行为不同,官方教程在训练和测试函数里分别切换模式。torch.no_grad():评估和推理时不需要梯度,包在这个块里可以不记录计算图,省内存、也更快。- 可复现:设
torch.manual_seed,但在不同硬件和版本上,完全一致的结果并不保证(Reproducibility)。
常见坑
| 现象 | 常见原因 | 怎么办 |
|---|---|---|
| loss 忽高忽低或越训越大 | 忘了 zero_grad,梯度在累加;或学习率太大 |
检查循环五步顺序;把学习率调小 10 倍试试 |
Expected all tensors to be on the same device |
模型在 GPU、数据在 CPU(或相反) | 模型 .to(device),每个 batch 也 .to(device) |
| 推理时显存不断上涨 | 没用 no_grad,或把带图的 loss 存进列表 |
推理包进 torch.no_grad();记录数值用 loss.item() |
| 评估结果每次都不一样 | 忘了 model.eval(),Dropout 还开着 |
评估前切到 eval,训练前切回 train |
.grad 是 None |
张量不是叶子,或没设 requires_grad |
只看参数的 .grad;中间结果需要时用 retain_grad() |
动手试试
- 跟着官方 60 分钟入门 训练第一个图像分类器。
- 做 Tensor Puzzles 的 21 道题,只用广播和基本运算实现常见张量函数。
- 本单元项目:跟着 micrograd 从零写一个约 100 行的自动求导引擎,再用 PyTorch 写同一个两层网络,对比两边算出的梯度是否一致。
- 想要中文、可直接运行的讲解,读《动手学深度学习》的 自动微分 一节(中文版见 zh.d2l.ai)。
学完这一课,就可以进入 神经网络是怎么工作的,看清每一层在做什么。
常见问题
PyTorch 和 TensorFlow/JAX 选哪个?
都能用。PyTorch 的写法是“命令式”的,模型就是普通 Python 程序,调试方便;本站学 AI 路线里的代码练习大多基于 PyTorch,跟着路线学选它最省事。
没有 GPU 能学 PyTorch 吗?
能。本课的例子在 CPU 上几秒就跑完。需要 GPU 时用 Google Colab,代码里只要写 device = 'cuda' if torch.cuda.is_available() else 'cpu'。
autograd 和反向传播是一回事吗?
反向传播是反向模式自动微分在神经网络上的特例。autograd 是 PyTorch 对反向模式自动微分的通用实现,任何由张量运算组成的程序都能求导,不限于神经网络。
为什么我的 loss 不下降?
先查三件事:有没有每步 zero_grad;学习率是否过大或过小;数据和标签是否对齐(打印一个 batch 看看形状和取值)。