Joint Modeling for Stochastic Interventions / experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json, math, random
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6SEED = 3146
  7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  8try:
  9    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 10except Exception:
 11    device = torch.device('cpu')
 12
 13def scm(n, sigma_x, seed):
 14    r = np.random.RandomState(seed)
 15    x = r.randn(n) * sigma_x
 16    u = r.randn(n); v = r.randn(n)
 17    m = x + u
 18    y = m + x + v
 19    return x.astype('float32'), m.astype('float32'), y.astype('float32')
 20
 21# Stage 1: exact Gaussian calculations and sampling verify the claimed selection effect.
 22def math_check():
 23    # For M=X+U, Y=M+X+V: E[X|M=m] = s2/(s2+1)m and E[Y|M=m] changes with intervention variance.
 24    m0 = 1.0
 25    analytic = {}
 26    for s in (1.0, 2.0):
 27        analytic[str(s)] = (1.0 + s*s/(s*s+1.0))*m0
 28    x,m,y = scm(1500000, 1.0, 11)
 29    selected = np.abs(m-m0) < .015
 30    empirical_train = float(y[selected].mean())
 31    x,m,y = scm(1500000, 2.0, 12)
 32    selected = np.abs(m-m0) < .015
 33    empirical_shift = float(y[selected].mean())
 34    # Joint marginal variance is Var(Y)=4 Var(X)+2, while mediator-only cannot encode this shift.
 35    var_formula = {str(s): 4*s*s+2 for s in (1.0,2.0)}
 36    return {'analytic_EY_given_M1': analytic, 'empirical_EY_given_M1': {'sigma1': empirical_train, 'sigma2': empirical_shift}, 'marginal_Y_variance_formula': var_formula}
 37
 38class Joint(nn.Module):
 39    def __init__(self):
 40        super().__init__()
 41        self.q = nn.Sequential(nn.Linear(1,32), nn.Tanh(), nn.Linear(32,2)) # mu, log sd of X
 42        self.m = nn.Sequential(nn.Linear(1,32), nn.Tanh(), nn.Linear(32,2)) # conditioned on x
 43        self.y = nn.Sequential(nn.Linear(2,32), nn.Tanh(), nn.Linear(32,2)) # x,m
 44    def forward(self,x,m):
 45        q=self.q(torch.zeros_like(x[:,None])); mp=self.m(x[:,None]); yp=self.y(torch.cat([x[:,None],m[:,None]],1))
 46        return q,mp,yp
 47
 48def normal_nll(z, pars):
 49    mu, logsd = pars[:,0], pars[:,1].clamp(-5,3)
 50    return .5*((z-mu)/logsd.exp())**2 + logsd + .5*math.log(2*math.pi)
 51
 52class Base(nn.Module):
 53    def __init__(self):
 54        super().__init__(); self.net=nn.Sequential(nn.Linear(1,32),nn.Tanh(),nn.Linear(32,2))
 55    def forward(self,m): return self.net(m[:,None])
 56
 57def train_eval():
 58    x,m,y=scm(24000,1.0,20)
 59    xt,mt,yt=scm(120000,2.0,21)
 60    X=torch.tensor(x,device=device); M=torch.tensor(m,device=device); Y=torch.tensor(y,device=device)
 61    # Baseline deliberately follows the common mediator-only predictor p(y|m).
 62    base=Base().to(device); opt=torch.optim.Adam(base.parameters(),lr=.01)
 63    for _ in range(350):
 64        ix=torch.randint(0,len(X),(128,),device=device)
 65        loss=normal_nll(Y[ix],base(M[ix])).mean(); opt.zero_grad(); loss.backward(); opt.step()
 66    # Joint likelihood q(x) + p(m|x) + p(y|x,m).
 67    joint=Joint().to(device); opt=torch.optim.Adam(joint.parameters(),lr=.01)
 68    for _ in range(450):
 69        ix=torch.randint(0,len(X),(128,),device=device)
 70        q,mp,yp=joint(X[ix],M[ix])
 71        loss=(normal_nll(X[ix],q)+normal_nll(M[ix],mp)+normal_nll(Y[ix],yp)).mean()
 72        opt.zero_grad(); loss.backward(); opt.step()
 73    with torch.no_grad():
 74        MT=torch.tensor(mt,device=device); XT=torch.tensor(xt,device=device); YT=torch.tensor(yt,device=device)
 75        base_nll=float(normal_nll(YT,base(MT)).mean().cpu())
 76        _,_,yp=joint(XT,MT)
 77        joint_nll=float(normal_nll(YT,yp).mean().cpu())
 78        # Estimated conditional mean at M=1 under the learned conditional model, versus baseline.
 79        one=torch.ones(2000,device=device); xx=torch.randn(2000,device=device)*2
 80        # draw mediator noise, then retain a narrow mediator selection
 81        mm=xx+torch.randn_like(xx)
 82        keep=(mm-1).abs()<.02
 83        _,_,pred=joint(xx[keep],mm[keep])
 84        joint_sel=float(pred[:,0].mean().cpu())
 85        bsel=float(base(torch.ones(2000,device=device))[:,0].mean().cpu())
 86        # Marginal predictive variance under target X~N(0,2), via structural heads.
 87        xs=torch.randn(20000,device=device)*2
 88        ms=joint.m(xs,)[0] if False else None
 89        mp=joint.m(xs[:,None]); mdraw=mp[:,0]+torch.randn_like(xs)*mp[:,1].clamp(-5,3).exp()
 90        yp=joint.y(torch.stack([xs,mdraw],1)); ydraw=yp[:,0]+torch.randn_like(xs)*yp[:,1].clamp(-5,3).exp()
 91        pred_var=float(ydraw.var().cpu())
 92    return {'test_shift_sigma2_nll':{'baseline_m_only':base_nll,'joint':joint_nll,'joint_minus_baseline':joint_nll-base_nll},'selected_M1_predicted_mean':{'baseline':bsel,'joint_conditional':joint_sel,'true_sigma2':1.8},'joint_marginal_predicted_Y_variance_sigma2':pred_var,'device':str(device)}
 93
 94if __name__=='__main__':
 95    out={'math_check':math_check()}
 96    try: out['neural_experiment']=train_eval()
 97    except Exception as e:
 98        # Explicit CPU fallback as required for shared GPU environments.
 99        device=torch.device('cpu'); out['neural_experiment']=train_eval(); out['cuda_error']=repr(e)
100    with open('results.json','w') as f: json.dump(out,f,indent=2)
101    print(json.dumps(out,indent=2))