Response-from-Hessian Regularizer / response_hessian_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, os
  2import numpy as np
  3import torch
  4
  5SEED = 2244
  6np.random.seed(SEED)
  7torch.manual_seed(SEED)
  8torch.set_default_dtype(torch.float64)
  9
 10
 11def device():
 12    return torch.device("cuda" if torch.cuda.is_available() else "cpu")
 13
 14
 15def omega(rho, curvature, potential=None):
 16    # A differentiable toy variational functional.  The quartic term makes
 17    # negative curvature bounded below, while rho=0 exposes its Hessian exactly.
 18    if potential is None:
 19        potential = torch.zeros_like(rho)
 20    return 0.5 * (curvature * rho.square()).sum() + 0.025 * rho.pow(4).sum() + (potential * rho).sum()
 21
 22
 23def hvp(rho, curvature, v, potential=None, create_graph=False):
 24    # Keep the graph when the HVP is used inside a trainable regularizer.
 25    rho_in = rho.detach().clone().requires_grad_(True)
 26    g = torch.autograd.grad(omega(rho_in, curvature, potential), rho_in, create_graph=True)[0]
 27    hv = torch.autograd.grad((g * v).sum(), rho_in, create_graph=create_graph)[0]
 28    return hv if create_graph else hv.detach()
 29
 30
 31def direct_hessian(rho, curvature, potential=None):
 32    rho = rho.detach().clone().requires_grad_(True)
 33    return torch.autograd.functional.hessian(lambda x: omega(x, curvature, potential), rho).detach()
 34
 35
 36def hess_regularizer(rho, curvature, nvec=16, eps=1e-3, kappa=2.0):
 37    # Stochastic Rayleigh estimator, exactly in the form proposed in the idea.
 38    vals = []
 39    for _ in range(nvec):
 40        v = torch.randn_like(rho)
 41        v = v / v.norm()
 42        vals.append((v * hvp(rho, curvature, v, create_graph=True)).sum())
 43    q = torch.stack(vals)
 44    neg = torch.relu(-q + eps).square().mean()
 45    high = torch.relu(q - kappa).square().mean()
 46    return neg + high, q.detach()
 47
 48
 49def run(device_name):
 50    dev = torch.device(device_name)
 51    n = 8
 52    # 1. Core autodiff claim: HVP equals dense Hessian times v.
 53    c = torch.linspace(0.15, 1.4, n, device=dev)
 54    r = torch.randn(n, device=dev) * 0.2
 55    v = torch.randn(n, device=dev)
 56    H = direct_hessian(r, c)
 57    hv_err = (hvp(r, c, v) - H @ v).abs().max().item()
 58    eigs = torch.linalg.eigvalsh(H).cpu().numpy()
 59
 60    # 2. Susceptibility sweep. For quadratic Omega, drho/dV=-H^-1 exactly.
 61    gammas = np.array([0.02, 0.04, 0.08, 0.16, 0.32, 0.64])
 62    suscept = []
 63    predicted = []
 64    for g in gammas:
 65        # use a fixed unit perturbation so the norm is analytically 1/gamma
 66        H_g = torch.eye(n, device=dev) * float(g)
 67        dv = torch.zeros(n, device=dev); dv[0] = 1.0
 68        dr = torch.linalg.solve(H_g, -dv)
 69        suscept.append(dr.norm().item()); predicted.append(1.0 / g)
 70    slope = float(np.polyfit(np.log(gammas), np.log(suscept), 1)[0])
 71    rel_err = float(np.max(np.abs(np.array(suscept)-predicted) / np.array(predicted)))
 72
 73    # 3. Zero crossing: explicit Euler response iteration is stable iff gamma*lambda < 2.
 74    # For the fixed-point minimization rho <- rho - eta(H rho + dV), eta=1.
 75    # Its amplification factor is |1-gamma|; instability starts at gamma=2.
 76    cross_g = np.array([0.5, 1.5, 2.0, 2.5])
 77    growth = []
 78    for g in cross_g:
 79        x = 1.0
 80        for _ in range(30): x = (1.0-g)*x
 81        growth.append(abs(x))
 82    # Negative curvature is directly visible as negative Rayleigh quotient.
 83    neg_c = torch.tensor(-0.2, device=dev)
 84    neg_rho = torch.zeros(8, device=dev)
 85    reg_neg, q_neg = hess_regularizer(neg_rho, neg_c, nvec=32, eps=1e-3, kappa=2.0)
 86
 87    # 4. Mini experiment: fit an observed curvature target that is spuriously negative.
 88    # Baseline can retain the negative mode; regularized fit rejects it.
 89    target = -0.20
 90    def fit(use_reg, target_value=target, steps=300):
 91        x = torch.tensor(-0.5, device=dev, requires_grad=True)
 92        opt = torch.optim.Adam([x], lr=0.03)
 93        rr = torch.zeros(8, device=dev)
 94        for _ in range(steps):
 95            opt.zero_grad()
 96            loss = (x-target_value).square()
 97            if use_reg:
 98                reg, _ = hess_regularizer(rr, x, nvec=16, eps=1e-3, kappa=2.0)
 99                loss = loss + 8.0 * reg
100            loss.backward(); opt.step()
101        return float(x.detach().cpu()), float((x-target_value)**2)
102    base, base_fit = fit(False)
103    regular, reg_fit = fit(True)
104
105    # Deliberately near-critical positive mode should not be pushed to kappa;
106    # only the negative-curvature branch is active.
107    soft_target = 0.02
108    soft_base, _ = fit(False, soft_target)
109    soft_reg, _ = fit(True, soft_target)
110
111    result = {
112        "device": str(dev),
113        "hvp_max_abs_error": hv_err,
114        "hessian_eigenvalues": eigs.tolist(),
115        "susceptibility_sweep": [{"lambda": float(g), "observed": float(s), "predicted_1_over_lambda": float(p)} for g,s,p in zip(gammas,suscept,predicted)],
116        "loglog_slope_observed": slope,
117        "loglog_slope_predicted": -1.0,
118        "max_relative_scaling_error": rel_err,
119        "euler_zero_crossing_predicted_gamma": 2.0,
120        "euler_growth_sweep": [{"gamma": float(g), "abs_amplification_after_30": float(a)} for g,a in zip(cross_g,growth)],
121        "negative_mode_mean_rayleigh": float(q_neg.mean().cpu()),
122        "negative_mode_regularizer": float(reg_neg.cpu()),
123        "curvature_fit": {"target_negative": target, "baseline_final": base, "regularized_final": regular, "baseline_mse": base_fit, "regularized_mse": reg_fit, "soft_target": soft_target, "soft_baseline_final": soft_base, "soft_regularized_final": soft_reg},
124    }
125    return result
126
127
128def main():
129    try:
130        d = device()
131        out = run(d)
132    except Exception as e:
133        if torch.cuda.is_available():
134            out = run("cpu")
135            out["cuda_error_fallback"] = repr(e)
136        else:
137            raise
138    with open("results.json", "w") as f: json.dump(out, f, indent=2)
139    print(json.dumps(out, indent=2))
140
141if __name__ == "__main__": main()