Følner-Gated Message Passing / stage2_folner_bench.py

Failed on benchmark

Raw ⬇ ZIP
 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()