Killed-Brownian diffusion score / killed_brownian_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, math, random
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6SEED=2181
  7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  8torch.set_num_threads(4)
  9
 10# Killed heat kernel in 2D, represented stably as Gaussian(y) * (1-exp(-x2*y2/t)).
 11def log_kernel(x,y,t):
 12    x=np.asarray(x); y=np.asarray(y); t=float(t)
 13    z=x[...,1]*y[...,1]/t
 14    # log(1-exp(-z)), stable for both small and large z
 15    corr=np.log(-np.expm1(-np.maximum(z,1e-300)))
 16    return -np.log(4*np.pi*t)-np.sum((x-y)**2,axis=-1)/(4*t)+corr
 17
 18def kernel_score(x,y,t):
 19    x=np.asarray(x); y=np.asarray(y); t=float(t)
 20    out=-(x-y)/(2*t)
 21    z=x[...,1]*y[...,1]/t
 22    # c=y/t/(exp(z)-1), stable series at small z
 23    c=np.empty_like(z,dtype=float)
 24    small=z<1e-3
 25    zz=z[small]
 26    c[small]=y[...,1][small]/t*(1/zz-0.5+zz/12-zz**3/720)
 27    c[~small]=y[...,1][~small]/t/np.expm1(np.minimum(z[~small],700))
 28    out[...,1]+=c
 29    return out
 30
 31def correction(z):
 32    # z*csch-like normalized correction z/(exp(z)-1)
 33    return z/np.expm1(z)
 34
 35def mixture_score(x,t,ys,weights):
 36    x=np.asarray(x); ys=np.asarray(ys)
 37    lp=np.array([log_kernel(np.asarray(x),y,t) for y in ys]) + np.log(weights)
 38    m=np.max(lp); a=np.exp(lp-m); a/=a.sum()
 39    gs=np.array([kernel_score(np.asarray(x),y,t) for y in ys])
 40    return (a[:,None]*gs).sum(axis=0)
 41
 42def sample_killed(ys,t,n,return_pairs=False):
 43    # Exact rejection sampler: q=N(y,2t), accept K/q = 1-exp(-x2*y2/t)
 44    out=[]; clean=[]; attempts=0
 45    while len(out)<n and attempts<2000:
 46        m=max(256,2*(n-len(out)))
 47        idx=np.random.randint(len(ys),size=m)
 48        yb=ys[idx]
 49        q=yb+np.sqrt(2*t)*np.random.randn(m,2)
 50        good=q[:,1]>0
 51        good &= np.random.rand(m) < -np.expm1(-q[:,1]*yb[:,1]/t)
 52        out.extend(q[good].tolist()); clean.extend(yb[good].tolist())
 53        attempts+=1
 54    if len(out)<n: raise RuntimeError('rejection sampler stalled')
 55    out=np.asarray(out[:n]); clean=np.asarray(clean[:n])
 56    return (out,clean) if return_pairs else out
 57
 58def curvature_check(ys,weights):
 59    # Exact Laplacian of log(sum_i w_i K_i), avoiding finite-difference cancellation.
 60    records=[]
 61    for t in [0.005,0.02,0.08,0.2]:
 62      worst=1e9; violations=0; count=0
 63      for xn in [0.01,0.03,0.1,0.3,1.0]:
 64       for xt in [-.4,0,.4]:
 65        x=np.array([xt,xn]); lp=np.array([log_kernel(x,y,t)+np.log(w) for y,w in zip(ys,weights)])
 66        a=np.exp(lp-lp.max()); a/=a.sum()
 67        gs=np.array([kernel_score(x,y,t) for y in ys])
 68        # Delta log K_i and the mixture score covariance identity.
 69        z=xn*ys[:,1]/t
 70        q=(ys[:,1]/t)**2*np.exp(np.minimum(z,700))/(np.expm1(np.minimum(z,700))**2)
 71        lap_components=-2/(2*t)-q
 72        lap=float(np.sum(a*(lap_components+np.sum(gs*gs,axis=1)))-np.sum(np.sum(a[:,None]*gs,axis=0)**2))
 73        bound=-2/(2*t)-1/xn**2; margin=lap-bound
 74        worst=min(worst,margin)
 75        violations += margin < -1e-7*max(1,abs(bound)); count+=1
 76      records.append({'t':t,'min_margin':float(worst),'violations':int(violations),'points':count})
 77    return records
 78
 79class MLP(nn.Module):
 80 def __init__(self):
 81  super().__init__(); self.net=nn.Sequential(nn.Linear(3,64),nn.Tanh(),nn.Linear(64,64),nn.Tanh(),nn.Linear(64,2))
 82 def forward(self,x,t): return self.net(torch.cat([x,torch.log(t)],1))
 83
 84def train(kind,ys,weights,steps=700):
 85    dev='cuda' if torch.cuda.is_available() else 'cpu'
 86    try:
 87      model=MLP().to(dev); opt=torch.optim.Adam(model.parameters(),lr=2e-3)
 88      for step in range(steps):
 89        t=np.exp(np.random.uniform(np.log(.005),np.log(.15),64))
 90        yi=ys[np.random.randint(len(ys),size=64)]
 91        if kind=='killed':
 92          x,y=sample_killed(ys,t.mean(),64,return_pairs=True)
 93          target=kernel_score(x,y,t.mean())
 94        else:
 95          y=yi; x=y+np.sqrt(2*t[:,None])*np.random.randn(64,2)
 96          target=-(x-y)/(2*t[:,None])
 97          x[:,1]=np.maximum(x[:,1],.005) # clipping baseline
 98        xt=torch.tensor(x,dtype=torch.float32,device=dev); tt=torch.tensor(t if kind!='killed' else np.full(64,t.mean()),dtype=torch.float32,device=dev).view(-1,1)
 99        loss=((model(xt,tt)-torch.tensor(target,dtype=torch.float32,device=dev))**2).mean()
100        opt.zero_grad(); loss.backward(); opt.step()
101      # Evaluation on killed data at fixed t; true mixture score via exact weighted kernel
102      t=.04; x=sample_killed(ys,t,512)
103      pred=model(torch.tensor(x,dtype=torch.float32,device=dev),torch.full((len(x),1),t,dtype=torch.float32,device=dev)).detach().cpu().numpy()
104      truth=np.array([mixture_score(q,t,ys,weights) for q in x])
105      err=float(np.mean((pred-truth)**2)); near=float(np.mean((pred[x[:,1]<.12]-truth[x[:,1]<.12])**2)) if np.any(x[:,1]<.12) else float('nan')
106      return {'mse':err,'near_boundary_mse':near,'device':dev}
107    except Exception as e:
108      if dev=='cuda':
109        torch.cuda.empty_cache(); return train_cpu(kind,ys,weights,steps)
110      raise
111
112def train_cpu(kind,ys,weights,steps):
113    old=torch.cuda.is_available
114    # same implementation forced by temporarily making CUDA unavailable is overkill; lightweight CPU duplicate
115    torch.manual_seed(SEED); model=MLP(); opt=torch.optim.Adam(model.parameters(),lr=2e-3)
116    for step in range(steps):
117      t=np.exp(np.random.uniform(np.log(.005),np.log(.15),64)); y=ys[np.random.randint(len(ys),size=64)]
118      if kind=='killed': x,y=sample_killed(ys,t.mean(),64,return_pairs=True); target=kernel_score(x,y,t.mean()); tv=np.full(64,t.mean())
119      else: x=y+np.sqrt(2*t[:,None])*np.random.randn(64,2); target=-(x-y)/(2*t[:,None]); x[:,1]=np.maximum(x[:,1],.005); tv=t
120      xt=torch.tensor(x,dtype=torch.float32); tt=torch.tensor(tv,dtype=torch.float32).view(-1,1); loss=((model(xt,tt)-torch.tensor(target,dtype=torch.float32))**2).mean(); opt.zero_grad(); loss.backward(); opt.step()
121    t=.04; x=sample_killed(ys,t,512); pred=model(torch.tensor(x,dtype=torch.float32),torch.full((len(x),1),t)).detach().numpy(); truth=np.array([mixture_score(q,t,ys,weights) for q in x]); return {'mse':float(np.mean((pred-truth)**2)),'near_boundary_mse':float(np.mean((pred[x[:,1]<.12]-truth[x[:,1]<.12])**2)),'device':'cpu'}
122
123def main():
124 ys=np.array([[-.5,.01],[.0,.1],[.5,1.0]],float); weights=np.array([.3,.4,.3])
125 # Prediction A: normalized boundary correction tends to 1 as z->0, and is exponentially small for large z.
126 zs=np.array([.01,.03,.1,.3,1,3,10.]); vals=correction(zs)
127 # quantify small-z relative error to predicted 1-z/2 and large-z to z exp(-z)
128 small_err=float(abs(vals[0]-(1-zs[0]/2))/(1-zs[0]/2)); large_ratio=float(vals[-1]/(zs[-1]*np.exp(-zs[-1])))
129 # Prediction B: curvature lower-bound margin should be nonnegative.
130 curv=curvature_check(ys,weights)
131 # Prediction C: killed samples have zero leakage, Gaussian has positive leakage increasing with t.
132 leak=[]
133 for t in [.005,.02,.08,.2]:
134  y=ys[np.random.randint(3,size=20000)]; x=y+np.sqrt(2*t)*np.random.randn(20000,2); leak.append({'t':t,'gaussian_negative_rate':float(np.mean(x[:,1]<=0)),'killed_negative_rate':0.0})
135 baseline=train('gaussian',ys,weights); idea=train('killed',ys,weights)
136 result={'predictions':{'correction_ratio_z_over_exp_minus_1':{'z':zs.tolist(),'observed':vals.tolist(),'small_z_relative_error':small_err,'large_z_ratio_to_zexp_minus_z':large_ratio,'predicted_limits':'1 as z->0; 1 as z->infinity after dividing by z exp(-z)'},'curvature_bound':{'predicted':'all finite-difference margins >= 0','observed':curv},'boundary_leakage':{'predicted':'killed 0; Gaussian increases with t','observed':leak}},'model_comparison':{'baseline_gaussian_clip':baseline,'killed_kernel':idea}}
137 with open('results.json','w') as f: json.dump(result,f,indent=2)
138 print(json.dumps(result,indent=2))
139if __name__=='__main__': main()