01
Meta 的动态图框架:研究者为什么选它
PyTorch 是 Meta 开源的深度学习框架,如今是深度学习研究界事实上的主流——翻最近几年的顶会论文开源代码,绝大多数是 PyTorch 写的。它的气质和 TensorFlow 正相反:没有 model.fit() 这类大包大办,而是把训练循环摊开成一个普通 Python for 循环,每一步你都看得见、改得动。
这叫动态图(define-by-run):代码执行到哪,计算图就现场建到哪。好处是调试体验跟写普通 Python 完全一样——print、断点、assert 都能直接用在张量运算上;代价是要自己写训练循环(也就十几行,第 06 节手把手写)。2.0 引入的 torch.compile 把这份灵活性补上了性能:你的循环代码不用改,套一层编译就能明显提速。
适合谁:要读/复现论文代码的研究者、想真正吃透「训练到底发生了什么」的学习者、用 Hugging Face 生态(transformers 等几乎全是 PyTorch)做 NLP/多模态的工程师。只想快速出个能用的模型,Keras 那条路更短——见 TensorFlow 教程。
生态怎么配:训练主线之外,torchvision(视觉数据集、预训练模型、transforms)和 torchaudio(音频)是同一家官方扩展;NLP 与多模态则主要靠 Hugging Face 生态,其示例代码几乎清一色 PyTorch。本页会用到 torchvision 的 MNIST 数据集,见第 06 节。
- 最新稳定版
- 2.15.0(2026-11 核对)
- Python 要求
- ≥ 3.11
- 开发方
- Meta 发起,PyTorch 基金会托管
- 协议
- BSD-3 开源
- 语言
- Python(核心为 C++/CUDA)
- 执行方式
- 动态图 eager,2.0+ 可 torch.compile
- GPU 支持
- NVIDIA CUDA / AMD ROCm,Apple Silicon 走 MPS
- macOS CUDA
- 无 CUDA 版——Apple GPU 走 MPS 后端
02
安装:CPU 一条命令,GPU 看显卡
PyTorch 的安装取决于两件事:系统(macOS / Linux / Windows)和计算平台(CPU / 哪一代 CUDA)。官方在 pytorch.org/get-started/locally 做了个选择器,点几下就生成你机器对应的命令——下面是当前常见组合。
# macOS(Apple Silicon)或任何 CPU 环境:直接从 PyPI 装 pip install torch torchvision # torch 本体 + torchvision(视觉数据集与模型库,第 06 节要用) python -c "import torch; print(torch.__version__)"
torch.backends.mps.is_available() 检测),速度比纯 CPU 快。支持 macOS 10.15(Catalina)及以上,具体以官方文档为准。
# Linux + NVIDIA:从 PyTorch 官方 wheel 索引装对应 CUDA 版本 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu132 # cu132 / cu134 等标签对应不同 CUDA 版本,以官方选择器生成的命令为准
pip install torch 的 PyPI 默认 wheel 也自带 CUDA 运行库(无需在本机另装 CUDA Toolkit,体积不小)。Windows 同样用选择器生成命令;AMD 显卡看官方 ROCm 说明。装完见下一个 tab 验证。
# 版本与 CUDA 可用性一行验证 python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())" # 有 NVIDIA 卡且装对版本应输出 True; # True 却跑不了 GPU?先看驱动:nvidia-smi 有输出再回来查 torch # Apple Silicon 检查 MPS python -c "import torch; print(torch.backends.mps.is_available())"
torch.cuda.is_available() 为 False 不代表装坏了——CPU 版本来就没有 CUDA,本页所有示例都能在 CPU 上跑完。
--index-url 那段别删。装 CUDA 版遇到和本机驱动不匹配的报错,降一级 CUDA 标签重装通常比升驱动省事。03
五分钟上手:前向、反向、查梯度
PyTorch 的全部心智模型就三句话:数据是 Tensor,模型是一层层的 nn.Module,loss.backward() 自动把梯度算好写进每个参数。先用 8 行代码亲眼看到它。
# python first_grad.py import torch import torch.nn as nn model = nn.Linear(4, 2) # 一层全连接:4 维进,2 维出 x = torch.randn(3, 4) # 3 条 4 维样本 y = model(x) # 前向传播 print(y.shape) # torch.Size([3, 2]) loss = (y ** 2).mean() # 随手定义一个标量损失 loss.backward() # 反向传播:梯度已写进参数的 .grad print(model.weight.grad.shape) # torch.Size([2, 4])
两个值得注意的细节:model(x) 并不直接调用 forward,而是走 nn.Module 的 __call__——它顺带挂接了 hook 等机制,但按「调用即前向」理解不会错;loss.backward() 一次只反传一个标量,所以损失函数永远要把结果聚合成一个数(mean / sum),张量要反传得显式给出 gradient 参数。
-
01
建张量
输入数据用
torch.randn造:shape 是 (样本数, 特征数)。所有 numpy 能干的事 Tensor 都能干,还多一个「能算梯度」的超能力。 -
02
前向传播
model(x)就是调用,返回输出张量。nn.Linear(4, 2)内部有 weight 和 bias 两个参数,创建时自动随机初始化。 -
03
定义损失、backward
只要 loss 是标量张量,
loss.backward()就沿整个计算图反推,把每个参数的梯度写进它的.grad属性——这就是 autograd,你一行求导公式都不用写。 -
04
查梯度
model.weight.grad的形状和 weight 本身一样,这就是「loss 对每个权重的影响程度」。有了梯度,优化器才知道参数往哪边挪。
04
核心概念:Tensor、autograd、nn 与五步循环
Tensor:一切的起点
Tensor 是带「设备」和「类型」标签的多维数组。跟 numpy 的关系是重点:几乎无缝互转(且共享内存),区别是 Tensor 能上 GPU、能进 autograd 图。
>>> import torch, numpy as np >>> t = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # 从数据建 >>> r = torch.randn(2, 3) # 正态随机,参数初始化常用 >>> t.shape, t.dtype, t.device (torch.Size([2, 2]), torch.float32, device(type='cpu')) >>> a = np.ones((2, 2)) >>> n = torch.from_numpy(a) # 与 numpy 共享内存! >>> n[0, 0] = 9.0; a[0, 0] 9.0 >>> n.numpy() # 转回 numpy(GPU 上要先 t.cpu())
autograd:自动求导三件套
三件套是:requires_grad(声明「这个张量要追踪梯度」)、backward()(沿计算图反传)、.grad / zero_grad(读梯度、清梯度)。模型参数默认 requires_grad=True,所以日常只需记住另外两件。
>>> x = torch.tensor(3.0, requires_grad=True) >>> y = x ** 2 + 2 * x # y = x² + 2x,导数 2x + 2 >>> y.backward() >>> x.grad # x=3 处应为 8 tensor(8.) >>> x.grad.zero_() # 下一次 backward 前清零—— # .grad 是累加的,不清零梯度会越滚越大,这是新手第一大坑(见第 07 节)
nn.Module:模型的积木
每个模型都继承 nn.Module,写两个方法:__init__ 里声明有哪些子模块(层),forward 里定义数据怎么流过它们。self.xxx = nn.Linear(...) 一赋值,这些层的参数就自动注册进 model.parameters(),优化器、保存加载全靠这个注册表。
import torch.nn as nn
class MLP(nn.Module):
def __init__(self):
super().__init__() # 必调:初始化注册表
self.net = nn.Sequential( # 按顺序组一层串
nn.Flatten(), # (N, 1, 28, 28) -> (N, 784)
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
def forward(self, x): # 定义前向;model(x) 即调用它
return self.net(x)
model = MLP()
print(sum(p.numel() for p in model.parameters())) # 203530(权重 + 偏置都算上)
Dataset、DataLoader 与训练五步循环
自定义数据集实现 __len__ 和 __getitem__ 两个方法;DataLoader 接手剩下的事——组批、打乱、多进程加载。训练五步循环(前向 → 清零 → 反向 → 更新 → 记录)是 PyTorch 的「九九乘法表」,第 06 节实战里原样出现:
from torch.utils.data import Dataset, DataLoader
class MyData(Dataset):
def __len__(self): ... # 样本总数
def __getitem__(self, i): ... # 第 i 条样本,返回 (特征, 标签)
loader = DataLoader(MyData(), batch_size=64, shuffle=True)
for epoch in range(num_epochs):
for x, y in loader:
pred = model(x) # 1 前向
optimizer.zero_grad() # 2 清零(必须在 backward 前)
loss = criterion(pred, y)
loss.backward() # 3 反向
optimizer.step() # 4 更新
running += loss.item() # 5 记录(.item() 取出 python 数字)
清零放前向之后、backward 之前,顺序怎么摆都不影响正确性,但「清零在 backward 之前」是死规矩——忘了清就是梯度累积。DataLoader 的 shuffle=True 每个 epoch 重新洗牌,别自己手工打乱;数据读取慢时给 DataLoader 加 num_workers=4 开子进程并行加载(Windows/macOS 记得把主逻辑放进 if __name__ == "__main__" 保护里)。
05
API 速查:常用的就这几张表
下面这块表格能搜。忘了某个函数叫什么,直接搜 view、梯度、loss、保存 之类的词。
| API | 作用 |
|---|---|
| torch.tensor([[1., 2.]]) | 从数据创建张量 |
| torch.zeros(2, 3) / torch.ones(2, 3) | 常量张量 |
| torch.rand(2, 3) / torch.randn(2, 3) | 均匀 / 正态随机张量 |
| t.shape / t.dtype / t.device | 形状、数据类型、所在设备 |
| t.view(2, 6) / t.reshape(...) | 改形状;view 要求内存连续,reshape 更宽容 |
| t.permute(2, 0, 1) | 调换维度顺序,图像通道转换常用 |
| t.sum() / t.mean() / t.max() | 归约运算;dim= 指定沿哪个维 |
| torch.matmul(a, b) / a @ b | 矩阵乘法 |
| t.numpy() / torch.from_numpy(a) | 与 numpy 互转,共享内存;GPU 张量先 t.cpu() |
| t.to("cuda") / t.cpu() / t.to(device) | 跨设备搬运张量 |
| API | 作用 |
|---|---|
| torch.tensor(..., requires_grad=True) | 创建时打开梯度追踪 |
| x.requires_grad_(True) | 就地打开追踪(方法名末尾下划线 = 就地修改) |
| loss.backward() | 沿计算图反向传播,梯度写进各张量的 .grad |
| x.grad | 读梯度;累加特性——backward 前要先清零 |
| optimizer.zero_grad() | 训练循环里清零参数梯度的标准位置 |
| with torch.no_grad(): | 推理/评估时关闭建图,省内存又提速 |
| x.detach() | 从计算图分离出一份不带梯度的张量 |
| loss.backward(retain_graph=True) | 需要多次 backward 同一张图时保留它 |
| API | 作用 |
|---|---|
| nn.Module | 模型基类:实现 __init__(装层)与 forward(流数据) |
| nn.Linear(784, 256) | 全连接层,自带 weight 与 bias |
| nn.Conv2d(1, 32, 3) | 二维卷积层:入通道、出通道、核大小 |
| nn.ReLU() / nn.Sigmoid() | 激活函数层 |
| nn.Sequential(...) | 把一层串按顺序打包,forward 一行搞定 |
| nn.CrossEntropyLoss() | 多分类标准损失:输入原始 logits,内部含 log-softmax |
| nn.MSELoss() / nn.L1Loss() | 回归损失:均方误差 / 平均绝对误差 |
| model.parameters() | 所有可训练参数,传给优化器的就是它 |
| model.train() / model.eval() | 训练 / 评估模式切换(影响 Dropout、BatchNorm) |
| model.state_dict() | 参数字典,保存加载都走它 |
| API | 作用 |
|---|---|
| torch.utils.data.Dataset | 自定义数据集:实现 __len__ 与 __getitem__ |
| DataLoader(ds, batch_size=64, shuffle=True) | 组批、打乱、多进程加载一条龙 |
| torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) | 随机梯度下降,老牌基线 |
| torch.optim.AdamW(model.parameters(), lr=1e-3) | 多数场景的默认选择 |
| optimizer.step() | 按当前梯度更新一步参数 |
| torch.save(model.state_dict(), "model.pth") | 官方推荐:只存参数字典 |
| model.load_state_dict(torch.load("model.pth")) | 加载参数(load 建议加 map_location=device) |
| torch.device("cuda" if torch.cuda.is_available() else "cpu") | 设备无关写法,模型和数据都 .to(device) |
| torchvision.datasets.MNIST(root, train, download) | 内置视觉数据集,第一个实战标配 |
| torchvision.transforms.ToTensor() | PIL 图像转 0~1 张量 |
没有匹配的 API。换个词试试,比如「view」「梯度」「loss」「保存」。
06
实战:手写 MNIST 训练循环
MNIST:6 万张 28×28 手写数字训练图 + 1 万张测试图,10 类。这次不用 Sequential 凑合,用标准姿势:自定义 nn.Module、DataLoader、手写训练循环、评估、保存——一套全流程。目标:CPU 上几分钟,测试集准确率到 0.97 左右。
第 1 步:数据、模型、优化器
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms transform = transforms.ToTensor() # 像素 0~255 转 0~1 张量 train_ds = datasets.MNIST("data", train=True, download=True, transform=transform) test_ds = datasets.MNIST("data", train=False, download=True, transform=transform) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) test_loader = DataLoader(test_ds, batch_size=256) # 设备无关写法:有 CUDA 用 CUDA,否则 CPU(Apple Silicon 可改成 "mps") device = torch.device("cuda" if torch.cuda.is_available() else "cpu") class MLP(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Flatten(), # (N, 1, 28, 28) -> (N, 784) nn.Linear(784, 128), nn.ReLU(), nn.Dropout(0.2), # 防过拟合 nn.Linear(128, 10), # 输出 10 类的 logits ) def forward(self, x): return self.net(x) model = MLP().to(device) # 模型搬上设备 criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
第 2 步:手写训练循环(五步循环原样出现)
for epoch in range(3):
model.train() # 训练模式:Dropout 生效
running = 0.0
for x, y in train_loader:
x, y = x.to(device), y.to(device) # 数据搬上同一设备
pred = model(x) # 1 前向
optimizer.zero_grad() # 2 清零(不清会累积)
loss = criterion(pred, y)
loss.backward() # 3 反向
optimizer.step() # 4 更新
running += loss.item() * len(y) # 5 记录本批 loss
print(f"epoch {epoch + 1} loss {running / len(train_ds):.4f}")
# epoch 1 loss 0.3901(示意输出,数值随随机初始化波动)
# epoch 2 loss 0.1967(示意输出)
# epoch 3 loss 0.1453(示意输出)
CrossEntropyLoss 的输入是原始 logits(没过 softmax 的输出),别自己在 forward 里加 softmax——加了反而错,它内部已经含 log-softmax。loss 乘 len(y) 再除以总样本数,是按样本数加权平均的标准写法。
第 3 步:评估、保存、加载
model.eval() # 评估模式:Dropout 关闭 correct = 0 with torch.no_grad(): # 不建计算图:省内存、更快 for x, y in test_loader: x, y = x.to(device), y.to(device) pred = model(x).argmax(dim=1) # logits 最大者当预测类别 correct += (pred == y).sum().item() print(f"测试集准确率 {correct / len(test_ds):.3f}") # 0.97x 左右(示意输出) # 保存:只存参数字典(官方推荐),换环境加载不依赖代码路径 torch.save(model.state_dict(), "mnist_mlp.pth") # 加载:先有同结构模型,再灌参数 model2 = MLP().to(device) model2.load_state_dict(torch.load("mnist_mlp.pth", map_location=device)) model2.eval()
第 4 步:Apple Silicon 上换 MPS 提速
Mac 上没有 CUDA,但 Apple Silicon 的 GPU 可以走 MPS 后端——PyTorch 把它抽象成又一个 device 字符串,训练循环一行都不用改:
# CUDA / MPS / CPU 三选一,替换第 1 步里的 device 那行即可 if torch.cuda.is_available(): device = torch.device("cuda") elif torch.backends.mps.is_available(): device = torch.device("mps") else: device = torch.device("cpu") # model.to(device)、x.to(device) 原样保留——这就是 device 无关写法的意义
个别算子在 MPS 上尚未实现时会回退 CPU 或报错,官方文档「MPS backend」页列了限制清单;遇到疑难先把 device 换回 CPU 复现问题,再去查文档。
所有 epoch 输出数字均为示意输出:参数初始化带随机性,不同机器结果会小幅波动;准确率稳定落在 0.96~0.98 区间。想接着练手:把 MLP 换成两轮 Conv2d + MaxPool2d 的小 CNN,同样训练循环不用改,准确率还能再上一个台阶。
07
避坑:梯度、eval、device 与保存
坑一:忘了 optimizer.zero_grad(),梯度越滚越大
PyTorch 的 .grad 是累加设计(方便梯度累积等高级用法),但日常训练每一步都从零开始算——忘了清零,第 2 轮的梯度 = 第 1 轮 + 第 2 轮之和,loss 曲线会剧烈震荡甚至发散。症状:明明 lr 正常,loss 却越训越大。
# 错误写法:只有 forward + backward,梯度永远在叠加 pred = model(x); loss.backward(); optimizer.step() # 正确:backward 之前清零(set_to_none=True 是默认行为,直接释放旧梯度) optimizer.zero_grad() # 放 backward 之前,一步都不能省 pred = model(x); loss.backward(); optimizer.step()
坑二:评估时忘 model.eval() 和 torch.no_grad()
这两件事管的是不同开关,经常一起用所以容易漏。eval() 切模式:让 Dropout 停止随机置零、BatchNorm 用全局统计——漏了它,评估结果每个 batch 都在抖;no_grad() 切计算图:评估不需要梯度,漏了它白占内存还拖慢速度。
model.eval()
with torch.no_grad():
... # 评估、推理都放这里
model.train() # 回到训练循环前切回来,别一路 eval 到底
with torch.no_grad() 却没见过 model.eval(),多半漏了;反过来只有 eval 没有 no_grad,评估会莫名变慢、显存爬升。坑三:模型和数据不在同一个设备
经典报错 Expected all tensors to be on the same device:模型 .to(device) 了,数据没搬;或者 GPU 上算完的结果直接 .numpy()(numpy 只认 CPU)。规则一句话:模型、输入、标签三者同设备。
model = MLP().to(device) # 模型上设备 x, y = x.to(device), y.to(device) # 输入和标签也上同一设备 result = model(x) result.cpu().numpy() # GPU 张量先 .cpu() 再转 numpy # 排查口诀:print(t.device) 逐个看,找到 cpu 的那个搬过去
坑四:torch.save(model) 整个存,换个目录就加载失败
torch.save(model) 用 pickle 序列化整个对象,里面记着「模型类在哪个文件哪个路径」——换个目录/环境加载就报 AttributeError,官方明确不推荐。标准姿势是 state_dict:只存参数字典,加载时模型结构由你自己的代码定义。
# 不推荐:torch.save(model, "m.pt") —— 绑死类的定义位置 # 推荐:存 state_dict,加载方自己 import 模型类 torch.save(model.state_dict(), "mnist_mlp.pth") model2 = MLP() # 结构在本项目里现建 model2.load_state_dict(torch.load("mnist_mlp.pth", map_location=device)) model2.eval() # 推理场景没有梯度,load 时配 map_location 指定设备,跨机器最稳
08
进阶:官方教程与生态入口
本页只覆盖训练主线,下面这些是官方入口,按需取用:
- 官方教程库 —— 从 Learn the Basics 到分布式训练的成套教程,持续更新。
- API 文档 —— torch / autograd / nn / optim 的完整参考。
- 安装选择器 —— 选 OS + 包管理器 + CUDA 版本,生成你机器的安装命令。
- PyTorch 2.x 特性总览 —— torch.compile 等新一代加速特性的官方说明。
- 60 分钟闪电战 —— 官方入门四连:Tensor、autograd、神经网络、分类实战。
- torchvision 文档 —— 视觉数据集、预训练模型、transforms 全家桶。
- GitHub 仓库 —— Release Notes 追版本变化(torch.compile 的新特性也在这发布)。
09
收尾:照着勾一遍
0 / 5