Characteristic-Invariant BT Monitor / bt_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, 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 evaluate
  8
  9# Characteristic-invariant BT monitor on a measured 2D local recurrent chart.
 10# The architecture remains exactly bench rnn_small; h[0:2] are perturbed and
 11# the other recurrent coordinates are held at zero for a cheap local monitor.
 12def monitor(model):
 13    rnn = model.rnn
 14    W = rnn.weight_hh_l0
 15    b = rnn.bias_ih_l0 + rnn.bias_hh_l0
 16    z = torch.zeros(2, device=W.device, dtype=W.dtype, requires_grad=True)
 17    h = torch.cat([z, torch.zeros(W.shape[1]-2, device=W.device, dtype=W.dtype)])
 18    # PyTorch GRU gate order is reset, update, new.
 19    gates = torch.mv(W, h) + b
 20    r, u, npre = gates.chunk(3, 0)
 21    rr, uu = torch.sigmoid(r), torch.sigmoid(u)
 22    nnv = torch.tanh(npre + rr * 0)  # zero external input; recurrent reset is in Wn below
 23    # Recompute candidate with reset applied to recurrent hidden contribution.
 24    whr, whu, whn = W.chunk(3, 0)
 25    bir, biu, bin_ = b.chunk(3, 0)
 26    rr = torch.sigmoid(torch.mv(whr, h) + bir)
 27    uu = torch.sigmoid(torch.mv(whu, h) + biu)
 28    nnv = torch.tanh(torch.mv(whn, rr*h) + bin_)
 29    hp = (1-uu)*nnv + uu*h
 30    out = hp[:2]
 31    rows=[]
 32    for i in range(2):
 33        rows.append(torch.autograd.grad(out[i], z, create_graph=True, retain_graph=True)[0])
 34    A = torch.stack(rows)
 35    # Invariants of the continuous vector field f=h'-h: J=A-I.
 36    J = A - torch.eye(2, device=A.device, dtype=A.dtype)
 37    e1 = torch.trace(J); e2 = torch.linalg.det(J)
 38    delta, tau = e2, e1
 39    q = torch.linalg.svd(J.detach()).Vh[-1]
 40    gd = torch.autograd.grad(delta, z, create_graph=True, retain_graph=True)[0]
 41    gt = torch.autograd.grad(tau, z, create_graph=True, retain_graph=True)[0]
 42    a = -0.5 * torch.dot(gd, q)
 43    bb = torch.dot(gt, q)
 44    return delta, tau, a, bb, J.detach()
 45
 46def seed_all(s):
 47    random.seed(s); np.random.seed(s); torch.manual_seed(s)
 48
 49def train_idea(seed, cfg, return_model=False):
 50    seed_all(seed)
 51    ds=get_dataset('dynamics', seed, n_train=400, n_test=400)
 52    model=make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 53    dev=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 54    try:
 55        model.to(dev); x,y=ds['xtr'].to(dev),ds['ytr'].to(dev)
 56        opt=torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
 57        lossf=nn.MSELoss()
 58        for ep in range(cfg['epochs']):
 59            model.train(); perm=torch.randperm(len(x),device=dev)
 60            for ix in perm.split(128):
 61                pred=model(x[ix]); loss=lossf(pred,y[ix])
 62                d,t,a,b,_=monitor(model)
 63                loss=loss + cfg['rho']*(d*d+t*t)
 64                opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(),5.0); opt.step()
 65        model.eval()
 66        with torch.no_grad(): metric=float(lossf(model(ds['xte'].to(dev)),ds['yte'].to(dev)).cpu())
 67    except Exception:
 68        # Required robust fallback: restart fully on CPU after any CUDA failure.
 69        seed_all(seed); dev=torch.device('cpu'); model=make_model('rnn_small',ds['input_shape'],ds['out_dim']).to(dev)
 70        x,y=ds['xtr'],ds['ytr']; opt=torch.optim.Adam(model.parameters(),lr=cfg['lr'],weight_decay=cfg['weight_decay'])
 71        for ep in range(cfg['epochs']):
 72            for ix in torch.randperm(len(x)).split(128):
 73                loss=lossf(model(x[ix]),y[ix]); d,t,a,b,_=monitor(model); loss=loss+cfg['rho']*(d*d+t*t)
 74                opt.zero_grad(); loss.backward(); opt.step()
 75        with torch.no_grad(): metric=float(lossf(model(ds['xte']),ds['yte']))
 76    if return_model: return metric, model, ds
 77    return metric
 78
 79def train_base(seed,cfg, return_model=False):
 80    seed_all(seed); ds=get_dataset('dynamics',seed,n_train=400,n_test=400)
 81    model=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
 82    raw=train_model(model,ds,epochs=cfg['epochs'],lr=cfg['lr'],batch=128,weight_decay=cfg['weight_decay'],log=lambda *_:None)
 83    metric = float(raw[1] if isinstance(raw,(tuple,list)) else raw.get('test', raw.get('metric')) if isinstance(raw,dict) else raw)
 84    return (metric,model,ds) if return_model else metric
 85
 86def signature(cfg):
 87    vals=[]
 88    for s in range(8):
 89        m,model,ds=train_idea(s,cfg,True)
 90        d,t,a,b,J=monitor(model)
 91        eig=torch.linalg.eigvals(J).cpu().numpy()
 92        # observed recurrent persistence is spectral radius of the trained local map.
 93        vals.append((float(abs(d)),float(abs(t)),float(np.max(np.abs(eig+1))),float(np.max(eig.real))))
 94    v=np.asarray(vals)
 95    return {'prediction':'BT monitor drives delta,tau toward zero and the trained local dynamics toward a near-zero continuous eigenvalue','trained_mean_abs_delta':float(v[:,0].mean()),'trained_mean_abs_tau':float(v[:,1].mean()),'observed_mean_discrete_radius':float(v[:,2].mean()),'observed_mean_continuous_max_real':float(v[:,3].mean()),'confirmed':bool(v[:,0].mean()<0.08 and v[:,1].mean()<0.08 and v[:,2].mean()>0.85)}
 96
 97def main():
 98    base_grid=[{'lr':x,'weight_decay':w,'epochs':8} for x in (0.001,0.003,0.01) for w in (0.0,1e-4)]
 99    # Same union of decisive Adam knobs is used for baseline and idea.
100    base=sweep_baseline(lambda c: (lambda s: train_base(s,c)),base_grid)
101    idea_grid=[dict(base['best_cfg'],rho=r) for r in (0.001,0.01,0.05)]
102    # idea uses best baseline lr/wd and two a-priori regularizer strengths
103    idea_runs=[(c,evaluate(lambda s,c=c: train_idea(s,c))) for c in idea_grid]
104    best_i,best_r=min(idea_runs,key=lambda z:z[1]['mean'])
105    rep=make_report('dynamics','rnn_small',base,best_r,{'idea_config':best_i,'idea_sweep':[{'cfg':c,'mean':r['mean']} for c,r in idea_runs],**signature(best_i)})
106    rep['custom_track']=None
107    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
108    print(json.dumps(rep,indent=2))
109if __name__=='__main__': main()