
本文详解如何在 PyTorch 中正确计算非标量输出(如向量或矩阵)关于输入向量的导数,重点解决 backward() 因输出非标量而报错的问题,并给出可验证的数值微分范式。
本文详解如何在 pytorch 中正确计算非标量输出(如向量或矩阵)关于输入向量的导数,重点解决 `backward()` 因输出非标量而报错的问题,并给出可验证的数值微分范式。
在 PyTorch 中,torch.autograd 默认仅支持标量输出(即 0 阶张量)对输入的自动求导。当目标函数输出为向量、矩阵或高阶张量(例如本例中 $ P(x)h \in \mathbb{R}^n $)时,直接调用 .backward() 会触发 RuntimeError: grad can be implicitly created only for scalar outputs 错误——这正是原代码失败的根本原因。
问题本质在于:PyTorch 的反向传播需明确“梯度流”的起点。对于非标量输出 $ y = f(x) \in \mathbb{R}^m $,y.backward(v) 实际计算的是雅可比-向量积 $ J_f(x)^\top v $,其中 $ v \in \mathbb{R}^m $ 是用户指定的梯度权重(即 grad_output)。若希望获得完整雅可比矩阵的行/列方向导数,需显式提供匹配形状的 v;而若仅需验证某特定方向 $ h $ 对应的导数作用结果(即 $ \partial_x (P(x)h) $),最常用且语义清晰的做法是传入 torch.ones_like(y),等价于对每个输出分量赋予单位权重,从而得到梯度的逐元素和(即雅可比每行求和),适用于多数数值验证场景。
以下是修正后的完整可运行示例:
import torch
def P(x, A):
x = x.unsqueeze(1) # [n] → [n, 1]
vector = A @ x # A is (n,n), x is (n,1) → (n,1)
denom = (vector.T @ vector).squeeze() # scalar
P_matrix = (vector @ vector.T) / denom # (n,n)
return P_matrix.squeeze() # ensures (n,n) even for n=2
# Setup
A = torch.tensor([[1.0, 0.5], [0.5, 1.3]], dtype=torch.float32)
x = torch.tensor([1.0, 2.0], dtype=torch.float32, requires_grad=True)
h = torch.tensor([2.0, -1.0], dtype=torch.float32)
# Forward: Pxh = P(x) @ h ∈ ℝ²
Pxh = P(x, A) @ h
# Critical fix: backward requires grad_output of same shape as Pxh
Pxh.backward(torch.ones_like(Pxh))
print("∂(P(x)h)/∂x =", x.grad)
# Output: tensor([ 0.4853, -0.2427])
✅ 关键要点总结:
- ✅ backward() 的 grad_output 参数必须与前向输出张量同形(此处 Pxh.shape == torch.Size([2]),故用 torch.ones_like(Pxh));
- ✅ 若需获取完整雅可比 $ \partial P(x)h / \partial x \in \mathbb{R}^{2\times2} $,应分别对 Pxh[0] 和 Pxh[1] 单独调用 backward() 并重置 x.grad,或使用 torch.autograd.functional.jacobian(更直观但开销略大);
- ⚠️ 注意 P(x) 的实现中 unsqueeze(1) 和 squeeze() 配合确保维度安全,避免因广播或冗余维度引发隐式错误;
- ? 此方法同样适用于 JAX:对应地应使用 jax.jacrev(lambda x: P(x, A) @ h)(x) 或 jax.grad 配合 jnp.sum(即 jax.grad(lambda x: jnp.sum(P(x, A) @ h))(x)),原理一致——将向量输出降维为标量以启用梯度计算。
通过这一规范实践,你不仅能修复原始报错,更能建立起对 PyTorch 自动微分机制中「梯度传播契约」的深层理解,为后续实现复杂张量导数验证(如优化器 Hessian 近似、物理模拟梯度建模等)奠定坚实基础。











