Diffusion Models & DDPM Mathematics
Derive the closed-form forward Gaussian transition in DDPM, the reverse posterior mean formula, Classifier-Free Guidance equations, and zero-dependency implementations in Python and Rust.
Generative Adversarial Networks (GANs) rely on an adversarial minimax equilibrium between generator and discriminator, which is prone to training instability, mode collapse, and vanishing gradients.
Denoising Diffusion Probabilistic Models (DDPM) (Sohl-Dickstein et al. 2015, Ho et al. 2020) model generation as a gradual stochastic thermodynamic process: systematically destroying signal via a forward Gaussian diffusion process and training a neural network to invert the trajectory step by step.
Directly sample any noisy timestep t in O(1) constant time without executing prior steps.
1. The Forward (Noising) Diffusion Process#
Given clean data distribution , the forward diffusion process adds small Gaussian noise across discrete timesteps according to variance schedule :
At timestep , all original structure is eradicated, and is indistinguishable from standard isotropic Gaussian noise .
2. Derivation of the Closed-Form Arbitrary Timestep Sample#
Simulating forward transitions step by step would require sequential computations. We derive how to sample directly from in constant time.
Let and define cumulative product .
Step 1: Express $x_t$ via Reparameterization
From the transition kernel definition:
Step 2: Unroll One Step Backward
Substitute into :
Step 3: Merge Two Independent Gaussian Variables
Recall that if and are independent random variables, their sum is Gaussian with summed variances:
Here, the total variance of the combined noise terms is:
Therefore:
Step 4: Inductive Generalization to Timestep $t$
Unrolling recursively back to :
With :
This establishes that , allowing instantaneous noising at arbitrary timesteps during training.
3. Reverse Process & The Simplified Loss Objective#
The true reverse conditional distribution is a Gaussian whose mean evaluates via Bayes' Rule to:
Substituting eliminates , leaving the reverse mean as a function of the injected noise :
Rather than predicting the clean image directly, the neural network is trained to predict the noise vector added at step :
Ho et al. proved that omitting the complex variational weighting coefficients produces superior sample visual quality, yielding the Simplified DDPM Loss:
4. Classifier-Free Guidance (CFG)#
To direct generation using text conditioning without training a separate noisy image classifier, Ho and Salimans (2021) introduced Classifier-Free Guidance (CFG).
During training, condition is dropped randomly (replaced by null condition ) with probability .
During inference sampling, the predicted noise is computed as an extrapolation between conditional and unconditional estimates:
Where is the Guidance Scale:
- When : Standard conditional generation.
- When : Amplifies alignment with prompt by pushing generation along the gradient direction away from unconditional space.
5. From-Scratch Implementation: Zero Dependencies#
The following implementations compute the DDPM Closed-Form Forward Transition and the Reverse Sampling Step from scratch in pure Python and pure Rust.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475import math import random from typing import List, Tuple class DDPMPrimitives: def __init__(self, timesteps: int = 100, beta_start: float = 0.0001, beta_end: float = 0.02): self.timesteps = timesteps # Linear variance schedule self.betas = [ beta_start + (beta_end - beta_start) * (t / (timesteps - 1)) for t in range(timesteps) ] self.alphas = [1.0 - b for b in self.betas] # Cumulative products: alpha_bar_t self.alpha_bars = [] prod = 1.0 for a in self.alphas: prod *= a self.alpha_bars.append(prod) def q_sample(self, x_0: List[float], t: int) -> Tuple[List[float], List[float]]: # x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon alpha_bar = self.alpha_bars[t] sqrt_alpha_bar = math.sqrt(alpha_bar) sqrt_one_minus_alpha_bar = math.sqrt(1.0 - alpha_bar) noise = [random.gauss(0.0, 1.0) for _ in range(len(x_0))] x_t = [ sqrt_alpha_bar * x_0[i] + sqrt_one_minus_alpha_bar * noise[i] for i in range(len(x_0)) ] return x_t, noise def p_sample_step( self, x_t: List[float], t: int, predicted_noise: List[float] ) -> List[float]: # Reverse step: mu_theta = (1 / sqrt(alpha_t)) * (x_t - (beta_t / sqrt(1 - alpha_bar_t)) * eps_pred) beta_t = self.betas[t] alpha_t = self.alphas[t] alpha_bar_t = self.alpha_bars[t] coeff = 1.0 / math.sqrt(alpha_t) noise_coeff = beta_t / math.sqrt(1.0 - alpha_bar_t) mean = [ coeff * (x_t[i] - noise_coeff * predicted_noise[i]) for i in range(len(x_t)) ] if t == 0: return mean # Add stochastic noise sigma_t * z sigma_t = math.sqrt(beta_t) return [mean[i] + sigma_t * random.gauss(0.0, 1.0) for i in range(len(x_t))] if __name__ == "__main__": random.seed(42) ddpm = DDPMPrimitives(timesteps=100) # 4-dimensional clean representation x_0 x_clean = [1.0, -1.0, 0.5, -0.5] print("Original Clean Signal x_0:", x_clean) # Directly jump to timestep 50 in O(1) time x_50, noise = ddpm.q_sample(x_clean, t=50) print("\nNoisy Sample at t=50 (Closed-Form):", [round(v, 4) for v in x_50]) # Simulate reverse step x_49 = ddpm.p_sample_step(x_50, t=50, predicted_noise=noise) print("Denoised Step to t=49:", [round(v, 4) for v in x_49])
6. Active Knowledge Checks#
What mathematical identity allows DDPM to jump directly to any arbitrary noisy timestep x_t in O(1) time without simulating intermediate steps?
In Classifier-Free Guidance (CFG), what is the function of the guidance scale parameter s in the equation eps_tilde = eps_uncond + s * (eps_cond - eps_uncond)?