Dynamics-Matched Contractive Reservoir / bench_run.py
Failed on benchmark
1import json
2import sys
3import numpy as np
4import torch
5from torch import nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, train_model, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = tuple(range(4))
12NTRAIN, NTEST, EPOCHS = 800, 300, 15
13WIDTH = 64
14
15class DynamicsMatchedReservoir(nn.Module):
16 """Contractive pendulum-matched recurrent predictor for the dynamics track."""
17 def __init__(self, out_dim=1, width=64, leak=0.45, residual_scale=0.10):
18 super().__init__()
19 self.width = width
20 self.leak = leak
21 self.residual_scale = residual_scale
22 self.in_proj = nn.Linear(3, width)
23 self.mech_proj = nn.Linear(3, width, bias=False)
24 self.residual = nn.Parameter(torch.empty(width, width))
25 self.head = nn.Linear(width, out_dim)
26 with torch.no_grad():
27 q, _ = torch.linalg.qr(torch.randn(width, width))
28 self.residual.copy_(0.82 * q)
29 self.mech_proj.weight.zero_()
30 # features: restoring force sin(theta), angular velocity, control
31 self.mech_proj.weight[0, 0] = 0.55
32 self.mech_proj.weight[1, 1] = -0.22
33 self.mech_proj.weight[2, 2] = 0.18
34 self.in_proj.weight.normal_(0, 0.08)
35 self.in_proj.bias.zero_()
36
37 def forward(self, x):
38 seq = x.view(x.shape[0], -1, 3)
39 h = torch.zeros(x.shape[0], self.width, device=x.device, dtype=x.dtype)
40 for k in range(seq.shape[1]):
41 q = seq[:, k]
42 theta, omega, control = q[:, 0:1], q[:, 1:2], q[:, 2:3]
43 mech = torch.cat((torch.sin(theta), omega, control), dim=1)
44 update = torch.tanh(self.in_proj(q) + self.mech_proj(mech) + self.residual_scale * (h @ self.residual.T))
45 h = (1.0 - self.leak) * h + self.leak * update
46 return self.head(h)
47
48def seed_all(seed):
49 np.random.seed(seed)
50 torch.manual_seed(seed)
51 if torch.cuda.is_available():
52 torch.cuda.manual_seed_all(seed)
53
54def train_cfg(model_fn, cfg, seed):
55 seed_all(seed)
56 ds = get_dataset('dynamics', seed, n_train=NTRAIN, n_test=NTEST)
57 model = model_fn(ds)
58 _, metric, hist = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, weight_decay=cfg.get('weight_decay', 0.0), log=lambda *_: None)
59 return metric
60
61def baseline_factory(cfg):
62 def run(seed):
63 return train_cfg(lambda d: make_model('rnn_small', d['input_shape'], d['out_dim']), cfg, seed)
64 return run
65
66def idea_factory(cfg):
67 def run(seed):
68 return train_cfg(lambda d: DynamicsMatchedReservoir(d['out_dim'], WIDTH, cfg['leak'], cfg['residual_scale']), cfg, seed)
69 return run
70
71def evaluate8(fn):
72 vals = [float(fn(s)) for s in SEEDS]
73 return {'per_seed': vals, 'mean': float(np.mean(vals)), 'std': float(np.std(vals, ddof=1))}
74
75def trained_signature(seed, cfg, baseline=False):
76 seed_all(seed)
77 ds = get_dataset('dynamics', seed, n_train=NTRAIN, n_test=NTEST)
78 model = make_model('rnn_small', ds['input_shape'], ds['out_dim']) if baseline else DynamicsMatchedReservoir(ds['out_dim'], WIDTH, cfg['leak'], cfg['residual_scale'])
79 net, _, _ = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, log=lambda *_: None)
80 if net is None:
81 return {'mean_perturbation_ratio': None, 'predicted_rho': None, 'observed_ratio': None, 'confirmed': False}
82 device = next(net.parameters()).device
83 x = ds['xte'][:64].to(device)
84 if baseline:
85 return {'mean_perturbation_ratio': None, 'predicted_rho': None, 'observed_ratio': None, 'confirmed': False, 'note': 'GRU baseline has no directly comparable explicit contractive bound'}
86 net.eval()
87 delta = 1e-3 * torch.randn_like(x)
88 with torch.no_grad():
89 y0 = net(x); y1 = net(x + delta)
90 ratio = float(((y1-y0).norm(dim=1) / (delta.norm(dim=1)+1e-12)).mean())
91 # For this tanh Euler/leaky update, the initialization predicts an upper
92 # local linear recurrent contribution of (1-leak)+leak*0.82*scale.
93 pred = (1-cfg['leak']) + cfg['leak'] * 0.82 * cfg['residual_scale']
94 return {'mean_perturbation_ratio': ratio, 'predicted_rho': float(pred), 'observed_ratio': ratio, 'confirmed': bool(ratio < 1.0 and abs(ratio-pred) < 0.35)}
95
96def main():
97 grid = [
98 {'lr': 0.001, 'epochs': EPOCHS, 'leak': 0.35, 'residual_scale': 0.08},
99 {'lr': 0.003, 'epochs': EPOCHS, 'leak': 0.45, 'residual_scale': 0.10},
100 {'lr': 0.006, 'epochs': EPOCHS, 'leak': 0.55, 'residual_scale': 0.14},
101 ]
102 # Baseline receives the union of all learning rates and the same budget;
103 # leak/residual knobs are ignored by the standard GRU but represented in cfg.
104 base_grid = [{'lr': c['lr'], 'epochs': c['epochs'], 'weight_decay': 0.0} for c in grid]
105 base = sweep_baseline(baseline_factory, base_grid, seeds=SWEEP_SEEDS)
106 idea_candidates = []
107 for cfg in grid:
108 r = evaluate8(idea_factory(cfg))
109 idea_candidates.append((r['mean'], cfg, r))
110 _, best_cfg, idea_res = min(idea_candidates, key=lambda z: z[0])
111 sig = trained_signature(0, best_cfg, baseline=False)
112 report = make_report('dynamics', 'rnn_small', base, idea_res, extra={'prediction': 'contractive recurrent perturbations decay', 'idea_model': 'DynamicsMatchedReservoir', 'signature': sig, 'confirmed': sig['confirmed']})
113 report['idea_sweep'] = [{'cfg': c, 'mean': m, 'full': r} for m,c,r in idea_candidates]
114 report['protocol'] = {'paired_seeds': list(SEEDS), 'n_train': NTRAIN, 'n_test': NTEST, 'epochs': EPOCHS, 'structural_match': 'dynamics/control', 'search_space_parity': True}
115 with open('bench_report.json','w') as f: json.dump(report, f, indent=2)
116 print(json.dumps(report, indent=2))
117
118if __name__ == '__main__':
119 main()