Tikhonov-Minimum-Norm Hypergradients / tikhonov_experiment.py
Failed on benchmark
1import json
2from pathlib import Path
3import numpy as np
4
5
6def cg(A, b, tol=1e-11, max_iter=1000):
7 x = np.zeros_like(b, dtype=float)
8 r = b - A(x)
9 p = r.copy()
10 rr = float(r @ r)
11 r0 = np.sqrt(rr)
12 if r0 == 0:
13 return x, 0, 0.0
14 for k in range(1, max_iter + 1):
15 Ap = A(p)
16 den = float(p @ Ap)
17 if den <= 0 or not np.isfinite(den):
18 raise RuntimeError("CG operator was not positive definite")
19 alpha = rr / den
20 x += alpha * p
21 r -= alpha * Ap
22 new_rr = float(r @ r)
23 if np.sqrt(new_rr) <= tol * max(1.0, r0):
24 return x, k, np.sqrt(new_rr)
25 p = r + (new_rr / rr) * p
26 rr = new_rr
27 return x, max_iter, np.linalg.norm(r)
28
29
30def rel(a, b):
31 return float(np.linalg.norm(a - b) / max(1e-15, np.linalg.norm(b)))
32
33
34def main():
35 rng = np.random.default_rng(2796)
36 q, _ = np.linalg.qr(rng.normal(size=(4, 4)))
37 eig = np.array([0.0, 0.1, 1.0, 10.0])
38 H = q @ np.diag(eig) @ q.T
39 # B is in Range(H), so the pseudoinverse derivative is finite.
40 B = q @ np.array([0.0, 1.0, -0.7, 0.4])
41 v_true = np.linalg.pinv(H) @ B
42 eps_values = np.logspace(-1, -8, 8)
43
44 stable = []
45 for eps in eps_values:
46 A = H + eps * np.eye(4)
47 v, it, residual = cg(lambda z: A @ z, B)
48 stable.append({
49 "eps": float(eps), "relative_error": rel(v, v_true),
50 "iterations": int(it), "residual": float(residual),
51 })
52
53 # Prediction 1: stable-range Tikhonov derivative converges to -H^+B.
54 # Prediction 2: for each positive eigencomponent, bias is exactly
55 # eps/(lambda+eps), hence asymptotically linear in eps.
56 component_errors = []
57 for eps in eps_values:
58 v = np.linalg.solve(H + eps * np.eye(4), B)
59 coeff = q.T @ v
60 true_coeff = q.T @ v_true
61 component_errors.append({
62 "eps": float(eps),
63 "lambda_0_error": float(abs(coeff[0] - true_coeff[0])),
64 "lambda_0p1_relative_error": float(abs(coeff[1] - true_coeff[1]) / abs(true_coeff[1])),
65 "predicted_lambda_0p1_relative_error": float(eps / (0.1 + eps)),
66 })
67
68 # Prediction 3: if B has a nullspace component, the regularized solve
69 # diverges as (B_null)/eps. We sweep eps and estimate the log-log slope.
70 B_bad = B + 0.8 * q[:, 0]
71 unstable = []
72 bad_norms = []
73 for eps in eps_values:
74 v = np.linalg.solve(H + eps * np.eye(4), B_bad)
75 null_coeff = float(q[:, 0] @ v)
76 bad_norms.append(np.linalg.norm(v))
77 unstable.append({"eps": float(eps), "norm": float(np.linalg.norm(v)), "null_component": null_coeff,
78 "eps_times_null_component": float(eps * null_coeff)})
79 slope = float(np.polyfit(np.log(eps_values[-5:]), np.log(bad_norms[-5:]), 1)[0])
80
81 # Tiny bilevel quadratic: f(x,theta)=1/2 x^T Hx - theta B^T x.
82 # The minimum-norm inner solution is x*=H^+B theta. Outer loss is
83 # 1/2||x-c||^2. Exact hypergradient is (x-c)^T H^+B.
84 c = q @ np.array([0.3, -0.2, 0.4, 0.1])
85 direction = v_true
86 theta0 = 2.0
87 exact_x = direction * theta0
88 exact_loss = 0.5 * np.sum((exact_x - c) ** 2)
89 exact_grad = float((exact_x - c) @ direction)
90
91 def damped_grad(theta, eps):
92 x = np.linalg.solve(H + eps * np.eye(4), B) * theta
93 dx = np.linalg.solve(H + eps * np.eye(4), B)
94 return float((x - c) @ dx), float(0.5 * np.sum((x - c) ** 2))
95
96 # Compare fixed damping and continuation (eps halves toward a floor).
97 fixed_eps = 0.1
98 theta_fixed = theta0
99 theta_cont = theta0
100 fixed_trace, cont_trace = [], []
101 lr = 0.015
102 for step in range(40):
103 gf, lf = damped_grad(theta_fixed, fixed_eps)
104 gc, lc = damped_grad(theta_cont, max(1e-7, 0.1 * 0.7 ** step))
105 theta_fixed -= lr * gf
106 theta_cont -= lr * gc
107 fixed_trace.append(lf)
108 cont_trace.append(lc)
109
110 out = {
111 "seed": 2796,
112 "eigenvalues": eig.tolist(),
113 "predictions": {
114 "stable_range_limit": "relative error -> 0 for B in Range(H)",
115 "positive_eigen_bias": "relative bias on lambda=0.1 component = eps/(0.1+eps)",
116 "nullspace_violation": "norm scales as eps^-1 when B has null component",
117 },
118 "stable_sweep": stable,
119 "component_sweep": component_errors,
120 "unstable_sweep": unstable,
121 "unstable_loglog_slope": slope,
122 "bilevel": {
123 "exact_minimum_norm_loss_at_theta0": exact_loss,
124 "exact_hypergradient_at_theta0": exact_grad,
125 "fixed_eps": fixed_eps,
126 "fixed_final_loss": fixed_trace[-1],
127 "continuation_final_loss": cont_trace[-1],
128 "fixed_final_theta": theta_fixed,
129 "continuation_final_theta": theta_cont,
130 "fixed_loss_trace": fixed_trace,
131 "continuation_loss_trace": cont_trace,
132 },
133 }
134 Path("results.json").write_text(json.dumps(out, indent=2))
135 print(json.dumps({
136 "stable_last_relative_error": stable[-1]["relative_error"],
137 "lambda01_observed_vs_predicted": [component_errors[-1]["lambda_0p1_relative_error"], component_errors[-1]["predicted_lambda_0p1_relative_error"]],
138 "unstable_loglog_slope": slope,
139 "exact_grad": exact_grad,
140 "fixed_final_loss": fixed_trace[-1],
141 "continuation_final_loss": cont_trace[-1],
142 }, indent=2))
143
144
145if __name__ == "__main__":
146 main()