Input-Aware Contracting Neural ODE / bench_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  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()