Killed-Brownian diffusion score / killed_brownian_experiment.py
Mechanism failed
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()