Commutator-Regularized Switched SSM / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import json, random, sys
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7from bench import get_dataset, train_model, sweep_baseline, make_report
8
9TAU = 0.20
10ALPHA = (0.5, 0.5)
11HIDDEN = 64
12SEEDS = tuple(range(8))
13SWEEP_SEEDS = tuple(range(4))
14
15class SwitchedSSM(nn.Module):
16 def __init__(self, input_shape, out_dim, comm_lambda=0.0):
17 super().__init__()
18 self.d = HIDDEN
19 self.lam = float(comm_lambda)
20 eye = torch.eye(self.d)
21 self.A = nn.Parameter(-0.7 * eye[None].repeat(2, 1, 1) + 0.03 * torch.randn(2, self.d, self.d))
22 self.U = nn.Parameter(0.08 * torch.randn(2, 3, self.d))
23 self.head = nn.Linear(self.d, out_dim)
24
25 def forward(self, x):
26 z = x.reshape(x.shape[0], -1, 3)
27 h = torch.zeros(x.shape[0], self.d, device=x.device, dtype=x.dtype)
28 for t in range(z.shape[1]):
29 for i, a in enumerate(ALPHA):
30 E = torch.matrix_exp(a * TAU * self.A[i])
31 h = h @ E.T + (a * z[:, t, :]) @ self.U[i]
32 return self.head(h)
33
34 def comm_penalty(self):
35 B0 = ALPHA[0] * TAU * self.A[0]
36 B1 = ALPHA[1] * TAU * self.A[1]
37 C = B0 @ B1 - B1 @ B0
38 return (C * C).sum()
39
40def seed_all(s):
41 random.seed(s); np.random.seed(s); torch.manual_seed(s)
42
43def train_idea(model, ds, epochs, lr):
44 try:
45 device = 'cuda' if torch.cuda.is_available() else 'cpu'
46 model = model.to(device)
47 x, y = ds['xtr'].to(device), ds['ytr'].to(device)
48 opt = torch.optim.Adam(model.parameters(), lr=lr)
49 lossf = nn.MSELoss()
50 for _ in range(epochs):
51 model.train()
52 for ix in torch.randperm(len(x), device=device).split(128):
53 loss = lossf(model(x[ix]), y[ix]) + model.lam * model.comm_penalty()
54 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0); opt.step()
55 model.eval()
56 with torch.no_grad(): metric = float(lossf(model(ds['xte'].to(device)), ds['yte'].to(device)).cpu())
57 return model, metric
58 except Exception:
59 if torch.cuda.is_available():
60 torch.cuda.empty_cache()
61 model = model.cpu(); x, y = ds['xtr'], ds['ytr']; opt = torch.optim.Adam(model.parameters(), lr=lr); lossf = nn.MSELoss()
62 for _ in range(epochs):
63 for ix in torch.randperm(len(x)).split(128):
64 loss = lossf(model(x[ix]), y[ix]) + model.lam * model.comm_penalty()
65 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0); opt.step()
66 with torch.no_grad(): metric = float(lossf(model(ds['xte']), ds['yte']))
67 return model, metric
68
69def train_base(cfg, seed, keep=False):
70 seed_all(seed); ds = get_dataset('dynamics', seed, n_train=400, n_test=200)
71 model = SwitchedSSM(ds['input_shape'], ds['out_dim'], 0.0)
72 # Uses the standard harness path for the matched no-penalty switched baseline.
73 model, metric, _ = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, weight_decay=0.0)
74 return (model, metric) if keep else metric
75
76def train_idea_cfg(cfg, seed, keep=False):
77 seed_all(seed); ds = get_dataset('dynamics', seed, n_train=400, n_test=200)
78 model = SwitchedSSM(ds['input_shape'], ds['out_dim'], cfg['comm_lambda'])
79 model, metric = train_idea(model, ds, cfg['epochs'], cfg['lr'])
80 return (model, metric) if keep else metric
81
82def base_factory(cfg):
83 return lambda seed: train_base(cfg, seed)
84
85def metric_and_models(fn, cfg):
86 vals=[]; models=[]
87 for s in SEEDS:
88 m,v = fn(cfg, s, True); vals.append(v); models.append(m)
89 return {'config':cfg, 'per_seed':vals, 'mean':float(np.mean(vals)), 'models':models}
90
91def diagnostics(models):
92 comm=[]; gain=[]
93 for m in models:
94 with torch.no_grad():
95 A=m.A.detach().cpu(); B0=ALPHA[0]*TAU*A[0]; B1=ALPHA[1]*TAU*A[1]
96 C=B0@B1-B1@B0; Phi=torch.matrix_exp(B1)@torch.matrix_exp(B0)
97 comm.append(float((C*C).sum())); gain.append(float(torch.linalg.svdvals(Phi).max()))
98 return {'observed_commutator_mean':float(np.mean(comm)), 'observed_cycle_gain_mean':float(np.mean(gain)), 'n_models':len(models)}
99
100def main():
101 # Equal union: every idea lr is included in baseline tuning.
102 grid=[{'lr':lr,'epochs':20} for lr in (0.003,0.006,0.009)]
103 tuned=sweep_baseline(base_factory, grid, seeds=SWEEP_SEEDS)
104 best_cfg=tuned['best_cfg']
105 base_full=metric_and_models(lambda c,s,k: train_base(c,s,k), best_cfg)
106 idea_runs=[]
107 for lam in (0.0, 0.01, 0.05):
108 cfg=dict(best_cfg, comm_lambda=lam)
109 idea_runs.append(metric_and_models(lambda c,s,k: train_idea_cfg(c,s,k), cfg))
110 idea=min(idea_runs, key=lambda r:r['mean'])
111 bd=diagnostics(base_full['models']); id=diagnostics(idea['models'])
112 report=make_report('dynamics','rnn_small',
113 {'best_cfg':best_cfg, 'sweep':tuned['sweep'], 'full':{'per_seed':base_full['per_seed'],'mean':base_full['mean']}},
114 {'config':idea['config'],'per_seed':idea['per_seed'],'mean':idea['mean']},
115 {'track_justification':'Dynamics directly tests switched latent stability/control and Lyapunov contraction.',
116 'mechanism_signature':{'prediction':'commutator regularization lowers trained ordered-cycle noncommutativity and cycle gain',
117 'baseline_observed':bd, 'idea_observed':id,
118 'commutator_ratio':id['observed_commutator_mean']/max(bd['observed_commutator_mean'],1e-12),
119 'cycle_gain_delta':id['observed_cycle_gain_mean']-bd['observed_cycle_gain_mean'],
120 'confirmed': bool(id['observed_commutator_mean'] < bd['observed_commutator_mean'] and id['observed_cycle_gain_mean'] <= bd['observed_cycle_gain_mean']*1.02)}})
121 report['idea_sweep']=[{k:r[k] for k in ('config','mean','per_seed')} for r in idea_runs]
122 Path('bench_report.json').write_text(json.dumps(report, indent=2, allow_nan=False))
123 print(json.dumps(report, indent=2))
124
125if __name__=='__main__': main()