Minimal Negative-Curvature L-BFGS / experiment.py
Failed on benchmark
1import json
2import math
3import numpy as np
4
5
6def euclidean_fix(s, y):
7 c = float(s @ y)
8 d = abs(c) - c
9 return y + d * s / float(s @ s)
10
11
12def metric_fix(s, y, B):
13 c = float(s @ y)
14 d = abs(c) - c
15 return y + d * (B @ s) / float(s @ B @ s)
16
17
18def bfgs_update(B, s, y):
19 Bs = B @ s
20 return B - np.outer(Bs, Bs) / (s @ Bs) + np.outer(y, y) / (s @ y)
21
22
23# A two-loop L-BFGS inverse-Hessian application. Pairs are (s,y), with y.T s > 0.
24def two_loop(g, pairs, H0_scale=1.0):
25 q = g.copy()
26 alphas = []
27 for s, y in reversed(pairs):
28 rho = 1.0 / (s @ y)
29 a = rho * (s @ q)
30 alphas.append(a)
31 q = q - a * y
32 if pairs:
33 s, y = pairs[-1]
34 gamma = (s @ y) / max(y @ y, 1e-30)
35 else:
36 gamma = H0_scale
37 r = gamma * q
38 for (s, y), a in zip(pairs, reversed(alphas)):
39 rho = 1.0 / (s @ y)
40 b = rho * (y @ r)
41 r = r + s * (a - b)
42 return r
43
44
45def objective(x):
46 # Smooth bounded nonconvex quartic in coordinate zero plus a stiff quadratic block.
47 return -0.5*x[0]**2 + 0.10*x[0]**4 + 0.05*np.sum(x[1:]**2)
48
49
50def grad(x):
51 g = 0.1*x.copy()
52 g[0] = -x[0] + 0.4*x[0]**3
53 return g
54
55
56def run_lbfgs(correct=False, seed=7, n=12, steps=180, memory=7):
57 rng = np.random.default_rng(seed)
58 x = np.r_[0.35, 0.1*rng.normal(0, 1.0, n-1)]
59 pairs = []
60 f0 = objective(x)
61 corrected = 0
62 negative = 0
63 backtracks = 0
64 accepted = []
65 for _ in range(steps):
66 g = grad(x)
67 p = -two_loop(g, pairs, H0_scale=1.0)
68 if not np.isfinite(p).all() or g @ p >= 0:
69 p = -g
70 f = objective(x)
71 slope = g @ p
72 t = 1.0
73 for _bt in range(35):
74 xn = x + t*p
75 if objective(xn) <= f + 1e-4*t*slope:
76 break
77 t *= 0.5
78 backtracks += int(round(-math.log2(t)))
79 if t < 2**-34:
80 break
81 gn = grad(xn)
82 s = xn - x
83 y = gn - g
84 c = float(s @ y)
85 scale = np.linalg.norm(s)*np.linalg.norm(y)
86 if c < -1e-4*scale:
87 negative += 1
88 if correct:
89 y = euclidean_fix(s, y)
90 corrected += 1
91 if s @ y > 1e-12*max(np.linalg.norm(s)*np.linalg.norm(y), 1.0):
92 pairs.append((s, y))
93 pairs = pairs[-memory:]
94 x = xn
95 accepted.append(objective(x))
96 return {
97 'initial_loss': f0, 'final_loss': float(objective(x)),
98 'best_loss': float(np.min(accepted)) if accepted else f0,
99 'negative_pairs': negative, 'corrected_pairs': corrected,
100 'backtracking_halvings': backtracks, 'steps': len(accepted),
101 'finite': bool(np.isfinite(x).all() and np.isfinite(objective(x)))
102 }
103
104
105def main():
106 rng = np.random.default_rng(123)
107 # Prediction 1/2: for c=-rho||s||||y||, Euclidean correction has
108 # s.T ytilde=|c| and ||delta||/||y||=2rho exactly.
109 sweep = []
110 for rho in [0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.9]:
111 s = rng.normal(size=9)
112 y0 = rng.normal(size=9)
113 # Construct y with prescribed normalized negative curvature.
114 sh = s / np.linalg.norm(s)
115 perp = y0 - sh*(sh @ y0)
116 perp /= np.linalg.norm(perp)
117 y = -rho*np.linalg.norm(y0)*sh + math.sqrt(max(1-rho*rho, 0))*np.linalg.norm(y0)*perp
118 c = s @ y
119 yt = euclidean_fix(s, y)
120 delta_ratio = np.linalg.norm(yt-y)/np.linalg.norm(y)
121 sweep.append({'rho': rho, 'observed_secant_over_abs_c': float((s@yt)/abs(c)),
122 'predicted_secant_over_abs_c': 1.0,
123 'observed_delta_ratio': float(delta_ratio),
124 'predicted_delta_ratio': 2*rho})
125
126 # Prediction 3: the BFGS update is SPD for every corrected pair.
127 pd_sweep = []
128 for dim in [3, 8, 20]:
129 mins = []
130 for trial in range(40):
131 A = rng.normal(size=(dim, dim))
132 B = A.T@A + 0.2*np.eye(dim)
133 s = rng.normal(size=dim)
134 y = -abs(rng.normal())*s + rng.normal(size=dim)*0.2
135 if s@y >= 0: y = y - (abs(s@y)+0.3)*s/(s@s)
136 yt = euclidean_fix(s, y)
137 Bn = bfgs_update(B, s, yt)
138 mins.append(float(np.linalg.eigvalsh((Bn+Bn.T)/2).min()))
139 pd_sweep.append({'dimension': dim, 'minimum_observed_eigenvalue': min(mins),
140 'predicted_lower_bound': 0.0})
141
142 baseline = run_lbfgs(correct=False)
143 idea = run_lbfgs(correct=True)
144 result = {
145 'math_verification': {
146 'prediction_1_secant_exactness': 's.T ytilde / abs(s.T y) = 1 for every rho',
147 'prediction_2_correction_scaling': '||ytilde-y||/||y|| = 2 rho for prescribed c=-rho||s||||y||',
148 'prediction_3_positive_definiteness': 'BFGS corrected update has strictly positive eigenvalues when B is SPD',
149 'sweep': sweep, 'spd_sweep': pd_sweep
150 },
151 'mini_experiment': {'baseline_ordinary_reject': baseline, 'idea_euclidean_correction': idea}
152 }
153 with open('results.json', 'w') as f:
154 json.dump(result, f, indent=2)
155 print(json.dumps(result, indent=2))
156
157
158if __name__ == '__main__':
159 main()