ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Bug已解决】How to use autograd.gradcheck in PyTorch? 解决方案

【Bug已解决】How to use autograd.gradcheck in PyTorch? 解决方案

【Bug已解决】How to use autograd.gradcheck in PyTorch? 解决方案

问题描述

torch.autograd.gradcheck是 PyTorch 中用于验证自定义 autograd Function 梯度正确性的工具。它通过数值微分(numerical differentiation)计算参考梯度,然后与 autograd 计算的解析梯度进行比较。

许多开发者在使用gradcheck时会遇到以下问题:

  1. 要求float64精度:gradcheck不接受float32输入
  2. 输入需要requires_grad=True:忘记设置导致报错
  3. 多输入/多输出函数的用法:如何传入多个张量
  4. 数值不稳定:某些函数在数值微分时不稳定
  5. gradgradcheck的使用:验证二阶梯度的正确性

典型错误:

import torch # 错误:使用 float32 x = torch.randn(5, requires_grad=True) # 默认 float32 # torch.autograd.gradcheck(torch.exp, x) # RuntimeError: gradcheck expects all tensors to be of dtype torch.float64 # 错误:忘记 requires_grad x = torch.randn(5, dtype=torch.float64) # 没有 requires_grad # torch.autograd.gradcheck(torch.exp, x) # RuntimeError: gradcheck requires input tensors to have requires_grad=True

错误复现

错误场景一:float32 输入

import torch x = torch.randn(5, requires_grad=True, dtype=torch.float32) # torch.autograd.gradcheck(lambda t: t.pow(2).sum(), x) # RuntimeError: gradcheck expects all tensors to be of dtype torch.float64

错误场景二:忘记 requires_grad

x = torch.randn(5, dtype=torch.float64) # 没有 requires_grad=True # torch.autograd.gradcheck(lambda t: t.exp().sum(), x) # RuntimeError: gradcheck requires input tensors to have requires_grad=True

错误场景三:多输入函数

x = torch.randn(5, dtype=torch.float64, requires_grad=True) y = torch.randn(5, dtype=torch.float64, requires_grad=True) # 错误:gradcheck 需要输入是 tuple # torch.autograd.gradcheck(lambda a, b: (a * b).sum(), x, y) # TypeError: gradcheck expects inputs as a tuple # 正确 # torch.autograd.gradcheck(lambda a, b: (a * b).sum(), (x, y))

错误场景四:自定义 Function 梯度错误

import torch class WrongSquare(torch.autograd.Function): @staticmethod def forward(ctx, x): return x ** 2 @staticmethod def backward(ctx, grad_output): # 错误:梯度应该是 2*x,但写成了 x return grad_output * x # 错误! x = torch.randn(5, dtype=torch.float64, requires_grad=True) # torch.autograd.gradcheck(lambda t: WrongSquare.apply(t).sum(), x) # RuntimeError: Jacobian mismatch for output 0 with respect to input 0

根因分析

1.gradcheck的工作原理

gradcheck使用有限差分法(finite differences)计算数值梯度:

数值梯度 ≈ (f(x + ε) - f(x - ε)) / (2ε)

其中ε是一个很小的值(默认1e-6)。

然后比较数值梯度和 autograd 计算的解析梯度,如果差异超过容差(atol/rtol),则报错。

2. 为什么需要float64

有限差分法涉及两个非常接近的数相减,会产生严重的精度损失(catastrophic cancellation)。float32只有约 7 位有效数字,无法提供足够的精度来计算可靠的数值梯度。float64有约 15-17 位有效数字,可以满足要求。

3.gradcheck的参数

torch.autograd.gradcheck( fn, # 要检查的函数 inputs, # 输入张量的 tuple eps=1e-6, # 有限差分的步长 atol=1e-5, # 绝对容差 rtol=1e-3, # 相对容差 raise_exception=True, # 不匹配时是否抛出异常 )

4.gradgradcheck的原理

gradgradcheck验证二阶梯度的正确性。它对gradcheck的结果再应用一次gradcheck,即验证 backward 函数本身的梯度。

解决方案

方案一:基本用法

import torch # 正确的 gradcheck 用法 x = torch.randn(5, dtype=torch.float64, requires_grad=True) # 检查 torch.exp 的梯度 result = torch.autograd.gradcheck(torch.exp, (x,)) print(f"torch.exp gradcheck: {result}") # True # 检查自定义函数 def my_square(x): return x.pow(2).sum() result = torch.autograd.gradcheck(my_square, (x,)) print(f"my_square gradcheck: {result}") # True

方案二:多输入函数

import torch x = torch.randn(5, dtype=torch.float64, requires_grad=True) y = torch.randn(5, dtype=torch.float64, requires_grad=True) # 多输入函数:输入必须是 tuple def my_func(a, b): return (a * b + a.exp() + b.log()).sum() result = torch.autograd.gradcheck(my_func, (x, y)) print(f"Multi-input gradcheck: {result}") # True

方案三:自定义 autograd Function

import torch class MyReLU(torch.autograd.Function): @staticmethod def forward(ctx, x): ctx.save_for_backward(x) return x.clamp(min=0) @staticmethod def backward(ctx, grad_output): x, = ctx.saved_tensors grad_x = grad_output * (x > 0).float() return grad_x # 测试 x = torch.randn(10, dtype=torch.float64, requires_grad=True) result = torch.autograd.gradcheck(lambda t: MyReLU.apply(t).sum(), (x,)) print(f"MyReLU gradcheck: {result}") # True

完整修复代码

""" PyTorch autograd.gradcheck 完整使用示例 涵盖基本函数、自定义 Function、多输入/输出、二阶梯度等 """ import torch import torch.nn as nn import torch.nn.functional as F import math # ============================================================ # 第一部分:基本 gradcheck 用法 # ============================================================ def demo_basic_gradcheck(): """基本 gradcheck 用法""" print("=" * 60) print("基本 gradcheck") print("=" * 60) # 创建 float64 输入 x = torch.randn(5, dtype=torch.float64, requires_grad=True) # 测试各种函数 functions = [ ("exp", lambda t: t.exp().sum()), ("log", lambda t: t.clamp(min=0.1).log().sum()), # 避免 log(0) ("square", lambda t: t.pow(2).sum()), ("cube", lambda t: t.pow(3).sum()), ("sigmoid", lambda t: torch.sigmoid(t).sum()), ("tanh", lambda t: torch.tanh(t).sum()), ("relu", lambda t: F.relu(t).sum()), ] for name, func in functions: try: result = torch.autograd.gradcheck(func, (x,)) print(f" {name:15s}: {'PASS' if result else 'FAIL'}") except RuntimeError as e: print(f" {name:15s}: ERROR - {str(e)[:60]}") # ============================================================ # 第二部分:自定义 autograd Function 验证 # ============================================================ class LeakyReLUManual(torch.autograd.Function): """手动实现的 LeakyReLU""" @staticmethod def forward(ctx, x, negative_slope=0.01): ctx.save_for_backward(x) ctx.negative_slope = negative_slope return torch.where(x >= 0, x, x * negative_slope) @staticmethod def backward(ctx, grad_output): x, = ctx.saved_tensors grad_x = torch.where(x >= 0, grad_output, grad_output * ctx.negative_slope) return grad_x, None # None 对应 negative_slope 的梯度 class SoftmaxManual(torch.autograd.Function): """手动实现的 Softmax""" @staticmethod def forward(ctx, x, dim=-1): x_max = x.max(dim=dim, keepdim=True)[0] exp_x = torch.exp(x - x_max) sum_exp = exp_x.sum(dim=dim, keepdim=True) output = exp_x / sum_exp ctx.save_for_backward(output) ctx.dim = dim return output @staticmethod def backward(ctx, grad_output): output, = ctx.saved_tensors # softmax 的 Jacobian: diag(s) - s * s^T # 对于向量: grad_input = s * (grad_output - sum(grad_output * s)) grad_sum = (grad_output * output).sum(dim=ctx.dim, keepdim=True) grad_input = output * (grad_output - grad_sum) return grad_input, None class MatMulManual(torch.autograd.Function): """手动实现的矩阵乘法""" @staticmethod def forward(ctx, a, b): ctx.save_for_backward(a, b) return a @ b @staticmethod def backward(ctx, grad_output): a, b = ctx.saved_tensors grad_a = grad_output @ b.t() grad_b = a.t() @ grad_output return grad_a, grad_b def demo_custom_functions(): """自定义 Function 验证""" print("\n" + "=" * 60) print("自定义 Function gradcheck") print("=" * 60) # LeakyReLU x = torch.randn(10, dtype=torch.float64, requires_grad=True) result = torch.autograd.gradcheck( lambda t: LeakyReLUManual.apply(t, 0.01).sum(), (x,) ) print(f" LeakyReLU: {'PASS' if result else 'FAIL'}") # Softmax x = torch.randn(5, 3, dtype=torch.float64, requires_grad=True) result = torch.autograd.gradcheck( lambda t: SoftmaxManual.apply(t, dim=-1).sum(), (x,) ) print(f" Softmax: {'PASS' if result else 'FAIL'}") # MatMul a = torch.randn(3, 4, dtype=torch.float64, requires_grad=True) b = torch.randn(4, 5, dtype=torch.float64, requires_grad=True) result = torch.autograd.gradcheck( lambda ta, tb: MatMulManual.apply(ta, tb).sum(), (a, b) ) print(f" MatMul: {'PASS' if result else 'FAIL'}") # ============================================================ # 第三部分:多输入/多输出函数 # ============================================================ def demo_multi_io(): """多输入/多输出 gradcheck""" print("\n" + "=" * 60) print("多输入/多输出 gradcheck") print("=" * 60) # 多输入 x = torch.randn(5, dtype=torch.float64, requires_grad=True) y = torch.randn(5, dtype=torch.float64, requires_grad=True) def multi_input(a, b): return (a * b + a.exp() - b.pow(2)).sum() result = torch.autograd.gradcheck(multi_input, (x, y)) print(f" 多输入: {'PASS' if result else 'FAIL'}") # 多输出(gradcheck 会检查所有输出对所有输入的 Jacobian) def multi_output(a, b): return (a * b).sum(), (a + b).sum(), (a - b).sum() result = torch.autograd.gradcheck(multi_output, (x, y)) print(f" 多输出: {'PASS' if result else 'FAIL'}") # 矩阵输入 a = torch.randn(3, 4, dtype=torch.float64, requires_grad=True) b = torch.randn(4, 2, dtype=torch.float64, requires_grad=True) def matmul_func(x, w): return (x @ w).sum() result = torch.autograd.gradcheck(matmul_func, (a, b)) print(f" 矩阵乘法: {'PASS' if result else 'FAIL'}") # ============================================================ # 第四部分:gradgradcheck(二阶梯度验证) # ============================================================ def demo_gradgradcheck(): """二阶梯度验证""" print("\n" + "=" * 60) print("gradgradcheck(二阶梯度)") print("=" * 60) # 简单函数 x = torch.randn(5, dtype=torch.float64, requires_grad=True) def my_func(t): return t.pow(3).sum() # f(x) = x^3, f'(x) = 3x^2, f''(x) = 6x result = torch.autograd.gradgradcheck(my_func, (x,)) print(f" x^3 二阶梯度: {'PASS' if result else 'FAIL'}") # 自定义 Function 的二阶梯度 class CubicFunction(torch.autograd.Function): @staticmethod def forward(ctx, x): ctx.save_for_backward(x) return x.pow(3) @staticmethod def backward(ctx, grad_output): x, = ctx.saved_tensors return grad_output * 3 * x.pow(2) # 导数 3x^2 x = torch.randn(5, dtype=torch.float64, requires_grad=True) result = torch.autograd.gradgradcheck( lambda t: CubicFunction.apply(t).sum(), (x,) ) print(f" 自定义 Cubic 二阶: {'PASS' if result else 'FAIL'}") # ============================================================ # 第五部分:常见错误与调试 # ============================================================ def demo_common_errors(): """常见错误演示""" print("\n" + "=" * 60) print("常见错误与修复") print("=" * 60) # 错误 1: float32 print("1. float32 输入:") x_f32 = torch.randn(5, dtype=torch.float32, requires_grad=True) try: torch.autograd.gradcheck(lambda t: t.exp().sum(), (x_f32,)) except RuntimeError as e: print(f" 错误: {str(e)[:70]}") print(f" 修复: 使用 dtype=torch.float64") # 错误 2: 没有 requires_grad print("\n2. 缺少 requires_grad:") x_no_grad = torch.randn(5, dtype=torch.float64) try: torch.autograd.gradcheck(lambda t: t.exp().sum(), (x_no_grad,)) except RuntimeError as e: print(f" 错误: {str(e)[:70]}") print(f" 修复: 设置 requires_grad=True") # 错误 3: 错误的梯度实现 print("\n3. 错误的梯度实现:") class WrongGrad(torch.autograd.Function): @staticmethod def forward(ctx, x): return x.pow(2) @staticmethod def backward(ctx, grad_output): return grad_output * x # 错误!应该是 2*x x = torch.randn(5, dtype=torch.float64, requires_grad=True) try: # 需要保存 x 到 ctx 才能在 backward 中使用 class WrongGradFixed(torch.autograd.Function): @staticmethod def forward(ctx, x): ctx.save_for_backward(x) return x.pow(2) @staticmethod def backward(ctx, grad_output): x, = ctx.saved_tensors return grad_output * x # 故意写错:应该是 2*x torch.autograd.gradcheck(lambda t: WrongGradFixed.apply(t).sum(), (x,)) except RuntimeError as e: print(f" 错误: Jacobian mismatch") print(f" 修复: backward 应返回 grad_output * 2 * x") # ============================================================ # 第六部分:实用工具 # ============================================================ def create_gradcheck_wrapper(name=None): """创建 gradcheck 包装器""" def gradcheck_decorator(func): """装饰器:自动将输入转为 float64 并运行 gradcheck""" def wrapper(*args, **kwargs): # 转换输入为 float64 test_args = [] for arg in args: if isinstance(arg, torch.Tensor): test_arg = arg.detach().double().requires_grad_(True) test_args.append(test_arg) else: test_args.append(arg) # 运行 gradcheck try: result = torch.autograd.gradcheck( lambda *a: func(*a, **kwargs) if isinstance(func(*a, **kwargs), torch.Tensor) else func(*a, **kwargs), tuple(test_args) ) label = name or func.__name__ print(f" [gradcheck] {label}: {'PASS' if result else 'FAIL'}") return func(*args, **kwargs) except Exception as e: print(f" [gradcheck] FAILED: {e}") raise return wrapper return gradcheck_decorator def demo_practical_usage(): """实际使用示例""" print("\n" + "=" * 60) print("实际使用示例") print("=" * 60) # 验证自定义损失函数 class FocalLossManual(torch.autograd.Function): """手动实现的 Focal Loss""" @staticmethod def forward(ctx, x, target, gamma=2.0): # x: logits, target: binary labels p = torch.sigmoid(x) pt = p * target + (1 - p) * (1 - target) alpha = 0.25 at = alpha * target + (1 - alpha) * (1 - target) loss = -at * (1 - pt).pow(gamma) * torch.log(pt.clamp(min=1e-8)) ctx.save_for_backward(p, target, pt, at) ctx.gamma = gamma return loss.sum() @staticmethod def backward(ctx, grad_output): p, target, pt, at = ctx.saved_tensors gamma = ctx.gamma # 数值梯度验证比较复杂,这里简化 # 实际使用时建议用 autograd 自动推导 return grad_output * torch.ones_like(p), None, None # 验证 x = torch.randn(10, dtype=torch.float64, requires_grad=True) target = torch.randint(0, 2, (10,), dtype=torch.float64) # 使用 PyTorch 内置操作实现,让 autograd 自动推导 def focal_loss_autograd(logits, target, gamma=2.0, alpha=0.25): p = torch.sigmoid(logits) pt = p * target + (1 - p) * (1 - target) at = alpha * target + (1 - alpha) * (1 - target) loss = -at * (1 - pt).pow(gamma) * torch.log(pt.clamp(min=1e-8)) return loss.sum() result = torch.autograd.gradcheck( lambda t: focal_loss_autograd(t, target), (x,) ) print(f" Focal Loss (autograd): {'PASS' if result else 'FAIL'}") # 验证注意力机制 def attention(q, k, v): """Scaled dot-product attention""" d_k = k.size(-1) scores = q @ k.transpose(-2, -1) / math.sqrt(d_k) attn = F.softmax(scores, dim=-1) return (attn @ v).sum() q = torch.randn(2, 3, 8, dtype=torch.float64, requires_grad=True) k = torch.randn(2, 3, 8, dtype=torch.float64, requires_grad=True) v = torch.randn(2, 3, 8, dtype=torch.float64, requires_grad=True) result = torch.autograd.gradcheck(attention, (q, k, v)) print(f" Attention: {'PASS' if result else 'FAIL'}") # ============================================================ # 主函数 # ============================================================ if __name__ == '__main__': demo_basic_gradcheck() demo_custom_functions() demo_multi_io() demo_gradgradcheck() demo_common_errors() demo_practical_usage() print("\n" + "=" * 60) print("所有示例运行完成!") print("=" * 60)

常见陷阱与注意事项

1. 必须使用float64

# 错误 x = torch.randn(5, dtype=torch.float32, requires_grad=True) # 正确 x = torch.randn(5, dtype=torch.float64, requires_grad=True)

2. 输入必须是 tuple

# 错误 torch.autograd.gradcheck(func, x) # 正确 torch.autograd.gradcheck(func, (x,))

3. 函数必须返回标量或张量

# gradcheck 要求函数输出是张量 # 如果输出是标量,gradcheck 会自动处理

4.eps的选择

# 默认 eps=1e-6 适用于大多数情况 # 如果函数在数值上不稳定,可以调整 torch.autograd.gradcheck(func, inputs, eps=1e-5) # 更大的步长

5. 不可微点

# ReLU 在 0 处不可微 x = torch.tensor([0.0], dtype=torch.float64, requires_grad=True) # gradcheck 可能失败,因为数值微分在不可微点不准确 # 避免在不可微点附近测试

6.check_undefined_grad

# 检查未定义梯度 torch.autograd.gradcheck(func, inputs, check_undefined_grad=False)

7.check_batched_grad

# PyTorch >= 1.9 支持批量梯度检查 torch.autograd.gradcheck(func, inputs, check_batched_grad=True)

8.raise_exception=False获取结果

result = torch.autograd.gradcheck(func, inputs, raise_exception=False) # result 是 bool,True 表示通过

总结

torch.autograd.gradcheck是验证自定义梯度实现正确性的重要工具。使用要点:

  1. 必须使用float64:数值微分需要高精度
  2. 输入需要requires_grad=True:否则无法计算梯度
  3. 输入必须是 tuple:(x,)而非x
  4. 多输入函数:gradcheck(func, (x, y, z))
  5. 自定义 Function:验证forward和backward的一致性
  6. gradgradcheck:验证二阶梯度的正确性
  7. 避免不可微点:如 ReLU 在 0 处
  8. 调整eps:数值不稳定时增大步长

在实现自定义 autograd Function 时,始终使用gradcheck验证梯度的正确性,可以避免训练中的隐蔽 bug,确保模型正确学习。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进