Rust in Everything
基于 tenferro-ad 0.8.0

复数输出求导不报错,给的是实部的梯度

主线位置:变换主线的「写函数」到「grad」:损失函数的输出 dtype。
版本:JAX 0.12.0.1(CPU,x64)、PyTorch 2.15.0(单线程)、tenferro-rs 0.8.0(CPU,native,单线程)
flowchart TB
  loss["损失忘了取模"] --> cdt["输出是 C64"]
  cdt --> jx["JAX grad"]
  jx --> err["TypeError"]
  cdt --> tt["torch backward"]
  tt --> err2["RuntimeError"]
  cdt --> tf["tenferro grad"]
  tf --> re["实部的梯度"]
  re --> div["静默发散"]

实验代码:tenferro 一侧是 bin p17_19_complex(release,CpuEngine::serial()),JAX / PyTorch 一侧是 p17_19_complex.py。M2 Max、macOS 27、rustc 1.99.0。

现象

本想写 Σ|z − w|²,手滑写成了 Σ(z − w)²。在 JAX 和 PyTorch 里,第一次求导就被拦下:

lossc = lambda z: jnp.sum((z - w) ** 2)
jax.grad(lossc)(np.zeros(2, complex))
((zt - torch.tensor(w)) ** 2).sum().backward()
jax grad(sum((z-w)^2)): TypeError: grad requires real-valued outputs (output dtype that is a sub-dtype of np.floating), but got complex128. For holomorphic differentiation, pass holomorphic=True. For differentiation of non-holomorphic functions involving complex outputs, use jax.vjp directly.
torch backward: RuntimeError: grad can be implicitly created only for real scalar outputs but got torch.complex128

同样的损失在 tenferro 里:

let d = (&zp - &w)?;
let lc = (&d * &d)?.reduce_sum(None)?;
let g = ad.grad(&lc, &zp)?;
// 和上一页一样,20 步 z -= 0.25 * g(z)
loss dtype C64
grad at z=0: [-2.000000+4.000000i, 2.000000+1.000000i]
z -= lr*grad: z = [0.999999-6648.513460i, -0.999999-1662.128365i] L = [-46993662.363995+0.009514i]
  • grad 接受复数输出,没有任何提示。
  • 下降 20 步,z 的实部到了 Re w,虚部发散;「损失」的实部降到 −4.7e7,看上去在「变小」。

出处

tenferro 的反向模式用 Hermitian 实内积 ⟨a, b⟩ = Re(conj(a)·b)(复数求导)。grad 的种子是实数 1,作为余切它只「看见」输出的实部,所以算出来的是 Re L 的梯度。这里 Re Σ(z − w)² = Σ[(x − a)² − (y − b)²],对 y 是开口向下的抛物线,梯度 2(x − a) − 2i(y − b) = 2·conj(z − w),起点是 −2+4i、2+1i,和上面的输出一致。沿它下降,就是在一个没有下界的鞍面上往下走。

AdContext::grad 的文档写的是「For complex scalar outputs, tenferro returns the Hermitian-adjoint cotangent」:复数输出是有意支持的,不检查它是不是「本该是实数」。JAX 的 grad 要求实数输出,holomorphic=True 时给的是导数 f'(z),是另一个量;PyTorch 的 backward() 不肯隐式造复数种子。

修法

损失写成实数 dtype。模长平方用 abs 再相乘(abs 的输出是 F64),或者对复数结果显式取实部(cast(DType::F64);convert 会拒绝 C64 到 F64):

let dz = (&zp - &w)?.abs()?;
let l = (&dz * &dz)?.reduce_sum(None)?;   // dtype F64
z -= lr*grad: z = [0.999999+1.999998i, -0.999999+0.500000i] L = 5.684e-12

这是上一页的同一个损失,梯度直接下降就收敛。

怎么确认没中招

  • 求导前断言损失的 dtype 是实数:assert_eq!(l.dtype(), DType::F64)。tenferro 的 grad 不替你做这个检查。
  • 优化循环里打印损失时,打印的是复数说明损失本身就写错了;Σ z·conj(z) 这种「数值上是实数、dtype 是复数」的写法同样会过,见 复数求导 的「复数类型的实损失」。

小结

  • JAX 和 PyTorch 对复数输出的 grad / backward() 报错;tenferro 返回实部的梯度,不报错。
  • 忘了取模的损失 Σ(z − w)² 的实部没有下界,沿 tenferro 的梯度下降会静默发散。
  • 求导前断言损失是实数 dtype。

💬 关联讨论

发起讨论
加载中...