Spectral Lookahead Gate / bench_spectral_gate.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
 1import sys, json, random
 2import numpy as np
 3import torch
 4import torch.nn as nn
 5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
 6from bench import get_dataset, train_model, sweep_baseline, make_report
 7from bench.protocol import evaluate
 8
 9H=4
10
11class GateGRU(nn.Module):
12    def __init__(self, idea=False, always=False, tau=0.08, horizon=4):
13        super().__init__(); self.idea=idea; self.always=always; self.tau=tau; self.horizon=horizon
14        self.cell=nn.GRUCell(3,64); self.head=nn.Linear(64,1)
15        self.last_signature={}
16    def _hidden(self,x):
17        z=x.view(x.shape[0],-1,3); h=torch.zeros(x.shape[0],64,device=x.device)
18        for k in range(z.shape[1]): h=self.cell(z[:,k],h)
19        return h,z
20    def forward(self,x):
21        h,z=self._hidden(x)
22        current=self.head(h)
23        if self.training or (not self.idea and not self.always):
24            return current
25        # Approximate local transition A by the recurrent Jacobian at the batch mean.
26        with torch.enable_grad():
27            u=z[:,-1].mean(0,keepdim=True).detach()
28            q=h.mean(0,keepdim=True).detach().requires_grad_(True)
29            hm=self.cell(u,q)
30            A=[]
31            for i in range(64):
32                A.append(torch.autograd.grad(hm[0,i],q,retain_graph=True)[0][0])
33            A=torch.stack(A).detach()
34        c=self.head.weight.detach()[0]
35        lam=float((c@A@c)/(c@c+1e-12)); r=float(torch.linalg.vector_norm(c@A-lam*c)/(torch.linalg.vector_norm(c)+1e-12))
36        active=(r>self.tau or lam<0)
37        self.last_signature={'residual':r,'lambda_hat':lam,'active':bool(active)}
38        if not self.idea: active=True
39        if not active: return current
40        # predictive heads are the shared readout applied to a cheap local rollout
41        outs=[current]; hh=h
42        u=z[:,-1]
43        for _ in range(self.horizon):
44            hh=self.cell(u,hh); outs.append(self.head(hh))
45        # alarm-oriented lookahead: max predicted scalar, while regression metric remains MSE
46        return torch.stack(outs,dim=0).amax(0)
47
48def seed_all(s):
49    random.seed(s); np.random.seed(s); torch.manual_seed(s)
50    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
51
52def run(seed,cfg,idea,always=False):
53    seed_all(seed); ds=get_dataset('dynamics',seed,n_train=800,n_test=300)
54    net=GateGRU(idea=idea,always=always,tau=cfg['tau'],horizon=cfg['horizon'])
55    net,metric,_=train_model(net,ds,epochs=cfg['epochs'],lr=cfg['lr'],batch=128,log=lambda *_: None)
56    if net is None: return float('nan'), {}
57    sig=dict(net.last_signature)
58    return float(metric),sig
59
60def main():
61    lrs=[1e-3,3e-3,1e-2]
62    # baseline sweep uses all lr values; decisive baseline knob is rollout policy, swept explicitly
63    grid=[{'lr':lr,'epochs':8,'tau':tau,'horizon':h} for lr in lrs for tau in [0.05,0.08,0.15] for h in [4,8]]
64    base=sweep_baseline(lambda cfg: (lambda s: run(s,cfg,False,always=True)[0]),grid)
65    best=base['best_cfg']; idea_grid=[dict(best),dict(best,lr=1e-3),dict(best,lr=1e-2),dict(best,tau=0.05),dict(best,tau=0.15)]
66    # same union is evaluated by baseline sweep above; idea selects best among three on 8 paired seeds
67    candidates=[]
68    for cfg in idea_grid:
69        ev=evaluate(lambda s,cfg=cfg: run(s,cfg,True)[0],seeds=tuple(range(8)))
70        candidates.append((ev,cfg))
71    idea,cfg=max(candidates,key=lambda z:-z[0]['mean'])
72    # paired comparison must use baseline best cfg and same eight seeds
73    b8=evaluate(lambda s: run(s,best,False,always=True)[0],seeds=tuple(range(8)))
74    sigs=[run(s,cfg,True)[1] for s in range(8)]
75    br=make_report('dynamics','rnn_small',{'best_cfg':best,'full':b8,'sweep':base},idea,{
76      'mechanism_signature':{
77        'trained_model':'GRUCell Jacobian and learned scalar head measured on test-time batches',
78        'predicted':'non-eigenvector transitions should activate rollout; eigen-like transitions should skip',
79        'observed_mean_residual':float(np.mean([q.get('residual',np.nan) for q in sigs])),
80        'observed_activation_rate':float(np.mean([q.get('active',False) for q in sigs])),
81        'observed_lambda_mean':float(np.mean([q.get('lambda_hat',np.nan) for q in sigs])),
82        'confirmed':bool(np.mean([q.get('active',False) for q in sigs])>0 and np.mean([q.get('residual',0) for q in sigs])>0.08)
83      },'idea_sweep':[(c,e) for e,c in candidates]})
84    with open('bench_report.json','w') as f: json.dump(br,f,indent=2)
85    print(json.dumps(br,indent=2))
86if __name__=='__main__': main()