Variance preserving: the chain ends close to a standard Gaussian, whatever the data scale, once \sqrt{\bar{\alpha}_T}\, \mathbf{x}_0 is negligible.
The Closed-Form Marginal, Checked by Simulation
torch.manual_seed(0)mix = d2l.GaussianMixture(means=[[-2.5, -1.5], [2.5, -1.5], [0.0, 2.5]], weights=[0.5, 0.3, 0.2], std=0.5)data, test = mix.sample(2000), mix.sample(2000)T =200beta = torch.linspace(1e-4, 0.1, T)alpha =1- betaalpha_bar = torch.cumprod(alpha, 0)print(f'alpha_bar_T = {alpha_bar[-1]:.1e}: after T steps the data are scaled 'f'by {alpha_bar[-1].sqrt():.3f}')torch.manual_seed(1)x = data.clone()for t inrange(T): # the chain, step by step x = alpha[t].sqrt() * x + beta[t].sqrt() * torch.randn_like(x)if t +1in (10, 50, 200): one_shot = (alpha_bar[t].sqrt() * data+ (1- alpha_bar[t]).sqrt() * torch.randn_like(data))print(f'step {t +1:3d}: chain: mean {[round(m, 3) for m in x.mean(0).tolist()]}, 'f'var {x.var(0).mean():.3f} | one shot: mean 'f'{[round(m, 3) for m in one_shot.mean(0).tolist()]}, 'f'var {one_shot.var(0).mean():.3f}')
alpha_bar_T = 3.2e-05: after T steps the data are scaled by 0.006
step 10: chain: mean [-0.454, -0.76], var 3.838 | one shot: mean [-0.457, -0.766], var 3.838
step 50: chain: mean [-0.338, -0.563], var 2.554 | one shot: mean [-0.344, -0.553], var 2.554
step 200: chain: mean [-0.036, -0.021], var 0.990 | one shot: mean [-0.003, 0.001], var 0.966
Any step is one Gaussian draw from the data; training never simulates the chain.
A score step of size \beta_t, noise \sigma_t, and a rescaling: a Langevin-like step whose drift is doubled and whose schedule the forward process fixes.
The Learned Predictor against the Optimum
def eps_star(x_t, t): # the optimal noise predictor at step index t a = alpha_bar[t]return-(1- a).sqrt() * mix.score(x_t, scale=a.sqrt().item(), sigma=(1- a).sqrt().item())torch.manual_seed(3)print(f'{"step":>4}{"E||eps_theta - eps*||^2":>24}{"E||eps - eps*||^2":>18} 'f'{"E||eps - eps_theta||^2":>23}')with torch.no_grad():for t in (9, 49, 99, 199): eps = torch.randn_like(test) x_t = alpha_bar[t].sqrt() * test + (1- alpha_bar[t]).sqrt() * eps pred, best = net(x_t, torch.full((len(test),), t)), eps_star(x_t, t)print(f'{t +1:>4}{((pred - best) **2).sum(1).mean():>24.3f} 'f'{((eps - best) **2).sum(1).mean():>18.3f} 'f'{((eps - pred) **2).sum(1).mean():>23.3f}')
A mean squared gap to \boldsymbol{\epsilon}^\star of at most a few hundredths at each step checked; the loss exceeds the irreducible noise variance by less than a tenth.
Ancestral Sampling Recovers the Data
Two hundred steps from a Gaussian cloud: no structure at half the schedule, the mixture’s layout by t = 50, detail at the end. The sampler adds no parameters beyond the forward schedule and \sigma_t^2 = \beta_t.
Recap
Forward: a fixed variance-preserving chain with closed-form marginals.
Reverse: Gaussian transitions with learned means; the bound splits into per-step Kullback–Leibler terms, and training uses its unweighted simplification.
Noise prediction turns every term into a regression; the simple loss is reweighted denoising score matching.
Sampling is Langevin-like with a fixed schedule. Next: the same model on images, with a U-Net and class conditioning.