Pullback random-attractor monitor / bench_pullback.py
Failed on benchmark
1import sys, json, math
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6
7_LAST_NET = None
8_LAST_X = None
9sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
10from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
11
12# Pullback monitor: replay the same additive latent perturbation for K replicas.
13def pairwise_rms(z):
14 d = z[:, None, :] - z[None, :, :]
15 return torch.sqrt(torch.mean(torch.sum(d*d, dim=-1)) + 1e-12)
16
17def pullback_penalty(net, x, steps=4, replicas=8, noise_scale=0.03, target_lambda=-0.08):
18 # Use the trained GRU's recurrent cell on a short observed dynamics prefix.
19 seq = x.view(x.shape[0], -1, 3)[:1]
20 with torch.no_grad():
21 _, h0 = net.rnn(seq)
22 h = h0[-1].detach().repeat(replicas, 1)
23 init = torch.randn_like(h)
24 init = init / (init.norm(dim=1, keepdim=True) + 1e-8) * 0.5
25 h = h + init
26 # Same noise vector at every step for all replicas; noise is diagnostic-only.
27 ds = []
28 for r in range(steps):
29 inp = torch.zeros(replicas, 3, device=h.device)
30 h = net.rnn(inp.unsqueeze(1), h.unsqueeze(0))[1][-1]
31 if noise_scale:
32 eps = torch.randn(1, h.shape[1], device=h.device) * noise_scale
33 h = h + eps
34 ds.append(pairwise_rms(h))
35 logs = torch.log(torch.stack(ds))
36 # Penalize growth beyond the desired contraction envelope.
37 t = torch.arange(1, steps + 1, device=h.device, dtype=h.dtype)
38 excess = torch.relu(logs[1:] - logs[:-1] - target_lambda)
39 return torch.mean(excess**2), float((logs[-1]-logs[0]).detach().cpu())
40
41def train_one(seed, cfg, idea):
42 global _LAST_NET, _LAST_X
43 torch.manual_seed(seed); np.random.seed(seed)
44 d = get_dataset('dynamics', seed, n_train=400, n_test=160)
45 net = make_model('rnn_small', d['input_shape'], d['out_dim'])
46 device = 'cuda' if torch.cuda.is_available() else 'cpu'
47 try:
48 net.to(device)
49 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
50 lossf = nn.MSELoss(); xtr,ytr=d['xtr'].to(device),d['ytr'].to(device)
51 for ep in range(cfg['epochs']):
52 net.train(); perm=torch.randperm(len(xtr),device=device)
53 for i in range(0,len(xtr),128):
54 ix=perm[i:i+128]; pred=net(xtr[ix]); loss=lossf(pred,ytr[ix])
55 if idea:
56 reg,_=pullback_penalty(net,xtr[ix],target_lambda=cfg['target_lambda'])
57 loss=loss+cfg['attr_weight']*reg
58 opt.zero_grad(); loss.backward(); opt.step()
59 net.eval()
60 with torch.no_grad(): metric=float(lossf(net(d['xte'].to(device)),d['yte'].to(device)).cpu())
61 _LAST_NET, _LAST_X = net, d['xte'][:1].to(device)
62 return metric
63 except RuntimeError:
64 # CPU fallback, matching the harness safety requirement.
65 net=net.cpu(); opt=torch.optim.Adam(net.parameters(),lr=cfg['lr'],weight_decay=cfg['weight_decay'])
66 xtr,ytr=d['xtr'],d['ytr']
67 for ep in range(cfg['epochs']):
68 perm=torch.randperm(len(xtr))
69 for i in range(0,len(xtr),128):
70 ix=perm[i:i+128]; loss=lossf(net(xtr[ix]),ytr[ix])
71 if idea: loss=loss+cfg['attr_weight']*pullback_penalty(net,xtr[ix],target_lambda=cfg['target_lambda'])[0]
72 opt.zero_grad(); loss.backward(); opt.step()
73 with torch.no_grad(): metric=float(lossf(net(d['xte']),d['yte']))
74 _LAST_NET, _LAST_X = net, d['xte'][:1]
75 return metric
76
77def signature(seed=0, cfg=None):
78 # Uses the final trained model retained by evaluate(), not an analytic toy.
79 global _LAST_NET, _LAST_X
80 if _LAST_NET is None:
81 return {'error': 'no trained model', 'confirmed': False}
82 net, x = _LAST_NET, _LAST_X
83 vals=[]
84 old = torch.backends.cudnn.enabled
85 try:
86 torch.backends.cudnn.enabled = False
87 net.eval()
88 with torch.no_grad():
89 seq=x.view(1,-1,3); _,h0=net.rnn(seq); h=h0[-1].repeat(16,1)
90 q=torch.randn_like(h); q=q/(q.norm(dim=1,keepdim=True)+1e-8)*.5; h+=q
91 for _ in range(6):
92 inp=torch.zeros(16,1,3,device=h.device)
93 h=net.rnn(inp,h.unsqueeze(0))[1][-1]
94 vals.append(float(pairwise_rms(h).cpu()))
95 except RuntimeError:
96 net=net.cpu(); x=x.cpu(); torch.backends.cudnn.enabled=False
97 with torch.no_grad():
98 seq=x.view(1,-1,3); _,h0=net.rnn(seq); h=h0[-1].repeat(16,1)
99 q=torch.randn_like(h); q=q/(q.norm(dim=1,keepdim=True)+1e-8)*.5; h+=q
100 for _ in range(6):
101 h=net.rnn(torch.zeros(16,1,3),h.unsqueeze(0))[1][-1]
102 vals.append(float(pairwise_rms(h)))
103 finally:
104 torch.backends.cudnn.enabled=old
105 observed=float(np.polyfit(np.arange(6),np.log(np.maximum(vals,1e-12)),1)[0])
106 return {'predicted_log_diameter_step': -0.08, 'observed_log_diameter_slope': observed,
107 'final_diameter': vals[-1], 'confirmed': bool(observed < 0 and abs(observed+0.08)<0.15)}
108
109def main():
110 # Union parity: every idea lr is also in baseline sweep.
111 grid=[{'lr':lr,'weight_decay':wd,'epochs':12,'attr_weight':0.0,'target_lambda':-0.08} for lr in (0.0015,0.003,0.006) for wd in (0.0,1e-4)]
112 base=sweep_baseline(lambda c: lambda s: train_one(s,c,False),grid)
113 idea_cfgs=[dict(c,attr_weight=w,target_lambda=t) for c in [base['best_cfg']] for w,t in ((0.001,-0.08),(0.01,-0.08),(0.03,-0.04))]
114 idea_runs=[]
115 for c in idea_cfgs:
116 idea_runs.append((c,evaluate(lambda s,c=c: train_one(s,c,True))))
117 best_cfg,best=min(idea_runs,key=lambda z:z[1]['mean'])
118 rep=make_report('dynamics','rnn_small',base,best,{'prediction':'trained hidden pullback diameter contracts exponentially under shared replay','config':best_cfg,**signature(0,best_cfg)})
119 rep['idea_sweep']=[{'cfg':c,'result':r} for c,r in idea_runs]
120 rep['track_justification']='Dynamics is structurally matched: the idea diagnoses stability and contraction of recurrent latent trajectories.'
121 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
122 print(json.dumps(rep,indent=2))
123if __name__=='__main__': main()