Hidden-Diffusion Irreversibility Monitor / stage2_bench.py
Failed on benchmark
1import sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7from bench import get_dataset, make_model, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10# Includes every idea learning rate, and sweeps the baseline's weight decay knob too.
11GRID = [
12 {'lr': 1e-3, 'weight_decay': 0.0},
13 {'lr': 1e-3, 'weight_decay': 1e-4},
14 {'lr': 3e-3, 'weight_decay': 0.0},
15 {'lr': 3e-3, 'weight_decay': 1e-4},
16 {'lr': 6e-3, 'weight_decay': 0.0},
17 {'lr': 6e-3, 'weight_decay': 1e-4},
18]
19# Three nearby settings, including the baseline's eventual best lr.
20IDEA_GRID = [
21 {'lr': 1e-3, 'weight_decay': 0.0, 'lam': 1e-3},
22 {'lr': 3e-3, 'weight_decay': 0.0, 'lam': 1e-3},
23 {'lr': 6e-3, 'weight_decay': 0.0, 'lam': 1e-3},
24]
25EPOCHS, BATCH = 6, 128
26
27
28def seed_all(seed):
29 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
30 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
31
32
33def forward_with_hidden(net, x):
34 seq = x.view(x.shape[0], -1, 3)
35 h, _ = net.rnn(seq)
36 return h, net.head(h[:, -1])
37
38
39def cross_diffusion_monitor(h):
40 """Empirical OU monitor on a two-dimensional learned latent subspace.
41
42 A local drift is fitted from hidden increments and residual covariance gives
43 D=residual_cov/(2 dt). The regularizer discourages cross diffusion relative
44 to diagonal diffusion; the task loss remains the primary benchmark metric.
45 """
46 if h.shape[0] < 4 or h.shape[1] < 2:
47 return h.new_zeros(()), h.new_zeros(())
48 z = h[:, :-1, :2].reshape(-1, 2)
49 dz = (h[:, 1:, :2] - h[:, :-1, :2]).reshape(-1, 2)
50 # Regression is detached: the monitor estimates latent dynamics, while the
51 # covariance penalty remains differentiable through residual increments.
52 with torch.no_grad():
53 xtx = z.T @ z + 1e-4 * torch.eye(2, device=z.device)
54 coef = torch.linalg.solve(xtx, z.T @ dz)
55 resid = dz - z @ coef.detach()
56 cov = (resid.T @ resid) / max(1, resid.shape[0] - 1)
57 d = cov / 2.0
58 dxy = d[0, 1]
59 scale = d.diag().mean().detach() + 1e-5
60 penalty = (dxy / scale).pow(2)
61 return penalty, dxy.detach()
62
63
64def run_one(seed, cfg, idea, n_train=400, n_test=100, diagnostics=False):
65 seed_all(seed)
66 ds = get_dataset('dynamics', seed, n_train=n_train, n_test=n_test)
67 net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
68 device = 'cuda' if torch.cuda.is_available() else 'cpu'
69 try:
70 net.to(device)
71 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'],
72 weight_decay=cfg['weight_decay'])
73 x, y = ds['xtr'].to(device), ds['ytr'].to(device)
74 for _ in range(EPOCHS):
75 net.train()
76 perm = torch.randperm(len(x), device=device)
77 for i in range(0, len(x), BATCH):
78 ind = perm[i:i+BATCH]
79 h, pred = forward_with_hidden(net, x[ind])
80 loss = ((pred-y[ind])**2).mean()
81 if idea:
82 reg, _ = cross_diffusion_monitor(h)
83 loss = loss + cfg['lam'] * reg
84 opt.zero_grad(); loss.backward(); opt.step()
85 net.eval()
86 with torch.no_grad():
87 hte, out = forward_with_hidden(net, ds['xte'].to(device))
88 metric = float(((out-ds['yte'].to(device))**2).mean().cpu())
89 if not diagnostics:
90 return metric
91 with torch.no_grad():
92 reg, dxy = cross_diffusion_monitor(hte)
93 out = out[:, 0].detach().cpu().numpy()
94 hh = hte[:, :, :2].reshape(-1, 2).detach().cpu().numpy()
95 # Behavior-derived output-spectrum proxy and reverse/forward gap.
96 centered = out - out.mean()
97 spectrum0 = float(np.mean(centered**2))
98 a = hh[:-1]; b = hh[1:]
99 dz = b-a
100 coef = np.linalg.solve(a.T@a + 1e-4*np.eye(2), a.T@dz)
101 r = dz-a@coef
102 cov = np.cov(r, rowvar=False) + 1e-4*np.eye(2)
103 inv = np.linalg.inv(cov)
104 # Gaussian transition log-likelihood forward vs reversed ordering.
105 rf = dz
106 rr = a-b-b@coef
107 gap = float(np.mean(-0.5*np.einsum('ij,jk,ik->i',rf,inv,rf) + 0.5*np.einsum('ij,jk,ik->i',rr,inv,rr)))
108 return metric, {'dxy': float(dxy.cpu()), 'output_power': spectrum0,
109 'reverse_forward_gap': gap, 'monitor': float(reg.cpu())}
110 except RuntimeError:
111 # Required robust CUDA fallback: recreate and execute on CPU.
112 if device != 'cuda': raise
113 torch.cuda.empty_cache()
114 return run_one_cpu(seed, cfg, idea, n_train, n_test, diagnostics)
115
116
117def run_one_cpu(seed, cfg, idea, n_train, n_test, diagnostics):
118 old = torch.cuda.is_available
119 ds = get_dataset('dynamics', seed, n_train=n_train, n_test=n_test)
120 net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
121 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
122 x, y = ds['xtr'], ds['ytr']
123 for _ in range(EPOCHS):
124 perm = torch.randperm(len(x))
125 for i in range(0, len(x), BATCH):
126 ind=perm[i:i+BATCH]; h,p=forward_with_hidden(net,x[ind]); loss=((p-y[ind])**2).mean()
127 if idea: loss=loss+cfg['lam']*cross_diffusion_monitor(h)[0]
128 opt.zero_grad(); loss.backward(); opt.step()
129 with torch.no_grad():
130 h,o=forward_with_hidden(net,ds['xte']); metric=float(((o-ds['yte'])**2).mean())
131 if not diagnostics: return metric
132 reg,dxy=cross_diffusion_monitor(h); q=o[:,0].numpy(); z=h[:,:,:2].reshape(-1,2).numpy()
133
134 z=h[:,:,:2].reshape(-1,2).numpy(); aa=z[:-1]; bb=z[1:]; dd=bb-aa
135 cc=np.cov(dd-aa@np.linalg.solve(aa.T@aa+1e-4*np.eye(2),aa.T@dd),rowvar=False)+1e-4*np.eye(2); ii=np.linalg.inv(cc)
136 gap=float(np.mean(-.5*np.einsum('ij,jk,ik->i',dd,ii,dd)+.5*np.einsum('ij,jk,ik->i',aa-bb-bb@np.linalg.solve(aa.T@aa+1e-4*np.eye(2),aa.T@dd),ii,aa-bb-bb@np.linalg.solve(aa.T@aa+1e-4*np.eye(2),aa.T@dd))))
137 return metric, {'dxy':float(dxy),'output_power':float(np.mean((q-q.mean())**2)), 'reverse_forward_gap':gap, 'monitor':float(reg)}
138
139
140def vals(fn): return [float(fn(s)) for s in SEEDS]
141
142
143def main():
144 def base_make(cfg): return lambda s: run_one(s, cfg, False)
145 base = sweep_baseline(base_make, GRID)
146 idea_runs=[]
147 for cfg in IDEA_GRID:
148 v=vals(lambda s, c=cfg: run_one(s,c,True))
149 idea_runs.append({'cfg':cfg,'mean':float(np.mean(v)),'per_seed':v})
150 best=min(idea_runs,key=lambda r:r['mean'])
151 idea_res={'mean':best['mean'],'std':float(np.std(best['per_seed'])),'per_seed':best['per_seed'],'n':8,'cfg':best['cfg'], 'sweep':idea_runs}
152 sig_base=[]; sig_idea=[]
153 for s in SEEDS:
154 _,b=run_one(s,base['best_cfg'],False,diagnostics=True)
155 _,a=run_one(s,best['cfg'],True,diagnostics=True)
156 sig_base.append(b); sig_idea.append(a)
157 def meanfield(rows,k): return float(np.mean([r[k] for r in rows]))
158 signature={'predicted': 'hidden cross diffusion can vary while observed output power is comparatively stable',
159 'baseline_observed_output_power_std':float(np.std([r['output_power'] for r in sig_base])),
160 'idea_observed_output_power_std':float(np.std([r['output_power'] for r in sig_idea])),
161 'baseline_hidden_dxy_std':float(np.std([r['dxy'] for r in sig_base])),
162 'idea_hidden_dxy_std':float(np.std([r['dxy'] for r in sig_idea])),
163 'idea_mean_reverse_forward_gap':meanfield(sig_idea,'reverse_forward_gap'),
164 'confirmed': bool(np.std([r['dxy'] for r in sig_idea]) > np.std([r['output_power'] for r in sig_idea]))}
165 report=make_report('dynamics','rnn_small',base,idea_res,{'mechanism_signature':signature,
166 'protocol_note':'Baseline and idea use identical rnn_small systems; only latent diffusion penalty differs.',
167 'idea_grid':idea_runs})
168 with open('bench_report.json','w') as f: json.dump(report,f,indent=2)
169 print(json.dumps(report,indent=2))
170
171if __name__=='__main__': main()