Weak Koopman Latent Dynamics / bench_weak_koopman.py
Failed on benchmark
1import sys, json, random
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
9from bench.models import rnn_small
10
11SEEDS = tuple(range(8))
12NTRAIN, NTEST, EPOCHS, BATCH = 1200, 400, 12, 128
13
14
15def seed_all(seed):
16 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
17 if torch.cuda.is_available():
18 try: torch.cuda.manual_seed_all(seed)
19 except Exception: pass
20
21
22def baseline_one(cfg, seed):
23 seed_all(seed)
24 d = get_dataset('dynamics', seed, n_train=NTRAIN, n_test=NTEST)
25 net = make_model('rnn_small', d['input_shape'], d['out_dim'])
26 _, metric, _ = train_model(net, d, epochs=EPOCHS, lr=cfg['lr'],
27 batch=BATCH, weight_decay=cfg['weight_decay'], log=lambda *_: None)
28 return float(metric)
29
30
31class WeakGRU(nn.Module):
32 """The canonical rnn_small architecture, exposing its latent GRU states."""
33 def __init__(self, hidden=64):
34 super().__init__()
35 self.rnn = nn.GRU(3, hidden, batch_first=True)
36 self.head = nn.Linear(hidden, 1)
37
38 def forward_states(self, x):
39 return self.rnn(x.view(x.shape[0], -1, 3))[0]
40
41 def forward(self, x):
42 h = self.forward_states(x)
43 return self.head(h[:, -1])
44
45
46def weak_weights(n=8):
47 # Uniform quadrature and a Hann test function vanishing at endpoints.
48 t = torch.arange(n, dtype=torch.float32)
49 u = t / (n - 1)
50 psi = torch.sin(np.pi * u) ** 2
51 dpsi = np.pi * torch.sin(2 * np.pi * u) / (n - 1)
52 return psi, -dpsi
53
54
55def idea_one(cfg, seed, return_model=False):
56 seed_all(seed)
57 d = get_dataset('dynamics', seed, n_train=NTRAIN, n_test=NTEST)
58 model = WeakGRU(64)
59 device = 'cuda' if torch.cuda.is_available() else 'cpu'
60 try:
61 model = model.to(device)
62 x, y = d['xtr'].to(device), d['ytr'].to(device)
63 opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
64 psi, wp = weak_weights(x.shape[1] // 3)
65 psi, wp = psi.to(device), wp.to(device)
66 # A jointly learned latent generator. This is the only training change.
67 A = nn.Parameter(torch.zeros(64, 64, device=device))
68 nn.init.normal_(A, std=0.01)
69 opt.add_param_group({'params': [A]})
70 for _ in range(EPOCHS):
71 model.train()
72 perm = torch.randperm(len(x), device=device)
73 for i in range(0, len(x), BATCH):
74 ix = perm[i:i+BATCH]
75 states = model.forward_states(x[ix])
76 pred = model.head(states[:, -1])
77 task = ((pred - y[ix]) ** 2).mean()
78 G = (states * psi[None, :, None]).sum(1)
79 # B_j = - integral psi' z; for one Hann window this is a vector.
80 Bv = (states * wp[None, :, None]).sum(1)
81 # A convention: dz/dt = A z, so weak prediction is G A^T.
82 weak_pred = G @ A.T
83 weak = ((Bv - weak_pred) ** 2).mean() / (states.detach().var() + 1e-4)
84 loss = task + cfg['alpha'] * weak
85 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(list(model.parameters()) + [A], 5.0); opt.step()
86 model.eval()
87 with torch.no_grad():
88 metric = float(((model(d['xte'].to(device)) - d['yte'].to(device)) ** 2).mean())
89 if return_model: return metric, model, A.detach(), d, device
90 return metric
91 except RuntimeError:
92 # Explicit CPU fallback for a shared or exhausted CUDA slot.
93 model = WeakGRU(64)
94 x, y = d['xtr'], d['ytr']; opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
95 psi, wp = weak_weights(x.shape[1] // 3); A = nn.Parameter(torch.randn(64,64)*0.01); opt.add_param_group({'params':[A]})
96 for _ in range(EPOCHS):
97 for i in range(0, len(x), BATCH):
98 states=model.forward_states(x[i:i+BATCH]); pred=model.head(states[:,-1]); task=((pred-y[i:i+BATCH])**2).mean()
99 G=(states*psi[None,:,None]).sum(1); Bv=(states*wp[None,:,None]).sum(1); weak=((Bv-G@A.T)**2).mean()/(states.detach().var()+1e-4)
100 opt.zero_grad(); (task+cfg['alpha']*weak).backward(); opt.step()
101 with torch.no_grad(): metric=float(((model(d['xte'])-d['yte'])**2).mean())
102 if return_model: return metric, model, A.detach(), d, 'cpu'
103 return metric
104
105
106def signature():
107 cfg={'lr':0.003,'weight_decay':0.0,'alpha':0.03}
108 metric, model, A, d, dev = idea_one(cfg, 0, True)
109 with torch.no_grad():
110 states=model.forward_states(d['xte'].to(dev)); psi,wp=weak_weights(states.shape[1]); psi,wp=psi.to(dev),wp.to(dev)
111 G=(states*psi[None,:,None]).sum(1); Bv=(states*wp[None,:,None]).sum(1); residual=Bv-G@A.T
112 observed=float(residual.pow(2).mean().sqrt()); predicted=float((Bv.detach().var()+1e-8).sqrt())
113 return {'model_test_mse':metric, 'weak_residual_rms_observed':observed,
114 'weak_scale_predicted_from_observed_B':predicted,
115 'relative_residual':observed/(predicted+1e-12),
116 'confirmed': bool(observed < predicted)}
117
118
119def main():
120 # Union parity: every idea learning rate is present in baseline sweep.
121 grid=[{'lr':lr,'weight_decay':wd} for lr in (0.001,0.003,0.01) for wd in (0.0,1e-4)]
122 base=sweep_baseline(lambda c: lambda s: baseline_one(c,s), grid)
123 best=base['best_cfg']
124 idea_grid=[{'lr':best['lr'],'weight_decay':best['weight_decay'],'alpha':a} for a in (0.01,0.03,0.1)]
125 idea_runs=[]
126 for c in idea_grid:
127 r=evaluate(lambda s, c=c: idea_one(c,s), SEEDS)
128 idea_runs.append((c,r))
129 idea_cfg, idea=min(idea_runs, key=lambda cr: cr[1]['mean'])
130 rep=make_report('dynamics','rnn_small',base,idea,{'weak_koopman':signature(), 'selected_cfg':idea_cfg,
131 'baseline_grid':grid, 'idea_grid':idea_grid, 'track_justification':'Actuated pendulum rollout has explicit dynamical stability/control structure.'})
132 rep['idea_sweep']=[{'cfg':c,'result':r} for c,r in idea_runs]
133 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
134 print(json.dumps(rep,indent=2))
135
136if __name__=='__main__': main()