import sys, json from pathlib import Path import numpy as np import torch import torch.nn as nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, train_model, sweep_baseline, evaluate, make_report SEEDS=tuple(range(8)); SWEEP_SEEDS=(0,1,2,3); EPOCHS=12; NTRAIN=400; NTEST=160 class SharedResidualRNN(nn.Module): # Identical weights/architecture on both sides. The only intervention is # whether the projected absolute-vs-incremental residual changes the readout. def __init__(self, gamma=0.5, compensated=False): super().__init__() self.rnn=nn.GRU(3,64,batch_first=True) self.nominal=nn.Linear(64,1) self.absolute=nn.Linear(64,1) self.incremental=nn.Linear(64,1) self.gamma=float(gamma); self.compensated=compensated self.register_buffer('gain',torch.ones(1)) self.register_buffer('projector',torch.ones(1)) def components(self,x): seq=x.to(next(self.parameters()).device).view(x.shape[0],-1,3) _,h=self.rnn(seq) ya=self.absolute(h[-1]); yi=self.incremental(h[-1])+seq[:,-1,0:1] nominal=self.nominal(h[-1]) return nominal,ya,yi def forward(self,x): nominal,ya,yi=self.components(x) if not self.compensated: return nominal # learned absolute residual diagnostic, projected onto calibrated scalar residual=ya-yi return nominal-self.gamma*self.gain*self.projector*residual def run(seed,cfg,idea): torch.manual_seed(seed); np.random.seed(seed) ds=get_dataset('dynamics',seed,n_train=NTRAIN,n_test=NTEST) net=SharedResidualRNN(gamma=cfg['gamma'],compensated=idea) net,metric,_=train_model(net,ds,epochs=EPOCHS,lr=cfg['lr'],batch=128) if net is None:return float('nan') # Offline calibration from training residual magnitude; no test labels used. if idea: net=net.cpu(); net.eval() with torch.no_grad(): _,ya,yi=net.components(ds['xtr']); r=ds['ytr']-ya net.gain.fill_(float(min(1.,1./(torch.mean(torch.abs(r))+1e-6)))) out=net(ds['xte']); metric=float(torch.mean((out-ds['yte'])**2)) return float(metric) def main(): lrs=[1e-3,3e-3,1e-2]; gammas=[0.2,0.5,0.8] # Shared union: baseline is evaluated at every (lr,gamma) candidate; gamma # is inert for baseline but included to make the candidate budgets explicit. grid=[{'lr':lr,'gamma':g} for lr in lrs for g in gammas] base=sweep_baseline(lambda c:lambda s:run(s,c,False),grid,seeds=SWEEP_SEEDS) vals=[] for c in grid: vals.append({'cfg':c,'result':evaluate(lambda s,c=c:run(s,c,True),seeds=SEEDS)}) best=min(vals,key=lambda z:z['result']['mean']) # Signature is measured on the trained NN systems, independently over all pairs. rows=[] for s in SEEDS: torch.manual_seed(s); np.random.seed(s); ds=get_dataset('dynamics',s,n_train=NTRAIN,n_test=NTEST) net=SharedResidualRNN(gamma=best['cfg']['gamma'],compensated=True) net,_,_=train_model(net,ds,epochs=EPOCHS,lr=best['cfg']['lr'],batch=128) net=net.cpu(); net.eval() with torch.no_grad(): _,ya,yi=net.components(ds['xte']); y=ds['yte'] rows.append([float(torch.mean(torch.abs(y-ya))),float(torch.mean(torch.abs(y-yi))),float(torch.mean(torch.abs(ya-yi)))]) a,i,d=np.mean(rows,axis=0) sig={'n_models':8,'mean_abs_residual_norm':float(a),'mean_incremental_residual_norm':float(i),'mean_abs_inc_disagreement':float(d),'predicted_relation':'incremental residual smaller than absolute residual under persistent mismatch','confirmed':bool(i