2025-08-23
算法
0

目录

一个形象的比喻
技术拆解:Autograd 是如何工作的?
1. 核心组件:
2. 计算图(Computation Graph)
3. 梯度的计算(反向传播)

在现代深度学习框架中,自动求导机制是当之无愧的核心与基石。它将开发者从繁琐的手动梯度计算中解放出来,让我们能够专注于模型架构的设计与创新。PyTorch中的autograd就是这样一个强大而灵活的引擎。本文将带你深入理解PyTorch Autograd的工作原理,从基本概念到内部机制。

Autograd 本质上是一个“自动记账和反向追责”的系统。

一个形象的比喻

使用一个复杂的配方烘焙蛋糕,这个过程就是模型的前向传播(Forward Pass)。

原料(Input & Parameters): 你有面粉、鸡蛋(输入数据 x),还有糖、黄油(模型参数 w 和 b)。为了能“追责”,你给糖和黄油贴上了特殊标签:requires_grad=True。面粉和鸡蛋不需要,因为它们是固定的,不是你想改变的。 配方步骤(Operations):

  • 步骤1: 将糖和黄油混合,得到“甜油混合物”。
  • 步骤2: 将鸡蛋和面粉混合,得到“面糊”。
  • 步骤3: 将“甜油混合物”和“面糊”混合,得到最终的“蛋糕糊”。
  • 步骤4: 烘焙“蛋糕糊”,得到最终的“蛋糕”。

评价(Calculate Loss): 你尝了一口蛋糕,发现“太甜了!”。你计算出一个“失望分数”(Loss),比如10分(分数越高越失望)。

追责(Backward Pass): 现在,你想知道到底是谁的错导致蛋糕这么甜。于是你对“失望分数”大喊一声:loss.backward()!

Autograd 系统启动,开始反向追溯:蛋糕 -> 蛋糕糊: “蛋糕太甜,显然是蛋糕糊的问题。”

蛋糕糊 -> 甜油混合物 & 面糊: “蛋糕糊太甜,面糊本身不甜,那问题肯定出在甜油混合物上。”

甜油混合物 -> 糖 & 黄油: “甜油混合物太甜,黄油不怎么甜,那罪魁祸首一定是糖!” 同时,黄油也可能有一点点责任。

最终结论: Autograd 计算出,“糖”对“失望分数”的贡献度(梯度)是8,“黄油”的贡献度是2。这意味着,减少糖的用量是降低“失望分数”最有效的方法。

这个“贡献度”就是梯度(Gradient)。在下一次做蛋糕时,你就会根据这个梯度信息,少放点糖,可能再微调一下黄油,这就是模型优化(Optimizer.step())。

技术拆解:Autograd 是如何工作的?

1. 核心组件:

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> 等。

2. 计算图(Computation Graph)

这是 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

以上是一个y=wx+by = wx + b的DAG示意图,通常输入是不需要requires_grad=True,因为输入不需要被优化,而参数和中间变量都需要requires_grad=True。每个一个张量如果被设置过requires_grad=True后,之后的操作计算出来的张量默认都会有requires_grad=True。比如图中的zz虽然没有设置这个参数,但是是通过乘法计算出来的中间变量,便自动会计算其梯度,目的是为了梯度顺着DAG被传递计算(链式法则)。

3. 梯度的计算(反向传播)

当你对一个标量(通常是 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}")

以上代码需要注意的是:

  1. torch.no_grad(): 如果你有一段代码不希望 Autograd 跟踪(比如在模型评估或推理时),把它放进 with torch.no_grad(): 代码块中。这可以大大节省内存和计算资源。
  2. .detach(): 这个方法会创建一个新的张量,它与原始张量共享数据,但脱离了当前的计算图。梯度不会通过这个新张量回传。只能对标量调用 backward(): 通常,我们只对最终的损失(一个标量)调用
  3. .backward()。如果你对一个向量或矩阵调用,你需要提供一个 gradient 参数,这涉及到更高级的向量-雅可比积(vector-Jacobian product)
  4. 梯度累积: PyTorch 的梯度是累积的(+=),而不是覆盖。这在某些高级应用(如梯度累积以适应小批量大小)中有用,但常规训练中,每次反向传播前必须用 optimizer.zero_grad() 清零。

本文作者:James

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!