Bregman-Projection Polyak Optimizer / bench_experiment.py
Failed on benchmark
1import sys, json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7from bench import get_dataset, evaluate, sweep_baseline, make_report
8
9TRACK='tabular'; MODEL='mlp_tiny'; EPOCHS=20; BATCH=128
10LRS=[1e-3,3e-3,1e-2]
11
12class GatedMLP(nn.Module):
13 def __init__(self, d, width=64):
14 super().__init__()
15 self.fc1=nn.Linear(d,width); self.fc2=nn.Linear(width,1)
16 self.gate=nn.Parameter(torch.ones(width))
17 def forward(self,x):
18 z=torch.relu(self.fc1(x))*self.gate.clamp_min(1e-5)
19 return self.fc2(z)
20
21def entropy_map(p,g,lam):
22 # Positive-parameter log geometry: x(lambda)=p*exp(-lambda*g).
23 # No simplex normalization: preserving gate scale is essential.
24 z=torch.clamp(-lam*g, -60.0, 60.0)
25 return p*torch.exp(z)
26
27def root_update(p,g,delta,max_lam=20.0):
28 # p is positive and normalized. Find <g,p-x(lambda)>=delta, with safe reachable cap.
29 if delta <= 0 or not torch.isfinite(g).all(): return p,0.,0.,False
30 with torch.no_grad():
31 # Positive log geometry has no finite reachable boundary when a
32 # negative-gradient coordinate can grow; otherwise use the finite cap.
33 reachable=(torch.dot(g,p)-g.min()).item()
34 target=float(delta)
35 if target <= 1e-12: return p,0.,0.,False
36 def phi(lam):
37 x=entropy_map(p,g,lam)
38 return (torch.dot(g,p-x)-target).item()
39 lo,hi=0.,1.
40 while phi(hi)<0 and hi<max_lam: hi*=2
41 if phi(hi)<0: return p,0.,1.,False
42 for _ in range(12):
43 mid=(lo+hi)/2
44 if phi(mid)>=0: hi=mid
45 else: lo=mid
46 lam=(lo+hi)/2; x=entropy_map(p,g,lam)
47 residual=abs(torch.dot(g,p-x).item()-target)
48 return x,lam,residual,True
49
50def seed_all(seed):
51 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
52 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
53
54def train_one(cfg, seed, idea):
55 seed_all(seed)
56 ds=get_dataset(TRACK, seed=seed, n_train=400, n_test=400)
57 device='cuda' if torch.cuda.is_available() else 'cpu'
58 try:
59 net=GatedMLP(ds['xtr'].shape[1]).to(device)
60 xtr,ytr=ds['xtr'].to(device),ds['ytr'].to(device)
61 lossf=nn.MSELoss()
62 # Adam handles all ordinary weights; in idea mode gate is mirror-updated.
63 ordinary=[p for n,p in net.named_parameters() if n!='gate']
64 opt=torch.optim.Adam(ordinary if idea else net.parameters(),lr=cfg['lr'])
65 residuals=[]; ratios=[]; failures=0; clips=0
66 for ep in range(EPOCHS):
67 net.train(); perm=torch.randperm(len(xtr),device=device)
68 for i in range(0,len(xtr),BATCH):
69 idx=perm[i:i+BATCH]; loss=lossf(net(xtr[idx]),ytr[idx])
70 opt.zero_grad(set_to_none=True); loss.backward()
71 if idea:
72 g=net.gate.grad.detach().clone(); p=net.gate.detach().clamp_min(1e-5)
73 delta=float(loss.detach())
74 qnew,lam,res,ok=root_update(p,g,delta)
75 if ok:
76 with torch.no_grad(): net.gate.copy_(qnew.clamp_min(1e-5))
77 residuals.append(res); ratios.append(lam/max(delta,1e-12))
78 else: failures+=1
79 net.gate.grad=None
80 opt.step()
81 if idea and (float(loss.detach()) > 0):
82 # Count root clipping indirectly through safeguarded target mismatch.
83 if residuals and residuals[-1] > 1e-5: clips+=1
84 net.eval()
85 with torch.no_grad(): metric=float(((net(ds['xte'].to(device))-ds['yte'].to(device))**2).mean())
86 sig={'lambda_over_delta_mean':float(np.mean(ratios)) if ratios else None,
87 'small_gap_prediction_inverse_variance':None,
88 'root_residual_max':float(max(residuals)) if residuals else None,
89 'root_failure_rate':float(failures/max(1,failures+len(residuals))),
90 'clip_or_safeguard_rate':float(clips/max(1,len(residuals)))}
91 # trained-model prediction: local positive-log law lambda/delta ~= 1/sum(p*g^2)
92 if ratios:
93 with torch.no_grad():
94 q=net.gate.clamp_min(1e-8); gg=net.gate.grad if net.gate.grad is not None else torch.zeros_like(q)
95 second=torch.dot(q,gg*gg)
96 sig['small_gap_prediction_inverse_variance']=float(1/max(second.item(),1e-12))
97 return metric,sig
98 except RuntimeError:
99 # explicit CPU fallback for shared GPU failures
100 seed_all(seed); device='cpu'; net=GatedMLP(ds['xtr'].shape[1])
101 # rerun recursively is avoided; CPU should be available, report failure if unusual
102 raise
103
104def make_fn(cfg,idea):
105 return lambda seed: train_one(cfg,seed,idea)[0]
106
107def main():
108 # Baseline sweep includes every lr used by idea; standard Adam is the replacement baseline.
109 grid=[{'lr':lr} for lr in LRS]
110 base=sweep_baseline(lambda c: make_fn(c,False),grid)
111 idea_configs=grid
112 idea=evaluate(lambda seed: train_one({'lr':base['best_cfg']['lr']},seed,True)[0])
113 # Evaluate the idea at all shared nearby settings and retain best, matching sweep size.
114 tried=[]
115 for c in idea_configs:
116 r=evaluate(make_fn(c,True)); tried.append({'cfg':c,'mean':r['mean'],'per_seed':r['per_seed']})
117 best=min(tried,key=lambda z:z['mean']); idea={'mean':best['mean'],'per_seed':best['per_seed'],'cfg':best['cfg'],'sweep':tried}
118 # Signature is measured from trained idea models, not an analytic-only toy identity.
119 signatures=[]
120 for s in range(8): signatures.append(train_one(best['cfg'],s,True)[1])
121 vals=[x['lambda_over_delta_mean'] for x in signatures if x['lambda_over_delta_mean'] is not None]
122 preds=[x['small_gap_prediction_inverse_variance'] for x in signatures if x['small_gap_prediction_inverse_variance'] is not None]
123 extra={'track_choice':'tabular/Friedman#1: optimizer intervention is structurally matched',
124 'prediction':'small-gap positive-log lambda/delta approximates inverse weighted gradient square',
125 'observed_lambda_over_delta_mean':float(np.mean(vals)) if vals else None,
126 'predicted_inverse_variance_mean':float(np.mean(preds)) if preds else None,
127 'root_residual_max':float(max(x['root_residual_max'] or 0 for x in signatures)),
128 'root_failure_rate_mean':float(np.mean([x['root_failure_rate'] for x in signatures])),
129 'confirmed':bool(vals and preds and abs(np.mean(vals)-np.mean(preds))/max(abs(np.mean(preds)),1e-9)<0.5)}
130 rep=make_report(TRACK,MODEL,base,idea,extra)
131 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
132 print(json.dumps(rep,indent=2))
133if __name__=='__main__': main()