Convex Bayesian Potential Head / 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=467
  7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  8torch.set_num_threads(4)
  9DT=torch.float64
 10
 11# Finite latent space makes the partition function exact (no Monte Carlo ambiguity).
 12xvals=torch.tensor([-2.,-1.,0.,1.,2.],dtype=DT)
 13prior=torch.tensor([.08,.20,.44,.20,.08],dtype=DT); prior/=prior.sum()
 14sigma=.65
 15
 16def make_data(n, seed):
 17    g=torch.Generator().manual_seed(seed)
 18    ix=torch.multinomial(prior,n,replacement=True,generator=g)
 19    x=xvals[ix]
 20    y=x+sigma*torch.randn(n,generator=g,dtype=DT)
 21    return x,y
 22
 23def base_features(x,y):
 24    # frozen nonlinear feature map; feature dimension is deliberately small
 25    z=torch.stack([x, y, x*y, x*x, y*y, torch.sin(y), torch.cos(y)],-1)
 26    W=torch.tensor([[.7,-.2,.4,.1,-.3,.5,.2],[-.4,.8,.2,-.5,.3,.1,-.6],
 27                    [.3,.2,-.7,.6,.1,-.4,.5],[.5,.1,.3,.4,-.6,.2,.7],
 28                    [-.2,.6,.5,.2,.4,-.5,.1], [.4,-.3,.1,-.2,.7,.6,-.4]],dtype=DT)
 29    b=torch.tensor([.1,-.2,.05,.3,-.1,.15],dtype=DT)
 30    return torch.tanh(z@W.T+b)
 31
 32def phi_particles(y):
 33    # [B,K,D], with each observation paired to all prior-supported x
 34    B=y.numel(); K=xvals.numel()
 35    return base_features(xvals[None,:].expand(B,K), y[:,None].expand(B,K)).reshape(B,K,-1)
 36
 37def exact_loss(theta, x, y):
 38    ph=phi_particles(y); target=base_features(x,y)
 39    logits=-torch.einsum('bkd,d->bk',ph,theta)+torch.log(prior)[None,:]
 40    return (torch.einsum('d,bd->b',theta,target)+torch.logsumexp(logits,1)).mean()
 41
 42def posterior(theta,y):
 43    ph=phi_particles(y)
 44    return torch.softmax(-torch.einsum('bkd,d->bk',ph,theta)+torch.log(prior)[None,:],1)
 45
 46class NeuralScore(nn.Module):
 47    def __init__(self,d=6,seed=0):
 48        super().__init__(); torch.manual_seed(seed)
 49        self.net=nn.Sequential(nn.Linear(7,16),nn.Tanh(),nn.Linear(16,1)).double()
 50    def forward(self,x,y):
 51        z=torch.stack([x,y,x*y,x*x,y*y,torch.sin(y),torch.cos(y)],-1)
 52        return self.net(z).squeeze(-1)
 53
 54def neural_loss(model,x,y):
 55    B=y.numel(); xx=xvals[None,:].expand(B,-1); yy=y[:,None].expand(B,xvals.numel())
 56    s=model(xx.reshape(-1),yy.reshape(-1)).reshape(B,-1)
 57    st=model(x,y)
 58    return (st+torch.logsumexp(-s+torch.log(prior)[None,:],1)).mean()
 59
 60def true_post(x_unused,y):
 61    # generative posterior p(x|y) under the known Gaussian observation model
 62    ll=-.5*((y[:,None]-xvals[None,:])/sigma)**2
 63    return torch.softmax(ll+torch.log(prior)[None,:],1)
 64
 65def posterior_kl(pred,y):
 66    t=true_post(None,y); return (t*(torch.log(t+1e-12)-torch.log(pred+1e-12))).sum(1).mean().item()
 67
 68def coverage(pred,y,level=.9):
 69    # shortest contiguous x interval containing requested posterior mass, then test true x
 70    # This is a discrete, transparent calibration proxy.
 71    t=true_post(None,y); covered=[]
 72    for i in range(y.numel()):
 73        p=pred[i]; best=None
 74        for lo in range(5):
 75            for hi in range(lo,5):
 76                if p[lo:hi+1].sum()>=level and (best is None or hi-lo<best[0]): best=(hi-lo,lo,hi)
 77        # expected coverage under true posterior, not sampled latent, is lower variance
 78        covered.append(t[i,best[1]:best[2]+1].sum().item())
 79    return float(np.mean(covered))
 80
 81def convex_fit(x,y,init):
 82    # Newton with covariance Hessian; damping only for numerical stability.
 83    th=init.clone(); history=[]
 84    for it in range(40):
 85        th.requires_grad_(True)
 86        loss=exact_loss(th,x,y); g=torch.autograd.grad(loss,th,create_graph=False)[0]
 87        ph=phi_particles(y); r=posterior(th,y); mu=(r[:,:,None]*ph).sum(1)
 88        centered=ph-mu[:,None,:]
 89        H=torch.einsum('bk,bkd,bke->de',r,centered,centered)/y.numel()
 90        H=H+1e-7*torch.eye(th.numel(),dtype=DT)
 91        try: step=torch.linalg.solve(H,g)
 92        except RuntimeError: step=torch.linalg.lstsq(H,g[:,None]).solution[:,0]
 93        # backtracking preserves objective descent
 94        old=loss.item(); alpha=1.
 95        with torch.no_grad():
 96            while alpha>1e-5 and exact_loss(th-alpha*step,x,y).item()>old-1e-4*alpha*(g@step).item(): alpha*=.5
 97            th=(th-alpha*step).detach()
 98        history.append(old)
 99        if g.norm().item()<2e-8: break
100    return th,history
101
102def run():
103    # Core calculus check at a random parameter and random direction.
104    x,y=make_data(64,11); theta=torch.randn(6,dtype=DT)*.3; theta.requires_grad_()
105    L=exact_loss(theta,x,y); grad=torch.autograd.grad(L,theta,create_graph=True)[0]
106    H=torch.autograd.functional.hessian(lambda t: exact_loss(t,x,y),theta).detach().numpy()
107    eig=np.linalg.eigvalsh((H+H.T)/2)
108    # finite difference directional derivative agrees with Hessian-vector product
109    v=torch.randn(6,dtype=DT); v/=v.norm(); eps=1e-4
110    tp=(theta.detach()+eps*v).requires_grad_(True)
111    tm=(theta.detach()-eps*v).requires_grad_(True)
112    gp=torch.autograd.grad(exact_loss(tp,x,y),tp)[0]
113    gm=torch.autograd.grad(exact_loss(tm,x,y),tm)[0]
114    fd=((gp-gm)/(2*eps)).dot(v).item(); quad=(v@torch.tensor(H)@v).item()
115    check={'hessian_min_eigenvalue':float(eig[0]),'gradient_hvp_fd':fd,'gradient_hvp_formula':quad,'hvp_abs_error':abs(fd-quad)}
116
117    trainx,trainy=make_data(256,21); testx,testy=make_data(512,22)
118    convex=[]; histories=[]
119    for s in [0,1,2,3,4]:
120        th,h=convex_fit(trainx,trainy,torch.randn(6,dtype=DT)*2*(s+1)); histories.append({'start':s,'iterations':len(h),'final_train_objective':h[-1]})
121        pr=posterior(th,testy); convex.append({'objective':exact_loss(th,testx,testy).item(),'posterior_KL':posterior_kl(pr,testy),'coverage90':coverage(pr,testy),'theta':th.tolist()})
122    neural=[]
123    for s in [0,1,2,3,4]:
124        model=NeuralScore(seed=100+s); opt=torch.optim.Adam(model.parameters(),lr=.025)
125        hs=[]
126        for it in range(500):
127            opt.zero_grad(); loss=neural_loss(model,trainx,trainy); loss.backward(); opt.step(); hs.append(loss.item())
128        B=testy.numel(); xx=xvals[None,:].expand(B,-1); yy=testy[:,None].expand(B,xvals.numel())
129        with torch.no_grad(): pr=torch.softmax(-model(xx.reshape(-1),yy.reshape(-1)).reshape(B,-1)+torch.log(prior)[None,:],1)
130        neural.append({'objective':neural_loss(model,testx,testy).item(),'posterior_KL':posterior_kl(pr,testy),'coverage90':coverage(pr,testy),'final_train_objective':hs[-1]})
131    out={'math_check':check,'convex_runs':convex,'convex_histories':histories,'neural_runs':neural,
132         'summary':{'convex_objective_mean_sd':[float(np.mean([z['objective'] for z in convex])),float(np.std([z['objective'] for z in convex]))],
133                    'neural_objective_mean_sd':[float(np.mean([z['objective'] for z in neural])),float(np.std([z['objective'] for z in neural]))],
134                    'convex_KL_mean_sd':[float(np.mean([z['posterior_KL'] for z in convex])),float(np.std([z['posterior_KL'] for z in convex]))],
135                    'neural_KL_mean_sd':[float(np.mean([z['posterior_KL'] for z in neural])),float(np.std([z['posterior_KL'] for z in neural]))],
136                    'convex_coverage_mean_sd':[float(np.mean([z['coverage90'] for z in convex])),float(np.std([z['coverage90'] for z in convex]))],
137                    'neural_coverage_mean_sd':[float(np.mean([z['coverage90'] for z in neural])),float(np.std([z['coverage90'] for z in neural]))]}}
138    with open('results.json','w') as f: json.dump(out,f,indent=2)
139    print(json.dumps(out,indent=2))
140if __name__=='__main__': run()