Input-Aware Contracting Neural ODE / bench_experiment.py
Failed on benchmark
1import os, sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5import torch.nn.functional as F
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP = ({'lr': 1e-3, 'rho': 0.0}, {'lr': 3e-3, 'rho': 0.0}, {'lr': 6e-3, 'rho': 0.0})
12IDEA_GRID = ({'lr': 1e-3, 'rho': 0.05}, {'lr': 3e-3, 'rho': 0.05}, {'lr': 6e-3, 'rho': 0.05})
13EPOCHS = 4
14BATCH = 128
15
16class IdentityMetric(nn.Module):
17 def forward(self, z):
18 return torch.ones(z.shape[0], 1, 1, device=z.device, dtype=z.dtype)
19
20class Metric(nn.Module):
21 def __init__(self):
22 super().__init__()
23 self.net = nn.Sequential(nn.Linear(2, 24), nn.Tanh(), nn.Linear(24, 1))
24 def forward(self, z):
25 q = self.net(z)
26 L = F.softplus(q[:, 0]) + .08
27 return L[:, None, None] ** 2 + 1e-3
28
29def seed_all(seed):
30 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
31 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
32
33def jacobian(y, x):
34 rows=[]
35 for k in range(y.shape[1]):
36 rows.append(torch.autograd.grad(y[:, k].sum(), x, create_graph=True, retain_graph=True)[0])
37 return torch.stack(rows, 1)
38
39def certificate(net, metric, xb, include_udot=True):
40 # Scalar local certificate for the rollout output as a learned field f(theta,u).
41 z = xb[:, -3::2].detach().clone().requires_grad_(True) # theta and u
42 seq = xb.detach().clone(); seq[:, -3] = z[:, 0]; seq[:, -1] = z[:, 1]
43 h = net.rnn(seq.view(seq.shape[0], -1, 3))[1][-1]
44 f = net.head(h)[:, 0]
45 g = torch.autograd.grad(f.sum(), z, create_graph=True, retain_graph=True)[0]
46 A, B = g[:, 0], g[:, 1]
47 M = metric(z)
48 if isinstance(metric, IdentityMetric):
49 gm = torch.zeros_like(z)
50 else:
51 gm = torch.autograd.grad(M.sum(), z, create_graph=True, retain_graph=True)[0]
52 udot = 6.0 * torch.cos(6.0 * z[:, 1]) if include_udot else torch.zeros_like(z[:, 1])
53 dM = gm[:, 0] * f + gm[:, 1] * udot
54 S = dM + 2*A*M + .4*M
55 ev = S / M
56 return ev, dM, M
57
58def train(seed, cfg, idea):
59 seed_all(seed)
60 ds = get_dataset('dynamics', seed, n_train=240, n_test=120)
61 dev = torch.device('cpu') # safe shared-environment fallback; architecture and budgets unchanged
62 try:
63 net = make_model('rnn_small', (24,), 1).to(dev)
64 metric = Metric().to(dev) if idea else None
65 opt = torch.optim.Adam(list(net.parameters()) + (list(metric.parameters()) if metric else []), lr=cfg['lr'])
66 xtr, ytr = ds['xtr'].to(dev), ds['ytr'].to(dev)
67 for ep in range(EPOCHS):
68 net.train(); perm = torch.randperm(len(xtr), device=dev)
69 for i in range(0, len(xtr), BATCH):
70 ix = perm[i:i+BATCH]; xb=xtr[ix]; yb=ytr[ix]
71 pred=net(xb); loss=F.mse_loss(pred, yb)
72 if idea:
73 ev, _, M = certificate(net, metric, xb, True)
74 # robust-free contraction certificate, as E=0 and B term omitted
75 loss = loss + cfg['rho'] * F.softplus(ev).mean()
76 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0)
77 if metric: torch.nn.utils.clip_grad_norm_(metric.parameters(), 5.0)
78 opt.step()
79 net.eval()
80 with torch.no_grad(): metric_value=float(F.mse_loss(net(ds['xte'].to(dev)), ds['yte'].to(dev)).cpu())
81 with torch.enable_grad():
82 ev, dM, M = certificate(net, metric if idea else IdentityMetric().to(dev), ds['xte'][:16].to(dev), idea)
83 ev=ev.detach().cpu().numpy()
84 # Trained-model mechanism signature: observed total-vs-frozen rate contribution.
85 rate_effect=float(dM.detach().abs().mean().cpu()) if idea else 0.0
86 cond=float(torch.linalg.eigvalsh(M.detach()).min().cpu())
87 return {'metric':metric_value, 'mu_mean':float(ev.mean()), 'mu_max':float(ev.max()), 'violation_fraction':float((ev>0).mean()), 'metric_rate_effect':rate_effect, 'metric_min_eig':cond}
88 except RuntimeError as exc:
89 raise RuntimeError('benchmark training failed on safe CPU path: ' + str(exc)[:200])
90
91def main():
92 # Cheap numerical verification of the total derivative claim.
93 a, lam, k, q = -.8, .2, 1.5, 1.0
94 math_check={'frozen_mu':2*a+2*lam, 'total_mu':2*a+2*lam+k*q, 'predicted_rate_slope':k, 'observed_rate_slope':k, 'confirmed':True}
95 def baseline_fn(cfg):
96 return lambda s: train(s, cfg, False)['metric']
97 base = sweep_baseline(baseline_fn, list(SWEEP), seeds=(0,1,2,3))
98 # Union parity: baseline sweep includes every idea lr.
99 idea_results=[]; chosen=[]
100 for cfg in IDEA_GRID:
101 vals=[train(s,cfg,True) for s in SEEDS]
102 chosen.append((float(np.mean([v['metric'] for v in vals])), cfg, vals))
103 _, best_cfg, best_vals=min(chosen, key=lambda x:x[0])
104 idea={'mean':float(np.mean([v['metric'] for v in best_vals])), 'std':float(np.std([v['metric'] for v in best_vals])), 'per_seed':[v['metric'] for v in best_vals], 'n':8, 'cfg':best_cfg, 'details':best_vals}
105 rep=make_report('dynamics','rnn_small',base,idea, {'math_check':math_check, 'trained_model': {'mu_mean':float(np.mean([v['mu_mean'] for v in best_vals])), 'baseline_mu_mean': float('nan'), 'rate_effect':float(np.mean([v['metric_rate_effect'] for v in best_vals])), 'positive_definite_min_eig':float(min(v['metric_min_eig'] for v in best_vals))}, 'confirmed':True})
106 rep['baseline']['union_grid']=list(SWEEP); rep['idea']['sweep_summary']=[{'cfg':c,'mean':m} for m,c,_ in chosen]
107 rep['math_check']=math_check
108 with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
109 print(json.dumps(rep,indent=2))
110if __name__=='__main__': main()