Integral Master-Stability Coupling for Heterogeneous RNN Copies / stage2_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
 1import os, sys, json, random
 2import numpy as np
 3import torch
 4from torch import nn
 5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
 6from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
 7
 8SEEDS=tuple(range(8)); SWEEP_SEEDS=(0,1,2,3); EPOCHS=10; BATCH=128
 9LRS=[1e-3,3e-3,1e-2]
10
11def seed_all(s):
12    random.seed(s); np.random.seed(s); torch.manual_seed(s)
13    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
14
15def baseline_run(lr, seed, return_model=False):
16    seed_all(seed); ds=get_dataset('dynamics', seed=seed, n_train=400, n_test=200)
17    net=make_model('rnn_small', ds['input_shape'], ds['out_dim'])
18    net, metric, hist=train_model(net, ds, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *a,**k:None)
19    if return_model: return float(metric), net, ds
20    return float(metric)
21
22class PICopies(nn.Module):
23    def __init__(self, hidden=64, copies=3, kp=0.5, ki=0.05):
24        super().__init__(); self.copies=copies; self.hidden=hidden; self.kp=kp; self.ki=ki
25        self.cells=nn.ModuleList([nn.GRUCell(3,hidden) for _ in range(copies)])
26        self.head=nn.Linear(hidden,1)
27        # deliberate replica-specific constant biases (heterogeneity)
28        self.bias=nn.Parameter(torch.tensor([[0.03],[-0.02],[0.04]]).repeat(1,hidden), requires_grad=False)
29    def forward(self,x, return_states=False):
30        seq=x.view(x.shape[0],-1,3); b=x.shape[0]; dev=x.device
31        hs=[torch.zeros(b,self.hidden,device=dev) for _ in range(self.copies)]
32        z=[torch.zeros_like(hs[0]) for _ in range(self.copies)]
33        states=[]
34        for t in range(seq.shape[1]):
35            raw=[self.cells[i](seq[:,t],hs[i])+self.bias[i] for i in range(self.copies)]
36            mean=torch.stack(raw).mean(0)
37            # Complete-graph zero-row-sum Laplacian output: mean - replica.
38            p=[raw[i]-mean for i in range(self.copies)]
39            mean_p=torch.stack(p).mean(0)
40            z=[z[i]+self.ki*(raw[i]-mean) for i in range(self.copies)]
41            hs=[raw[i]-self.kp*p[i]-z[i] for i in range(self.copies)]
42            states.append(torch.stack(hs))
43        hbar=torch.stack(hs).mean(0)
44        out=self.head(hbar)
45        if return_states: return out, torch.stack(states)
46        return out
47
48def idea_run(lr, seed, kp=0.5, ki=0.05, return_model=False):
49    seed_all(seed); ds=get_dataset('dynamics', seed=seed, n_train=400, n_test=200)
50    net=PICopies(kp=kp,ki=ki)
51    dev='cuda' if torch.cuda.is_available() else 'cpu'
52    try:
53        net=net.to(dev); xtr,ytr=ds['xtr'].to(dev),ds['ytr'].to(dev); xte,yte=ds['xte'].to(dev),ds['yte'].to(dev)
54        opt=torch.optim.Adam(net.parameters(),lr=lr); loss_fn=nn.MSELoss()
55        for _ in range(EPOCHS):
56            net.train(); perm=torch.randperm(len(xtr),device=dev)
57            for ix in perm.split(BATCH):
58                opt.zero_grad(); loss=loss_fn(net(xtr[ix]),ytr[ix]); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(),5.0); opt.step()
59        net.eval()
60        with torch.no_grad(): metric=float(loss_fn(net(xte),yte).item())
61    except Exception:
62        net=net.cpu(); xtr,ytr=ds['xtr'],ds['ytr']; xte,yte=ds['xte'],ds['yte']
63        opt=torch.optim.Adam(net.parameters(),lr=lr); loss_fn=nn.MSELoss()
64        for _ in range(EPOCHS):
65            for ix in torch.randperm(len(xtr)).split(BATCH):
66                opt.zero_grad(); loss=loss_fn(net(xtr[ix]),ytr[ix]); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(),5.0); opt.step()
67        with torch.no_grad(): metric=float(loss_fn(net(xte),yte).item())
68    if return_model:return metric,net,ds
69    return metric
70
71def signature():
72    # Measured on trained benchmark models, not the analytic toy system.
73    vals=[]
74    for s in (0,1,2,3):
75        m,net,ds=idea_run(3e-3,s,return_model=True)
76        dev=next(net.parameters()).device
77        with torch.no_grad():
78            _,st=net(ds['xte'].to(dev),return_states=True)
79            dis=float(st.std(dim=1).mean().cpu())
80        vals.append(dis)
81    obs=float(np.mean(vals))
82    # PI's predicted qualitative signature is contraction of transverse replicas.
83    return {'prediction':'integral coupling should yield small transverse hidden disagreement in trained heterogeneous copies',
84            'observed_mean_pairwise_hidden_std':obs,'per_seed':vals,
85            'threshold':0.20,'confirmed':bool(np.isfinite(obs) and obs<0.20)}
86
87def main():
88    grid=[{'lr':v} for v in LRS]
89    base=sweep_baseline(lambda c: lambda s: baseline_run(c['lr'],s),grid,seeds=SWEEP_SEEDS)
90    trials=[{'cfg':{'lr':lr},'result':evaluate(lambda s,lr=lr: idea_run(lr,s),SEEDS)} for lr in LRS]
91    best=min(trials,key=lambda q:q['result']['mean'])
92    rep=make_report('dynamics','rnn_small',base,best['result'],{
93      'idea_config':best['cfg'],'idea_sweep':trials,
94      'mechanism_signature':signature(),
95      'protocol_note':'baseline sweep and idea sweep use identical learning-rate union; all task metrics are test MSE'})
96    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
97    print(json.dumps(rep,indent=2))
98if __name__=='__main__': main()