【神经网络】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)为什么存储的张量对于标量的损失函数梯度值 而不是张量对于向量,矩阵或者三维高阶张量的损失函数梯度值?
总结:
- 优化目标唯一性:深度学习的目标是最小化一个数值(总损失)。只有标量才有“最小值”的概念,向量或矩阵没有单一的“最小值”方向。
- 梯度下降法的要求:参数更新公式 W=W−η⋅∇LW=W−η⋅∇L 中,∇L∇L 必须是一个与 WW 同形的张量。这只有在 LL 是标量时才成立。
- 计算效率:存储完整的雅可比矩阵(尤其是对于大规模神经网络)需要巨大的内存。而标量损失的梯度向量不仅内存占用小,而且直接可用于更新参数。
- "标量损失函数" 是反向传播能够启动并计算出一个确定梯度值的前提条件。
如果损失函数不是一个标量(例如是一个向量或者矩阵),PyTorch 就无法直接给出一个唯一梯度,因为此时涉及的是雅可比矩阵(Jacobian Matrix) 而非简单的梯度向量。
- 数学原理:标量函数L:R ** n→R
输入是 n 维向量(模型参数),输出是一个数(损失值 Loss)。
此时,梯度 ∇L 是一个与输入形状相同的向量,表示Loss随每个参数变化的速率。
∇L=[∂L/∂W1, ∂L/∂W2, ..., ∂L/∂Wn ] ** T
这就是
.grad存储的内容。
-
数学原理:向量函数 F:R ** n→R ** m(m>1):
如果损失函数输出是一个向量(例如每个样本都有一个独立的Loss,组成一个向量Lvec),那么它对参数W的导数不再是一个向量,而是一个雅可比矩阵(如图)。
![image]()
问题在于:优化器(如SGD)需要的是一个方向来更新参数W。面对一个矩阵,我们无法直接说往"哪个方向走能最小化整个矩阵"。我们需要将这个矩阵"压缩"成一个向量,才能进行参数更新。
- 核心逻辑:链式法则
反向传播的本质是链式法则。链式法则要求从后往前传递"误差信号"。
起点必须单一:为了计算∂L / ∂ W,我们需要一个最终的标量L 作为求导的起点。
多维输出的处理:如果你的模型输出的是向量(比如Batch Size=32,算出32个Loss值),PyTorch无法判断哪个Loss值更重要。
- 是想要最小化它们总和,还是平均值,还是最大值? 只有当你通过.sum()、.mean() 等操作将这32个值合并成一个标量后,数学上才存在唯一的"总损失对参数的梯度"。
- 【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?
- 手动赋值
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)
- 梯度累加机制
PyTorch 的
.backward()默认行为是累加梯度(accumulate),而不是覆盖。
如果你在一个训练循环中,没有调用optimizer.zero_grad()或model.zero_grad(),那么上一轮迭代计算出的梯度会保留在.grad中。
- 场景:在第二轮训练的
backward()调用之前,.grad存储的是第一轮训练的梯度值,显然不为None。- 最佳实践:这就是为什么官方文档建议每次迭代开始前必须清零梯度,否则梯度会无限累加导致训练发散。
- 从检查点加载(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
- 特殊初始化 或 自定义逻辑
在某些高级用法中,开发者可能会编写自定义初始化脚本,预先为某些参数的
.grad赋予初始值(例如在某些元学习或二阶优化算法中,可能需要预置梯度信息)。
(2)雅可比矩阵



浙公网安备 33010602011771号