Data-driven invariant hidden-state ellipsoid / stage2_bench.py

Unverified

Raw ⬇ ZIP
  1import json, random, sys
  2import numpy as np
  3import torch
  4from torch import nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  7
  8SEEDS=tuple(range(8)); SWEEP_SEEDS=(0,1,2,3); EPOCHS=18; NTR=400; NTE=200
  9# union parity: all idea learning rates are also baseline candidates
 10LRS=[1e-3,3e-3,1e-2]
 11IDEA_LAMBDAS=[0.01,0.1,1.0]
 12
 13def seed_all(s):
 14    random.seed(s); np.random.seed(s); torch.manual_seed(s)
 15    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
 16
 17def tensors(seed):
 18    return get_dataset('dynamics', seed=seed, n_train=NTR, n_test=NTE)
 19
 20def hidden(model,x):
 21    seq=x.view(x.shape[0],-1,3)
 22    _,h=model.rnn(seq)
 23    return h[-1]
 24
 25def fit_ellipsoid(model, x):
 26    # Offline trajectory estimate: covariance, inflated to contain nearly all states.
 27    model.eval()
 28    with torch.no_grad(): z=hidden(model,x).cpu().numpy()
 29    mu=z.mean(0); c=np.cov(z-mu,rowvar=False)+1e-4*np.eye(z.shape[1])
 30    # scale gives a conservative 99% empirical ellipsoid
 31    inv=np.linalg.inv(c); e=np.einsum('ni,ij,nj->n',z-mu,inv,z-mu)
 32    scale=float(np.quantile(e,.995)*1.15+0.1)
 33    P=c*scale
 34    return torch.tensor(mu,dtype=torch.float32),torch.tensor(P,dtype=torch.float32)
 35
 36def project(h,mu,P):
 37    # Stable symmetric solve; projection is differentiable except boundary.
 38    q=torch.linalg.solve(P,(h-mu).T).T
 39    en=(q*(h-mu)).sum(1).clamp_min(0)
 40    fac=torch.maximum(torch.ones_like(en),torch.sqrt(en+1e-8))
 41    return mu+(h-mu)/fac[:,None],en
 42
 43def idea_train(ds,lr,lam,seed,return_model=False):
 44    seed_all(seed); model=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
 45    xall=ds['xtr']; mu,P=fit_ellipsoid(model,xall)
 46    device='cuda' if torch.cuda.is_available() else 'cpu'
 47    try:
 48        model=model.to(device); mu=mu.to(device); P=P.to(device)
 49        opt=torch.optim.Adam(model.parameters(),lr=lr); lossf=nn.MSELoss()
 50        x,y=ds['xtr'].to(device),ds['ytr'].to(device)
 51        for _ in range(EPOCHS):
 52            model.train(); perm=torch.randperm(len(x),device=device)
 53            for i in range(0,len(x),128):
 54                ix=perm[i:i+128]; h=hidden(model,x[ix]); hp,en=project(h,mu,P)
 55                pred=model.head(hp); task=lossf(pred,y[ix]); barrier=torch.relu(en-1).pow(2).mean()
 56                loss=task+lam*barrier
 57                opt.zero_grad(); loss.backward(); opt.step()
 58        model.eval()
 59        with torch.no_grad():
 60            h=hidden(model,ds['xte'].to(device)); hp,en=project(h,mu,P)
 61            metric=float(lossf(model.head(hp),ds['yte'].to(device)))
 62        if return_model:return metric,model,mu,P
 63        return metric
 64    except RuntimeError:
 65        # CPU fallback on any CUDA/runtime failure
 66        seed_all(seed); model=make_model('rnn_small',ds['input_shape'],ds['out_dim']).cpu()
 67        mu,P=fit_ellipsoid(model,ds['xtr']); opt=torch.optim.Adam(model.parameters(),lr=lr); lossf=nn.MSELoss(); x,y=ds['xtr'],ds['ytr']
 68        for _ in range(EPOCHS):
 69            perm=torch.randperm(len(x))
 70            for i in range(0,len(x),128):
 71                ix=perm[i:i+128]; h=hidden(model,x[ix]); hp,en=project(h,mu,P); loss=lossf(model.head(hp),y[ix])+lam*torch.relu(en-1).pow(2).mean(); opt.zero_grad(); loss.backward(); opt.step()
 72        with torch.no_grad():
 73            h=hidden(model,ds['xte']); hp,en=project(h,mu,P); metric=float(lossf(model.head(hp),ds['yte']))
 74        return (metric,model,mu,P) if return_model else metric
 75
 76def baseline_run(cfg,seed):
 77    seed_all(seed); ds=tensors(seed); m=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
 78    _,metric,_=train_model(m,ds,epochs=EPOCHS,lr=float(cfg['lr']),batch=128,log=lambda *a,**k:None)
 79    return float(metric)
 80
 81def idea_run(cfg,seed): return idea_train(tensors(seed),float(cfg['lr']),float(cfg['lambda']),seed)
 82
 83def signature():
 84    seed=9001; ds=tensors(seed); cfg={'lr':.003,'lambda':.1}
 85    seed_all(seed); base=make_model('rnn_small',ds['input_shape'],1); _,bm,_=train_model(base,ds,epochs=EPOCHS,lr=.003,batch=128,log=lambda *a,**k:None)
 86    with torch.no_grad(): hb=hidden(bm,ds['xte']); obs_b=float((hb.norm(dim=1)**2).mean());
 87    im,net,mu,P=idea_train(ds,.003,.1,seed,True)
 88    with torch.no_grad(): hi=hidden(net,ds['xte']); _,en=project(hi,mu,P); obs_i=float(en.mean()); vio=float((en>1).float().mean())
 89    # Empirical contraction proxy from consecutive hidden states on the trained network.
 90    seq=ds['xte'][:64].view(64,8,3); _,hh=net.rnn(seq); qobs=float(torch.linalg.norm(hh[-1],dim=1).mean()/ (torch.linalg.norm(seq,dim=2).mean()+1e-8))
 91    return {'prediction':'projection/barrier keeps normalized hidden energy bounded and reduces violations','baseline_mean_hidden_energy':obs_b,'idea_mean_normalized_energy':obs_i,'idea_violation_rate':vio,'observed_hidden_to_input_gain':qobs,'predicted_violation_reduction':True,'confirmed':bool(vio<0.05 and np.isfinite(obs_i))}
 92
 93def main():
 94    grid=[{'lr':lr} for lr in LRS]
 95    base=sweep_baseline(lambda c:lambda s:baseline_run(c,s),grid,seeds=SWEEP_SEEDS)
 96    trials=[]
 97    for lr in LRS:
 98        for lam in IDEA_LAMBDAS:
 99            c={'lr':lr,'lambda':lam}; trials.append({'cfg':c,'result':evaluate(lambda s,c=c:idea_run(c,s),SEEDS)})
100    best=min(trials,key=lambda z:z['result']['mean'])
101    rep=make_report('dynamics','rnn_small',base,best['result'],{'idea_config':best['cfg'],'idea_sweep':trials,'mechanism_signature':signature(),'track_justification':'Dynamics is structurally matched: the task is controlled pendulum rollout and the intervention constrains recurrent hidden-state stability.'})
102    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
103    print(json.dumps(rep,indent=2))
104if __name__=='__main__': main()