Reachable-Set Risk Head for Early-Warning Rollouts / bench_reachable.py
Failed on benchmark
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()