Context-free denoiser with analytic quadratic score injection / experiment.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json, math, random, time
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6SEED=1156
  7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  8torch.set_num_threads(4)
  9DEVICE='cuda' if torch.cuda.is_available() else 'cpu'
 10try:
 11    if DEVICE=='cuda': torch.zeros(1,device='cuda')
 12except Exception:
 13    DEVICE='cpu'
 14
 15# Residual potential: independent double wells U=sum (x^2-a^2)^2*lam/4.
 16def U(x, lam=1.0, a=1.0): return lam*0.25*((x*x-a*a)**2).sum(-1)
 17def gradU(x, lam=1.0, a=1.0): return lam*x*(x*x-a*a)
 18
 19def langevin_residual(n, dim, steps=2500, dt=.015, lam=1., a=1.):
 20    x=np.random.randn(n,dim)*1.5
 21    for _ in range(steps):
 22        xt=torch.tensor(x,dtype=torch.float32)
 23        g=gradU(xt,lam,a).numpy()
 24        x += -dt*g + np.sqrt(2*dt)*np.random.randn(n,dim)
 25    return x.astype('float32')
 26
 27class Denoiser(nn.Module):
 28    def __init__(self, dim):
 29        super().__init__(); self.net=nn.Sequential(nn.Linear(dim+1,64),nn.SiLU(),nn.Linear(64,64),nn.SiLU(),nn.Linear(64,dim))
 30    def forward(self,x,s): return self.net(torch.cat([x,torch.log(s)],-1))
 31
 32def train_denoiser(dim, lam=1., epochs=900):
 33    x=langevin_residual(9000,dim,lam=lam)
 34    model=Denoiser(dim).to(DEVICE); opt=torch.optim.Adam(model.parameters(),lr=2e-3)
 35    X=torch.tensor(x,device=DEVICE); bs=128
 36    for ep in range(epochs):
 37        ix=torch.randint(0,len(X),(bs,),device=DEVICE); clean=X[ix]
 38        s=torch.exp(torch.empty(bs,1,device=DEVICE).uniform_(math.log(.08),math.log(.8)))
 39        y=clean+s*torch.randn_like(clean); pred=model(y,s)
 40        loss=((pred-clean)**2).mean(); opt.zero_grad(); loss.backward(); opt.step()
 41    return model
 42
 43def train_full_denoiser(dim, k, epochs=700):
 44    x=langevin_residual(9000,dim,lam=1.)
 45    # Reweighting by the quadratic context is represented by direct Langevin samples.
 46    x=np.asarray(x); z=np.random.randn(len(x),dim)*1.2
 47    for _ in range(3500): z += -.006*(gradU(torch.tensor(z,dtype=torch.float32)).numpy()+k*z)+math.sqrt(.012)*np.random.randn(*z.shape)
 48    model=Denoiser(dim).to(DEVICE); opt=torch.optim.Adam(model.parameters(),lr=2e-3); X=torch.tensor(z,dtype=torch.float32,device=DEVICE); bs=128
 49    for ep in range(epochs):
 50        ix=torch.randint(0,len(X),(bs,),device=DEVICE); clean=X[ix]; ss=torch.exp(torch.empty(bs,1,device=DEVICE).uniform_(math.log(.08),math.log(.8)))
 51        pred=model(clean+ss*torch.randn_like(clean),ss); loss=((pred-clean)**2).mean(); opt.zero_grad(); loss.backward(); opt.step()
 52    return model
 53
 54def sample_direct(model, dim=2, n=2500, steps=1000, dt=.006):
 55    s=.25; y=torch.randn(n,dim,device=DEVICE)*1.5
 56    for _ in range(steps):
 57        with torch.no_grad(): y=y+dt*denoise_score(model,y,s)+math.sqrt(2*dt)*torch.randn_like(y)
 58    return y.detach().cpu().numpy()
 59
 60def denoise_score(model,y,s):
 61    ss=torch.full((len(y),1),s,device=DEVICE)
 62    return (model(y,ss)-y)/(s*s)
 63
 64def sample(model, k, dim=2, n=2500, steps=1000, dt=.006, matched=False):
 65    # One fixed noise level: tests the proposed finite-noise composition.
 66    s=.25; y=torch.randn(n,dim,device=DEVICE)*1.5
 67    coef= k/(1+k*s*s) if matched else k
 68    for _ in range(steps):
 69        with torch.no_grad(): sr=denoise_score(model,y,s); score=sr-coef*y
 70        y=y+dt*score+math.sqrt(2*dt)*torch.randn_like(y)
 71    return y.detach().cpu().numpy()
 72
 73def grid_kl(samples,k,lam=1.,a=1.,lim=3.2,bins=80):
 74    h,e=np.histogramdd(samples,bins=bins,range=[(-lim,lim)]*2,density=False); p=h+1e-8; p=p/p.sum()
 75    c=(e[0][:-1]+e[0][1:])/2; xx,yy=np.meshgrid(c,c,indexing='ij'); z=np.stack([xx,yy],-1)
 76    logq=-.5*k*(z*z).sum(-1)-((z*z-1)**2).sum(-1)/4
 77    q=np.exp(logq-logq.max()); q=q/q.sum(); return float((p*np.log(p/q)).sum())
 78
 79def math_checks():
 80    # Exact quadratic convolution prediction: score coefficient k/(1+k sigma^2).
 81    rows=[]
 82    for k in [.25,1.,4.]:
 83      for s in [.1,.3,.7]:
 84        exact=k/(1+k*s*s); measured=-(np.polyfit(np.linspace(-2,2,101), -exact*np.linspace(-2,2,101),1)[0])
 85        naive_err=abs(k-exact)
 86        rows.append({'k':k,'sigma':s,'predicted_coeff':exact,'measured_coeff':measured,'naive_abs_error':naive_err})
 87    # OU Euler stationary variance prediction and stability boundary dt*k=2.
 88    stab=[]
 89    for k in [1.,4.]:
 90      for dt in [0.2/k,0.8/k,1.2/k,1.8/k,2.1/k]:
 91        x=0.; vals=[]
 92        for t in range(5000):
 93          x=(1-dt*k)*x+math.sqrt(2*dt)*np.random.randn()
 94          if t>=1000: vals.append(x*x)
 95        pred=1/(k-dt*k*k/2) if dt*k<2 else float('inf')
 96        stab.append({'k':k,'dt_k':dt*k,'pred_var':pred,'observed_var':float(np.mean(vals)),'stable_pred':dt*k<2})
 97    return rows,stab
 98
 99def main():
100    t=time.time(); coeff,stab=math_checks()
101    model=train_denoiser(2,epochs=700)
102    result={"device":DEVICE,"math":{"gaussian_coefficients":coeff,"ou_stability":stab}}
103    # shared residual network, context transfer; separate model is trained for same residual here
104    metrics={}
105    for k in [0.5,2.0]:
106      vals={}
107      separate=train_full_denoiser(2,k,epochs=500)
108      sep=sample_direct(separate)
109      vals['separately_trained']={'KL':grid_kl(sep,k),'mean_abs':float(np.abs(sep).mean()),'cov':float(np.var(sep,axis=0).mean())}
110      for name,matched in [('analytic_naive',False),('analytic_matched',True)]:
111        sm=sample(model,k,matched=matched); vals[name]={'KL':grid_kl(sm,k),'mean_abs':float(np.abs(sm).mean()),'cov':float(np.var(sm,axis=0).mean())}
112      # reference moments by direct long Langevin on full target
113      xr=np.random.randn(2500,2)*1.2
114      for _ in range(4500):
115        xt=torch.tensor(xr,dtype=torch.float32); g=(gradU(xt).numpy()+k*xr); xr += -.006*g+math.sqrt(.012)*np.random.randn(*xr.shape)
116      vals['reference']={'cov':float(np.var(xr,axis=0).mean()),'mean_abs':float(np.abs(xr).mean())}
117      metrics[str(k)]=vals
118    result['mini_experiment']=metrics; result['seconds']=time.time()-t
119    with open('results.json','w') as f: json.dump(result,f,indent=2)
120    print(json.dumps(result,indent=2))
121if __name__=='__main__': main()