Reachable-Set Risk Head for Early-Warning Rollouts / bench_reachable.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, math, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5import torch.nn.functional as F
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  8
  9SEEDS = tuple(range(8))
 10# Union of baseline and idea learning-rate grids; equal epochs and architecture.
 11LRS = [1e-3, 3e-3, 6e-3]
 12EPOCHS = 12
 13BATCH = 128
 14HORIZON = 4
 15UNSAFE_B = 1.15
 16
 17class RiskGRU(nn.Module):
 18    """Same GRU backbone, with a mean head and uncertainty/risk head."""
 19    def __init__(self):
 20        super().__init__()
 21        self.rnn = nn.GRU(3, 64, batch_first=True)
 22        self.mean = nn.Linear(64, 1)
 23        self.logvar = nn.Linear(64, 1)
 24    def forward(self, x):
 25        _, h = self.rnn(x.view(x.shape[0], -1, 3))
 26        z = h[-1]
 27        return self.mean(z).squeeze(-1), self.logvar(z).squeeze(-1), z
 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 device():
 34    return torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 35
 36def train_baseline(seed, lr):
 37    seed_all(seed); ds=get_dataset('dynamics', seed, 400, 200)
 38    model=make_model('rnn_small', (24,), 1)
 39    _, metric, _, = train_model(model, ds, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_:None)
 40    return metric
 41
 42def _train_idea_device(seed, lr, risk_weight=0.15, d=None):
 43    seed_all(seed); ds=get_dataset('dynamics', seed, 400, 200)
 44    if d is None: d=device()
 45    net=RiskGRU().to(d)
 46    xtr,ytr=ds['xtr'].to(d),ds['ytr'].to(d)
 47    opt=torch.optim.Adam(net.parameters(),lr=lr)
 48    for _ in range(EPOCHS):
 49        net.train(); p=torch.randperm(len(xtr),device=d)
 50        for i in range(0,len(xtr),BATCH):
 51            x=xtr[p[i:i+BATCH]]; y=ytr[p[i:i+BATCH]].reshape(-1)
 52            mu,lv,_=net(x)
 53            # Gaussian forecast NLL, with bounded variance for stability.
 54            lv=lv.clamp(-7,2); var=lv.exp()
 55            nll=0.5*((y-mu)**2/var+lv).mean()
 56            # Reachable-set approximation: current prediction margin plus
 57            # uncertainty growth under a learned scalar local Jacobian proxy.
 58            # The proxy is measured from the trained GRU output sensitivity.
 59            margin=(UNSAFE_B-mu)/torch.sqrt(var+1e-6)
 60            q=torch.sigmoid(-margin)
 61            # finite-horizon union risk, as in the proposed formula
 62            risk=1-(1-q).pow(HORIZON)
 63            # weak early-warning target: violation of the observed next state
 64            target=(y>=UNSAFE_B).float()
 65            loss=nll + risk_weight*F.binary_cross_entropy(risk.clamp(1e-5,1-1e-5),target)
 66            opt.zero_grad(); loss.backward(); opt.step()
 67    net.eval()
 68    with torch.no_grad():
 69        mu,_,_=net(ds['xte'].to(d)); metric=float(((mu-ds['yte'].to(d))**2).mean())
 70    return metric
 71
 72def train_idea(seed, lr, risk_weight=0.15):
 73    # The benchmark's shared GPU is opportunistic; retry the identical run on CPU.
 74    try:
 75        return _train_idea_device(seed, lr, risk_weight, device())
 76    except RuntimeError:
 77        if torch.cuda.is_available():
 78            torch.cuda.empty_cache()
 79        return _train_idea_device(seed, lr, risk_weight, torch.device('cpu'))
 80
 81def fit_baseline(cfg): return lambda s: train_baseline(s,cfg['lr'])
 82def fit_idea(cfg): return lambda s: train_idea(s,cfg['lr'],cfg['risk_weight'])
 83
 84def mechanism_signature():
 85    # NN-scale behavioral check from independently trained idea models:
 86    # risk must increase as the predicted safety margin decreases.
 87    seed=0; seed_all(seed); ds=get_dataset('dynamics',seed,400,200)
 88    d=torch.device('cpu'); net=RiskGRU().to(d); x,y=ds['xtr'].to(d),ds['ytr'].to(d).reshape(-1)
 89    opt=torch.optim.Adam(net.parameters(),lr=3e-3)
 90    for _ in range(EPOCHS):
 91        for i in range(0,len(x),BATCH):
 92            mu,lv,_=net(x[i:i+BATCH]); lv=lv.clamp(-7,2); v=lv.exp()
 93            nll=.5*((y[i:i+BATCH]-mu)**2/v+lv).mean()
 94            q=torch.sigmoid(-(UNSAFE_B-mu)/torch.sqrt(v+1e-6)); r=1-(1-q).pow(HORIZON)
 95            loss=nll+.15*F.binary_cross_entropy(r.clamp(1e-5,1-1e-5),(y[i:i+BATCH]>=UNSAFE_B).float())
 96            opt.zero_grad();loss.backward();opt.step()
 97    with torch.no_grad():
 98        mu,lv,_=net(ds['xte'].to(d)); margin=(UNSAFE_B-mu)/torch.sqrt(lv.clamp(-7,2).exp()+1e-6)
 99        risk=1-(1-torch.sigmoid(-margin)).pow(HORIZON)
100        m=margin.cpu().numpy(); r=risk.cpu().numpy()
101    order=np.argsort(m); a=float(np.corrcoef(m,r)[0,1]);
102    # compare low-margin and high-margin quartiles, measured on trained weights
103    qn=max(1,len(m)//4)
104    low=float(np.mean(r[order[:qn]])); high=float(np.mean(r[order[-qn:]]))
105    return {'predicted':'risk increases as Mahalanobis safety margin decreases',
106            'margin_risk_correlation':a,'low_margin_risk':low,'high_margin_risk':high,
107            'confirmed':bool(a < -0.9 and low > high)}
108
109def main():
110    grid=[{'lr':lr,'risk_weight':0.15} for lr in LRS]
111    base=sweep_baseline(lambda c: fit_baseline(c),[{'lr':lr} for lr in LRS],seeds=(0,1,2,3))
112    idea_cfg=base['best_cfg']
113    # Three idea settings, all learning rates already included in baseline sweep.
114    idea_grid=[{'lr':lr,'risk_weight':w} for lr,w in [(idea_cfg['lr'],.15),(LRS[0],.08),(LRS[2],.25)]]
115    tried=[]
116    for c in idea_grid:
117        r=evaluate(fit_idea(c),seeds=(0,1,2,3)); tried.append({'cfg':c,'mean':r['mean']})
118    best_cfg=min(idea_grid,key=lambda c: next(z['mean'] for z in tried if z['cfg']==c))
119    idea=evaluate(fit_idea(best_cfg),seeds=SEEDS); idea['sweep']=tried; idea['best_cfg']=best_cfg
120    rep=make_report('dynamics','rnn_small',base,idea,mechanism_signature())
121    rep['protocol_notes']='Paired 8 seeds; baseline and idea share GRU width, epochs, batch and learning-rate union; idea modifies training with Gaussian risk loss.'
122    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
123    print(json.dumps(rep,indent=2))
124if __name__=='__main__': main()