Convex Bayesian Potential Head / bench_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, math
  2import numpy as np
  3import torch
  4from torch import nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset as bench_get_dataset, evaluate, sweep_baseline, make_report
  7
  8DEVICE='cuda' if torch.cuda.is_available() else 'cpu'
  9try: torch.set_num_threads(4)
 10except Exception: pass
 11
 12# Registered bench track; its target is a bounded 2-D energy surface.
 13TRACK='bounded_energy_regression'
 14K=64
 15side=8
 16axis=torch.linspace(-1.5,1.5,side)
 17grid=torch.cartesian_prod(axis,axis)
 18LOGPRIOR=torch.full((K,),-math.log(K))
 19
 20def seed_all(s):
 21    np.random.seed(s); torch.manual_seed(s)
 22    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
 23
 24def data(seed):
 25    return bench_get_dataset(TRACK,seed,n_train=400,n_test=400)
 26
 27def phi(latent, obs):
 28    # Frozen neural-style features, shared by both separately trained systems.
 29    a,b=latent[...,0],latent[...,1]
 30    o=obs[...,0] if obs.shape[-1] else obs
 31    z=torch.stack((a,b,o,a*o,b*o,a*a,b*b,o*o,torch.sin(a),torch.cos(b)),-1)
 32    W=torch.tensor([[.31,-.22,.18,.11,-.14,.25,.07,-.09,.13,.21],
 33                    [-.12,.28,.24,-.19,.17,.08,-.16,.12,.22,-.11],
 34                    [.21,.07,-.31,.26,.09,-.12,.19,.15,-.18,.17],
 35                    [.16,.19,.05,.22,-.27,.13,.11,-.21,.14,.09],
 36                    [-.24,.11,.16,.08,.21,-.17,.23,.18,.07,-.15],
 37                    [.09,-.18,.27,.14,.12,.22,-.08,.16,-.11,.24]],device=latent.device)
 38    bias=torch.tensor([.1,-.2,.05,.3,-.1,.15],device=latent.device)
 39    return torch.tanh(z@W.t()+bias)
 40
 41class NeuralPotential(nn.Module):
 42    def __init__(self,hidden=32):
 43        super().__init__(); self.net=nn.Sequential(nn.Linear(10,hidden),nn.Tanh(),nn.Linear(hidden,1))
 44    def forward(self,latent,obs):
 45        a,b=latent[...,0],latent[...,1]; o=obs[...,0] if obs.shape[-1] else obs
 46        z=torch.stack((a,b,o,a*o,b*o,a*a,b*b,o*o,torch.sin(a),torch.cos(b)),-1)
 47        return self.net(z).squeeze(-1)
 48
 49def convex_loss(theta,latent_obs,latent_true,obs,ret=False):
 50    # observation is scalar target energy; latent_true is the coordinate pair.
 51    b=obs.shape[0]; particles=grid.to(obs.device)[None].expand(b,-1,-1); oo=obs[:,None].expand(b,K,1)
 52    p=phi(particles,oo); target=phi(latent_true,obs)
 53    logits=-torch.einsum('bkd,d->bk',p,theta)+LOGPRIOR.to(obs.device)
 54    loss=(torch.einsum('bd,d->b',target,theta)+torch.logsumexp(logits,1)).mean()
 55    return (loss,p,torch.softmax(logits,1)) if ret else loss
 56
 57def neural_loss(net,latent_true,obs,ret=False):
 58    b=obs.shape[0]; particles=grid.to(obs.device)[None].expand(b,-1,-1); oo=obs[:,None].expand(b,K,1)
 59    logits=-net(particles.reshape(-1,2),oo.reshape(-1,1)).reshape(b,K)+LOGPRIOR.to(obs.device)
 60    loss=(net(latent_true,obs)+torch.logsumexp(logits,1)).mean()
 61    return (loss,logits,torch.softmax(logits,1)) if ret else loss
 62
 63def posterior_mean(model,obs,idea):
 64    b=obs.shape[0]; p=grid.to(obs.device)[None].expand(b,-1,-1); oo=obs[:,None].expand(b,K,1)
 65    if idea: logits=-torch.einsum('bkd,d->bk',phi(p,oo),model)+LOGPRIOR.to(obs.device)
 66    else: logits=-model(p.reshape(-1,2),oo.reshape(-1,1)).reshape(b,K)+LOGPRIOR.to(obs.device)
 67    return (torch.softmax(logits,1)[...,None]*p).sum(1)
 68
 69def train_one(seed,lr,idea,epochs=28,return_model=False):
 70    seed_all(seed); d=data(seed)
 71    # Bench x is latent coordinate and y is observed energy.
 72    latent=torch.as_tensor(d['xtr'],device=DEVICE); obs=torch.as_tensor(d['ytr'],device=DEVICE)
 73    if idea:
 74        model=nn.Parameter(torch.randn(6,device=DEVICE)*.15); opt=torch.optim.Adam([model],lr=lr)
 75    else:
 76        model=NeuralPotential().to(DEVICE); opt=torch.optim.Adam(model.parameters(),lr=lr)
 77    n=len(obs)
 78    for _ in range(epochs):
 79        order=torch.randperm(n,device=DEVICE)
 80        for j in range(0,n,64):
 81            ix=order[j:j+64]; opt.zero_grad(set_to_none=True)
 82            loss=convex_loss(model,None,latent[ix],obs[ix]) if idea else neural_loss(model,latent[ix],obs[ix])
 83            loss.backward(); opt.step()
 84    lt=torch.as_tensor(d['xte'],device=DEVICE); ot=torch.as_tensor(d['yte'],device=DEVICE)
 85    with torch.no_grad(): metric=((posterior_mean(model,ot,idea)-lt)**2).mean().item()
 86    return (metric,model,d) if return_model else metric
 87
 88GRID_CFG=[{'lr':.01},{'lr':.03},{'lr':.08}]
 89def base_fn(cfg): return lambda s: train_one(s,cfg['lr'],False)
 90def idea_fn(cfg): return lambda s: train_one(s,cfg['lr'],True)
 91
 92def signature(seed,lr):
 93    _,_,d=train_one(seed,lr,False,return_model=True); _,theta,_=train_one(seed,lr,True,return_model=True)
 94    obs=torch.as_tensor(d['yte'][:96],device=DEVICE); true=torch.as_tensor(d['xte'][:96],device=DEVICE)
 95    with torch.no_grad():
 96        _,p,r=convex_loss(theta,None,true,obs,True)
 97        target=phi(true,obs); grad=(target-(r[...,None]*p).sum(1)).mean(0)
 98        mu=(r[...,None]*p).sum(1); c=p-mu[:,None,:]
 99        H=torch.einsum('bk,bkd,bke->de',r,c,c)/len(obs)
100        mineig=float(torch.linalg.eigvalsh((H+H.T)/2)[0])
101    return {'prediction':'trained convex posterior head has covariance Hessian PSD',
102            'observed_gradient_norm':float(grad.norm()),
103            'observed_covariance_min_eigenvalue':mineig,
104            'confirmed':bool(mineig>=-1e-5)}
105
106def main():
107    base=sweep_baseline(base_fn,GRID_CFG)
108    trials=[{'cfg':c,'result':evaluate(idea_fn(c))} for c in GRID_CFG]
109    best=min(trials,key=lambda z:z['result']['mean'])
110    rep=make_report(TRACK,'energy_potential_mlp',base,best['result'],{
111        'idea_sweep':trials,
112        'structural_match':'Registered bounded_energy_regression: 2-D energy surface; latent coordinates are normalized particles conditioned on observed energy.',
113        'mechanism_signature':signature(0,best['cfg']['lr'])})
114    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
115    print(json.dumps(rep,indent=2))
116if __name__=='__main__': main()