
# 访问梯度(`.grad`)
在计算标量张量(通常是损失)相对于计算图中其他张量的梯度时,PyTorch 使用 `backward()` 方法。此方法触发梯度计算,但它不会直接返回梯度。相反,PyTorch 会将计算出的梯度存储在张量自身的一个特殊属性中:即 `.grad` 属性。
这个属性主要为计算图中的*叶*张量填充,即那些你通过设置 `requires_grad=True` 明确要求进行梯度跟踪的张量。请记住,叶张量通常是你直接创建的,例如模型参数或输入,而不是通过运算生成的中间张量。
`.grad` 属性包含一个与它所属的原始张量形状相同的张量。`.grad` 张量中的每个元素表示标量(调用 `backward()` 的对象)相对于原始张量中对应元素的偏导数。如果 L*L* 是标量损失,w*w* 是一个张量参数,那么在调用 `L.backward()` 之后,属性 `w.grad` 将包含表示 ∂L∂w∂*w*∂*L* 的张量。
我们通过一个简单例子来说明这一点:
```scala 3
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.backward()
println(f"y 对 x 的梯度 (dy/dx): {x.grad}")
println(f"y 对 w 的梯度 (dy/dw): {w.grad}")
println(f"y 对 b 的梯度 (dy/db): {b.grad}")
val z = torch.tensor(4.0, requires_grad=false)
println(f"张量 z 的梯度 (requires_grad=False): {z.grad}")
```
```java
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).add(b);
y.backward();
System.out.printf("y 对 x 的梯度 (dy/dx): %s%n", x.grad());
System.out.printf("y 对 w 的梯度 (dy/dw): %s%n", w.grad());
System.out.printf("y 对 b 的梯度 (dy/db): %s%n", b.grad());
Tensor z = tensor(new DoublePointer(4.0), requires_grad(false));
System.out.printf("张量 z 的梯度 (requires_grad=False): %s%n", z.grad());
```
**预期输出:**
```
y 对 x 的梯度 (dy/dx): 3.0
y 对 w 的梯度 (dy/dw): 2.0
y 对 b 的梯度 (dy/db): 1.0
张量 z 的梯度 (requires_grad=False): None
```
在这个例子中:
1. 我们定义了 `x`、`w` 和 `b`,并设置 `requires_grad=True`,将它们标记为我们想要梯度的叶节点。
2. 我们执行了操作 y=w∗x+b*y*=*w*∗*x*+*b*。PyTorch 在后台构建了一个计算图。
3. 我们调用了 `y.backward()`。Autograd 从 `y` 开始向后遍历图以计算梯度。
- ∂y∂x=w=3.0∂*x*∂*y*=*w*=3.0
- ∂y∂w=x=2.0∂*w*∂*y*=*x*=2.0
- ∂y∂b=1=1.0∂*b*∂*y*=1=1.0
4. 计算出的梯度存储在 `x.grad`、`w.grad` 和 `b.grad` 中。访问这些属性会显示计算出的张量值。
5. 张量 `z` 是在 `requires_grad=False` 的情况下创建的,因此它未参与 Autograd 跟踪的梯度计算,其 `.grad` 属性保持为 `None`。
需要记住的是,梯度默认是累积的。如果你在不清除梯度的情况下,对图中可能不同的部分(或相同的部分)多次调用 `backward()`,新计算的梯度将被*添加*到 `.grad` 属性中已经存在的值上。这种行为是故意的,对于像跨小批量进行梯度累积这样的情况很有用,但在典型的训练循环中,你需要在每个反向传播步骤之前明确地将梯度清零。这通常通过 `optimizer.zero_grad()` 完成,我们将在构建训练循环时更详细地讨论它。
目前,要点是,在 `loss.backward()` 之后,更新模型参数所需的梯度可以直接在这些参数张量的 `.grad` 属性中可用。
# 禁用梯度追踪
尽管Autograd自动追踪操作并计算梯度的能力对模型训练不可或缺,但在某些情况下,这种追踪是不必要甚至不希望的。具体来说,在模型评估(推理)期间,或者当你执行不应影响梯度计算的操作时,追踪历史会消耗内存和计算资源,而没有任何益处。PyTorch提供了选择性禁用梯度追踪的方法。
### 为什么要禁用梯度追踪?
1. **模型评估(推理):** 当你使用训练好的模型对新数据进行预测时,你不会更新其权重。在此阶段计算梯度没有意义。禁用梯度追踪可以显著减少内存使用(因为无需存储计算图),并加快前向传播的速度。
2. **冻结模型参数:** 在微调期间,你可能希望冻结预训练模型的一部分(例如,早期的卷积层),而只训练后面的层。你需要告知PyTorch不要为这些冻结的参数计算或存储梯度。
3. **内存效率:** Autograd存储的计算图会消耗大量内存,特别是对于复杂的模型或长序列。在不需要时关闭追踪可以避免这种额外开销。
### 使用 `torch.no_grad()` 上下文管理器
禁用代码块梯度追踪最常用且推荐的方式是使用 `torch.no_grad()` 上下文管理器。在此 `with` 块内执行的任何PyTorch操作都会表现得如同所有输入张量都不需要梯度,即使它们最初设置了 `requires_grad=True`。
```scala 3
import torch.*
// 示例张量
val x = torch.randn(2, 2, requires_grad=true)
val w = torch.randn(2, 2, requires_grad=true)
val b = torch.randn(2, 2, requires_grad=true)
// 在no_grad上下文之外的操作
val y = x * w + b
println(f"y.requires_grad: {y.requires_grad}")
println(f"y.grad_fn: {y.grad_fn}")
println("\n进入torch.no_grad()上下文:")
with torch.no_grad():
val z = x * w + b
println(f" z.requires_grad: {z.requires_grad}") // 输出:z.requires_grad: False
println(f" z.grad_fn: {z.grad_fn}") // 输出:z.grad_fn: None
// 即使输入需要梯度,输出也不会
val k = x * 5
println(f" k.requires_grad: {k.requires_grad}") // 输出:k.requires_grad: False
// 在上下文之外,如果输入需要梯度,追踪会恢复
println("\n退出torch.no_grad()上下文:")
val p = x * w
println(f"p.requires_grad: {p.requires_grad}") // 输出:p.requires_grad: True
println(f"p.grad_fn: {p.grad_fn}") // 输出:p.grad_fn: <MulBackward0 object at ...>
```
```java
var tensorOptions = 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));
var gradFlag = torch.requires_grad(true);
Tensor x2 = torch.randn(new LongArrayRef(new LongPointer(2, 2)), tensorOptions);
Tensor w2 = torch.randn(new LongArrayRef(new LongPointer(2, 2)), tensorOptions);
Tensor b2 = torch.randn(new LongArrayRef(new LongPointer(2, 2)), gradFlag);
Tensor y2 = x2.mul(w2).add(b2);
System.out.printf("\ny2.requires_grad: %b%n", y2.requires_grad());
System.out.printf("y2.grad_fn: %s%n", y2.grad_fn());
System.out.println("\n进入torch.no_grad()上下文:");
try (NoGradGuard noGradGuard = new NoGradGuard()) {
Tensor z2 = x2.mul(w2).add(b2);
System.out.printf(" z2.requires_grad: %b%n", z2.requires_grad());
System.out.printf(" z2.grad_fn: %s%n", z2.grad_fn());
Tensor k2 = x2.mul(new Scalar(5));
System.out.printf(" k2.requires_grad: %b%n", k2.requires_grad());
}
System.out.println("\n退出torch.no_grad()上下文:");
Tensor p = x2.mul(w2);
System.out.printf("p.requires_grad: %b%n", p.requires_grad());
System.out.printf("p.grad_fn: %s%n", p.grad_fn());
```
如示例所示,`with torch.no_grad():` 块内的操作会产生 `requires_grad=False` 且没有关联 `grad_fn` 的输出(`z`、`k`),这表明它们已脱离计算图历史。这正是你在评估循环中想要的结果:
```scala 3
// 评估循环片段
model.eval() // 将模型设置为评估模式(对dropout、batchnorm等层很重要)
var total_loss = 0.0
var correct_predictions = 0
with torch.no_grad(): // 禁用评估期间的梯度计算
for inputs, labels in validation_dataloader:
val inputs = inputs.to(device) // 将数据移动到相应的设备
val labels = labels.to(device) // 将数据移动到相应的设备
val outputs = model(inputs) // 前向传播
val loss = criterion(outputs, labels) // 计算损失
total_loss += loss.item()
val predicted = torch.max(outputs.data, 1).values
correct_predictions += (predicted == labels).sum().item()
// 计算平均损失和准确率...
```
```java
import org.pytorch.*;
import org.pytorch.data.Dataset;
import org.pytorch.data.DataLoader;
import java.util.Iterator;
public class ModelEvaluationExample {
private Module model;
private Loss criterion;
private DataLoader validationDataloader;
private Device device;
public void evaluateModel() {
model.eval();
double totalLoss = 0.0;
long correctPredictions = 0;
int totalSamples = 0;
try (NoGradGuard noGradGuard = NoGradGuard.create()) {
Iterator<IValue[]> iterator = validationDataloader.iterator();
while (iterator.hasNext()) {
IValue[] batch = iterator.next();
Tensor inputs = batch[0].toTensor();
Tensor labels = batch[1].toTensor();
inputs = inputs.to(device);
labels = labels.to(device);
Tensor outputs = model.forward(IValue.from(inputs)).toTensor();
Tensor lossTensor = criterion.forward(outputs, labels).toTensor();
double lossValue = lossTensor.item().doubleValue();
totalLoss += lossValue;
Tensor[] maxResult = torch.max(outputs, 1);
Tensor predicted = maxResult[1];
Tensor correct = predicted.eq(labels).toType(Tensor.DoubleType);
correctPredictions += correct.sum().item().longValue();
totalSamples += labels.size(0);
inputs.close();
labels.close();
outputs.close();
lossTensor.close();
predicted.close();
correct.close();
maxResult[0].close();
}
}
double avgLoss = totalLoss / validationDataloader.size();
double accuracy = (double) correctPredictions / totalSamples;
System.out.printf("验证集平均损失: %.4f%n", avgLoss);
System.out.printf("验证集准确率: %.4f%%%n", accuracy * 100);
}
private static org.pytorch.torch.Torch torch() {
return org.pytorch.torch.Torch.INSTANCE;
}
}
```
### 使用 `.detach()` 方法
另一种阻止特定张量进行梯度追踪的方法是使用 `.detach()` 方法。此方法会创建一个*新*张量,它与原始张量共享底层数据存储,但明确地脱离了当前计算图。它将拥有 `requires_grad=False`。
```scala 3
import torch.*
val a = torch.randn(3, 3, requires_grad=true)
val b = a * 2
println(f"b.requires_grad: {b.requires_grad}")
println(f"b.grad_fn: {b.grad_fn}")
val c = b.detach()
println(f"\n分离b以创建c后:")
println(f"c.requires_grad: {c.requires_grad}")
println(f"c.grad_fn: {c.grad_fn}")
println(f"\n原始张量 b 仍保持连接:")
println(f"b.requires_grad: {b.requires_grad}")
println(f"b.grad_fn: {b.grad_fn}")
val d = c + 1
println(f"\n在分离张量 c 上的操作:")
println(f"d.requires_grad: {d.requires_grad}")
```
```java
Tensor a2 = torch.randn(new LongArrayRef(new LongPointer(3, 3)), tensorOptions);
Tensor b3 = a2.mul(new Scalar(2));
System.out.printf("\nb3.requires_grad: %b%n", b3.requires_grad());
System.out.printf("b3.grad_fn: %s%n", b3.grad_fn());
Tensor c2 = b2.detach();
System.out.printf("\n分离b2以创建c2后:");
System.out.printf("c2.requires_grad: %b%n", c2.requires_grad());
System.out.printf("c2.grad_fn: %s%n", c2.grad_fn());
System.out.printf("\n原始张量 b2 仍保持连接:");
System.out.printf("b2.requires_grad: %b%n", b2.requires_grad());
System.out.printf("b2.grad_fn: %s%n", b2.grad_fn());
Tensor d2 = c2.add(new Scalar(1));
System.out.printf("\n在分离张量 c2 上的操作:");
System.out.printf("d2.requires_grad: %b%n", d2.requires_grad());
```
**何时使用 `.detach()` 与 `torch.no_grad()`?**
- 当你希望执行一*段*操作而不追踪梯度时,使用 `torch.no_grad()`,这通常用于推理或评估代码段。为此目的,它通常更高效。
- 当你需要将*特定张量*从计算图中移除时,使用 `.detach()`,例如为了记录其值、在不应影响梯度的操作中使用它(如更新指标),或将其传递给期望非梯度追踪张量的函数,同时可能仍需要在其他地方使用原始张量的梯度历史。由于 `.detach()` 共享数据,*原地*修改分离的张量会影响原始张量,如果处理不当,这可能会对梯度计算产生影响。
### 原地修改 `requires_grad`
你也可以直接原地修改张量的 `requires_grad` 属性,但与上下文管理器或 `.detach()` 相比,这种临时禁用方法通常不太常见。它通常用于定义你明确不希望训练的参数。
```scala 3
val my_tensor = torch.randn(5, requires_grad=true)
println(f"初始requires_grad: {my_tensor.requires_grad}")
my_tensor.requires_grad_(false)
println(f"requires_grad_(false)后: {my_tensor.requires_grad}")
```
```java
Tensor my_tensor = torch.randn(new LongArrayRef(new LongPointer(5)), tensorOptions);
System.out.printf("\n初始requires_grad: %b%n", my_tensor.requires_grad());
my_tensor.requires_grad_(false);
System.out.printf("requires_grad_(false)后: %b%n", my_tensor.requires_grad());
```
使用 `torch.no_grad()` 是进行高效推理和评估的标准做法,而 `.detach()` 在你需要将特定张量从梯度历史中隔离时,提供更细致的控制。了解何时以及如何禁用梯度追踪对于编写高效且正确的PyTorch代码非常重要,特别是在你进一步学习基础训练循环之后。
# 梯度累积
对标量张量(如损失值)调用 `.backward()` 会触发计算图中所有 `requires_grad=True` 的张量的梯度。需要了解的一个主要特性是,PyTorch 默认会*累积*梯度。
### 默认梯度累积行为
如果您在多次调用 `.backward()` 之间不清除梯度,PyTorch 会将新计算的梯度添加到叶张量(参数)的`.grad`属性中已有的值上。
我们用一个简单例子来说明这一点:
```scala 3
import torch.*
// 创建一个需要梯度的张量
val x = torch.tensor([2.0], requires_grad=true)
// 执行一些操作
val y = x * x
val z = y * 3 // z = 3 * x^2
// 第一次反向传播
// dz/dx = 6*x = 6*2 = 12
z.backward(retain_graph=true) // retain_graph=true 允许后续的反向传播调用
println(f"After first backward pass, x.grad: {x.grad}")
# 执行另一个操作(可以相同也可以不同)
z.backward()
println(f"After second backward pass, x.grad: {x.grad}")
x.grad.zero_()
println(f"After zeroing, x.grad: {x.grad}")
```
```java
Tensor x3 = tensor(new DoublePointer(2.0), requires_grad(true));
Tensor y3 = x3.mul(x3);
Tensor z3 = y3.mul(new Scalar(3));
z3.backward(new Tensor(), new BoolOptional(true),false, new TensorArrayRefOptional());
System.out.printf("After first backward pass, x.grad: %s%n", x3.grad());
z3.backward();
System.out.printf("After second backward pass, x.grad: %s%n", x3.grad());
x3.grad().zero_();
System.out.printf("After zeroing, x.grad: %s%n", x3.grad());
```
运行此代码会产生类似于以下的输出:
```text
After first backward pass, x.grad: tensor([12.])
After second backward pass, x.grad: tensor([24.])
After zeroing, x.grad: tensor([0.])
```
请注意,第二次调用 `z.backward()` 如何将新计算的梯度(12)加到之前存储的梯度(12)上,结果为 24。这种累积是刻意为之的,并且有重要的用途。
### 为何累积梯度?模拟更大批量
这种默认行为的主要原因是为了方便**梯度累积**。当训练大型模型需要大批量数据以实现稳定收敛,但现有 GPU 内存无法一次性容纳如此大的批量时,此方法就很有用。
与其处理一个大批量,不如这样操作:
1. 将大批量分成若干个更小的迷你批量。
2. 每次处理一个迷你批量:执行前向传播并计算损失。
3. 对当前迷你批量的损失调用 `.backward()`。为此迷你批量计算的梯度将添加到模型参数的 `.grad` 属性中。
4. 对大批量内的所有迷你批量重复步骤 2-3。
5. *在*处理完所有迷你批量并累积其梯度后,使用 `optimizer.step()` 执行一次优化器更新步。此步骤使用所有迷你批量梯度的*总和*来更新模型权重,从而有效地模拟了更大批量的一次更新步。
6. 非常重要的一点是,*在*开始处理*下一个*大批量(如果不是累积,则为下一个迷你批量)*之前*,使用 `optimizer.zero_grad()` **清除梯度**。
这使您可以使用模型所需的有效批量大小进行训练,即使它不能一次性完全载入内存,从而牺牲计算时间来提高内存效率。
### 标准训练中 `optimizer.zero_grad()` 的必要性
在标准的训练循环中,您在每次迭代中处理一个批量、计算损失、计算梯度并更新权重,您通常*不希望*来自前一个批量的梯度影响当前的更新步。每个批量的梯度计算应该是独立的。
由于 PyTorch 默认累积梯度,如果在计算新批量的梯度之前未能清除它们,将导致不正确的更新。优化器将使用新旧梯度的混合,从而损害训练过程。
这就是为什么在标准的 PyTorch 训练循环中,您几乎总能看到 `optimizer.zero_grad()` 被调用的原因。它将优化器管理的所有参数的 `.grad` 属性重置,以确保随后的 `.backward()` 调用完全基于当前批量的损失来计算梯度。
典型的训练迭代结构如下所示:
```scala 3
// 假设 model, dataloader, loss_fn, 和 optimizer 已定义
// 遍历 epoch...
// 遍历批量...
// 1. 获取数据批量
val data_batch = dataloader.next()
val inputs = data_batch._1
val labels = data_batch._2
inputs, labels = inputs.to(device), labels.to(device) // 将数据移动到适当的设备
// 2. 清零梯度
// 重要:在处理新批量之前清除之前的梯度
optimizer.zero_grad()
// 3. 前向传播:计算模型预测
val outputs = model(inputs)
// 4. 计算损失
val loss = loss_fn(outputs, labels)
// 5. 反向传播:计算梯度
loss.backward()
// 6. 优化器更新步:更新模型权重
optimizer.step()
// ... (记录日志、评估等)
```
```java
```
`optimizer.zero_grad()` 的放置位置很重要。它应该在您计算当前迭代的损失并执行反向传播*之前*发生,确保当前批量的梯度计算有一个干净的开始。虽然它通常放在循环的开头,但技术上它只需要在 `loss.backward()` 之前发生。然而,将其放在开头是常见做法,并且能清楚地划分新批量处理的开始。
总而言之,梯度累积是 PyTorch 的一个内置功能,对于模拟更大的批量数据很有用。然而,在标准训练循环中,您必须通过在每次迭代开始时调用 `optimizer.zero_grad()` 来明确阻止这种累积,以确保模型更新仅基于当前批量的数据是正确的。
# 动手实践:Autograd 运用
实际例子演示 PyTorch Autograd 系统。这些练习会引导您设置梯度要求、执行反向传播、查看梯度、观察累积以及禁用梯度跟踪。请确保您已安装 PyTorch 并能导入 `torch` 库。
### 设置
首先,导入 PyTorch:
```scala 3
import torch
```
### 例子 1:基本梯度计算
我们从一个非常简单的计算开始,并跟踪梯度。我们将定义两个张量 `x` 和 `w`,其中 `w` 表示我们想要优化的权重。我们将计算一个简单的输出 `y`,然后计算一个标量损失 `L`。
1. **创建张量**:将 `x` 定义为一个包含一些数据的张量,将 `w` 定义为一个需要计算其梯度的张量(使用 `requires_grad=True`)。
```scala 3
// 输入数据
val x = torch.tensor([2.0, 4.0, 6.0])
// 权重张量 - 需要计算梯度
val w = torch.tensor([0.5], requires_grad=true)
println(f"x: {x}")
println(f"w: {w}")
println(f"x.requires_grad: {x.requires_grad}")
println(f"w.requires_grad: {w.requires_grad}")
```
```java
// 1. 创建输入张量x(无梯度,对应原代码的 [2.0, 4.0, 6.0])
float[] xData = {2.0f, 4.0f, 6.0f};
Tensor x = torch.from_blob(new FloatPointer(xData), new long[]{3});
float[] wData = {0.5f};
TensorOptions wOptions = new TensorOptions()
.device(new DeviceOptional(new Device(DeviceType.CPU)))
.requires_grad(new BoolOptional(true));
Tensor w = torch.from_blob(new FloatPointer(wData), new long[]{1}, wOptions);
System.out.println("x.requires_grad: " + x.requires_grad());
System.out.println("w.requires_grad: " + w.requires_grad());
```
请注意,`x` 默认情况下不需要梯度,而我们为 `w` 显式设置了它。
2. **定义计算**:执行一个简单的运算。任何通过涉及 `requires_grad=True` 的张量运算而得到的张量,其 `requires_grad` 也会是 `True`。
```scala 3
val y = w * x
val L = y.mean()
println(f"y: {y}")
println(f"L: {L}")
println(f"y.requires_grad: {y.requires_grad}")
println(f"L.requires_grad: {L.requires_grad}")
```
```java
Tensor x4 = tensor(new DoublePointer(2.0, 4.0, 6.0));
Tensor w4 = tensor(new DoublePointer(0.5), requires_grad(true));
System.out.printf("\nx: %s%n", x4);
System.out.printf("w4: %s%n", w4);
System.out.printf("x.requires_grad: %b%n", x4.requires_grad());
System.out.printf("w4.requires_grad: %b%n", w4.requires_grad());
Tensor y4 = w4.mul(x4);
Tensor L = y4.mean();
System.out.printf("y: %s%n", y4);
System.out.printf("L: %s%n", L);
System.out.printf("y.requires_grad: %b%n", y4.requires_grad());
System.out.printf("L.requires_grad: %b%n", L.requires_grad());
```
您会看到 `y` 和 `L` 现在都需要梯度,因为它们依赖于 `w`。
3. **计算梯度**:在最终的标量输出 (`L`) 上使用 `.backward()` 方法来计算整个图中的梯度。
```scala 3
L.backward()
```
4. **查看梯度**:查看张量 `w` 的 `.grad` 属性。
```scala 3
println(f"Gradient dL/dw: {w.grad}")
println(f"Gradient dL/dx: {x.grad}")
```
我们来分析 `w.grad` 的结果。计算过程为: yi=w∗xi*y**i*=*w*∗*x**i* L=13∑yi=13(wx1+wx2+wx3)*L*=31∑*y**i*=31(*w**x*1+*w**x*2+*w**x*3) 梯度 ∂L∂w∂*w*∂*L* 为:
∂L∂w=13(x1+x2+x3)∂*w*∂*L*=31(*x*1+*x*2+*x*3)
当 x=[2.0,4.0,6.0]*x*=[2.0,4.0,6.0] 时,梯度为 13(2.0+4.0+6.0)=12.03=4.031(2.0+4.0+6.0)=312.0=4.0。这与输出 `tensor([4.])` 相符。因为 `x` 在创建时没有设置 `requires_grad=True`,所以它的梯度未被计算,仍为 `None`。
### 例子 2:梯度与计算图
Autograd 动态构建图。我们来看一个稍微复杂一点的例子。
1. **创建张量**:
```scala 3
val a = torch.tensor(2.0, requires_grad=true)
val b = torch.tensor(3.0, requires_grad=true)
val c = torch.tensor(4.0, requires_grad=false)
println(f"a: {a}, requires_grad={a.requires_grad}")
println(f"b: {b}, requires_grad={b.requires_grad}")
println(f"c: {c}, requires_grad={c.requires_grad}")
```
```java
Tensor a5 = tensor(new DoublePointer(2.0), requires_grad(true));
Tensor b5 = tensor(new DoublePointer(3.0), requires_grad(true));
Tensor c5 = tensor(new DoublePointer(4.0), requires_grad(false));
System.out.printf("\na: %s, requires_grad= %b%n", a5, a5.requires_grad());
System.out.printf("b: %s, requires_grad= %b%n", b5, b5.requires_grad());
System.out.printf("c: %s, requires_grad= %b%n", c5, c5.requires_grad());
```
2. **定义计算**:
```scala 3
val d = a * b
val e = d + c
val f = e * 2
println(f"d: {d}, requires_grad={d.requires_grad}")
println(f"e: {e}, requires_grad={e.requires_grad}")
println(f"f: {f}, requires_grad={f.requires_grad}")
```
```java
Tensor d5 = a5.mul(b5);
Tensor e5 = d5.add(c5);
Tensor f5 = e5.mul(new Scalar(2));
System.out.printf("d: %s, requires_grad= %b%n", d5, d5.requires_grad());
System.out.printf("e: %s, requires_grad= %b%n", e5, e5.requires_grad());
System.out.printf("f: %s, requires_grad= %b%n", f5, f5.requires_grad());
```
3. **计算并查看梯度**:
```scala 3
f.backward()
println(f"Gradient df/da: {a.grad}")
println(f"Gradient df/db: {b.grad}")
println(f"Gradient df/dc: {c.grad}")
```
```java
f5.backward();
System.out.printf("Gradient df/da: %s%n", a5.grad());
System.out.printf("Gradient df/db: %s%n", b5.grad());
System.out.printf("Gradient df/dc: %s%n", c5.grad());
if (b5.grad() != null) {
System.out.printf("Before zeroing, b5.grad: %s, 梯度即将归零%n", b5.grad());
b5.grad().zero_();
System.out.printf("After zeroing, b5.grad: %s, 梯度已归零%n", b5.grad());
}
```
我们手动计算一下: d=a×b*d*=*a*×*b* e=d+c=a×b+c*e*=*d*+*c*=*a*×*b*+*c* f=2×e=2(a×b+c)*f*=2×*e*=2(*a*×*b*+*c*)
∂f∂a=2×b=2×3.0=6.0∂*a*∂*f*=2×*b*=2×3.0=6.0 ∂f∂b=2×a=2×2.0=4.0∂*b*∂*f*=2×*a*=2×2.0=4.0 ∂f∂c=2∂*c*∂*f*=2
`a` 和 `b` 的计算梯度是匹配的。由于 `c` 定义时 `requires_grad=False`,Autograd 没有跟踪涉及 `c` 的操作来计算关于 `c` 本身的梯度,因此 `c.grad` 为 `None`。
### 例子 3:梯度累积
默认情况下,每次调用 `.backward()` 时,梯度都会累积到 `.grad` 属性中。这对于计算多个损失的梯度或模拟更大的批次大小等情况很有用,但在标准训练循环中,需要显式地将梯度清零。
1. **设置**:我们再次使用一个简单的设置。
```scala 3
val x = torch.tensor(5.0, requires_grad=true)
val y = x * x
println(f"Initial x.grad: {x.grad}")
```
```java
float[] xData = {5.0f};
TensorOptions xOptions = new TensorOptions()
.device(new DeviceOptional(new Device(DeviceType.CPU)))
.requires_grad(new BoolOptional(true))
.dtype(new ScalarTypeOptional(ScalarType.Float));
Tensor x = torch.from_blob(new FloatPointer(xData), new long[]{}, xOptions);
Tensor y = x.mul(x);
System.out.println("Initial x.grad: " + x.grad());
```
2. **第一次反向传播**:
```scala 3
y.backward(retain_graph=True)
println(f"x.grad after 1st backward: {x.grad}")
```
```java
float[] gradData = {1.0f};
Tensor gradient = torch.from_blob(new FloatPointer(gradData), new long[]{});
BoolOptional retainGraph = new BoolOptional(true);
boolean createGraph = false;
TensorArrayRefOptional inputs = new TensorArrayRefOptional();
y.backward(gradient, retainGraph, createGraph, inputs);
```
3. **第二次反向传播(累积)**:再次调用 `backward`,*不*清零梯度。
```scala 3
y.backward(retain_graph=True)
println(f"x.grad after 2nd backward: {x.grad}")
```
```java
float[] gradData = {1.0f};
Tensor gradient = Tensor.fromBlob(new FloatPointer(gradData), new long[]{});
BoolOptional retainGraph = new BoolOptional(true);
boolean createGraph = false;
TensorArrayRefOptional inputs = new TensorArrayRefOptional();
y.backward(gradient, retainGraph, createGraph, inputs);
System.out.print("x.grad after 1st backward: ");
System.out.println(x.grad().getDataAsFloatArray()[0]);
y.backward(gradient, retainGraph, createGraph, inputs);
System.out.print("x.grad after 2nd backward: ");
System.out.println(x.grad().getDataAsFloatArray()[0]);
```
梯度被累积(相加)到之前的值上。
4. **清零梯度**:手动清零梯度。在典型的训练循环中,这通常通过 `optimizer.zero_grad()` 完成。
```scala 3
if x.grad is not None:
x.grad.zero_() // 原位清零
println(f"x.grad after zeroing: {x.grad}") // 预期结果:0.0
```
5. **第三次反向传播(清零后)**:
```scala 3
// 最后一次反向传播不需要 retain_graph
y.backward() // 最后一次反向传播不需要 retain_graph
println(f"x.grad after 3rd backward: {x.grad}") // 预期结果:10.0
```
梯度清零后会重新计算。在训练循环中忘记清零梯度是常见的错误原因。
### 例子 4:禁用梯度跟踪
有时,您需要执行操作而不跟踪其梯度计算,最常见的情况是在模型评估(推理)期间或在优化步骤之外调整参数时。
1. **使用 `torch.no_grad()`**:这个上下文管理器是禁用代码块梯度跟踪的标准方法。
```scala 3
// 上下文管理器 torch.no_grad()
val a = torch.tensor(2.0, requires_grad=true)
println(f"Outside context: a.requires_grad = {a.requires_grad}")
with torch.no_grad():
print(f"Inside context: a.requires_grad = {a.requires_grad}") # 仍然是 True
b = a * 2
print(f"Inside context: b = {b}, b.requires_grad = {b.requires_grad}") # False!
// 在上下文之外,如果输入需要梯度,计算会恢复跟踪
val c = a * 3
println(f"Outside context: c = {c}, c.requires_grad = {c.requires_grad}") // True
```
```java
// 25. no_grad上下文管理器的另一个示例
Tensor a7 = tensor(new DoublePointer(2.0), requires_grad(true));
System.out.printf("\nOutside context: a.requires_grad = %b%n", a7.requires_grad());
try (NoGradGuard noGradGuard2 = new NoGradGuard()) {
System.out.printf("Inside context: a.requires_grad = %b%n", a7.requires_grad());
Tensor b7 = a7.mul(new Scalar(2));
System.out.printf("Inside context: b7 = %s, b7.requires_grad = %b%n", b7, b7.requires_grad());
}
Tensor c7 = a7.mul(new Scalar(3));
System.out.printf("Outside context: c7 = %s, c7.requires_grad = %b%n", c7, c7.requires_grad());
```
在 `torch.no_grad()` 块内部,尽管 `a` 需要梯度,但生成的张量 `b` 却不需要。这使得块内的操作更节省内存且更快,因为反向传播的历史不会被保存。
2. **使用 `.detach()`**:这个方法会创建一个*新*张量,它共享相同的数据,但与计算历史分离。它不需要梯度。
```scala 3
val c = a.detach()
println(f"a.requires_grad: {a.requires_grad}")
println(f"c.requires_grad: {c.requires_grad}")
val d = a.detach()
println(f"a.requires_grad: {a.requires_grad}")
println(f"d.requires_grad: {d.requires_grad}")
val e = c * 3
println(f"e.requires_grad: {e.requires_grad}")
val L1 = b.mean()
L1.backward()
println(f"Gradient dL1/da: {a.grad}")
if a.grad is not None:
a.grad.zero_()
# 尝试通过 'd' 进行反向传播 - 它不会影响 'a' 的梯度
try:
# L2 = d.mean() # 最终需要一个需要梯度的计算
// 示例:再次使用 'a' 与分离后的结果
val L2 = (a + d).mean() // L2 = (a + a.detach()*3).mean()
L2.backward()
println(f"Gradient dL2/da: {a.grad}") // 只计算来自 'a' 路径的梯度 (1.0)
catch RuntimeError as e:
println(f"Error demonstrating backward with detached: {e}")
// 如果最终的标量不依赖于
// 分离后任何需要梯度的输入,您可能会得到一个错误。
// 这里,L2 依赖于 'a',所以梯度是 1.0。
// 经由 'd' 的路径对 a.grad 没有贡献。
// 修改 c(分离的张量) - 它会影响 a,因为它们共享数据!
with torch.no_grad():
c[0] = 100.0 // 原位修改 c(对标量使用索引)
println(f"After modifying c, a = {a}") // 'a' 也改变了!
println(f"After modifying c, c = {c}")
```
```java
// 26. 张量分离的更多示例
Tensor c8 = a7.detach();
System.out.printf("\na.requires_grad: %b%n", a7.requires_grad());
System.out.printf("c.requires_grad: %b%n", c8.requires_grad());
Tensor d8 = a7.detach();
System.out.printf("a.requires_grad: %b%n", a7.requires_grad());
System.out.printf("d.requires_grad: %b%n", d8.requires_grad());
Tensor e8 = c8.mul(new Scalar(3));
System.out.printf("e.requires_grad: %b%n", e8.requires_grad());
Tensor L1 = b5.mean();
L1.backward();
System.out.printf("Gradient dL1/da: %s%n", a7.grad());
if (a7.grad() != null) {
System.out.printf("Before zeroing, a.grad: %s, 梯度即将归零%n", a7.grad());
a7.grad().zero_();
System.out.printf("After zeroing, a.grad: %s, 梯度已归零%n", a7.grad());
}
try {
Tensor L2 = a7.add(d8).mean();
L2.backward();
System.out.printf("Gradient dL2/da: %s%n", a7.grad());
} catch (RuntimeException e) {
System.out.printf("Error demonstrating backward with detached: %s%n", e.getMessage());
}
try (NoGradGuard noGradGuard3 = new NoGradGuard()) {
}
System.out.printf("After modifying c, a = %s%n", a7);
System.out.printf("After modifying c, c = %s%n", c8);
```
`detach()` 在您想在计算中使用张量的值但阻止梯度通过该特定路径回流时很有用,或者当您需要一个没有梯度历史的张量时(例如,用于绘图或日志记录)。请注意,它共享数据存储,因此原位修改会影响原始张量,除非您先 `.clone()` 它(`c = a.detach().clone()`)。
这些练习展示了 Autograd 的核心机制。您已经练习了启用梯度跟踪、执行反向传播、查看计算出的梯度、理解累积以及在需要时禁用跟踪。掌握这些操作对在 PyTorch 中构建和训练神经网络来说非常重要。