Finite-Horizon Walk Reciprocity Control / bench_walk_reciprocity.py
Mechanism confirmed, baseline not beaten
1import sys, json, math, random
2from pathlib import Path
3import numpy as np
4sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
5import torch
6from bench import get_dataset, make_model, sweep_baseline, make_report
7
8SEEDS = tuple(range(8))
9L = 4
10G = 0.8
11BATCH = 128
12EPOCHS = 12
13LRS = [1e-3, 3e-3, 6e-3]
14WDS = [0.0, 1e-4]
15LAMBDAS = [0.05, 0.2, 0.8]
16
17
18def seed_all(seed):
19 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
20 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
21
22
23def walk_energy(A, L=L, g=G):
24 s = torch.linalg.matrix_norm(A, ord=2) + 1e-6
25 A = A / s
26 v = A
27 r = torch.zeros((), device=A.device, dtype=A.dtype)
28 for k in range(1, L + 1):
29 d = v - v.transpose(0, 1)
30 r = r + (g ** (2*k-2)) * (d*d).sum() / A.shape[0]
31 v = A @ v
32 return r
33
34
35def interaction(model):
36 # GRU recurrent gate matrix is the local directed interaction proxy.
37 w = model.rnn.weight_hh_l0
38 h = w.shape[1]
39 return w[2*h:3*h, :]
40
41
42def train_one(seed, lr, wd, lam=0.0, target=0.0, return_model=False):
43 seed_all(seed)
44 ds = get_dataset('dynamics', seed, n_train=400, n_test=200)
45 net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
46 device = 'cuda' if torch.cuda.is_available() else 'cpu'
47 try:
48 net = net.to(device)
49 xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
50 xte, yte = ds['xte'].to(device), ds['yte'].to(device)
51 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=wd)
52 hist = []; e_hist = []
53 for ep in range(EPOCHS):
54 net.train(); perm = torch.randperm(len(xtr), device=device); total = 0.0
55 for start in range(0, len(xtr), BATCH):
56 idx = perm[start:start+BATCH]
57 pred = net(xtr[idx]); task = ((pred-ytr[idx])**2).mean()
58 r = walk_energy(interaction(net))
59 loss = task + lam * torch.relu(r-target)**2
60 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
61 total += float(task.detach()) * len(idx)
62 hist.append(total / len(xtr)); e_hist.append(float(walk_energy(interaction(net)).detach().cpu()))
63 net.eval()
64 with torch.no_grad():
65 metric = float(((net(xte)-yte)**2).mean().cpu())
66 observed_r = float(walk_energy(interaction(net)).cpu())
67 out = {'seed': seed, 'metric': metric, 'final_train_loss': hist[-1],
68 'walk_energy': observed_r, 'lambda': lam, 'lr': lr, 'weight_decay': wd,
69 'epochs': EPOCHS, 'device': device}
70 if return_model: out['_model'] = net
71 return out
72 except (RuntimeError, torch.cuda.OutOfMemoryError):
73 if device == 'cuda':
74 torch.cuda.empty_cache()
75 seed_all(seed)
76 net = make_model('rnn_small', ds['input_shape'], ds['out_dim']).to('cpu')
77 xtr, ytr = ds['xtr'], ds['ytr']; xte, yte = ds['xte'], ds['yte']
78 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=wd)
79 hist=[]
80 for ep in range(EPOCHS):
81 perm=torch.randperm(len(xtr)); total=0.0
82 for start in range(0,len(xtr),BATCH):
83 idx=perm[start:start+BATCH]; task=((net(xtr[idx])-ytr[idx])**2).mean()
84 r=walk_energy(interaction(net)); loss=task+lam*torch.relu(r-target)**2
85 opt.zero_grad(set_to_none=True); loss.backward(); opt.step(); total += float(task.detach())*len(idx)
86 hist.append(total/len(xtr))
87 net.eval()
88 with torch.no_grad(): metric=float(((net(xte)-yte)**2).mean()); observed_r=float(walk_energy(interaction(net)))
89 out={'seed':seed,'metric':metric,'final_train_loss':hist[-1],'walk_energy':observed_r,'lambda':lam,'lr':lr,'weight_decay':wd,'epochs':EPOCHS,'device':'cpu'}
90 if return_model: out['_model']=net
91 return out
92 raise
93
94
95def base_fn(cfg):
96 return lambda seed: train_one(seed, cfg['lr'], cfg['weight_decay'], 0.0, 0.0)['metric']
97
98
99def run():
100 # Calibrate target from a separate baseline first-epoch-like collection.
101 cal = [train_one(s, 3e-3, 0.0, 0.0) for s in SEEDS]
102 c = float(np.median([r['walk_energy'] for r in cal]) / (1-math.sqrt(1-G*G)))
103 target = c * (1-math.sqrt(1-G*G))
104 grid = [{'lr': lr, 'weight_decay': wd} for lr in LRS for wd in WDS]
105 # Harness sweep is used for baseline selection; each config runs paired seeds.
106 sweep = sweep_baseline(lambda cfg: base_fn(cfg), grid, seeds=SEEDS[:4])
107 best_cfg = sweep['best_cfg']
108 idea_cfgs = [{'lr': best_cfg['lr'], 'weight_decay': best_cfg.get('weight_decay',0.0), 'lambda': z} for z in LAMBDAS]
109 idea_runs = []
110 for cfg in idea_cfgs:
111 rows = [train_one(s, cfg['lr'], cfg['weight_decay'], cfg['lambda'], target) for s in SEEDS]
112 idea_runs.append({'config': cfg, 'mean_metric': float(np.mean([r['metric'] for r in rows])), 'per_seed': rows})
113 chosen = min(idea_runs, key=lambda z:z['mean_metric'])
114 base_block = {'best_cfg': best_cfg, 'sweep': sweep['sweep'], 'full': sweep['full']}
115 idea_block = {'best_config': chosen['config'], 'sweep': idea_runs, 'per_seed': [r['metric'] for r in chosen['per_seed']]}
116 base_rows = [dict(seed=s, metric=m) for s,m in zip(SEEDS, sweep['full']['per_seed'])]
117 base_sig_rows = [train_one(s, best_cfg['lr'], best_cfg.get('weight_decay',0.0), 0.0, 0.0) for s in SEEDS]
118 # Signature is measured on independently trained models and tests predicted suppression.
119 idea_metric_rows = [train_one(s, chosen['config']['lr'], chosen['config']['weight_decay'], chosen['config']['lambda'], target) for s in SEEDS]
120 pairs = [(b['walk_energy'], i['walk_energy']) for b,i in zip(base_sig_rows, idea_metric_rows)]
121 base_e = float(np.mean([x[0] for x in pairs])); idea_e = float(np.mean([x[1] for x in pairs]))
122 sig = {'prediction': 'walk regularization lowers finite-horizon forward/backward energy',
123 'baseline_walk_energy_mean': base_e, 'idea_walk_energy_mean': idea_e,
124 'relative_change_pct': 100*(idea_e-base_e)/max(base_e,1e-12),
125 'confirmed': bool(idea_e < base_e)}
126 report = make_report('dynamics', 'rnn_small', base_block, idea_block, {'mechanism_signature': sig})
127 report['calibration'] = {'g': G, 'L': L, 'phi_star': 1-math.sqrt(1-G*G), 'c_median': c, 'target': target}
128 Path('bench_report.json').write_text(json.dumps(report, indent=2))
129 print(json.dumps(report, indent=2))
130
131if __name__ == '__main__': run()