MACHINE LEARNING · PYTORCH · 研究者最爱的动态图框架

PyTorch:像写 Python 一样写深度学习

这页带你把 PyTorch 2.x 装起来、跑起来:Tensor 当数组用、autograd 自动求导、nn.Module 搭积木,最后手写一个完整的 MNIST 训练循环——五步循环背下来,任何深度学习论文代码你都能读懂骨架。

zsh — ~/ml-playground
$ pip install torch torchvision
Successfully installed torch-2.15.0 …
$ python
>>> import torch; torch.__version__
'2.15.0'
>>> torch.rand(2, 3)
tensor([[0.3380, 0.3845, 0.3217], [0.8337, 0.9050, 0.2650]])(示意输出) 装好了,先在 CPU 上跑
本页目录

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 后端
为什么研究者偏爱它:模型就是一个 Python 类(nn.Module),训练循环就是 for 循环,没有隐藏的执行引擎。读别人的代码 = 读普通的面向对象 Python。这份「所见即所得」让新想法从论文到代码的路径最短,也让 2.x 的 torch.compile 有底气承诺「代码不用改,跑起来更快」。

02

安装:CPU 一条命令,GPU 看显卡

PyTorch 的安装取决于两件事:系统(macOS / Linux / Windows)和计算平台(CPU / 哪一代 CUDA)。官方在 pytorch.org/get-started/locally 做了个选择器,点几下就生成你机器对应的命令——下面是当前常见组合。

Terminal
# macOS(Apple Silicon)或任何 CPU 环境:直接从 PyPI 装
pip install torch torchvision

# torch 本体 + torchvision(视觉数据集与模型库,第 06 节要用)
python -c "import torch; print(torch.__version__)"
macOS 的 wheel 是 CPU 构建,跑训练走 Apple Silicon 的 MPS 后端(torch.backends.mps.is_available() 检测),速度比纯 CPU 快。支持 macOS 10.15(Catalina)及以上,具体以官方文档为准。
pip 源与体积提醒:CUDA 版 wheel 有几个 GB,下不动先确认网络;公司内网/国内环境常配 pip 镜像,但 PyTorch 的 CUDA wheel 只在官方索引上,--index-url 那段别删。装 CUDA 版遇到和本机驱动不匹配的报错,降一级 CUDA 标签重装通常比升驱动省事。

03

五分钟上手:前向、反向、查梯度

PyTorch 的全部心智模型就三句话:数据是 Tensor,模型是一层层的 nn.Module,loss.backward() 自动把梯度算好写进每个参数。先用 8 行代码亲眼看到它。

first_grad.py
# 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 对每个权重的影响程度」。有了梯度,优化器才知道参数往哪边挪。

这就是 PyTorch 的骨架:前向算结果,backward 算梯度。剩下的「用梯度更新参数、循环多少轮」在第 06 节补全——补全后就是完整的训练循环。跟 TensorFlow 的 GradientTape 对应着看,概念一一映射。

04

核心概念:Tensor、autograd、nn 与五步循环

Tensor:一切的起点

Tensor 是带「设备」和「类型」标签的多维数组。跟 numpy 的关系是重点:几乎无缝互转(且共享内存),区别是 Tensor 能上 GPU、能进 autograd 图。

python
>>> 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,所以日常只需记住另外两件。

python
>>> 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(),优化器、保存加载全靠这个注册表。

mlp.py
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 节实战里原样出现:

train_loop.py · 五步骨架
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)跨设备搬运张量

06

实战:手写 MNIST 训练循环

MNIST:6 万张 28×28 手写数字训练图 + 1 万张测试图,10 类。这次不用 Sequential 凑合,用标准姿势:自定义 nn.Module、DataLoader、手写训练循环、评估、保存——一套全流程。目标:CPU 上几分钟,测试集准确率到 0.97 左右。

第 1 步:数据、模型、优化器

mnist_train.py · 准备
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 步:手写训练循环(五步循环原样出现)

mnist_train.py · 训练
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 步:评估、保存、加载

mnist_train.py · 评估与保存
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 字符串,训练循环一行都不用改:

mnist_train.py · 设备探测三选一
# 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 却越训越大。

python
# 错误写法:只有 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() 切计算图:评估不需要梯度,漏了它白占内存还拖慢速度。

python
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)。规则一句话:模型、输入、标签三者同设备。

python
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:只存参数字典,加载时模型结构由你自己的代码定义。

python
# 不推荐: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 的新特性也在这发布)。
学完下一步去哪:① 拿 torchvision 的 CIFAR-10 把 MLP 换成 CNN,练完对卷积的手感;② 读 Hugging Face 课程,transformers 生态全是 PyTorch 代码;③ 回 TensorFlow 教程 用 Keras 重写同一个 MNIST,体会「高层 API」和「手写循环」两条路线的取舍;④ 模型能跑了再回头看 torch.compile 和 AMP 混合精度加速。

09

收尾:照着勾一遍

0 / 5

已复制