自动微分:PyTorch 的核心魔法#
神经网络模块:搭建计算图中我们学会了构建神经网络结构。现在的问题是:如何让网络"学习"?
在 反向传播算法中,我们学习了反向传播算法的原理——通过链式法则将损失梯度从输出层传回输入层。但如果手动实现这个算法,代码会非常复杂且容易出错。
PyTorch 的自动微分(autograd)就是解决方案:它自动构建计算图并执行反向传播,让我们只需关注前向计算,梯度会自动计算。
直觉理解:自动微分是什么?#
类比:自动记账系统
想象你经营一家连锁餐厅:
手动反向传播:每道菜卖了多少钱,你需要手动追踪每一笔成本(食材、人工、租金),然后计算每个分店应该调整什么——极其繁琐且容易出错。
自动微分:你只需记录每笔交易(前向传播),系统自动生成财务报表(梯度),告诉你每个分店该如何调整。
核心洞察: 自动微分 = 自动构建计算图 + 自动执行反向传播 + 自动存储梯度
从理论到代码#
计算图的自动构建#
什么是动态计算图?#
PyTorch 使用动态计算图(Dynamic Computational Graph),这意味着:
图在运行时构建:每次前向传播都会重新构建图
图结构可以变化:支持 Python 控制流(if、for、while)
内存高效:反向传播后可以释放中间结果
关键区别:
叶子节点(Leaf Node):用户创建的张量(
requires_grad=True),梯度会保存中间节点:运算产生的张量,默认不保存梯度(除非设置
retain_graph=True)
创建可追踪的张量#
1import torch
2
3# 叶子节点:requires_grad=True 告诉 PyTorch"我要跟踪这个张量"
4x = torch.tensor([2.0, 3.0], requires_grad=True) # 输入特征
5w = torch.tensor([1.0, 2.0], requires_grad=True) # 权重
6b = torch.tensor(0.5, requires_grad=True) # 偏置
7
8print(f"x.is_leaf: {x.is_leaf}") # True:用户创建的叶子节点
9print(f"w.is_leaf: {w.is_leaf}") # True
10
11# 中间节点:通过运算产生
12z = x * w # 逐元素乘法,形状 [2]
13y = z.sum() + b # 求和后加偏置,标量
14
15print(f"z.is_leaf: {z.is_leaf}") # False:运算产生
16print(f"y.is_leaf: {y.is_leaf}") # False
梯度计算:.backward() 的魔力#
标量输出的梯度计算#
1import torch
2
3# 创建叶子节点
4x = torch.tensor(2.0, requires_grad=True) # 对应 back-propagation 中的示例
5y = torch.tensor(3.0, requires_grad=True)
6
7# 前向传播:f = (x + y) × y
8a = x + y # a = 5
9f = a * y # f = 15
10
11print(f"前向结果: f = {f.item()}") # 15.0
12
13# 反向传播:自动计算梯度
14f.backward()
15
16# 查看梯度
17print(f"∂f/∂x = {x.grad}") # 3.0(与理论一致!)
18print(f"∂f/∂y = {y.grad}") # 8.0(与理论一致!)
19
20# 梯度验证(手工计算):
21# ∂f/∂x = ∂f/∂a × ∂a/∂x = y × 1 = 3 ✓
22# ∂f/∂y = ∂f/∂a × ∂a/∂y + ∂f/∂y(直接) = y + a = 3 + 5 = 8 ✓
代码解释:
requires_grad=True:告诉 PyTorch 跟踪这个张量的所有操作.backward():从输出节点开始,沿计算图反向传播,计算所有叶子节点的梯度.grad:存储计算得到的梯度值
非标量输出的处理#
当输出不是标量时,需要指定权重向量:
import torch
# 输出是向量而非标量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2 # y = [2, 4, 6]
# 错误:y 是向量,不能直接 backward()
# y.backward() # RuntimeError!
# 正确:提供一个权重向量(相当于计算 v^T × J)
# 这里我们计算 y 每个元素对 x 的梯度的加权和
v = torch.tensor([1.0, 1.0, 1.0]) # 权重向量
y.backward(v) # 等价于计算 sum(y) 的梯度
print(f"x.grad = {x.grad}") # [2, 2, 2]
# 实际应用:通常我们会将损失降为标量
loss = y.sum() # 标量
loss.backward() # 可以直接调用
梯度控制技巧#
梯度累积与清零#
关键问题:PyTorch 的 .backward() 默认会累积梯度!
1import torch
2
3x = torch.tensor(2.0, requires_grad=True)
4
5# 第一次前向+反向
6y1 = x ** 2 # y1 = 4
7y1.backward()
8print(f"第一次反向后 x.grad = {x.grad}") # 4.0(dy1/dx = 2x = 4)
9
10# 第二次前向+反向(不清零)
11y2 = x ** 3 # y2 = 8
12y2.backward()
13print(f"第二次反向后 x.grad = {x.grad}") # 16.0!(4 + 12,累积了!)
14
15# 正确做法:每次反向前清零
16x.grad.zero_() # 清零梯度
17y3 = x ** 2
18y3.backward()
19print(f"清零后 x.grad = {x.grad}") # 4.0
训练循环中的标准模式:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for batch in dataloader:
# 1. 清零旧梯度
optimizer.zero_grad() # 或 model.zero_grad()
# 2. 前向传播
output = model(batch.input)
loss = criterion(output, batch.target)
# 3. 反向传播
loss.backward()
# 4. 更新参数
optimizer.step()
禁用梯度计算#
在某些情况下,我们不需要计算梯度:
1import torch
2
3x = torch.tensor(2.0, requires_grad=True)
4
5# 方式1:使用 torch.no_grad() 上下文管理器(推荐)
6with torch.no_grad():
7 y = x * 2
8 print(f"y.requires_grad: {y.requires_grad}") # False
9 # y.backward() # 错误!无法反向传播
10
11# 方式2:使用 .detach() 从计算图中分离
12z = x * 3
13z_detached = z.detach()
14print(f"z_detached.requires_grad: {z_detached.requires_grad}") # False
15
16# 方式3:设置 requires_grad=False(全局)
17w = torch.tensor(2.0, requires_grad=False)
18output = w * x
19print(f"output.requires_grad: {output.requires_grad}") # True(x 需要梯度)
何时禁用梯度?
场景 |
原因 |
代码 |
|---|---|---|
模型推理 |
不需要更新参数 |
|
特征提取 |
冻结预训练模型 |
|
数值计算 |
避免梯度开销 |
|
保存张量 |
避免保留计算图 |
|
实际应用:神经网络训练#
完整示例:线性回归#
import torch
import torch.nn as nn
# 生成数据:y = 2x + 1 + 噪声
torch.manual_seed(42)
X = torch.randn(100, 1) # 100个样本,1个特征
y_true = 2 * X + 1 + 0.1 * torch.randn(100, 1)
# 定义模型:对应 {doc}`../math-fundamentals/gradient-descent` 中的线性模型
model = nn.Linear(1, 1) # 输入1维,输出1维
# 损失函数和优化器
criterion = nn.MSELoss() # 均方误差
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
# 训练循环
for epoch in range(100):
# 1. 清零梯度
optimizer.zero_grad()
# 2. 前向传播
y_pred = model(X) # 计算预测值
loss = criterion(y_pred, y_true) # 计算损失
# 3. 反向传播(自动计算梯度)
loss.backward()
# 4. 更新参数
optimizer.step()
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")
# 查看学习到的参数
print(f"\n学习到的权重: {model.weight.item():.4f}(真实值: 2.0)")
print(f"学习到的偏置: {model.bias.item():.4f}(真实值: 1.0)")
查看计算图#
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2
# 查看梯度函数(对应计算图中的边)
print(f"y.grad_fn: {y.grad_fn}") # <PowBackward0 object>
print(f"y.grad_fn.next_functions: {y.grad_fn.next_functions}")
# 查看是否需要梯度
print(f"x.requires_grad: {x.requires_grad}") # True
print(f"y.requires_grad: {y.requires_grad}") # True
高级主题#
自定义梯度计算#
偶尔需要自定义梯度计算规则:
import torch
from torch.autograd import Function
class MyReLU(Function):
"""
自定义 ReLU 激活函数,带梯度裁剪
对应 {doc}`../math-fundamentals/activation-functions` 中的 ReLU
"""
@staticmethod
def forward(ctx, input):
"""前向传播:max(0, x)"""
ctx.save_for_backward(input) # 保存用于反向传播的张量
return input.clamp(min=0)
@staticmethod
def backward(ctx, grad_output):
"""反向传播:梯度裁剪"""
input, = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input[input < 0] = 0 # ReLU 的梯度规则
return grad_input
# 使用自定义函数
my_relu = MyReLU.apply
x = torch.tensor([-1.0, 2.0, -3.0, 4.0], requires_grad=True)
y = my_relu(x)
y.sum().backward()
print(f"输入: {x}")
print(f"输出: {y}")
print(f"梯度: {x.grad}") # [0, 1, 0, 1]
梯度检查#
验证梯度计算是否正确:
import torch
from torch.autograd import gradcheck
# 定义需要测试的函数
def func(x):
return x ** 3 + x ** 2
# 使用双精度浮点数进行数值梯度检查
test_input = torch.randn(3, 4, dtype=torch.double, requires_grad=True)
# 验证梯度
result = gradcheck(func, test_input, eps=1e-6, atol=1e-4)
print(f"梯度检查通过: {result}") # True 表示梯度计算正确
总结#
核心概念回顾#
概念 |
解释 |
代码 |
|---|---|---|
计算图 |
记录张量操作的 DAG |
自动构建 |
叶子节点 |
用户创建的可训练参数 |
|
反向传播 |
从输出回传梯度 |
|
梯度存储 |
存储在 |
|
梯度清零 |
避免梯度累积 |
|
禁用梯度 |
推理时节省内存 |
|
下一步#
掌握了自动微分后,下一节 优化器:用梯度更新参数 我们将学习如何使用这些梯度来更新模型参数——从简单的 SGD 到自适应学习率的 Adam,掌握优化算法的核心原理。
从"计算梯度"到"使用梯度优化",让我们继续深入!
贡献者与修订历史
查看详细修订记录
-
b20ef3e2026-04-28 - Heyan Zhu: docs: update pytorch practice section with detailed explanations and code examples -
dcecce42026-01-26 - Heyan Zhu: docs: enrich math fundamentals documentation with code captions and TikZ visualizations -
0c291d72025-12-10 - Heyan Zhu: docs: restructure course materials and add new content