【神经网络】grad_fn 和 grad

1. grad_fn

它是动态计算图的一部分,记录了输入张量所执行的操作。用于在反向传播时追踪计算路径。

(1)核心作用

  • 记录操作历史:它记录 输入张量 是通过什么运算(如加法,乘法,矩阵运算等)得到的。如 y = x + z; 那么 g.grad_fn 会指向一个 AddBackword0 对象
  • 构建计算图链接:通过 grad_fn,PyTorch 可以从输出张量一步步回溯到输入张量(叶子张量),形成一条完整的反向传播链条。
  • 提供求导公式:每个 grad_fn 对象内部不仅知道前向传播怎么算,还知道对应的反向传播求导规则(即如何计算梯度)

(2)注意:

  • 叶子节点时,由于没有前驱操作 所以grad_fn 为 None ;
  • 非叶子节点,由其他张量运算得到的中间结果张量,grad_fn 不为 None。

2. grad

它是张量的一个属性,用于存储当前张量相对于某个标量损失函数的梯度值。

(1)核心作用:

  • 存储梯度结果:当调用 .backword() 进行反向传播后, PyTorch计算出的梯度值会被累加并存储在叶子节点的 .grad 属性中
  • 参数更新依据:优化器(如 SGD、Adam)读取 .grad 中的数值,根据梯度下降算法更新模型的权重和偏置。

(2)注意:

  • 仅叶子节点保留 grad:默认情况下,叶子节点的:requires_grad = Flase,需要开启 requires_grad 才会保留梯度值。
  • 中间节点的梯度值在反向传播计算完成之后,通常会被释放以节省内存,除非显式调用.retain_grad()。
  • 初始为None: 在第一次反向传播之前,.grad 的值通常为 None
  • 梯度累加:PyTorch 不会自动清零梯度。每次调用 .backword() 时,新计算的梯度会累加到现有 .grad 上。因此,在每次训练迭代开始之前,通常需要手动调用 optimizer.zero_grad() 或 tensor.grad_zero() 来清零梯度。

3. 对比

特性 grad_fn grad
‌含义‌ ‌梯度函数‌(Gradient Function) ‌梯度值‌(Gradient Value)
‌类型‌ Function 对象(如 AddBackward0) Tensor(张量)
‌作用阶段‌ ‌前向传播时构建‌,反向传播时使用 ‌反向传播后生成‌,优化更新时使用
‌存在位置‌ 所有非叶子节点(中间张量)都有;叶子节点为 None 通常只存在于需要更新的叶子节点(参数)中
‌核心功能‌ 记录“怎么算出来的”,提供反向求导逻辑 存储“算出来的结果”,即导数的具体数值
‌是否为空‌ 叶子节点为 None,中间节点有值 反向传播前为 None,反向传播后有值

4. 代码示例

import torch

# 1. 创建叶子节点 x,设置 requires_grad=True
x = torch.tensor(2.0, requires_grad=True)
print(f"x.grad_fn: {x.grad_fn}")  # 输出: None (叶子节点无前驱操作)
print(f"x.grad: {x.grad}")        # 输出: None (尚未反向传播)

# 2. 前向传播:构建计算图
y = x ** 2       # y 是中间节点
print(f"y.grad_fn: {y.grad_fn}")  # 输出: <PowBackward0 object> (记录的是平方操作)
print(f"y.grad: {y.grad}")        # 输出: None (中间节点默认不存梯度)

z = y * 3        # z 是中间节点
print(f"z.grad_fn: {z.grad_fn}")  # 输出: <MulBackward0 object> (记录的是乘法操作)

# 3. 反向传播:计算梯度
z.backward()     # 从 z 开始反向传播,计算 dz/dx

# 4. 查看结果
print(f"x.grad: {x.grad}")        # 输出: tensor(12.) 
# 解释: dz/dx = d(3*x^2)/dx = 6x = 6*2 = 12. 
# 梯度值存储在叶子节点 x 的 grad 属性中

5. 答疑

(1)为什么存储的张量对于标量的损失函数梯度值 而不是张量对于向量,矩阵或者三维高阶张量的损失函数梯度值?

总结:

  1. 优化目标唯一性‌:深度学习的目标是‌最小化一个数值‌(总损失)。只有标量才有“最小值”的概念,向量或矩阵没有单一的“最小值”方向。
  2. 梯度下降法的要求‌:参数更新公式 W=W−η⋅∇LW=W−η⋅∇L 中,∇L∇L 必须是一个与 WW 同形的张量。这只有在 LL 是标量时才成立。
  3. 计算效率‌:存储完整的雅可比矩阵(尤其是对于大规模神经网络)需要巨大的内存。而标量损失的梯度向量不仅内存占用小,而且直接可用于更新参数。
  1. "标量损失函数" 是反向传播能够启动并计算出一个确定梯度值的前提条件。

如果损失函数不是一个标量(例如是一个向量或者矩阵),PyTorch 就无法直接给出一个唯一梯度,因为此时涉及的是雅可比矩阵(Jacobian Matrix) 而非简单的梯度向量。

  1. 数学原理:标量函数L:R ** n→R

输入是 n 维向量(模型参数),输出是‌一个数‌(损失值 Loss)。

此时,梯度 ∇L 是一个与输入形状相同的向量,表示Loss随每个参数变化的速率。

∇L=[∂L/∂W1, ∂L/∂W2, ..., ∂L/∂Wn ] ** T

这就是 .grad 存储的内容。

  1. 数学原理:向量函数 F:R ** n→R ** m‌(m>1):

    如果损失函数输出是一个向量(例如每个样本都有一个独立的Loss,组成一个向量Lvec),那么它对参数W的导数不再是一个向量,而是一个雅可比矩阵(如图)。
    image

问题在于:优化器(如SGD)需要的是一个方向来更新参数W。面对一个矩阵,我们无法直接说往"哪个方向走能最小化整个矩阵"。我们需要将这个矩阵"压缩"成一个向量,才能进行参数更新。

  1. 核心逻辑:链式法则

反向传播的本质是链式法则。链式法则要求从后往前传递"误差信号"。

  • 起点必须单一:为了计算∂L / ∂ W,我们需要一个最终的标量L 作为求导的起点。

  • 多维输出的处理:如果你的模型输出的是向量(比如Batch Size=32,算出32个Loss值),PyTorch无法判断哪个Loss值更重要。

    • 是想要最小化它们总和,还是平均值,还是最大值? 只有当你通过.sum()、.mean() 等操作将这32个值合并成一个标量后,数学上才存在唯一的"总损失对参数的梯度"。
  1. 【PyTorch】工程实现 backward() 设计。
y = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
z = y ** 2  # z 也是向量 [1.0, 4.0, 9.0]
z.backward() # ❌ 报错!RuntimeError: grad can be implicitly created only for scalar outputs

推荐使用:

(1)转换为标量(推荐) ‌:

loss = z.mean() # 变成标量
loss.backward() # ✅ 成功,计算平均损失对 y 的梯度

(2)提供外部梯度 (推荐) ‌:

如果你确实想计算向量对向量的雅可比向量积(Vector-Jacobian Product),你需要手动传入一个与输出形状相同的 gradient 参数(相当于给每个输出分量赋予权重):

z.backward(gradient=torch.tensor([1.0, 1.0, 1.0])) 
# 这等价于先求和再求导

(2)哪种情况下,第一次反向传播之前 .grad 不为None?

  1. 手动赋值
import torch
x = torch.tensor(2.0, requires_grad=True)
print(x.grad)  # 输出: None

# 手动赋值
x.grad = torch.tensor(5.0)
print(x.grad)  # 输出: tensor(5.)  <-- 此时还没调用 backward,但 grad 不为 None

x.backward()   # 注意:默认情况下,backward 会累加梯度
print(x.grad)  # 输出: tensor(7.) (5.0 + 2.0)
  1. 梯度累加机制

PyTorch 的 .backward()默认行为是‌累加梯度‌(accumulate),而不是覆盖。
如果你在一个训练循环中,没有调用 optimizer.zero_grad()model.zero_grad(),那么上一轮迭代计算出的梯度会保留在 .grad 中。

  • 场景‌:在第二轮训练的 backward() 调用之前,.grad 存储的是第一轮训练的梯度值,显然不为 None
  • 最佳实践‌:这就是为什么官方文档建议每次迭代开始前必须清零梯度,否则梯度会无限累加导致训练发散。
  1. 从检查点加载(Checkpoint Loading)

当你使用 torch.load() 加载一个之前保存的训练状态(state_dict)时,如果保存的内容中包含了优化器状态或显式保存了梯度信息,恢复后的模型参数的 .grad 可能会直接恢复为之前保存的值,而不是 None


import torch
import torch.nn as nn
import torch.optim as optim
import os
import tempfile

# 定义一个简单的神经网络模型
class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.fc1 = nn.Linear(10, 5)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(5, 1)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

def save_checkpoint(model, optimizer, epoch, loss, path):
    """
    保存检查点,包含模型状态、优化器状态、epoch和loss
    """
    checkpoint = {
        'epoch': epoch,
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
        'loss': loss,
    }
    torch.save(checkpoint, path)
    print(f"Checkpoint saved to {path}")

def load_checkpoint(path, model, optimizer):
    """
    从检查点加载模型和优化器状态
    """
    if not os.path.exists(path):
        raise FileNotFoundError(f"Checkpoint file not found: {path}")
    
    # 加载检查点
    checkpoint = torch.load(path, map_location='cpu', weights_only=False)
    
    # 恢复模型状态
    model.load_state_dict(checkpoint['model_state_dict'])
    
    # 恢复优化器状态
    optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
    
    epoch = checkpoint['epoch']
    loss = checkpoint['loss']
    
    print(f"Checkpoint loaded from {path}")
    print(f"Resuming from epoch: {epoch}, last loss: {loss:.4f}")
    
    return model, optimizer, epoch, loss

def main():
    # 初始化模型和优化器
    model = SimpleModel()
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    
    # 创建临时目录用于存储检查点
    with tempfile.TemporaryDirectory() as tmpdirname:
        checkpoint_path = os.path.join(tmpdirname, 'model_checkpoint.pth')
        
        # --- 模拟训练过程并保存检查点 ---
        print("--- Step 1: Simulating Training and Saving Checkpoint ---")
        dummy_input = torch.randn(4, 10)
        dummy_target = torch.randn(4, 1)
        
        # 模拟几个epoch的训练
        for epoch in range(1, 4):
            model.train()
            optimizer.zero_grad()
            output = model(dummy_input)
            loss = nn.MSELoss()(output, dummy_target)
            loss.backward()
            optimizer.step()
            print(f"Epoch [{epoch}/3], Loss: {loss.item():.4f}")
            
            # 每个epoch保存一次检查点(实际项目中通常根据策略保存)
            save_checkpoint(model, optimizer, epoch, loss.item(), checkpoint_path)
        
        # --- 模拟中断后从检查点恢复 ---
        print("\n--- Step 2: Loading Checkpoint and Resuming ---")
        
        # 创建一个新的模型实例和优化器(模拟重新启动程序)
        new_model = SimpleModel()
        new_optimizer = optim.SGD(new_model.parameters(), lr=0.01)
        
        # 加载之前的检查点
        loaded_model, loaded_optimizer, start_epoch, last_loss = load_checkpoint(
            checkpoint_path, new_model, new_optimizer
        )
        
        # 验证加载后的模型是否能继续训练
        print("\n--- Step 3: Verifying Loaded Model by Running One More Epoch ---")
        loaded_model.train()
        loaded_optimizer.zero_grad()
        output = loaded_model(dummy_input)
        loss = nn.MSELoss()(output, dummy_target)
        loss.backward()
        loaded_optimizer.step()
        print(f"Epoch [{start_epoch + 1}/4], Loss: {loss.item():.4f}")
        print("Demo completed successfully.")

if __name__ == '__main__':
    main()

# -----------------------输出结果
# --- Step 1: Simulating Training and Saving Checkpoint ---
# Epoch [1/3], Loss: 1.3697
# Checkpoint saved to C:\Users\Administrator\AppData\Local\Temp\tmpq2zq2okk\model_checkpoint.pth
# Epoch [2/3], Loss: 1.2662
# Checkpoint saved to C:\Users\Administrator\AppData\Local\Temp\tmpq2zq2okk\model_checkpoint.pth
# Epoch [3/3], Loss: 1.1678
# Checkpoint saved to C:\Users\Administrator\AppData\Local\Temp\tmpq2zq2okk\model_checkpoint.pth

# --- Step 2: Loading Checkpoint and Resuming ---
# Checkpoint loaded from C:\Users\Administrator\AppData\Local\Temp\tmpq2zq2okk\model_checkpoint.pth
# Resuming from epoch: 3, last loss: 1.1678
  1. 特殊初始化 或 自定义逻辑

在某些高级用法中,开发者可能会编写自定义初始化脚本,预先为某些参数的 .grad 赋予初始值(例如在某些元学习或二阶优化算法中,可能需要预置梯度信息)。

(2)雅可比矩阵

image

posted @ 2026-05-12 11:44  静心笃行。  阅读(27)  评论(0)    收藏  举报