import sys, json, math, random from pathlib import Path import numpy as np import torch from torch import nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report TRACK='stochastic_intervention_scm' MODEL='mlp_tiny' SEEDS=tuple(range(8)) LRS=(0.003,0.01,0.03) EPOCHS=30 BATCH=128 def nll(z, p): mu=p[:,0] ls=p[:,1].clamp(-4,3) return .5*((z-mu)/ls.exp())**2 + ls + .5*math.log(2*math.pi) class Joint(nn.Module): def __init__(self): super().__init__() self.q=nn.Sequential(nn.Linear(1,16),nn.Tanh(),nn.Linear(16,2)) self.m=nn.Sequential(nn.Linear(2,16),nn.Tanh(),nn.Linear(16,2)) # Same mlp_tiny-style outcome network as the baseline. self.y=nn.Sequential(nn.Linear(3,16),nn.Tanh(),nn.Linear(16,2)) def forward(self,c,x,m): return self.q(c), self.m(torch.cat([c,x],1)), self.y(torch.cat([c,x,m],1)) def seed_all(seed): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) def baseline_metric(cfg, seed): seed_all(seed) d=get_dataset(TRACK, seed=seed, n_train=400, n_test=400) # Standard mediator-only baseline receives context and mediator; intervention slot is zero. d=dict(d) d['xtr']=torch.cat([d['xtr'][:,0:1],torch.zeros_like(d['xtr'][:,1:2]),d['xtr'][:,2:3]],1) d['xte']=torch.cat([d['xte'][:,0:1],torch.zeros_like(d['xte'][:,1:2]),d['xte'][:,2:3]],1) model=make_model(MODEL,d['input_shape'],d['out_dim']) _, metric, _=train_model(model,d,epochs=EPOCHS,lr=cfg['lr'],batch=BATCH,log=lambda *a,**k:None) return float(metric) def idea_metric(cfg, seed, return_sig=False): seed_all(seed) d=get_dataset(TRACK, seed=seed, n_train=400, n_test=400) try: device=torch.device('cuda' if torch.cuda.is_available() else 'cpu'); torch.zeros(1,device=device) except Exception: device=torch.device('cpu') c=d['xtr'][:,0:1].to(device); x=d['xtr'][:,1:2].to(device); m=d['xtr'][:,2:3].to(device); y=d['ytr'][:,0].to(device) model=Joint().to(device); opt=torch.optim.Adam(model.parameters(),lr=cfg['lr']) g=torch.Generator(device=device); g.manual_seed(seed+901); n=len(y) model.train() for _ in range(EPOCHS): for start in range(0,n,BATCH): ix=torch.randperm(n,generator=g,device=device)[start:start+BATCH] q,mp,yp=model(c[ix],x[ix],m[ix]) loss=(nll(x[ix,0],q)+nll(m[ix,0],mp)+nll(y[ix],yp)).mean() opt.zero_grad(); loss.backward(); opt.step() d=get_dataset(TRACK, seed=seed+5000, n_train=400, n_test=400) ct=d['xte'][:,0:1].to(device); xt=d['xte'][:,1:2].to(device); mt=d['xte'][:,2:3].to(device); yt=d['yte'][:,0].to(device) model.eval() with torch.no_grad(): _,_,p=model(ct,xt,mt); pred=p[:,0]; mse=((pred-yt)**2).mean() keep=(mt[:,0]-1).abs()<.12 sel_pred=pred[keep].mean(); sel_obs=yt[keep].mean() result=float(mse.cpu()) if return_sig: return result,float(sel_pred.cpu()),float(sel_obs.cpu()) return result def main(): grid=[{'lr':x} for x in LRS] base=sweep_baseline(lambda cfg: (lambda seed: baseline_metric(cfg,seed)),grid,seeds=(0,1,2,3)) # Search-space parity: idea evaluates exactly the same lr union; baseline sweep did too. idea_trials=[] for cfg in grid: rr=evaluate(lambda seed,cfg=cfg: idea_metric(cfg,seed),seeds=SEEDS) idea_trials.append({'cfg':cfg,'mean':rr['mean'],'std':rr['std'],'per_seed':rr['per_seed'],'n':rr['n']}) best=min(idea_trials,key=lambda z:z['mean']) idea={'mean':best['mean'],'std':best['std'],'per_seed':best['per_seed'],'n':best['n'],'best_cfg':best['cfg'],'sweep':idea_trials} # Behavior signature comes from the trained systems, evaluated on the same selected-M test cases. sig_base=[]; sig_idea=[] for s in SEEDS: # Train/evaluate idea returns its selected behavior. Baseline behavior is measured by a # separately trained baseline system, not an analytic identity. seed_all(s); d=get_dataset(TRACK,seed=s,n_train=400,n_test=400) d2=dict(d); d2['xtr']=torch.cat([d['xtr'][:,0:1],torch.zeros_like(d['xtr'][:,1:2]),d['xtr'][:,2:3]],1); d2['xte']=torch.cat([d['xte'][:,0:1],torch.zeros_like(d['xte'][:,1:2]),d['xte'][:,2:3]],1) bm=make_model(MODEL,d2['input_shape'],d2['out_dim']); train_model(bm,d2,epochs=EPOCHS,lr=base['best_cfg']['lr'],batch=BATCH,log=lambda *a,**k:None) td=get_dataset(TRACK,seed=s+5000,n_train=400,n_test=400); z=torch.cat([td['xte'][:,0:1],torch.zeros_like(td['xte'][:,1:2]),td['xte'][:,2:3]],1) bdev=next(bm.parameters()).device; z=z.to(bdev); ty=td['yte'].to(bdev) with torch.no_grad(): bp=bm(z)[:,0] keep=(td['xte'][:,2]-1).abs()<.12; sig_base.append((float(bp[keep.to(bdev)].mean().cpu()),float(ty[keep.to(bdev)].mean().cpu()))) _,ip,io=idea_metric(best['cfg'],s,True); sig_idea.append((ip,io)) sig={'baseline_predicted_selected_mean':float(np.mean([x[0] for x in sig_base])),'idea_predicted_selected_mean':float(np.mean([x[0] for x in sig_idea])),'observed_selected_mean':float(np.mean([x[1] for x in sig_idea])),'baseline_abs_error':float(abs(np.mean([x[0]-x[1] for x in sig_base]))),'idea_abs_error':float(abs(np.mean([x[0]-x[1] for x in sig_idea]))),'confirmed':bool(abs(np.mean([x[0]-x[1] for x in sig_idea])) < abs(np.mean([x[0]-x[1] for x in sig_base]))) } rep=make_report(TRACK,MODEL,base,idea,extra={'custom_track':{'name':TRACK,'file':'custom_stochastic_intervention_track.py','domain':'causal-world-model'},'mechanism_signature':sig}) Path('bench_report.json').write_text(json.dumps(rep,indent=2)) print(json.dumps(rep,indent=2)) if __name__=='__main__': main()