复数输出求导不报错,给的是实部的梯度
主线位置:变换主线的「写函数」到「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。