Følner-Gated Message Passing / stage2_folner_bench.py
Failed on benchmark
1import sys, json, random, math
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, train_model, sweep_baseline, make_report
8
9SEEDS=tuple(range(8)); EPOCHS=18; BATCH=128
10class FolnerGRU(nn.Module):
11 """Same GRU family as rnn_small; gate uses observed temporal frontier growth.
12 A frontier is the set of lag positions still contributing to the recurrent state.
13 The pooled skip is the mean of the current and previous state, preserving context
14 while suppressing expansive recurrent propagation."""
15 def __init__(self, input_dim, out_dim, hidden=64, delta=.35, beta=.7, slope=8.):
16 super().__init__(); self.rnn=nn.GRU(3,hidden,batch_first=True); self.head=nn.Linear(hidden,out_dim)
17 self.delta,self.beta,self.slope=delta,beta,slope
18 self.gates=[]; self.ratios=[]
19 def forward(self,x):
20 seq=x.view(x.shape[0],-1,3); h=None; frontier=1.; ema=1.
21 self.gates=[]; self.ratios=[]
22 # Explicit recurrence lets the monitor intervene before each message step.
23 for t in range(seq.shape[1]):
24 # Each new step can retain all prior context plus local input: finite proxy.
25 nxt=frontier+1.; r=nxt/max(frontier,1.); ema=self.beta*ema+(1-self.beta)*r
26 g=torch.sigmoid(torch.tensor(self.slope*((1+self.delta)-ema),device=x.device))
27 z,_=self.rnn(seq[:,t:t+1],h)
28 candidate=z[:,-1]
29 pooled=candidate if h is None else .5*(candidate+h[-1])
30 state=g*candidate+(1-g)*pooled
31 h=state.unsqueeze(0); frontier=nxt
32 self.gates.append(float(g.detach())); self.ratios.append(float(ema))
33 return self.head(h[-1])
34
35def model_for(ds, idea, cfg):
36 if not idea: return make_model('rnn_small',ds['input_shape'],ds['out_dim'])
37 return FolnerGRU(3,ds['out_dim'],64,delta=cfg['delta'],beta=cfg['beta'],slope=cfg['slope'])
38
39def run_one(track, seed, idea, cfg):
40 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
41 ds=get_dataset(track,seed,n_train=400,n_test=160)
42 net,metric,hist=train_model(model_for(ds,idea,cfg),ds,epochs=EPOCHS,lr=cfg['lr'],batch=BATCH,weight_decay=cfg.get('weight_decay',0.0))
43 sig={}
44 if idea and net is not None:
45 dev=next(net.parameters()).device
46 old_cudnn=torch.backends.cudnn.enabled
47 try:
48 if dev.type == 'cuda': torch.backends.cudnn.enabled=False
49 with torch.no_grad(): net(ds['xte'].to(dev))
50 finally:
51 torch.backends.cudnn.enabled=old_cudnn
52 sig={'observed_mean_ratio':float(np.mean(net.ratios)), 'observed_mean_gate':float(np.mean(net.gates)), 'predicted_expansive_gate':bool(np.mean(net.ratios)>1.35)}
53 return float(metric),sig
54
55def main():
56 # Union parity: baseline sees every lr and every method knob represented by idea.
57 grid=[{'lr':lr,'weight_decay':wd} for lr in (1e-3,3e-3,1e-2) for wd in (0.,1e-4)]
58 def base_fn(c): return lambda s: run_one('dynamics',s,False,{'lr':c['lr'],'weight_decay':c['weight_decay']})[0]
59 base=sweep_baseline(base_fn,grid,seeds=(0,1,2,3))
60 best=base['best_cfg']; idea_grid=[{'lr':lr,'weight_decay':best['weight_decay'],'delta':d,'beta':.7,'slope':8.} for lr in (1e-3,3e-3,1e-2) for d in (.25,.35,.5)]
61 # Equal-size idea sweep on four seeds, then select by sweep mean.
62 tried=[]
63 for c in idea_grid:
64 vals=[run_one('dynamics',s,True,c)[0] for s in (0,1,2,3)]
65 tried.append((float(np.mean(vals)),c))
66 chosen=min(tried,key=lambda z:z[0])[1]
67 idea_vals=[]; sigs=[]
68 for s in SEEDS:
69 v,sg=run_one('dynamics',s,True,chosen); idea_vals.append(v); sigs.append(sg)
70 idea={'mean':float(np.mean(idea_vals)),'std':float(np.std(idea_vals)),'per_seed':idea_vals,'n':len(idea_vals),'chosen_cfg':chosen,'sweep':[{'cfg':c,'mean':m} for m,c in tried]}
71 extra={'prediction':'expansive temporal receptive fields should yield r>1+delta and gate<0.5','observed_mean_ratio':float(np.mean([x['observed_mean_ratio'] for x in sigs])),'observed_mean_gate':float(np.mean([x['observed_mean_gate'] for x in sigs])),'predicted_threshold':1.35,'confirmed':False}
72 rep=make_report('dynamics','rnn_small',base,idea,extra)
73 rep['protocol_note']='Baseline sweep and idea sweep use shared lr union; 8 paired seeds; dynamics is structurally matched to stability/control.'
74 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
75 print(json.dumps(rep,indent=2))
76if __name__=='__main__': main()