Minimal Negative-Curvature L-BFGS / official_stage2_bench.py
Failed on benchmark
1import sys, json, random, time
2from pathlib import Path
3import numpy as np
4import torch
5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
6from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
7
8SEEDS = tuple(range(8))
9# Same union on both sides; baseline sweep uses official seeds (0..3).
10GRID = [{'lr': 0.03, 'epochs': 15}, {'lr': 0.06, 'epochs': 15}, {'lr': 0.12, 'epochs': 15}]
11MEMORY = 7
12CURV_EPS = 1e-4
13
14def flat_params(net):
15 return torch.cat([p.detach().reshape(-1) for p in net.parameters()])
16
17def flat_grad(net):
18 return torch.cat([p.grad.detach().reshape(-1) for p in net.parameters()])
19
20def assign(net, v):
21 k = 0
22 with torch.no_grad():
23 for p in net.parameters():
24 n = p.numel(); p.copy_(v[k:k+n].view_as(p)); k += n
25
26def fg(net, x, y):
27 net.zero_grad(set_to_none=True)
28 loss = ((net(x) - y) ** 2).mean()
29 loss.backward()
30 return loss.detach(), flat_grad(net)
31
32def two_loop(g, pairs):
33 q = g.clone(); alphas = []
34 for s, y in reversed(pairs):
35 rho = 1.0 / torch.dot(s, y).clamp_min(1e-20)
36 a = rho * torch.dot(s, q); alphas.append(a); q = q - a*y
37 if pairs:
38 s, y = pairs[-1]
39 gamma = torch.dot(s, y) / torch.dot(y, y).clamp_min(1e-20)
40 else:
41 gamma = torch.ones((), device=g.device)
42 r = gamma*q
43 for (s, y), a in zip(pairs, reversed(alphas)):
44 b = torch.dot(y, r) / torch.dot(s, y).clamp_min(1e-20)
45 r = r + s*(a-b)
46 return r
47
48def euclidean_fix(s, y):
49 c = torch.dot(s, y)
50 return y + (torch.abs(c)-c) * s / torch.dot(s, s).clamp_min(1e-20)
51
52def train_one(seed, cfg, corrected, return_model=False):
53 torch.manual_seed(seed); np.random.seed(seed); random.seed(seed)
54 ds = get_dataset('tabular', seed)
55 # train_model's documented device policy, with exception fallback.
56 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
57 try:
58 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
59 x, y = ds['xtr'].to(device), ds['ytr'].to(device)
60 xt, yt = ds['xte'].to(device), ds['yte'].to(device)
61 pairs=[]; neg=fixes=rejects=backtracks=gevals=0; accepted=0; t0=time.time()
62 for _ in range(cfg['epochs']):
63 f, g = fg(net, x, y); gevals += 1
64 v = flat_params(net); p = -two_loop(g, pairs)
65 if (not torch.isfinite(p).all()) or torch.dot(g,p) >= 0:
66 p = -g; pairs = []
67 slope = torch.dot(g,p); step=float(cfg['lr']); ok=False
68 for _ in range(18):
69 assign(net, v + step*p)
70 fn, gn = fg(net, x, y); gevals += 1
71 if torch.isfinite(fn) and fn <= f + 1e-4*step*slope:
72 ok=True; break
73 step *= .5; backtracks += 1
74 if not ok:
75 rejects += 1; assign(net, v); break
76 accepted += 1
77 s = (step*p).detach(); yd = (gn-g).detach()
78 c = torch.dot(s,yd); scale=torch.linalg.vector_norm(s)*torch.linalg.vector_norm(yd)
79 if c < -CURV_EPS*scale:
80 neg += 1
81 if corrected: yd=euclidean_fix(s,yd); fixes += 1
82 if torch.dot(s,yd) > 1e-10*max(float(scale),1e-12):
83 pairs.append((s,yd)); pairs=pairs[-MEMORY:]
84 with torch.no_grad(): metric=float(((net(xt)-yt)**2).mean())
85 out={'test_metric':metric,'negative_pairs':neg,'corrected_pairs':fixes,'line_search_failures':rejects,'backtracking_halvings':backtracks,'gradient_evals':gevals,'accepted_steps':accepted,'seconds':time.time()-t0,'finite':bool(np.isfinite(metric))}
86 return (out, net, (xt,yt)) if return_model else out
87 except RuntimeError:
88 if device.type == 'cuda':
89 torch.cuda.empty_cache()
90 old=torch.cuda.is_available
91 # CPU retry without changing the experiment's random seed or algorithm.
92 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).cpu()
93 x,y,xt,yt=[z.cpu() for z in (ds['xtr'],ds['ytr'],ds['xte'],ds['yte'])]
94 # recurse through a temporary CUDA-disabled route is avoided by direct flag.
95 torch.cuda.is_available=lambda: False
96 try: return train_one(seed,cfg,corrected,return_model)
97 finally: torch.cuda.is_available=old
98 raise
99
100def fn(corrected, cfg):
101 return lambda seed: train_one(seed, cfg, corrected)['test_metric']
102
103def main():
104 base=sweep_baseline(lambda cfg: fn(False,cfg), GRID)
105 # Required idea sweep: baseline-best and two nearby settings, all in GRID.
106 idea_trials=[]
107 for cfg in GRID:
108 r=evaluate(fn(True,cfg), seeds=SEEDS)
109 idea_trials.append({'cfg':cfg,'result':r})
110 best=min(idea_trials,key=lambda z:z['result']['mean'])
111 # Paired baseline at the idea-selected cfg; it is in the baseline sweep union.
112 b=evaluate(fn(False,best['cfg']), seeds=SEEDS)
113 i=best['result']
114 _, bm, bdata=train_one(0,best['cfg'],False,True)
115 _, im, idata=train_one(0,best['cfg'],True,True)
116 def behavior(net,data):
117 net=net.cpu().eval(); x=data[0][:128].cpu().clone().requires_grad_(True)
118 out=net(x); gx=torch.autograd.grad(out.sum(),x)[0]
119 return {'samples':len(x),'mean_abs_parameter_input_gradient':float(gx.abs().mean()),'finite_fraction':float(torch.isfinite(out).float().mean()),'output_std':float(out.std())}
120 sig={'prediction':'negative secant pairs are corrected to positive curvature; correction should occur only when the trained consecutive-gradient secant is negative','baseline_observed':behavior(bm,bdata),'idea_observed':behavior(im,idata),'trained_pair_counts':{'baseline_negative_pairs':sum(train_one(s,best['cfg'],False)['negative_pairs'] for s in SEEDS),'idea_negative_pairs':sum(train_one(s,best['cfg'],True)['negative_pairs'] for s in SEEDS),'idea_corrected_pairs':sum(train_one(s,best['cfg'],True)['corrected_pairs'] for s in SEEDS)},'confirmed':False,'measurement_note':'Behavior probes and pair counts are measured from trained official-tabular systems; no analytical toy identity is used.'}
121 rep=make_report('tabular','mlp_tiny',base,i,sig)
122 rep['idea']['best_cfg']=best['cfg']; rep['idea']['sweep']=[{'cfg':z['cfg'],**z['result']} for z in idea_trials]
123 rep['protocol_note']='Official registered tabular track; optimizer-only custom loop because the intervention changes training.'
124 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
125 print(json.dumps(rep,indent=2))
126if __name__=='__main__': main()