在现代深度学习框架中,自动求导机制是当之无愧的核心与基石。它将开发者从繁琐的手动梯度计算中解放出来,让我们能够专注于模型架构的设计与创新。PyTorch中的autograd就是这样一个强大而灵活的引擎。本文将带你深入理解PyTorch Autograd的工作原理,从基本概念到内部机制。
Autograd 本质上是一个“自动记账和反向追责”的系统。
使用一个复杂的配方烘焙蛋糕,这个过程就是模型的前向传播(Forward Pass)。
原料(Input & Parameters): 你有面粉、鸡蛋(输入数据 x),还有糖、黄油(模型参数 w 和 b)。为了能“追责”,你给糖和黄油贴上了特殊标签:requires_grad=True
。面粉和鸡蛋不需要,因为它们是固定的,不是你想改变的。
配方步骤(Operations):
评价(Calculate Loss): 你尝了一口蛋糕,发现“太甜了!”。你计算出一个“失望分数”(Loss),比如10分(分数越高越失望)。
追责(Backward Pass): 现在,你想知道到底是谁的错导致蛋糕这么甜。于是你对“失望分数”大喊一声:loss.backward()!
Autograd 系统启动,开始反向追溯:蛋糕 -> 蛋糕糊: “蛋糕太甜,显然是蛋糕糊的问题。”
蛋糕糊 -> 甜油混合物 & 面糊: “蛋糕糊太甜,面糊本身不甜,那问题肯定出在甜油混合物上。”
甜油混合物 -> 糖 & 黄油: “甜油混合物太甜,黄油不怎么甜,那罪魁祸首一定是糖!” 同时,黄油也可能有一点点责任。
最终结论: Autograd 计算出,“糖”对“失望分数”的贡献度(梯度)是8,“黄油”的贡献度是2。这意味着,减少糖的用量是降低“失望分数”最有效的方法。
这个“贡献度”就是梯度(Gradient)。在下一次做蛋糕时,你就会根据这个梯度信息,少放点糖,可能再微调一下黄油,这就是模型优化(Optimizer.step())。
torch.Tensor: PyTorch 中的基本数据结构。它有三个关键属性与 Autograd 相关:
requires_grad: 一个布尔值。如果为 True,Autograd 会开始跟踪对此张量的所有操作。模型参数(如权重 w 和偏置 b)默认需要设置它为 True。
grad: 存储计算出的梯度值。在调用 backward() 之前,它通常是 None。backward() 执行后,对于 requires_grad=True 的张量,这个属性会被填充上梯度值。
grad_fn: 记录创建该张量的操作(函数)。如果一个张量是用户直接创建的,它的 grad_fn 为 None,这种张量被称为叶子节点(leaf node)。如果它是通过某个操作得到的,grad_fn 就会指向一个记录该操作的对象,例如 <AddBackward0>、<MulBackward0> 等。
这是 Autograd 的核心数据结构。它是一个有向无环图(DAG),用来记录操作和张量之间的关系。
图的构建(前向传播): 当你执行任何一个操作时,如果输入的张量中至少有一个设置了 requires_grad=True,PyTorch 就会动态地构建这个图。
节点(Nodes): 图中的节点是张量(Tensor)。
边(Edges): 边是产生输出张量的操作(Function),由 grad_fn 属性连接。
x (leaf) w (leaf, requires_grad=True) \ / \ / (MulBackward0) <- z.grad_fn | z = w * x | b (leaf, requires_grad=True) \ / \ / (AddBackward0) <- y.grad_fn | y
以上是一个的DAG示意图,通常输入是不需要requires_grad=True
,因为输入不需要被优化,而参数和中间变量都需要requires_grad=True
。每个一个张量如果被设置过requires_grad=True
后,之后的操作计算出来的张量默认都会有requires_grad=True
。比如图中的虽然没有设置这个参数,但是是通过乘法计算出来的中间变量,便自动会计算其梯度,目的是为了梯度顺着DAG被传递计算(链式法则)。
当你对一个标量(通常是 loss)调用 .backward() 时,Autograd 会做以下事情:
启动引擎: 从你调用 .backward() 的那个张量(比如 loss)开始。
反向遍历图: 沿着 grad_fn 链,从后往前遍历计算图。
应用链式法则: 在每个节点(操作)上,它会使用链式法则来计算梯度。例如,y 的梯度会传递给 z 和 b,然后 z 的梯度会再传递给 w 和 x。
累积梯度: 将计算出的梯度累积到各个叶子节点(leaf nodes)的 .grad 属性中。这就是为什么在每个训练迭代开始时,我们通常需要调用 optimizer.zero_grad() 来清空上一轮的梯度。
下面给出一个代码实例:
import torch # 1. 准备数据和参数 (原料) # x 是输入数据,不需要梯度 x = torch.tensor(2.0) # y_true 是真实标签 y_true = torch.tensor(5.0) # w 和 b 是模型参数,需要计算梯度来优化它们 w = torch.tensor(1.0, requires_grad=True) b = torch.tensor(1.0, requires_grad=True) print(f"w.grad: {w.grad}") # -> None, 因为还没开始追责 print(f"b.grad: {b.grad}") # -> None # 2. 前向传播 (烘焙蛋糕), 动态构建计算图 # y_pred = w * x + b y_pred = w * x + b # 打印 y_pred 的 grad_fn,可以看到它是由一个加法操作创建的 print(f"y_pred.grad_fn: {y_pred.grad_fn}") # -> <AddBackward0 object> # 3. 计算损失 (评价蛋糕) loss = (y_pred - y_true)**2 print(f"loss.grad_fn: {loss.grad_fn}") # -> <PowBackward0 object> # 4. 反向传播 (开始追责) loss.backward() # 5. 查看梯度 (追责结果) # loss = (w*x + b - y_true)^2 # d(loss)/dw = 2 * (w*x + b - y_true) * x = 2 * (1*2 + 1 - 5) * 2 = -8.0 # d(loss)/db = 2 * (w*x + b - y_true) * 1 = 2 * (1*2 + 1 - 5) * 1 = -4.0 print(f"w.grad: {w.grad}") # -> tensor(-8.) print(f"b.grad: {b.grad}") # -> tensor(-4.) # x 不需要梯度,所以 x.grad 仍然是 None print(f"x.grad: {x.grad}") # -> None # 6. 使用梯度进行优化 (决定下次怎么做蛋糕) # 这是一个简化的手动优化步骤,实际中会用 optimizer with torch.no_grad(): # 在优化步骤中,我们不希望构建计算图 w -= 0.01 * w.grad b -= 0.01 * b.grad # 清空梯度,为下一次迭代做准备 w.grad.zero_() b.grad.zero_() print(f"Updated w: {w}") print(f"Updated b: {b}")
以上代码需要注意的是:
本文作者:James
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!