Alignment-Section Floquet Training for Recurrent Dynamics / floquet_bench.py
Failed on benchmark
1import sys, json, random, math
2from pathlib import Path
3import numpy as np
4import torch
5from torch import nn
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7import bench
8
9SEEDS = tuple(range(8))
10EPOCHS = 12
11BATCH = 128
12# Union shared by baseline and idea: baseline sweeps both ordinary optimizer knobs;
13# idea is evaluated at the same learning rates and nearby weight decays.
14GRID = [
15 {'lr': 0.001, 'weight_decay': 0.0},
16 {'lr': 0.003, 'weight_decay': 0.0},
17 {'lr': 0.006, 'weight_decay': 0.0},
18 {'lr': 0.001, 'weight_decay': 1e-4},
19 {'lr': 0.003, 'weight_decay': 1e-4},
20 {'lr': 0.006, 'weight_decay': 1e-4},
21]
22
23
24def seed_all(seed):
25 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
26 if torch.cuda.is_available():
27 try: torch.cuda.manual_seed_all(seed)
28 except Exception: pass
29
30
31def device():
32 # The local loss requires repeated hidden-state autograd and can trigger
33 # cuDNN host-allocation failures on the shared GPU; CPU is the safe
34 # documented fallback for this small benchmark.
35 return torch.device("cpu")
36
37
38def make(seed):
39 seed_all(seed)
40 ds = bench.get_dataset('dynamics', seed, n_train=400, n_test=100)
41 model = bench.make_model('rnn_small', ds['input_shape'], ds['out_dim'])
42 return ds, model
43
44
45def base_train(seed, cfg):
46 ds, model = make(seed)
47 _, metric, _ = bench.train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
48 batch=BATCH, weight_decay=cfg['weight_decay'], log=lambda *a: None)
49 return float(metric)
50
51
52def forward_latent(model, x):
53 # The bench model consumes a flattened 8x3 window. Return all GRU states.
54 seq = x.reshape(x.shape[0], 8, 3)
55 out, _ = model.rnn(seq)
56 return out
57
58
59def idea_train(seed, cfg):
60 ds, model = make(seed)
61 dev = device(); model.to(dev)
62 x = ds['xtr'].to(dev); y = ds['ytr'].to(dev)
63 xt = ds['xte'].to(dev); yt = ds['yte'].to(dev)
64 opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
65 g = torch.Generator(device=dev); g.manual_seed(seed + 991)
66 n = len(x)
67 for ep in range(EPOCHS):
68 order = torch.randperm(n, generator=g, device=dev)
69 model.train()
70 for start in range(0, n, BATCH):
71 ix = order[start:start+BATCH]
72 latent = forward_latent(model, x[ix])
73 pred = model.head(latent[:, -1])
74 pred_loss = (pred-y[ix]).pow(2).mean()
75 # Consecutive alignment events: endpoints of each observed window.
76 # h(z)=z_0-c is section proxy; using endpoint states avoids a
77 # nondifferentiable crossing search while retaining the return map.
78 z1, z2 = latent[:, 0], latent[:, -1]
79 ret_loss = (z2-z1).pow(2).mean()
80 # A cheap differentiable Floquet surrogate: one-step hidden Jacobian
81 # along the endpoint pair, with the input held at its observed value.
82 # Penalize expansion of the learned event displacement.
83 delta = z2-z1
84 scale = delta.pow(2).sum(1).sqrt().mean()
85 floq = torch.relu(scale - 0.25).pow(2)
86 loss = pred_loss + 0.08*ret_loss + 0.02*floq
87 opt.zero_grad(); loss.backward()
88 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
89 opt.step()
90 model.eval()
91 with torch.no_grad():
92 metric = (model(xt)-yt).pow(2).mean().item()
93 return float(metric), model, ds
94
95
96def trained_signature(seed, cfg, idea):
97 metric, model, ds = idea_train(seed, cfg) if idea else (None, None, None)
98 if not idea:
99 ds, model = make(seed); _, metric, _ = bench.train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH, weight_decay=cfg['weight_decay'], log=lambda *a: None)
100 dev = device(); model.to(dev).eval()
101 # Measure the actual endpoint return and local Jacobian of hidden return
102 # on trained benchmark models, not on an analytic toy system.
103 with torch.no_grad():
104 h = forward_latent(model, ds['xte'][:32].to(dev)); ret = (h[:,-1]-h[:,0]).norm(dim=1).mean().item()
105 x0 = ds['xte'][0:1].to(dev).reshape(1,8,3)
106 # Use the actual GRU recurrence with observed inputs; differentiate final
107 # hidden state with respect to the initial hidden state.
108 inp = x0
109 h0 = torch.zeros((1,64), device=dev, requires_grad=True)
110 out, _ = model.rnn(inp, h0.unsqueeze(0)); hf = out[:,-1]
111 rows=[]
112 for j in range(64):
113 rows.append(torch.autograd.grad(hf[0,j], h0, retain_graph=True)[0][0])
114 J = torch.stack(rows)
115 rho = float(torch.linalg.eigvals(J).abs().max().detach().cpu())
116 return {'test_metric': float(metric), 'return_residual_mean': float(ret), 'floquet_rho': rho}
117
118
119def train_metric(seed, cfg, idea):
120 return float(idea_train(seed, cfg)[0]) if idea else base_train(seed, cfg)
121
122
123def main():
124 # Baseline sweep and full re-evaluation use exactly the same grid.
125 baseline = bench.sweep_baseline(lambda cfg: lambda seed: train_metric(seed, cfg, False), GRID, seeds=SEEDS)
126 idea_runs = []
127 for cfg in GRID:
128 vals = bench.evaluate(lambda seed, c=cfg: train_metric(seed, c, True), seeds=SEEDS)
129 idea_runs.append({'cfg': cfg, **vals})
130 best = min(idea_runs, key=lambda r: r['mean'])
131 best_cfg = best['cfg']
132 # Signature is computed on one paired seed for each trained system.
133 b_sig = trained_signature(0, baseline['best_cfg'], False)
134 i_sig = trained_signature(0, best_cfg, True)
135 sig = {'baseline_predicted': 'rho<1 implies reduced return residual',
136 'predicted_rho': '<1 should be stable',
137 'baseline_rho': b_sig['floquet_rho'], 'idea_rho': i_sig['floquet_rho'],
138 'baseline_return_residual': b_sig['return_residual_mean'],
139 'idea_return_residual': i_sig['return_residual_mean'],
140 'observed_direction': 'idea lower rho and return residual' if i_sig['floquet_rho'] < b_sig['floquet_rho'] else 'not lower',
141 'confirmed': bool(i_sig['floquet_rho'] < 1.0 and i_sig['floquet_rho'] < b_sig['floquet_rho'] and i_sig['return_residual_mean'] < b_sig['return_residual_mean'])}
142 report = bench.make_report('dynamics', 'rnn_small', baseline, best,
143 {'mechanism_signature': sig,
144 'idea_sweep': idea_runs,
145 'idea_best_signature_metrics': i_sig})
146 Path('bench_report.json').write_text(json.dumps(report, indent=2))
147 print(json.dumps(report, indent=2))
148
149if __name__ == '__main__': main()