【Java深度学习】PyTorch On Java 系列课程 第三章 07 :张量梯度【AI Inf

访问梯度(.grad

收藏

在计算标量张量(通常是损失)相对于计算图中其他张量的梯度时,PyTorch 使用 backward() 方法。此方法触发梯度计算,但它不会直接返回梯度。相反,PyTorch 会将计算出的梯度存储在张量自身的一个特殊属性中:即 .grad 属性。

这个属性主要为计算图中的张量填充,即那些你通过设置 requires_grad=True 明确要求进行梯度跟踪的张量。请记住,叶张量通常是你直接创建的,例如模型参数或输入,而不是通过运算生成的中间张量。

.grad 属性包含一个与它所属的原始张量形状相同的张量。.grad 张量中的每个元素表示标量(调用 backward() 的对象)相对于原始张量中对应元素的偏导数。如果 LL 是标量损失,ww 是一个张量参数,那么在调用 L.backward() 之后,属性 w.grad 将包含表示 ∂L∂w∂wL 的张量。

我们通过一个简单例子来说明这一点:

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.0 * 2.0 + 1.0 = 7.0

// 计算梯度
y.backward()

// 访问存储在 .grad 属性中的梯度
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}")

        // 10. 基本梯度访问
        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 = 3.0 * 2.0 + 1.0 = 7.0
        
        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. 我们定义了 xwb,并设置 requires_grad=True,将它们标记为我们想要梯度的叶节点。
  2. 我们执行了操作 y=w∗x+by=wx+b。PyTorch 在后台构建了一个计算图。
  3. 我们调用了 y.backward()。Autograd 从 y 开始向后遍历图以计算梯度。
    • ∂y∂x=w=3.0∂xy=w=3.0
    • ∂y∂w=x=2.0∂wy=x=2.0
    • ∂y∂b=1=1.0∂by=1=1.0
  4. 计算出的梯度存储在 x.gradw.gradb.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

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}") // 输出:y.requires_grad: True
println(f"y.grad_fn: {y.grad_fn}")           // 输出:y.grad_fn: <AddBackward0 object at ...>

// 在no_grad上下文内执行操作
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 ...>
        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 的输出(zk),这表明它们已脱离计算图历史。这正是你在评估循环中想要的结果:

// 评估循环片段
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()

// 计算平均损失和准确率...
import org.pytorch.*;
import org.pytorch.data.Dataset;
import org.pytorch.data.DataLoader;
import java.util.Iterator;

public class ModelEvaluationExample {
    // 假设已有预定义的模型、损失函数、验证数据集和设备
    private Module model; // 你的模型(如ResNet、自定义模型)
    private Loss criterion; // 损失函数(如CrossEntropyLoss)
    private DataLoader validationDataloader; // 验证数据集加载器
    private Device device; // 运行设备(CPU/CUDA)

    public void evaluateModel() {
        // 1. 将模型设置为评估模式(对应model.eval())
        model.eval();

        // 初始化统计变量
        double totalLoss = 0.0;
        long correctPredictions = 0;
        int totalSamples = 0;

        // 2. 禁用梯度计算(对应with torch.no_grad())
        try (NoGradGuard noGradGuard = NoGradGuard.create()) {
            // 3. 遍历验证数据集
            Iterator<IValue[]> iterator = validationDataloader.iterator();
            while (iterator.hasNext()) {
                IValue[] batch = iterator.next();
                // 从批次中获取输入和标签(适配DataLoader的输出格式)
                Tensor inputs = batch[0].toTensor();
                Tensor labels = batch[1].toTensor();

                // 4. 将数据移动到指定设备
                inputs = inputs.to(device);
                labels = labels.to(device);

                // 5. 前向传播
                Tensor outputs = model.forward(IValue.from(inputs)).toTensor();

                // 6. 计算损失
                Tensor lossTensor = criterion.forward(outputs, labels).toTensor();
                double lossValue = lossTensor.item().doubleValue();
                totalLoss += lossValue;

                // 7. 计算预测结果(对应torch.max(outputs.data, 1).values)
                // torch.max(input, dim) 返回 (values, indices),这里取indices(预测类别)
                Tensor[] maxResult = torch.max(outputs, 1);
                Tensor predicted = maxResult[1]; // 第1个元素是索引(预测类别)

                // 8. 统计正确预测数(对应(predicted == labels).sum().item())
                // 比较预测值和标签,生成布尔张量后转换为浮点型求和
                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(); // 释放max返回的values张量
            }
        }

        // 9. 计算平均损失和准确率
        double avgLoss = totalLoss / validationDataloader.size();
        double accuracy = (double) correctPredictions / totalSamples;

        // 打印评估结果
        System.out.printf("验证集平均损失: %.4f%n", avgLoss);
        System.out.printf("验证集准确率: %.4f%%%n", accuracy * 100);
    }

    // 辅助方法:获取PyTorch的torch工具类(需确保依赖正确)
    private static org.pytorch.torch.Torch torch() {
        return org.pytorch.torch.Torch.INSTANCE;
    }

    // 注意:需根据实际场景初始化model/criterion/validationDataloader/device
    // 例如:
    // model = Module.load("your_model.pt");
    // criterion = Loss.crossEntropyLoss();
    // validationDataloader = new DataLoader(validationDataset, new DataLoader.Options().batchSize(32));
    // device = Device.isCudaAvailable() ? new Device(Device.Type.CUDA) : new Device(Device.Type.CPU);
}

使用 .detach() 方法

另一种阻止特定张量进行梯度追踪的方法是使用 .detach() 方法。此方法会创建一个张量,它与原始张量共享底层数据存储,但明确地脱离了当前计算图。它将拥有 requires_grad=False

import torch.*

// 需要梯度的原始张量
val a = torch.randn(3, 3, requires_grad=true)
val b = a * 2
println(f"b.requires_grad: {b.requires_grad}") // 输出:b.requires_grad: True
println(f"b.grad_fn: {b.grad_fn}")           // 输出:b.grad_fn: <MulBackward0 object at ...>

// 分离张量 'b'
val c = b.detach()
println(f"\n分离b以创建c后:")
println(f"c.requires_grad: {c.requires_grad}") // 输出:c.requires_grad: False
println(f"c.grad_fn: {c.grad_fn}")           // 输出:c.grad_fn: None

// 重要的是,原始张量 'b' 未改变
println(f"\n原始张量 b 仍保持连接:")
println(f"b.requires_grad: {b.requires_grad}") // 输出:b.requires_grad: True
println(f"b.grad_fn: {b.grad_fn}")           // 输出:b.grad_fn: <MulBackward0 object at ...>

// 使用分离张量 'c' 的操作将不会被追踪
val d = c + 1
println(f"\n在分离张量 c 上的操作:")
println(f"d.requires_grad: {d.requires_grad}") // 输出:d.requires_grad: False

        // 13. 张量分离
        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() 相比,这种临时禁用方法通常不太常见。它通常用于定义你明确不希望训练的参数。

// 初始张量
val my_tensor = torch.randn(5, requires_grad=true)
println(f"初始requires_grad: {my_tensor.requires_grad}") // 输出:初始requires_grad: True

// 原地禁用梯度追踪
my_tensor.requires_grad_(false) // 注意下划线表示原地操作
println(f"requires_grad_(false)后: {my_tensor.requires_grad}") // 输出:requires_grad_(false)后: False
       // 14. 原地禁用梯度追踪
        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属性中已有的值上。

我们用一个简单例子来说明这一点:

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 进行演示
// 注意:在实际应用中,您可能会基于新的输入或模型的不同部分计算新的损失。
z.backward() // 第二次反向传播
// 我们预期新梯度 (12) 会被加到已有的梯度 (12) 上
println(f"After second backward pass, x.grad: {x.grad}")

// 手动清零梯度
x.grad.zero_()
println(f"After zeroing, x.grad: {x.grad}")

        // 15. 多次反向传播
        Tensor x3 = tensor(new DoublePointer(2.0), requires_grad(true));
        
        Tensor y3 = x3.mul(x3);
        Tensor z3 = y3.mul(new Scalar(3));  // z = 3 * x^2
        
        z3.backward(new Tensor(), new BoolOptional(true),false, new TensorArrayRefOptional());  // retain_graph=true
        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());
        

运行此代码会产生类似于以下的输出:

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() 调用完全基于当前批量的损失来计算梯度。

典型的训练迭代结构如下所示:

// 假设 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()

    // ... (记录日志、评估等)


optimizer.zero_grad() 的放置位置很重要。它应该在您计算当前迭代的损失并执行反向传播之前发生,确保当前批量的梯度计算有一个干净的开始。虽然它通常放在循环的开头,但技术上它只需要在 loss.backward() 之前发生。然而,将其放在开头是常见做法,并且能清楚地划分新批量处理的开始。

总而言之,梯度累积是 PyTorch 的一个内置功能,对于模拟更大的批量数据很有用。然而,在标准训练循环中,您必须通过在每次迭代开始时调用 optimizer.zero_grad() 来明确阻止这种累积,以确保模型更新仅基于当前批量的数据是正确的。

动手实践:Autograd 运用

收藏

实际例子演示 PyTorch Autograd 系统。这些练习会引导您设置梯度要求、执行反向传播、查看梯度、观察累积以及禁用梯度跟踪。请确保您已安装 PyTorch 并能导入 torch 库。

设置

首先,导入 PyTorch:

import torch

例子 1:基本梯度计算

我们从一个非常简单的计算开始,并跟踪梯度。我们将定义两个张量 xw,其中 w 表示我们想要优化的权重。我们将计算一个简单的输出 y,然后计算一个标量损失 L

  1. 创建张量:将 x 定义为一个包含一些数据的张量,将 w 定义为一个需要计算其梯度的张量(使用 requires_grad=True)。

    // 输入数据
    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}")
    

        // 1. 创建输入张量x(无梯度,对应原代码的 [2.0, 4.0, 6.0])
        float[] xData = {2.0f, 4.0f, 6.0f};
        // 默认创建的张量requires_grad=false(无需计算梯度)
        Tensor x = torch.from_blob(new FloatPointer(xData), new long[]{3});

        // 2. 创建权重张量w(需计算梯度,对应requires_grad=true)
        float[] wData = {0.5f};
        // 通过TensorOptions显式设置requiresGrad=true
        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);

        // 3. 打印张量及梯度属性(模拟原代码的println)
       
        System.out.println("x.requires_grad: " + x.requires_grad());
        System.out.println("w.requires_grad: " + w.requires_grad());

请注意,x 默认情况下不需要梯度,而我们为 w 显式设置了它。

  1. 定义计算:执行一个简单的运算。任何通过涉及 requires_grad=True 的张量运算而得到的张量,其 requires_grad 也会是 True

    // 前向传播:y = w * x
    val y = w * x
    
    // 定义一个简单的标量损失 L(例如,y 的均值)
    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}")
    
       // 17-18. 简单的线性回归示例
         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());
         
    
    

    您会看到 yL 现在都需要梯度,因为它们依赖于 w

  2. 计算梯度:在最终的标量输出 (L) 上使用 .backward() 方法来计算整个图中的梯度。

    // 执行反向传播
    L.backward()
    
  3. 查看梯度:查看张量 w.grad 属性。

    // 梯度存储在 w.grad 中
    println(f"Gradient dL/dw: {w.grad}")
    
    // x 不需要梯度,因此它的梯度为 None
    println(f"Gradient dL/dx: {x.grad}")
    

    我们来分析 w.grad 的结果。计算过程为: yi=w∗xiy**i=wx**i L=13∑yi=13(wx1+wx2+wx3)L=31∑y**i=31(w**x1+w**x2+w**x3) 梯度 ∂L∂w∂wL 为:

    ∂L∂w=13(x1+x2+x3)∂wL=31(x1+x2+x3)

    当 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. 创建张量

    // 输入数据
    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}")
    
    
            // 19-21. 复杂计算图和梯度检查
         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. 定义计算

    // 前向传播
    val d = a * b
    val e = d + c
    val f = e * 2
    
    println(f"d: {d}, requires_grad={d.requires_grad}") // True(依赖于 a, b)
    println(f"e: {e}, requires_grad={e.requires_grad}") // True(依赖于 d)
    println(f"f: {f}, requires_grad={f.requires_grad}") // True(依赖于 e)
    
          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. 计算并查看梯度

    // 从最终的标量输出 f 进行反向传播
    f.backward()
    
    // 检查梯度
    println(f"Gradient df/da: {a.grad}")
    println(f"Gradient df/db: {b.grad}")
    println(f"Gradient df/dc: {c.grad}") // 预期结果:None
    
    
            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());  // 预期结果:null
         
         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×bd=a×b e=d+c=a×b+ce=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∂af=2×b=2×3.0=6.0 ∂f∂b=2×a=2×2.0=4.0∂bf=2×a=2×2.0=4.0 ∂f∂c=2∂cf=2

    ab 的计算梯度是匹配的。由于 c 定义时 requires_grad=False,Autograd 没有跟踪涉及 c 的操作来计算关于 c 本身的梯度,因此 c.gradNone

例子 3:梯度累积

默认情况下,每次调用 .backward() 时,梯度都会累积到 .grad 属性中。这对于计算多个损失的梯度或模拟更大的批次大小等情况很有用,但在标准训练循环中,需要显式地将梯度清零。

  1. 设置:我们再次使用一个简单的设置。

    // 输入数据
    val x = torch.tensor(5.0, requires_grad=true)
    val y = x * x
    println(f"Initial x.grad: {x.grad}") // 最初应为 None
    
    
    
         // 1. 创建标量张量x(值为5.0,开启梯度计算,对应requires_grad=true)
         // 标量张量的形状为空数组new long[]{}
         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);
    
         // 2. 执行张量运算 y = x * x
         Tensor y = x.mul(x); // mul()方法实现张量乘法,对应Python的 * 运算符
    
         // 3. 打印初始梯度值(原代码中x.grad最初为None)
         // Java中未计算梯度时,x.grad()返回null
         System.out.println("Initial x.grad: " + x.grad());
    
  2. 第一次反向传播

    // 对 y 执行反向传播。注意:backward() 通常需要一个标量。
    // 如果在非标量张量上调用,需要提供梯度参数。
    // 为了演示,我们计算 y 对 x 的梯度(即 2x)。
    // 我们将使用 y.backward(gradient=torch.tensor(1.0)) 来实现。
    // 通常,您会有一个从 y 导出的标量损失 L。
    // 如果 y 是多元素,则 L = y.mean();如果 y 是标量,则 L = y。
    y.backward(retain_graph=True) // 对于多次反向传播需要 retain_graph=True
    println(f"x.grad after 1st backward: {x.grad}") // 预期结果:2*x = 10.0
    
            // ========== 2. 构造原生backward方法的所有参数 ==========
         // 参数1: gradient - 反向传播的梯度张量(对应原代码的torch.tensor(1.0))
         // @Const @ByRef(nullValue = "at::Tensor{}") 表示默认是空张量,这里显式传标量1.0
         float[] gradData = {1.0f};
         Tensor gradient = torch.from_blob(new FloatPointer(gradData), new long[]{});
    
         // 参数2: retain_graph - 是否保留计算图(对应原代码的retain_graph=True)
         // @ByVal(nullValue = "std::optional<bool>(std::nullopt)") 表示默认是std::nullopt
         // BoolOptional.of(true) 等价于 Python 的 retain_graph=True
         BoolOptional retainGraph = new BoolOptional(true);
    
         // 参数3: create_graph - 是否创建计算图(默认false,反向传播时不构建二阶导数图)
         // @Cast("bool") boolean create_graph/*=false*/
         boolean createGraph = false;
    
         // 参数4: inputs - 要计算梯度的输入张量列表(默认空,计算所有叶子节点梯度)
         // @ByVal(nullValue = "std::optional<at::TensorList>(std::nullopt)")
         TensorArrayRefOptional inputs = new TensorArrayRefOptional();
    
         // ========== 3. 调用原生backward方法 ==========
         // 直接调用底层native方法,参数完全匹配你提供的签名
         y.backward(gradient, retainGraph, createGraph, inputs);
    
    
  3. 第二次反向传播(累积):再次调用 backward清零梯度。

    // 再次调用 backward
    y.backward(retain_graph=True) // 再次调用 backward
    println(f"x.grad after 2nd backward: {x.grad}") // 预期结果:10.0 + 10.0 = 20.0
    
    
         // ========== 2. 构造backward方法的通用参数 ==========
         // 参数1: gradient - 反向传播的梯度张量(标量1.0)
         float[] gradData = {1.0f};
         Tensor gradient = Tensor.fromBlob(new FloatPointer(gradData), new long[]{});
    
         // 参数2: retain_graph - 保留计算图(必须为true,否则第二次调用会报错)
         BoolOptional retainGraph = new BoolOptional(true);
    
         // 参数3: create_graph - 不创建二阶导数图(默认false)
         boolean createGraph = false;
    
         // 参数4: inputs - 空列表,计算所有叶子节点梯度
         TensorArrayRefOptional inputs = new TensorArrayRefOptional();
    
         // ========== 3. 第一次调用反向传播 ==========
         y.backward(gradient, retainGraph, createGraph, inputs);
         // 打印第一次反向传播后的梯度(预期10.0)
         System.out.print("x.grad after 1st backward: ");
         System.out.println(x.grad().getDataAsFloatArray()[0]);
    
         // ========== 4. 第二次调用反向传播 ==========
         // 复用相同参数(retain_graph仍为true,支持多次调用)
         y.backward(gradient, retainGraph, createGraph, inputs);
         // 打印第二次反向传播后的梯度(预期10.0+10.0=20.0)
         System.out.print("x.grad after 2nd backward: ");
         System.out.println(x.grad().getDataAsFloatArray()[0]);
    
    

    梯度被累积(相加)到之前的值上。

  4. 清零梯度:手动清零梯度。在典型的训练循环中,这通常通过 optimizer.zero_grad() 完成。

    // 手动清零梯度
    if x.grad is not None:
        x.grad.zero_() // 原位清零
    println(f"x.grad after zeroing: {x.grad}") // 预期结果:0.0
    
  5. 第三次反向传播(清零后)

    // 最后一次反向传播不需要 retain_graph
    y.backward() // 最后一次反向传播不需要 retain_graph
    println(f"x.grad after 3rd backward: {x.grad}") // 预期结果:10.0
    

    梯度清零后会重新计算。在训练循环中忘记清零梯度是常见的错误原因。

例子 4:禁用梯度跟踪

有时,您需要执行操作而不跟踪其梯度计算,最常见的情况是在模型评估(推理)期间或在优化步骤之外调整参数时。

  1. 使用 torch.no_grad():这个上下文管理器是禁用代码块梯度跟踪的标准方法。

    // 上下文管理器 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
    
            // 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():这个方法会创建一个张量,它共享相同的数据,但与计算历史分离。它不需要梯度。

    // 分离 a,创建一个不需要梯度的新张量 c
    val c = a.detach()
    println(f"a.requires_grad: {a.requires_grad}") // True
    println(f"c.requires_grad: {c.requires_grad}") // False
    
    // 分离 a,创建一个不需要梯度的新张量 c
    val d = a.detach()
    println(f"a.requires_grad: {a.requires_grad}") // True
    println(f"d.requires_grad: {d.requires_grad}") // False
    
    // 涉及 c 的操作不会跟踪回 a
    val e = c * 3 // d 不需要梯度
    println(f"e.requires_grad: {e.requires_grad}") // False
    
    // 如果您对涉及 'b' 的计算执行反向传播,
    // 它会流回 'a'。如果您使用 'd',则不会。
    val L1 = b.mean() // 依赖于 'a'
    L1.backward()
    println(f"Gradient dL1/da: {a.grad}") // 预期结果:2*a = 10.0
    
    // 在下一次反向调用前清零梯度
    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}")
    
    
    // 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()) {
             // c8是标量,无法直接索引修改,所以这里不执行修改操作
         }
         
         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 中构建和训练神经网络来说非常重要。

picture.image

0
0
0
0
评论
未登录
暂无评论