Adversarially calibrated neural residualization / experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, math, time
  2import numpy as np
  3from sklearn.neural_network import MLPRegressor
  4
  5# Adversarially calibrated residualization MVP.
  6# The implementation follows the displayed constraint literally.
  7
  8def edit_weights(t, F, w0=None, tau=0.0, kappa=0.0):
  9    """Solve the stated local edit problem for the important w0=1 case.
 10    Since w=1 has objective zero and exactly zero (w-1) moments, it is the
 11    unique minimizer whenever w0=1 (the objective is strictly convex).
 12    """
 13    n = len(t)
 14    if w0 is None: w0 = np.ones(n)
 15    w0 = np.asarray(w0)
 16    if np.allclose(w0, 1.0):
 17        w = np.ones(n)
 18    else:
 19        # Small projected-gradient fallback for non-unit initial weights.
 20        w = np.maximum(w0.copy(), 0.0)
 21        A = t[:, None] * F
 22        lr = 0.2 / (np.linalg.norm(A, 2)**2 / n + 1e-8)
 23        for _ in range(2000):
 24            mom = ((w-1)[:, None] * A).mean(0)
 25            viol = np.maximum(np.abs(mom)-tau, 0)
 26            grad = (w-w0)/n
 27            if np.any(viol):
 28                grad += ((A @ (np.sign(mom)*viol)) / n)
 29            w = np.maximum(w-lr*grad, 0)
 30            w *= len(w)/w.sum()
 31    energy = np.mean(w*t*t)
 32    # The stated rejection rule: retain a feasible-energy solution if possible.
 33    if energy < kappa:
 34        return np.ones(n) if np.mean(t*t) >= kappa else w
 35    return w
 36
 37def moments(w,t,F):
 38    return np.mean((w-1)[:,None]*t[:,None]*F, axis=0)
 39
 40def toy_verification(seed=123):
 41    rng=np.random.default_rng(seed); n=600
 42    x=rng.normal(size=(n,8)); t=0.5*x[:,0]+rng.normal(size=n)
 43    F=np.tanh(x[:,:4])
 44    out={"tau_sweep":[], "critic_scale_sweep":[], "dimension_sweep":[]}
 45    for tau in [0,1e-4,1e-2,0.1,1.0]:
 46        w=edit_weights(t,F,tau=tau,kappa=.01)
 47        out["tau_sweep"].append({"tau":tau,"edit_l2":float(np.linalg.norm(w-1)),"max_moment":float(np.max(np.abs(moments(w,t,F))))})
 48    for scale in [0.1,1,10]:
 49        Fs=scale*F
 50        w=edit_weights(t,Fs,tau=.01,kappa=.01)
 51        out["critic_scale_sweep"].append({"scale":scale,"edit_l2":float(np.linalg.norm(w-1)),"max_moment":float(np.max(np.abs(moments(w,t,Fs))))})
 52    for d in [1,2,4,8]:
 53        Fs=np.tanh(x[:,:d])
 54        w=edit_weights(t,Fs,tau=.01,kappa=.01)
 55        out["dimension_sweep"].append({"dimension":d,"edit_l2":float(np.linalg.norm(w-1)),"max_moment":float(np.max(np.abs(moments(w,t,Fs))))})
 56    return out
 57
 58def dml_once(seed, n=500, reverse=False):
 59    rng=np.random.default_rng(seed); p=20; beta=1.0
 60    X=rng.normal(size=(n,p));
 61    # Imbalanced nuisance difficulty: high-frequency outcome versus smooth treatment,
 62    # then the reverse. A small MLP intentionally underfits the high-frequency part.
 63    smooth_pi=0.8*np.sin(X[:,0])+0.3*X[:,1]
 64    hard_mu=1.5*np.sin(5*X[:,0])+0.7*np.cos(4*X[:,1])+0.3*X[:,2]
 65    if not reverse: mu, pi=hard_mu, smooth_pi
 66    else: mu, pi=smooth_pi, hard_mu/1.5
 67    T=pi+rng.normal(size=n)
 68    Y=mu+beta*T+0.8*rng.normal(size=n)
 69    idx=np.arange(n); rng.shuffle(idx); folds=np.array_split(idx,2)
 70    ry=np.zeros(n); rt=np.zeros(n)
 71    for te in folds:
 72        tr=np.setdiff1d(idx,te,assume_unique=False)
 73        my=MLPRegressor(hidden_layer_sizes=(32,16),early_stopping=False,max_iter=100,
 74                        random_state=seed, solver='adam', learning_rate_init=.003)
 75        mt=MLPRegressor(hidden_layer_sizes=(32,16),early_stopping=False,max_iter=100,
 76                        random_state=seed+17, solver='adam', learning_rate_init=.003)
 77        my.fit(X[tr],Y[tr]); mt.fit(X[tr],T[tr])
 78        ry[te]=Y[te]-my.predict(X[te]); rt[te]=T[te]-mt.predict(X[te])
 79    beta_hat=float(np.sum(rt*ry)/np.sum(rt*rt))
 80    F=np.tanh(X[:,:4]); w=edit_weights(rt,F,tau=.02,kappa=.1*np.mean(rt*rt))
 81    cal=float(np.sum(w*rt*ry)/np.sum(w*rt*rt))
 82    ess=float(w.sum()**2/np.sum(w*w)); viol=float(np.max(np.abs(moments(w,rt,F))))
 83    return beta_hat,cal,ess,viol
 84
 85def mini_experiment():
 86    rows=[]
 87    for reverse in [False,True]:
 88        vals=[dml_once(100+i,reverse=reverse) for i in range(8)]
 89        a=np.array(vals)
 90        rows.append({"regime":"hard_mu" if not reverse else "hard_pi",
 91          "baseline_abs_bias":float(np.mean(np.abs(a[:,0]-1))),
 92          "idea_abs_bias":float(np.mean(np.abs(a[:,1]-1))),
 93          "baseline_rmse":float(np.sqrt(np.mean((a[:,0]-1)**2))),
 94          "idea_rmse":float(np.sqrt(np.mean((a[:,1]-1)**2))),
 95          "mean_ess":float(np.mean(a[:,2])),"max_violation":float(np.max(a[:,3])),
 96          "paired_max_difference":float(np.max(np.abs(a[:,0]-a[:,1])))})
 97    return rows
 98
 99if __name__=='__main__':
100    t=time.time(); result={"toy":toy_verification(),"mini":mini_experiment(),"runtime_sec":time.time()-t}
101    print(json.dumps(result,indent=2))