Differentially Passive Neural Blocks / bench_experiment.py
Failed on benchmark
1import sys, json, random, math
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, train_model, evaluate, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = (0, 1, 2, 3)
12EPOCHS = 8
13BATCH = 128
14ALPHA = 0.08
15MARGIN = 0.01
16PENALTY_WEIGHT = 0.08
17
18# The intervention is a sampled discrete differential-contraction penalty on
19# the recurrent state transition. The task head and all non-intervention
20# architecture are exactly bench's rnn_small.
21def device_and_seed(seed):
22 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
23 dev = 'cuda' if torch.cuda.is_available() else 'cpu'
24 try:
25 if dev == 'cuda': torch.cuda.empty_cache()
26 except Exception:
27 dev = 'cpu'
28 return dev
29
30def transition_jacobian(net, x, h):
31 """Differentiable closed-form Jacobian of one GRU step wrt h."""
32 c = net.rnn; wi, wh = c.weight_ih_l0, c.weight_hh_l0
33 gi = x @ wi.T + c.bias_ih_l0; gh = h @ wh.T + c.bias_hh_l0
34 ir, iz, inn = gi.chunk(3, -1); hr, hz, hnn = gh.chunk(3, -1)
35 r = torch.sigmoid(ir + hr); z = torch.sigmoid(iz + hz)
36 n = torch.tanh(inn + r * hnn)
37 d = h.shape[-1]
38 Dr = torch.diag_embed(r * (1-r)); Dz = torch.diag_embed(z * (1-z))
39 Dn = torch.diag_embed(1-n*n); D1mz = torch.diag_embed(1-z)
40 Dh = torch.diag_embed(h.squeeze(1) - n.squeeze(1))
41 # PyTorch GRU reset-after-matrix convention: n=tanh(inn+r*(Wh_n h+b)).
42 Jr = Dr @ wh[:d]
43 Jz = Dz @ wh[d:2*d]
44 Jn = Dn @ (wh[2*d:] + torch.diag_embed(hnn.squeeze(1)) @ Jr)
45 return (D1mz @ Jn - Dh @ Jz + torch.diag_embed(z.squeeze(1)))[0]
46
47def recurrent_outputs(net, x):
48 seq = x.view(x.shape[0], -1, 3)
49 out, _ = net.rnn(seq)
50 return seq, out
51
52def contraction_penalty(net, x, alpha=ALPHA):
53 seq, hs = recurrent_outputs(net, x)
54 vals = []
55 I = torch.eye(hs.shape[-1], device=x.device)
56 for b in range(min(2, x.shape[0])):
57 for t in (seq.shape[1] - 1,):
58 M = transition_jacobian(net, seq[b:b+1, t], hs[b, t:t+1])
59 lam = torch.linalg.eigvalsh(M.T @ M - (1.0-alpha) * I)[-1]
60 vals.append(torch.nn.functional.softplus(lam + MARGIN) ** 2)
61 return torch.stack(vals).mean()
62
63def train_idea(net, d, epochs=EPOCHS, lr=3e-3, seed=0):
64 dev = next(net.parameters()).device
65 net.train(); opt = torch.optim.Adam(net.parameters(), lr=lr)
66 x, y = d['xtr'].to(dev), d['ytr'].to(dev)
67 gen = torch.Generator(device=dev).manual_seed(seed + 91)
68 n = len(x)
69 for _ in range(epochs):
70 for ix in torch.randperm(n, generator=gen, device=dev).split(BATCH):
71 xb, yb = x[ix], y[ix]
72 opt.zero_grad(); pred = net(xb)
73 task = ((pred - yb) ** 2).mean()
74 pen = contraction_penalty(net, xb)
75 (task + PENALTY_WEIGHT * pen).backward()
76 torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0)
77 opt.step()
78 net.eval()
79 with torch.no_grad(): return float(((net(d['xte'].to(dev))-d['yte'].to(dev))**2).mean().cpu())
80
81def run_idea(cfg):
82 def fn(seed):
83 dev = device_and_seed(seed)
84 d = get_dataset('dynamics', seed, n_train=400, n_test=400)
85 try:
86 net = make_model('rnn_small', d['input_shape'], d['out_dim']).to(dev)
87 return train_idea(net, d, lr=cfg['lr'], seed=seed)
88 except Exception:
89 if dev != 'cuda': raise
90 net = make_model('rnn_small', d['input_shape'], d['out_dim']).cpu()
91 return train_idea(net, {k:(v.cpu() if torch.is_tensor(v) else v) for k,v in d.items()}, lr=cfg['lr'], seed=seed)
92 return fn
93
94def run_base(cfg):
95 def fn(seed):
96 device_and_seed(seed)
97 d = get_dataset('dynamics', seed, n_train=400, n_test=400)
98 # Required standard path; bench itself supplies GPU->CPU fallback.
99 _, metric, _ = train_model(make_model('rnn_small', d['input_shape'], d['out_dim']), d, epochs=EPOCHS, lr=cfg['lr'])
100 return float(metric)
101 return fn
102
103def signature(seed, cfg, idea):
104 dev = device_and_seed(seed); d = get_dataset('dynamics', seed, 400, 400)
105 try:
106 net = make_model('rnn_small', d['input_shape'], d['out_dim']).to(dev)
107 if idea: train_idea(net, d, lr=cfg['lr'], seed=seed)
108 else:
109 train_model(net, d, epochs=EPOCHS, lr=cfg['lr'])
110 x = d['xte'][:16].to(dev)
111 with torch.no_grad(): seq, hs = recurrent_outputs(net, x)
112 vals=[]; ratios=[]; I=torch.eye(hs.shape[-1], device=dev)
113 for b in range(4):
114 M=transition_jacobian(net,seq[b:b+1,0],hs[b,0:1])
115 vals.append(float(torch.linalg.eigvalsh(M.T@M-(1-ALPHA)*I)[-1].detach().cpu()))
116 h1=hs[b,0:1].detach(); h2=h1+0.01*torch.ones_like(h1)
117 with torch.no_grad():
118 a=transition_jacobian(net, seq[b:b+1,0], h1) @ (h1-h1).T
119 # finite perturbation through the actual GRU recurrence
120 def step(q):
121 return recurrent_outputs(net, torch.cat([seq[b:b+1,0:1], seq[b:b+1,0:1]], 1))[1][:,0:1]
122 c=net.rnn; xx=seq[b:b+1,0]
123 def actual(q):
124 gi=xx@c.weight_ih_l0.T+c.bias_ih_l0; gh=q@c.weight_hh_l0.T+c.bias_hh_l0
125 ir,iz,inn=gi.chunk(3,-1); hr,hz,hnn=gh.chunk(3,-1)
126 rr=torch.sigmoid(ir+hr); zz=torch.sigmoid(iz+hz); nnx=torch.tanh(inn+rr*hnn)
127 return (1-zz)*nnx+zz*q
128 aa,zz=actual(h1),actual(h2)
129 ratios.append(float((torch.linalg.vector_norm(zz-aa)/torch.linalg.vector_norm(h2-h1)).cpu()))
130 return {'predicted_ratio_bound': math.sqrt(1-ALPHA), 'observed_mean_ratio': float(np.mean(ratios)), 'predicted_lmi_bound': 0.0, 'observed_max_lmi': max(vals), 'confirmed': float(np.mean(ratios)) <= math.sqrt(1-ALPHA)+0.03 and max(vals) <= 0.03}
131 except Exception as e:
132 return {'predicted_ratio_bound': math.sqrt(1-ALPHA), 'observed_mean_ratio': None, 'predicted_lmi_bound': 0.0, 'observed_max_lmi': None, 'confirmed': False, 'error': repr(e)}
133
134def main():
135 grid=[{'lr':1e-3},{'lr':3e-3},{'lr':1e-2}]
136 base=sweep_baseline(run_base, grid, seeds=SWEEP_SEEDS)
137 idea_runs=[]
138 for cfg in grid:
139 r=evaluate(run_idea(cfg), seeds=SEEDS)
140 idea_runs.append((r,cfg))
141 best_idea,best_cfg=min(idea_runs, key=lambda z:z[0]['mean'])
142 report=make_report('dynamics','rnn_small',base,best_idea,{'mechanism_signature': signature(0,best_cfg,True), 'idea_sweep':[{'cfg':c,'result':r} for r,c in idea_runs], 'structural_match':'Dynamics track directly tests recurrent trajectory sensitivity and long-horizon stability.'})
143 report['baseline']['union_grid']=grid
144 report['selected_idea_cfg']=best_cfg
145 Path('bench_report.json').write_text(json.dumps(report,indent=2))
146 print(json.dumps(report,indent=2))
147if __name__=='__main__': main()