Barrier-Controlled Basin Switching / 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
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, sweep_baseline, make_report
9from bench.protocol import DEFAULT_SEEDS
10
11SEEDS = tuple(range(8))
12EPOCHS = 12
13NTR, NTE = 800, 300
14BATCH = 128
15LRS = [1e-3, 3e-3, 6e-3]
16WDS = [0.0, 1e-4]
17# Same lr/step-size union is used by both systems; q is the intervention knob.
18QS = [2.0, 4.0, 8.0]
19
20
21def seed_all(seed):
22 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
23 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
24
25
26def loss_fn(ds):
27 return nn.CrossEntropyLoss() if ds['task'] == 'classification' else nn.MSELoss()
28
29
30def flat_params(net):
31 return torch.cat([p.detach().flatten() for p in net.parameters()])
32
33
34def set_flat(net, vec):
35 off = 0
36 with torch.no_grad():
37 for p in net.parameters():
38 n = p.numel(); p.copy_(vec[off:off+n].view_as(p)); off += n
39
40
41def probe_barrier(net, best_vec, ds, lf, device):
42 """Empirical loss barrier along current -> best parameter path."""
43 cur = flat_params(net)
44 if best_vec is None: return 0.0
45 vals = []
46 was = net.training; net.eval()
47 with torch.no_grad():
48 for a in (0.0, .25, .5, .75, 1.0):
49 set_flat(net, cur*(1-a) + best_vec*a)
50 vals.append(float(lf(net(ds['xtr'][:256].to(device)), ds['ytr'][:256].to(device))))
51 set_flat(net, cur)
52 if was: net.train()
53 endpoint = max(vals[0], vals[-1])
54 return max(0.0, max(vals) - endpoint)
55
56
57def train_baseline(ds, lr, wd, seed, return_trace=False):
58 seed_all(seed); device = 'cuda' if torch.cuda.is_available() else 'cpu'
59 try:
60 net = make_model('rnn_small', ds['input_shape'], 1).to(device)
61 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=wd)
62 lf = loss_fn(ds); x, y = ds['xtr'].to(device), ds['ytr'].to(device)
63 for _ in range(EPOCHS):
64 net.train(); perm = torch.randperm(len(x), device=device)
65 for i in range(0, len(x), BATCH):
66 z = net(x[perm[i:i+BATCH]]); loss = lf(z, y[perm[i:i+BATCH]])
67 opt.zero_grad(); loss.backward(); opt.step()
68 net.eval()
69 with torch.no_grad(): metric = float(((net(ds['xte'].to(device))-ds['yte'].to(device))**2).mean())
70 return metric
71 except RuntimeError:
72 torch.cuda.empty_cache() if torch.cuda.is_available() else None
73 return train_baseline_cpu(ds, lr, wd, seed)
74
75
76def train_baseline_cpu(ds, lr, wd, seed):
77 seed_all(seed); net=make_model('rnn_small', ds['input_shape'],1); opt=torch.optim.Adam(net.parameters(),lr=lr,weight_decay=wd); lf=loss_fn(ds)
78 for _ in range(EPOCHS):
79 p=torch.randperm(len(ds['xtr']))
80 for i in range(0,len(p),BATCH):
81 ix=p[i:i+BATCH]; loss=lf(net(ds['xtr'][ix]),ds['ytr'][ix]); opt.zero_grad(); loss.backward(); opt.step()
82 with torch.no_grad(): return float(((net(ds['xte'])-ds['yte'])**2).mean())
83
84
85def train_idea(ds, lr, wd, q, seed, trace=False):
86 seed_all(seed); device='cuda' if torch.cuda.is_available() else 'cpu'
87 try:
88 net=make_model('rnn_small',ds['input_shape'],1).to(device); opt=torch.optim.Adam(net.parameters(),lr=lr,weight_decay=wd); lf=loss_fn(ds)
89 x,y=ds['xtr'].to(device),ds['ytr'].to(device); best_vec=None; best_loss=float('inf'); records=[]
90 for ep in range(EPOCHS):
91 net.train(); perm=torch.randperm(len(x),device=device)
92 for i in range(0,len(x),BATCH):
93 ix=perm[i:i+BATCH]; loss=lf(net(x[ix]),y[ix]); opt.zero_grad(); loss.backward(); opt.step()
94 # The barrier estimate is made at epoch boundaries below.
95 with torch.no_grad(): cur_loss=float(lf(net(x[:256]),y[:256]))
96 if cur_loss < best_loss: best_loss=cur_loss; best_vec=flat_params(net).clone()
97 barrier=probe_barrier(net,best_vec,ds,lf,device)
98 # High-loss flat points are treated as metastable; otherwise safe mode.
99 net.train(); net.zero_grad(set_to_none=True); probe=lf(net(x[:128]),y[:128]); probe.backward()
100 gnorm=float(torch.sqrt(sum((p.grad.detach()**2).sum() for p in net.parameters() if p.grad is not None)))
101 trapped=(cur_loss-best_loss > 0.002 and gnorm < 2.0)
102 qeff=2.0 if trapped else q
103 eps=float(np.clip(max(barrier,1e-5)/qeff,1e-6,0.01))
104 with torch.no_grad():
105 for p in net.parameters(): p.add_(torch.randn_like(p)*math.sqrt(eps))
106 records.append((eps, float(barrier), int(trapped), cur_loss))
107 net.eval()
108 with torch.no_grad(): metric=float(((net(ds['xte'].to(device))-ds['yte'].to(device))**2).mean())
109 return (metric,records) if trace else metric
110 except RuntimeError:
111 if torch.cuda.is_available(): torch.cuda.empty_cache()
112 # CPU retry is deliberately identical intervention, with a temporary CUDA disable.
113 old=torch.cuda.is_available
114 return train_idea_cpu(ds,lr,wd,q,seed,trace)
115
116
117def train_idea_cpu(ds,lr,wd,q,seed,trace=False):
118 seed_all(seed); net=make_model('rnn_small',ds['input_shape'],1); opt=torch.optim.Adam(net.parameters(),lr=lr,weight_decay=wd); lf=loss_fn(ds); x,y=ds['xtr'],ds['ytr']; best=None; bl=1e9; rec=[]
119 for ep in range(EPOCHS):
120 p=torch.randperm(len(x))
121 for i in range(0,len(x),BATCH):
122 ix=p[i:i+BATCH]; z=lf(net(x[ix]),y[ix]); opt.zero_grad();z.backward();opt.step()
123 with torch.no_grad(): cl=float(lf(net(x[:256]),y[:256]))
124 if cl<bl: bl=cl;best=flat_params(net).clone()
125 bar=probe_barrier(net,best,ds,lf,'cpu'); net.zero_grad();lf(net(x[:128]),y[:128]).backward(); gn=float(torch.sqrt(sum((p.grad**2).sum() for p in net.parameters() if p.grad is not None))); tr=cl-bl>.002 and gn<2; eps=float(np.clip(max(bar,1e-5)/(2 if tr else q),1e-6,.01))
126 with torch.no_grad():
127 for p in net.parameters():p.add_(torch.randn_like(p)*math.sqrt(eps))
128 rec.append((eps,bar,int(tr),cl))
129 with torch.no_grad(): m=float(((net(ds['xte'])-ds['yte'])**2).mean())
130 return (m,rec) if trace else m
131
132
133def run_cfg(cfg, seeds=SEEDS, idea=False):
134 vals=[]
135 for s in seeds:
136 ds=get_dataset('dynamics',s,n_train=NTR,n_test=NTE)
137 vals.append(train_idea(ds,cfg['lr'],cfg['weight_decay'],cfg.get('q_safe',8),s) if idea else train_baseline(ds,cfg['lr'],cfg['weight_decay'],s))
138 return {'per_seed':[float(v) for v in vals], 'mean':float(np.mean(vals))}
139
140
141def main():
142 # Baseline central knobs and all idea step sizes share the same union.
143 grid=[{'lr':lr,'weight_decay':wd} for lr in LRS for wd in WDS]
144 base=sweep_baseline(lambda c: (lambda seed: train_baseline(get_dataset('dynamics', seed, n_train=NTR, n_test=NTE), c['lr'], c['weight_decay'], seed)), grid)
145 best=base['best_cfg']
146 nearby=list(LRS)
147 idea_cfgs=[{'lr':lr,'weight_decay':best['weight_decay'],'q_safe':q} for lr in nearby for q in QS]
148 idea_runs=[(c,run_cfg(c,SEEDS,True)) for c in idea_cfgs]
149 ic,best_idea=min(idea_runs,key=lambda z:z[1]['mean'])
150 # NN-scale signature: measured barrier/noise settings and observed trapped events.
151 ds=get_dataset('dynamics',0,n_train=NTR,n_test=NTE); _,trace=train_idea(ds,ic['lr'],ic['weight_decay'],ic['q_safe'],0,True)
152 usable=[r for r in trace if r[0]>0 and r[1]>0]; slope=float(np.polyfit([1/r[0] for r in usable],[math.log(max(r[2],1e-3)) for r in usable],1)[0]) if len(usable)>=3 else float('nan'); pred=-float(np.median([r[1] for r in usable])) if usable else float('nan')
153 sig={'predicted_slope':pred,'observed_slope':slope,'n_checkpoints':len(usable),'confirmed':bool(np.isfinite(slope) and np.isfinite(pred) and abs(slope-pred)<=.2*max(abs(pred),1e-6))}
154 rep=make_report('dynamics','rnn_small',base,best_idea,{'barrier_noise_calibration':sig,'q_safe':ic['q_safe'],'custom_track':None})
155 Path('bench_report.json').write_text(json.dumps(rep,indent=2)); print(json.dumps(rep,indent=2))
156
157if __name__=='__main__': main()