Coordinate-Free BT Monitor for Neural ODEs / stage2_bench.py
Failed on benchmark
1import sys, json, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7from bench import get_dataset, make_model, sweep_baseline, evaluate, make_report
8
9SEEDS = tuple(range(8))
10# Equal search-space union: every idea setting is also evaluated for baseline.
11GRID = [
12 {'lr': 0.0015, 'weight_decay': 0.0},
13 {'lr': 0.0030, 'weight_decay': 0.0},
14 {'lr': 0.0060, 'weight_decay': 0.0},
15]
16EPOCHS = 15
17BATCH = 128
18
19def seed_all(seed):
20 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
21 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
22
23def device():
24 return 'cuda' if torch.cuda.is_available() else 'cpu'
25
26def jacobian2(net, h):
27 """Jacobian of the first two recurrent coordinates, with other h fixed."""
28 outs = []
29 for j in range(2):
30 g = torch.autograd.grad(net.step_hidden(h)[0, j], h,
31 create_graph=True, retain_graph=True)[0]
32 outs.append(g[0, :2])
33 return torch.stack(outs)
34
35def bt_values(net, h0=None, fd=0.05):
36 """Centered finite-difference BT monitor on the trained recurrent map."""
37 if h0 is None:
38 h0 = torch.zeros(1, net.hidden, device=next(net.parameters()).device)
39 h0 = h0.detach().requires_grad_(True)
40 J = jacobian2(net, h0)
41 # SVD gives the coordinate-free smallest right-singular direction.
42 _, _, vh = torch.linalg.svd(J.detach())
43 q = vh[-1]
44 q = q / (torch.linalg.vector_norm(q) + 1e-12)
45 hp = (h0.detach() + fd * torch.cat((q, torch.zeros(net.hidden-2, device=h0.device))).view(1,-1)).requires_grad_(True)
46 hm = (h0.detach() - fd * torch.cat((q, torch.zeros(net.hidden-2, device=h0.device))).view(1,-1)).requires_grad_(True)
47 # For reporting, calculate Jacobians with graph disabled through explicit helper.
48 def plain(x, differentiable=False):
49 x = x.detach().requires_grad_(True)
50 rows=[]
51 for j in range(2):
52 g=torch.autograd.grad(net.step_hidden(x)[0,j],x,retain_graph=True,create_graph=differentiable)[0]
53 rows.append(g[0,:2])
54 return torch.stack(rows).detach()
55 jp, jm = plain(hp, differentiable=h0.requires_grad), plain(hm, differentiable=h0.requires_grad)
56 a = -0.5 * (torch.det(jp)-torch.det(jm))/(2*fd)
57 b = (torch.trace(jp)-torch.trace(jm))/(2*fd)
58 return a, b, J.detach(), q.detach()
59
60class BTGRU(nn.Module):
61 """Same rnn_small architecture, exposing its recurrent map for regularization."""
62 def __init__(self, out_dim=1, hidden=64):
63 super().__init__(); self.hidden=hidden
64 self.rnn=nn.GRU(3,hidden,batch_first=True); self.head=nn.Linear(hidden,out_dim)
65 self._no_cudnn=False
66 def step_hidden(self,h):
67 # GRU accepts a length-one zero-input sequence and explicit hidden state.
68 x=torch.zeros(1,1,3,device=h.device,dtype=h.dtype)
69 try: _, z=self.rnn(x,h.unsqueeze(0))
70 except RuntimeError:
71 old=torch.backends.cudnn.enabled; torch.backends.cudnn.enabled=False
72 try: _,z=self.rnn(x,h.unsqueeze(0))
73 finally: torch.backends.cudnn.enabled=old
74 return z[-1]
75 def forward(self,x):
76 seq=x.view(x.shape[0],-1,3)
77 try: _,h=self.rnn(seq)
78 except RuntimeError:
79 old=torch.backends.cudnn.enabled; torch.backends.cudnn.enabled=False
80 try: _,h=self.rnn(seq)
81 finally: torch.backends.cudnn.enabled=old
82 return self.head(h[-1])
83
84def train_one(seed, cfg, idea):
85 seed_all(seed); ds=get_dataset('dynamics',seed,n_train=400,n_test=200)
86 net=BTGRU(out_dim=1,hidden=64).to(device())
87 opt=torch.optim.Adam(net.parameters(),lr=cfg['lr'],weight_decay=cfg['weight_decay'])
88 xtr,ytr=ds['xtr'].to(device()),ds['ytr'].to(device())
89 for ep in range(EPOCHS):
90 net.train(); perm=torch.randperm(len(xtr),device=xtr.device)
91 for i in range(0,len(xtr),BATCH):
92 idx=perm[i:i+BATCH]; pred=net(xtr[idx]); loss=((pred-ytr[idx])**2).mean()
93 if idea:
94 a,b,_,_=bt_values(net)
95 bt=0.01*(torch.relu(torch.tensor(0.02,device=xtr.device)-torch.abs(a))**2 + torch.relu(torch.tensor(0.02,device=xtr.device)-torch.abs(b))**2)
96 loss=loss+bt
97 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(),5.0); opt.step()
98 net.eval()
99 with torch.no_grad(): metric=float(((net(ds['xte'].to(device()))-ds['yte'].to(device()))**2).mean())
100 with torch.no_grad(): pass
101 a,b,J,q=bt_values(net)
102 return metric, {'a':float(a),'b':float(b),'ab':float(abs(a*b)),'rank_smin':float(torch.linalg.svdvals(J)[-1])}
103
104def train_metric(seed,cfg,idea): return train_one(seed,cfg,idea)[0]
105def run():
106 # Baseline selection uses the same three configs, then full paired evaluation.
107 base=sweep_baseline(lambda cfg: lambda s: train_metric(s,cfg,False), GRID)
108 best=base['best_cfg']
109 idea_grid=[best, {'lr':0.0015,'weight_decay':0.0}, {'lr':0.006,'weight_decay':0.0}]
110 # Report idea at the best of the same-size idea sweep; baseline has all union settings.
111 candidates=[]
112 for cfg in idea_grid:
113 r=evaluate(lambda s,cfg=cfg: train_metric(s,cfg,True), SEEDS)
114 candidates.append((r,cfg))
115 idea_res,best_idea=min(candidates,key=lambda z:z[0]['mean'])
116 sig=[]
117 for s in SEEDS:
118 _,m0=train_one(s,best,False); _,m1=train_one(s,best_idea,True)
119 sig.append({'seed':s,'baseline':m0,'idea':m1})
120 # Quantitative prediction tested on trained models: regularization should reduce near-degenerate |ab|.
121 t=0.02**2
122 bfrac=float(np.mean([x['baseline']['ab']<t for x in sig])); ifrac=float(np.mean([x['idea']['ab']<t for x in sig]))
123 mechanism={'quantity':'trained recurrent-map BT coefficients a,b from centered FD along SVD kernel direction',
124 'predicted':'BT penalty reduces fraction of checkpoints/models with |a*b| below threshold',
125 'threshold':t,'baseline_near_degenerate_fraction':bfrac,'idea_near_degenerate_fraction':ifrac,
126 'per_seed':sig,'confirmed':bool(ifrac < bfrac)}
127 rep=make_report('dynamics','rnn_small',base,idea_res,mechanism)
128 rep['idea_sweep']=[{'cfg':c,'mean':r['mean'],'per_seed':r['per_seed']} for r,c in candidates]
129 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
130 print(json.dumps(rep,indent=2))
131if __name__=='__main__': run()