Lyapunov-sign-preserving neural time stepping / bench_stage2.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, math, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset, make_model, train_model, sweep_baseline, make_report
  7from bench.protocol import DEFAULT_SEEDS
  8
  9EPOCHS, BATCH = 12, 128
 10GRID = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 6e-3}]
 11
 12
 13def seed_all(seed):
 14    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 15    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 16
 17
 18def controlled_forward(net, x, stabilized=False):
 19    seq = x.reshape(x.shape[0], 8, 3)
 20    gru, head = net.rnn, net.head
 21    h = torch.zeros(1, x.shape[0], gru.hidden_size, device=x.device, dtype=x.dtype)
 22    for k in range(8):
 23        raw, _ = gru(seq[:, k:k+1, :], h)
 24        proposed = raw[:, 0, :]
 25        h = (h[:, 0, :] + (0.5 if stabilized else 1.0) *
 26             (proposed - h[:, 0, :])).unsqueeze(0)
 27    return head(h[0])
 28
 29
 30def train_idea(ds, lr, seed):
 31    seed_all(seed)
 32    net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 33    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 34    try:
 35        net.to(device)
 36        opt = torch.optim.Adam(net.parameters(), lr=lr)
 37        x, y = ds['xtr'].to(device), ds['ytr'].to(device)
 38        for _ in range(EPOCHS):
 39            net.train(); perm = torch.randperm(len(x), device=device)
 40            for i in range(0, len(x), BATCH):
 41                ix = perm[i:i+BATCH]
 42                loss = nn.functional.mse_loss(controlled_forward(net, x[ix], True), y[ix])
 43                opt.zero_grad(); loss.backward(); opt.step()
 44        net.eval()
 45        with torch.no_grad():
 46            m = float(((controlled_forward(net, ds['xte'].to(device), True) - ds['yte'].to(device)) ** 2).mean())
 47        return net, m
 48    except RuntimeError:
 49        if device.type != 'cuda': raise
 50        torch.cuda.empty_cache()
 51        net = make_model('rnn_small', ds['input_shape'], ds['out_dim']).cpu()
 52        opt = torch.optim.Adam(net.parameters(), lr=lr)
 53        x, y = ds['xtr'], ds['ytr']
 54        for _ in range(EPOCHS):
 55            perm = torch.randperm(len(x))
 56            for i in range(0, len(x), BATCH):
 57                ix = perm[i:i+BATCH]
 58                loss = nn.functional.mse_loss(controlled_forward(net, x[ix], True), y[ix])
 59                opt.zero_grad(); loss.backward(); opt.step()
 60        with torch.no_grad():
 61            m = float(((controlled_forward(net, ds['xte'], True)-ds['yte'])**2).mean())
 62        return net, m
 63
 64
 65def baseline_factory(cfg):
 66    def run(seed):
 67        ds = get_dataset('dynamics', int(seed), 400, 200)
 68        seed_all(int(seed)); net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 69        _, metric, _ = train_model(net, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH, log=lambda *_: None)
 70        return metric
 71    return run
 72
 73
 74def idea_factory(cfg):
 75    return lambda seed: train_idea(get_dataset('dynamics', int(seed), 400, 200), cfg['lr'], int(seed))[1]
 76
 77
 78def train_one_baseline(ds, lr, seed):
 79    seed_all(seed); net=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
 80    net,m,_=train_model(net,ds,epochs=EPOCHS,lr=lr,batch=BATCH,log=lambda *_:None)
 81    return net,m
 82
 83
 84def trained_exponent(net, x, stabilized):
 85    """Exact JVP estimate on a trained GRU; called on CPU to avoid cuDNN JVP limits."""
 86    net = net.cpu().eval(); x = x.cpu(); gru = net.rnn
 87    seq = x[:1].reshape(1, 8, 3)
 88    q = torch.randn(gru.hidden_size); q /= q.norm(); total = 0.0
 89    h = torch.zeros(gru.hidden_size)
 90    for k in range(8):
 91        u = seq[:, k:k+1, :]
 92        def fmap(v):
 93            raw, _ = gru(u, v.reshape(1,1,-1)); r = raw[0,0]
 94            return v + 0.5*(r-v) if stabilized else r
 95        _, v = torch.autograd.functional.jvp(fmap, h, q)
 96        n = float(v.norm().detach()); total += math.log(max(n,1e-12)); q=v.detach()/max(n,1e-12)
 97        with torch.no_grad():
 98            raw,_=gru(u,h.reshape(1,1,-1)); r=raw[0,0]
 99            h=(h+0.5*(r-h) if stabilized else r).detach()
100    return total/8.0
101
102
103def signature(seed=0):
104    ds=get_dataset('dynamics',seed,400,200)
105    bnet,bm=train_one_baseline(ds,3e-3,seed+1000)
106    inet,im=train_idea(ds,3e-3,seed+1000)
107    eb=trained_exponent(bnet,ds['xte'],False); ei=trained_exponent(inet,ds['xte'],True)
108    return {'trained_baseline_mse':bm,'trained_idea_mse':im,
109            'baseline_discrete_exponent':eb,'controlled_discrete_exponent':ei,
110            'predicted_sign_preserved':bool(eb<0),'observed_sign_preserved':bool(ei<0),
111            'confirmed':bool((eb<0)==(ei<0))}
112
113
114def main():
115    base=sweep_baseline(baseline_factory, GRID, seeds=(0,1,2,3))
116    vals=[idea_factory(base['best_cfg'])(s) for s in DEFAULT_SEEDS]
117    idea={'mean':float(np.mean(vals)),'std':float(np.std(vals)),'per_seed':vals,'n':len(vals)}
118    sig=signature(0)
119    idea_sweep=[]
120    for cfg in GRID:
121        vals4=[idea_factory(cfg)(s) for s in (0,1,2,3)]
122        idea_sweep.append({'cfg':cfg,'mean':float(np.mean(vals4))})
123    rep=make_report('dynamics','rnn_small',base,idea,extra={'mechanism_signature':sig,'idea_sweep':idea_sweep,'track_justification':'Controlled pendulum rollout is the built-in stability/control/Lyapunov match.','protocol':'8 paired seeds; baseline and idea share lr union; standard test MSE.'})
124    out={'bench_report':rep}; print(json.dumps(out,indent=2)); open('bench_report.json','w').write(json.dumps(out,indent=2))
125
126if __name__=='__main__': main()