有效地训练神经网络需要调整模型参数以最小化损失函数,这通常使用梯度下降或其变体。这个过程的主要部分是计算损失函数相对于每个参数的梯度,数学上表示为损失 LL 和参数 ww 的 ∂L∂w∂w∂L。对于复杂模型,手动推导和实现这些梯度计算是不切实际的。
本章介绍 PyTorch 的自动微分引擎 Autograd,它旨在自动计算这些梯度。我们将了解 PyTorch 如何在对张量执行操作时动态构建计算图。你将学习如何使用 requires_grad=True 标记张量以进行梯度计算,使用 .backward() 触发反向传播以计算梯度,以及查看存储在 .grad 属性中的结果梯度。我们还将涉及梯度累积,使用 optimizer.zero_grad() 清零梯度的必要性,以及如何使用 torch.no_grad() 等上下文临时禁用梯度计算,以提高推理或评估期间的效率。
训练神经网络包括迭代地调整模型参数(权重和偏置)以最小化损失函数。这种调整依赖于知晓每个参数的微小变化如何影响最终的损失值。从数学上讲,这种敏感性由损失函数对每个参数的梯度来体现。对于损失 LL 和参数 ww,我们需要计算 ∂L∂w∂w∂L。
对于非常简单的模型,手动使用微积分规则计算这些梯度是可行的,但对于当今常见的深度多层网络来说,这很快就会变得异常复杂且容易出错。想象一下为拥有数百万参数的模型推导导数!这时,**自动微分(AD)**就派上用场了。
AD 是一系列以数值方式评估由计算机程序定义的函数导数的方法。与符号微分(它操作数学表达式,常导致复杂且低效的公式)或数值微分(它使用有限差分近似导数,可能存在截断和舍入误差)不同,AD 通过在构成整体计算的基本运算(加法、乘法、三角函数等)层面系统地应用微积分的链式法则,高效地计算出精确的梯度。
链式法则:AD 的核心
其核心是,AD 依赖于链式法则。如果有一系列函数,例如 y=f(x)y=f(x) 和 z=g(y)z=g(y),链式法则告诉我们如何找到复合函数 z=g(f(x))z=g(f(x)) 对 xx 的导数:
dzdx=dzdy⋅dydxdxd**z=dyd**z⋅dxd**y
AD 将复杂的计算分解为一系列基本运算。然后,它计算每个小步骤的局部导数,并使用链式法则将它们组合起来以得到整体梯度。
考虑一个简单例子:L=(w⋅x+b)2L=(w⋅x+b)2。令 y=w⋅x+by=w⋅x+b。则 L=y2L=y2。 要找到 ∂L∂w∂w∂L,链式法则给出:
∂L∂w=∂L∂y⋅∂y∂w∂w∂L=∂y∂L⋅∂w∂y
我们知道 ∂L∂y=2y∂y∂L=2y 和 ∂y∂w=x∂w∂y=x。将 yy 代回,我们得到:
∂L∂w=(2(w⋅x+b))⋅x∂w∂L=(2(w⋅x+b))⋅x
AD 自动完成这个过程,即使运算链非常长。
正向模式与反向模式
在 AD 中应用链式法则主要有两种方式:
- 正向模式(正向累积): 通过从输入到输出遍历计算步骤来计算导数。它计算一个输入的改变如何影响所有中间变量和最终输出。当输入数量相对于输出数量较少时,它比较高效。
- 反向模式(反向累积): 通过从最终输出到输入反向遍历计算步骤来计算导数。它计算最终输出的改变如何受到所有中间变量和输入的影响。当输出数量相对于输入数量较少时,这种模式明显更高效,这正是深度学习中的情况,因为我们通常只有一个标量损失值和数百万个参数(损失函数的输入)。
PyTorch 的 Autograd 系统使用反向模式自动微分。
Autograd 如何使用 AD
当您对 requires_grad 属性设置为 True 的 PyTorch 张量执行操作时,PyTorch 会在后台构建一个有向无环图(DAG)。这个图通常被称为计算图,它记录了操作序列(节点)和涉及的张量(边)。
让我们直观地看一下 L=(a⋅x+b)2L=(a⋅x+b)2 的简单计算图,假设 aa、xx 和 bb 是输入张量(或之前计算的结果),并且我们想得到 ∂L∂a∂a∂L、∂L∂x∂x∂L 和 ∂L∂b∂b∂L。
输入运算axb+dL/dadL/dxdL/dbdL/d(ax)^2dL/dyL (损失)dL/dL=1
L=(a⋅x+b)2L=(a⋅x+b)2 计算图的表示。实线表示正向传播,构建图。虚线表示反向传播期间梯度的流动,应用链式法则。
当您在最终输出张量(通常是标量损失 LL)上调用 .backward() 时,Autograd 会从该输出开始并反向遍历图。在每个步骤(节点),它根据后续节点的梯度和当前节点执行运算的局部导数来计算梯度,有效地应用了链式法则。然后,对每个需要梯度的张量(如模型参数)计算出的梯度会累积到它们的 .grad 属性中。
这种机制使 PyTorch 能够自动计算由张量运算序列定义的任意复杂模型的梯度,让您摆脱手动推导的繁琐且容易出错的任务。接下来的章节将演示如何实际使用 Autograd 的功能:定义需要梯度的张量、隐式构建计算图、触发反向传播、访问梯度以及控制梯度计算。
收藏
PyTorch 运用计算图作为其通过 Autograd 自动计算梯度的底层机制。每次你执行涉及需要梯度的张量运算时(你很快会了解如何指定这一点),PyTorch 都会动态构建一个图,表示计算序列。
可以将此图视为一个有向无环图 (DAG),其中:
- 节点代表张量或对其执行的运算。
- 边代表数据(张量)的流动以及运算和张量之间的函数依赖关系。
考虑一个简单的计算:
import torch
// Tensors that require gradients
val x = torch.tensor(2.0, requires_grad=true)
val w = torch.tensor(3.0, requires_grad=true)
val b = torch.tensor(1.0, requires_grad=true)
// Operations
val y = w * x // Intermediate result 'y'
val z = y + b // Final result 'z'
println(f"Result z: {z}")
// 01. 基本梯度跟踪
Tensor x = tensor(new DoublePointer(2.0), requires_grad(true));
Tensor w = tensor(new DoublePointer(3.0), requires_grad(true));
Tensor b = tensor(new DoublePointer(1.0), requires_grad(true));
Tensor y = w.mul(x); // 中间结果 'y'
Tensor z = y.add(b); // 最终结果 'z'
System.out.printf("Result z: %s%n", z);
输出:
Result z: 7.0
当这些代码行执行时,PyTorch 会在后台构建一个图。它看起来像这样:
x (数据=2.0, requires_grad=True)w (数据=3.0, requires_grad=True)b (数据=1.0, requires_grad=True)+y (wx 的结果)grad_fn=z (y+b 的结果)grad_fn=
这是 z=(w∗x)+bz=(w∗x)+b 的计算图表示。蓝色框代表输入张量,黄色椭圆代表运算,绿色框代表输出/中间张量。边表示数据流和运算间的依赖关系。由运算产生的张量上的
grad_fn属性指向创建它们的函数。
动态特性
PyTorch 计算图的一个重要特点是它们的动态特性。与那些要求你在运行计算前定义整个图结构的框架不同,PyTorch 会在你的 Python 代码执行时动态构建图。
- 灵活性: 这允许标准的 Python 控制流语句(如
if条件或for循环)直接影响每次迭代中的图结构。如果你的模型架构需要在正向传播期间根据输入数据进行更改,PyTorch 会很自然地处理这种情况。 - 调试: 动态图通常更容易使用标准 Python 工具进行调试,因为图的构建与你熟悉的程序执行同时进行。
正向传播和反向传播
- 正向传播: 当你执行张量运算(如
y = w * x)时,你正在进行正向传播。PyTorch 会记录所涉及的运算和张量,从而构建图。由 Autograd 追踪的运算产生的张量将具有grad_fn属性(如示例中的y和z)。此属性引用了创建该张量的函数,并保存了对其输入的引用,从而形成了图中的反向链接。用户创建的张量(如x、w、b)通常具有grad_fn=None。 - 反向传播: 当你稍后在标量张量(通常是最终的损失值)上调用
.backward()时,Autograd 会从该节点向后遍历此图。它使用微积分的链式法则,在每一步都由grad_fn指引,以计算标量输出相对于最初标记为requires_grad=True的张量(通常是模型参数或输入)的梯度。
叶张量和梯度
在 Autograd 中:
- 叶张量: 这些是位于图“开始”处的张量。通常,它们是用户直接创建的张量(例如,使用
torch.tensor()、torch.randn())且requires_grad=True。模型参数(nn.Parameter,我们稍后会看到)也是叶张量。 - 非叶张量(中间张量): 这些是在图内运算后产生的张量(如上文的
y和z)。它们关联着grad_fn。
默认情况下,在 .backward() 调用期间计算的梯度仅保留并累积在具有 requires_grad=True 的叶张量的 .grad 属性中。中间张量的梯度会进行计算,但使用后通常会被丢弃以节省内存,除非另有明确请求(例如,使用 .retain_grad())。
理解这种图结构对于掌握 Autograd 的工作方式非常重要。它将你在模型中定义的正向计算与优化所需的梯度计算直接关联起来。接下来,我们将研究如何使用 requires_grad 来明确控制梯度跟踪。
requires_grad)收藏
神经网络训练的根基在于计算损失函数相对于模型参数的梯度。PyTorch 的 Autograd 引擎自动处理这项复杂的任务。但是 Autograd 怎么知道哪些计算需要被追踪以便进行微分呢?答案在于 PyTorch 张量的一个特定属性:requires_grad。
requires_grad 属性
每个 PyTorch 张量都有一个布尔属性,名为 requires_grad。此属性充当一个标志,告诉 Autograd 是否应记录涉及此张量的操作,以便稍后进行可能的梯度计算。
默认情况下,当你创建一个张量时,它的 requires_grad 属性被设置为 False。
import torch.*
// 默认行为:requires_grad 为 False
val x = torch.tensor([1.0, 2.0, 3.0])
println(f"Tensor x: {x}")
println(f"x.requires_grad: {x.requires_grad}")
// 显式创建另一个张量并将 requires_grad 设置为 False
val y = torch.tensor([4.0, 5.0, 6.0], requires_grad=false)
println(f"\nTensor y: {y}")
println(f"y.requires_grad: {y.requires_grad}")
// 02. 默认requires_grad行为
Tensor x2 = tensor(new DoublePointer(1.0, 2.0, 3.0));
System.out.printf("\nTensor x: %s%n", x2);
System.out.printf("x.requires_grad: %b%n", x2.requires_grad());
Tensor y2 = tensor(new DoublePointer(4.0, 5.0, 6.0), requires_grad(false));
System.out.printf("\nTensor y: %s%n", y2);
System.out.printf("y.requires_grad: %b%n", y2.requires_grad());
这种默认行为对效率来说是合理的。在典型的工作流程中,许多张量不需要梯度。例如,输入数据或目标标签通常是固定的,不需要计算相对于它们自身的梯度。不必要地追踪操作会消耗额外的内存和计算资源。
启用梯度追踪
要指示 PyTorch 追踪某个特定张量的操作并准备进行梯度计算,你需要将其 requires_grad 属性设置为 True。有两种主要方式可以做到这一点:
-
在张量创建时: 将
requires_grad=True作为参数传递给张量创建函数。// 在创建时启用梯度追踪 val w = torch.tensor([0.5, -1.0], requires_grad=true) println(f"Tensor w: {w}") println(f"w.requires_grad: {w.requires_grad}")// 03. 创建时启用梯度追踪 Tensor w2 = tensor(new DoublePointer(0.5, -1.0), requires_grad(true)); System.out.printf("\nTensor w: %s%n", w2); System.out.printf("w2.requires_grad: %b%n", w2.requires_grad()); -
在张量创建后(原地修改): 对现有张量使用原地方法
.requires_grad_(True)。// 在创建后启用梯度追踪 val b = torch.tensor([0.1]) println(f"Tensor b (before): {b}") println(f"b.requires_grad (before): {b.requires_grad}") // 在创建后启用梯度追踪 b.requires_grad_(true) println(f"\nTensor b (after): {b}") println(f"b.requires_grad (after): {b.requires_grad}")// 在创建后启用梯度追踪 b2.requires_grad_(true); System.out.printf("\nTensor b (after): %s%n", b2); System.out.printf("b2.requires_grad (after): %b%n", b2.requires_grad());
重要提示: 梯度计算通常只对浮点张量(如 torch.float32 或 torch.float64)有意义。导数涉及连续变化,这与浮点类型相符。尝试对整数张量设置 requires_grad=True 通常会导致错误或出现意料之外的行为,因为梯度并非以相同方式为离散值定义的。如果你尝试计算直接涉及被追踪操作的整数张量的梯度,PyTorch 通常会抛出 RuntimeError。
// 尝试对整数张量设置 requires_grad
try:
val int_tensor = torch.tensor([1, 2], dtype=torch.int64, requires_grad=true)
// 这一行可能不会立即出错,但后续涉及它的 backward() 调用会出错。
println(f"Integer tensor created with requires_grad=True: {int_tensor.requires_grad}")
// 让我们尝试一个简单的操作,这可能会在以后导致问题
val result = int_tensor * 2.0 // 乘以浮点数看看是否会引起问题
println(f"Result requires_grad: {result.requires_grad}")
// 如果我们尝试反向传播,这很可能会失败
// result.backward()
catch (e: RuntimeError) =>
println(f"\n对整数张量设置 requires_grad 时出错: {e}")
# 最佳实践:对需要梯度的参数/计算使用浮点张量
float_tensor = torch.tensor([1.0, 2.0], requires_grad=True)
print(f"\n已创建 requires_grad=True 的浮点张量: {float_tensor.requires_grad}")
// 05. 尝试对整数张量设置requires_grad
try {
var tensorOptions2 = new TensorOptions()
.layout(new LayoutOptional(Layout.Strided))
.dtype(new ScalarTypeOptional(ScalarType.Long))
.device(new DeviceOptional(new Device(DeviceType.CPU)))
.memory_format(new MemoryFormatOptional(MemoryFormat.Contiguous))
.requires_grad(new BoolOptional(true));
Tensor int_tensor = torch.tensor(new LongPointer(1L, 2L), tensorOptions2);
System.out.printf("Integer tensor created with requires_grad=True: %b%n", int_tensor.requires_grad());
Tensor result = int_tensor.mul(new Scalar(2.0)); // 乘以浮点数
System.out.printf("Result requires_grad: %b%n", result.requires_grad());
} catch (RuntimeException e) {
System.out.printf("\n对整数张量设置requires_grad时出错: %s%n", e.getMessage());
}
// 最佳实践:使用浮点张量
Tensor float_tensor = tensor(new DoublePointer(1.0, 2.0), requires_grad(true));
System.out.printf("\n已创建requires_grad=True的浮点张量: %b%n", float_tensor.requires_grad());
requires_grad 的传播
重要的一点是,requires_grad 状态会在操作中传播。如果参与操作的任何输入张量具有 requires_grad=True,则该操作产生的输出张量将自动具有 requires_grad=True。这确保了涉及参数(通常具有 requires_grad=True)的整个计算链都得到追踪。
让我们通过一个例子说明:
// 定义张量:x(输入)、w(权重)、b(偏置)
val x = torch.tensor([1.0, 2.0]) // 输入数据,不需要梯度
val w = torch.tensor([0.5, -1.0], requires_grad=true) // 权重参数,追踪梯度
val b = torch.tensor([0.1], requires_grad=true) // 偏置参数,追踪梯度
println(f"x requires_grad: {x.requires_grad}")
println(f"w requires_grad: {w.requires_grad}")
println(f"b requires_grad: {b.requires_grad}")
// 执行操作:y = w * x + b
// 注意:PyTorch 处理 b 的广播
val intermediate = w * x
println(f"\nintermediate (w * x) requires_grad: {intermediate.requires_grad}")
val y = intermediate + b
println(f"y requires_grad: {y.requires_grad}")
// 06. 更复杂的计算图
Tensor x3 = tensor(new DoublePointer(1.0, 2.0)); // 输入数据,不需要梯度
Tensor w3 = tensor(new DoublePointer(0.5, -1.0), requires_grad(true)); // 权重参数
Tensor b3 = tensor(new DoublePointer(0.1), requires_grad(true)); // 偏置参数
System.out.printf("\nx3 requires_grad: %b%n", x3.requires_grad());
System.out.printf("w3 requires_grad: %b%n", w3.requires_grad());
System.out.printf("b3 requires_grad: %b%n", b3.requires_grad());
Tensor intermediate = w.mul(x);
System.out.printf("\nintermediate (w * x) requires_grad: %b%n", intermediate.requires_grad());
Tensor y3 = intermediate.add(b3);
System.out.printf("y3 requires_grad: %b%n", y3.requires_grad());
注意,即使 x 不需要梯度,但由于 w 需要梯度,所以 w * x 的结果 (intermediate) 也需要梯度。接着,由于 intermediate 需要梯度(并且 b 也需要),最终输出 y 也具有 requires_grad=True。
.grad_fn 属性
这种传播与 PyTorch 构建计算图的方式紧密关联。当一个新张量由某个操作创建,并且其 requires_grad 为 True 时,PyTorch 会将一个 .grad_fn 属性附加到这个新张量上。该属性引用了执行此操作的函数(例如 AddBackward0 或 MulBackward0),并且知道如何在反向传播过程中计算相应的梯度。
用户直接创建的张量(如我们上面的 x、w 和 b 示例)在图中被认为是“叶”张量。如果它们具有 requires_grad=True,它们的 .grad_fn 为 None,因为它们不是由图中被追踪的操作创建的。对需要梯度的张量进行操作所产生的张量是“非叶”张量,并将具有 .grad_fn。
让我们查看前面示例中的 .grad_fn:
println(f"\nx.grad_fn: {x.grad_fn}")
println(f"w.grad_fn: {w.grad_fn}")
println(f"b.grad_fn: {b.grad_fn}")
println(f"intermediate.grad_fn: {intermediate.grad_fn}") // 乘法的结果
println(f"y.grad_fn: {y.grad_fn}") // 加法的结果
// 07. 检查grad_fn
System.out.printf("\nx3.grad_fn: %s%n", x3.grad_fn());
System.out.printf("w3.grad_fn: %s%n", w3.grad_fn());
System.out.printf("b3.grad_fn: %s%n", b3.grad_fn());
System.out.printf("intermediate.grad_fn: %s%n", intermediate.grad_fn());
System.out.printf("y3.grad_fn: %s%n", y3.grad_fn());
你可以看到 x、w 和 b(我们的叶张量)的 grad_fn 为 None。相比之下,intermediate 有一个 MulBackward0 函数,而 y 有一个 AddBackward0 函数,这表明了创建它们的那些操作。这条 grad_fn 引用链就是Autograd 使用的动态计算图。
叶张量操作与结果xrequires_grad=Falsegrad_fn=None*输入wrequires_grad=Truegrad_fn=None输入brequires_grad=Truegrad_fn=None+输入intermediaterequires_grad=Truegrad_fn=输出yrequires_grad=Truegrad_fn=输出输入
y = w * x + b的计算图简化视图。需要梯度的张量用蓝色突出显示。注意操作符(*、+)如何创建新张量(intermediate、y),如果通过其输入启用了梯度追踪,这些新张量将通过grad_fn引用其创建操作。
通过对我们希望优化的张量(通常是模型参数,如权重 w 和偏置 b)设置 requires_grad=True,我们让 Autograd 能够构建此图,并将计算从最终输出(通常是损失)追溯到这些参数,为使用 .backward() 进行梯度计算的步骤做好准备,我们将在接下来介绍这一点。
backward())收藏
在使用 PyTorch 时,张量会被设置,并且需要梯度的张量会被标记为 requires_grad=True。PyTorch 会在其动态计算图中忠实地跟踪这些张量上的操作。为了计算这些梯度,backward() 方法是必不可少的。
backward() 方法是驱动 PyTorch 自动微分的引擎。当在一个张量上调用它时,通常是模型最终的标量损失值,它会启动使用链式法则在整个计算图中计算梯度。它会计算被调用的张量相对于图中所有 requires_grad=True 的“叶”张量的梯度(这些通常是你的模型参数或你需要梯度的初始输入)。
启动梯度计算
你几乎总是在一个标量张量上调用 backward(),这通常是你的损失函数计算的结果。例如,如果 loss 包含代表模型批次误差的单个数值:
import torch.*
// 示例设置(想象这些是模型的结果)
val x = torch.tensor(2.0, requires_grad=true)
val w = torch.tensor(3.0, requires_grad=true)
val b = torch.tensor(1.0, requires_grad=true)
// 执行一些操作(构建图)
val y = w * x + b // y = 3*2 + 1 = 7
val loss = y * y // loss = 7*7 = 49 (a scalar)
// 反向传播之前,梯度为 None
println(f"Gradient for x before backward: {x.grad}")
println(f"Gradient for w before backward: {w.grad}")
println(f"Gradient for b before backward: {b.grad}")
// 计算梯度
loss.backward()
// 反向传播之后,梯度被填充
println(f"Gradient for x after backward: {x.grad}") // d(loss)/dx = d(y^2)/dx = 2*y*(dy/dx) = 2*y*w = 2*7*3 = 42
println(f"Gradient for w after backward: {w.grad}") // d(loss)/dw = d(y^2)/dw = 2*y*(dy/dw) = 2*y*x = 2*7*2 = 28
println(f"Gradient for b after backward: {b.grad}") // d(loss)/db = d(y^2)/db = 2*y*(dy/db) = 2*y*1 = 2*7*1 = 14
Tensor x4 = tensor(new DoublePointer(2.0), requires_grad(true));
Tensor w4 = tensor(new DoublePointer(3.0), requires_grad(true));
Tensor b4 = tensor(new DoublePointer(1.0), requires_grad(true));
Tensor y4 = w4.mul(x4).add(b4); // y = 3*2 + 1 = 7
Tensor loss = y4.mul(y4); // loss = 7*7 = 49 (标量)
// 反向传播之前,梯度为null
System.out.printf("\nGradient for x before backward: %s%n", x4.grad());
System.out.printf("Gradient for w before backward: %s%n", w4.grad());
System.out.printf("Gradient for b before backward: %s%n", b4.grad());
// 计算梯度
loss.backward();
// 反向传播之后,梯度被填充
System.out.printf("\nGradient for x after backward: %s%n", x4.grad()); // d(loss)/dx = 42
System.out.printf("Gradient for w after backward: %s%n", w4.grad()); // d(loss)/dw = 28
System.out.printf("Gradient for b after backward: %s%n", b4.grad()); // d(loss)/db = 14
输出:
Gradient for x before backward: None
Gradient for w before backward: None
Gradient for b before backward: None
Gradient for x after backward: 42.0
Gradient for w after backward: 28.0
Gradient for b after backward: 14.0
如你所见,调用 loss.backward() 计算了梯度 ∂loss∂x∂x∂loss、∂loss∂w∂w∂loss 和 ∂loss∂b∂b∂loss 并将它们存储在 x、w 和 b 张量各自的 .grad 属性中。
为什么在标量上调用 .backward()?
Autograd 被设计用于计算雅可比向量积 (JVP)。当你在一个标量张量 LL 上调用 backward() 时,它隐式地等同于以 1.01.0 的起始梯度调用 backward()。这使得 PyTorch 可以使用链式法则,从标量损失向后传播,高效地计算所有参数 pp 的梯度 ∂L∂p∂p∂L。
如果你尝试在一个非标量张量(即包含多个元素的张量)上调用 .backward(),PyTorch 无法隐式知道如何根据最终(未显式)的标量损失来为该张量中每个元素的梯度加权。你将收到一个运行时错误,要求提供 gradient 参数:
// 继续前面的例子,但使用非标量 y
val x_vector = torch.tensor([2.0, 4.0], requires_grad=true)
val w = torch.tensor(3.0, requires_grad=true)
val b = torch.tensor(1.0, requires_grad=true)
// y_non_scalar 现在是非标量张量,包含两个元素:[7.0, 13.0]
val y_non_scalar = w * x_vector + b
try:
y_non_scalar.backward() # 这将导致错误
catch (e: RuntimeError) =>
println(f"Error calling backward() on non-scalar: {e}")
// 要使其工作,需要提供一个与 y_non_scalar 形状匹配的梯度张量
// 这代表了某个最终损失相对于 y_non_scalar 的梯度。
// 为演示目的,我们使用 torch.ones_like(y_non_scalar)
val grad_tensor = torch.ones_like(y_non_scalar)
y_non_scalar.backward(gradient=grad_tensor)
println(f"Gradient for x_vector after y_non_scalar.backward(gradient=...): {x_vector.grad}")
println(f"Gradient for w after y_non_scalar.backward(gradient=...): {w.grad}")
// 09. 对非标量张量的反向传播
Tensor x_vector = tensor(new DoublePointer(2.0, 4.0), requires_grad(true));
Tensor w5 = tensor(new DoublePointer(3.0), requires_grad(true));
Tensor b5 = tensor(new DoublePointer(1.0), requires_grad(true));
// y_non_scalar 现在是非标量张量,包含两个元素:[7.0, 13.0]
Tensor y_non_scalar = w.mul(x_vector).add(b);
try {
y_non_scalar.backward(); // 这将导致错误
} catch (RuntimeException e) {
System.out.printf("\nError calling backward() on non-scalar: %s%n", e.getMessage());
}
// 使用gradient参数来解决这个问题
Tensor grad_tensor = ones_like(y_non_scalar);
y_non_scalar.backward(grad_tensor,new BoolOptional(),true,new TensorArrayRefOptional());
System.out.printf("Gradient for x_vector after y_non_scalar.backward(gradient=...): %s%n", x_vector.grad());
System.out.printf("Gradient for w after y_non_scalar.backward(gradient=...): %s%n", w.grad());
输出:
Error calling backward() on non-scalar: grad can be implicitly created only for scalar outputs
Gradient for x_vector after y_non_scalar.backward(gradient=...): tensor([3., 3.])
Gradient for w after y_non_scalar.backward(gradient=...): 6.0
在大多数标准的训练循环中,你将计算一个单一的标量损失值,代表一个批次或样本的误差,并直接在该标量上调用 loss.backward(),无需提供 gradient 参数。
计算图和梯度流
loss.backward() 触发对创建 loss 的操作图进行反向遍历。
输入 (requires_grad=True)操作输出 (标量) x=2.0w=3.0b=1.0+d(loss)/dxd(loss)/dwy=7.0d(loss)/dbwx+bpow(2)d(loss)/dyloss=49.0 subgraphcluster_intermediateloss.backward() 从这里开始
一个简化的计算图,显示了输入
x、w、b、中间结果y和最终的标量loss。虚线红色箭头说明了在loss.backward()过程中计算x、w和b梯度所经过的路径。
默认情况下,PyTorch 在 backward() 被调用后会清除计算图的中间缓冲区,以节省内存。这意味着如果你需要在图的相同部分多次调用 backward()(这种情况较少,通常用于高级技术或调试),你需要在第一次 backward() 调用时传入 retain_graph=True。然而,对于标准训练,你会构建一个图,计算损失,调用 backward(),更新权重,然后为下一个批次重复这个过程,这会构建一个新的图。
理解 backward() 对于在 PyTorch 中训练模型非常重要。它是将模型输出和损失函数与需要调整的参数联系起来的机制。在接下来的章节中,我们将了解优化器如何访问和使用这些计算出的梯度。
