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.

04. Generative AI, Large Language Models & Diffusion9 min read

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.

Closed-Form SamplingDiffusion Theory
x_t = sqrt(alpha_bar_t) x_0 + sqrt(1 - alpha_bar_t) epsilon

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 x0∼q(x0)x_0 \sim q(x_0), the forward diffusion process adds small Gaussian noise across TT discrete timesteps according to variance schedule β1,β2,…,βT∈(0,1)\beta_1, \beta_2, \dots, \beta_T \in (0, 1):

q(xt∣xt−1)=N(xt;1−βtxt−1,βtI)q(x_t | x_{t-1}) = \mathcal{N}\left( x_t; \sqrt{1 - \beta_t} x_{t-1}, \beta_t I \right)
Architecture Diagram
Rendering diagram...

At timestep T≈1000T \approx 1000, all original structure is eradicated, and xTx_T is indistinguishable from standard isotropic Gaussian noise N(0,I)\mathcal{N}(0, I).

2. Derivation of the Closed-Form Arbitrary Timestep Sample#

Simulating tt forward transitions step by step would require tt sequential computations. We derive how to sample xtx_t directly from x0x_0 in O(1)O(1) constant time.

Let αt=1−βt\alpha_t = 1 - \beta_t and define cumulative product αˉt=∏s=1tαs\bar{\alpha}_t = \prod_{s=1}^t \alpha_s.

1

Step 1: Express $x_t$ via Reparameterization

From the transition kernel definition:

xt=αtxt−1+1−αtϵt−1,where ϵt−1∼N(0,I)x_t = \sqrt{\alpha_t} x_{t-1} + \sqrt{1 - \alpha_t} \epsilon_{t-1}, \quad \text{where } \epsilon_{t-1} \sim \mathcal{N}(0, I)
2

Step 2: Unroll One Step Backward

Substitute xt−1=αt−1xt−2+1−αt−1ϵt−2x_{t-1} = \sqrt{\alpha_{t-1}} x_{t-2} + \sqrt{1 - \alpha_{t-1}} \epsilon_{t-2} into xtx_t:

xt=αt(αt−1xt−2+1−αt−1ϵt−2)+1−αtϵt−1x_t = \sqrt{\alpha_t} \Big( \sqrt{\alpha_{t-1}} x_{t-2} + \sqrt{1 - \alpha_{t-1}} \epsilon_{t-2} \Big) + \sqrt{1 - \alpha_t} \epsilon_{t-1}
xt=αtαt−1xt−2+αt(1−αt−1)ϵt−2+1−αtϵt−1x_t = \sqrt{\alpha_t \alpha_{t-1}} x_{t-2} + \sqrt{\alpha_t (1 - \alpha_{t-1})} \epsilon_{t-2} + \sqrt{1 - \alpha_t} \epsilon_{t-1}
3

Step 3: Merge Two Independent Gaussian Variables

Recall that if X∼N(0,σ12I)X \sim \mathcal{N}(0, \sigma_1^2 I) and Y∼N(0,σ22I)Y \sim \mathcal{N}(0, \sigma_2^2 I) are independent random variables, their sum is Gaussian with summed variances:

X+Y∼N(0,(σ12+σ22)I)X + Y \sim \mathcal{N}\Big(0, (\sigma_1^2 + \sigma_2^2) I\Big)

Here, the total variance of the combined noise terms is:

σtotal2=(αt(1−αt−1)) ⁣2+(1−αt) ⁣2=αt(1−αt−1)+1−αt\sigma_{\text{total}}^2 = \left( \sqrt{\alpha_t (1 - \alpha_{t-1})} \right)^{\!2} + \left( \sqrt{1 - \alpha_t} \right)^{\!2} = \alpha_t(1 - \alpha_{t-1}) + 1 - \alpha_t
=αt−αtαt−1+1−αt=1−αtαt−1= \alpha_t - \alpha_t \alpha_{t-1} + 1 - \alpha_t = 1 - \alpha_t \alpha_{t-1}

Therefore:

xt=αtαt−1xt−2+1−αtαt−1ϵ∗,where ϵ∗∼N(0,I)x_t = \sqrt{\alpha_t \alpha_{t-1}} x_{t-2} + \sqrt{1 - \alpha_t \alpha_{t-1}} \epsilon^*, \quad \text{where } \epsilon^* \sim \mathcal{N}(0, I)
4

Step 4: Inductive Generalization to Timestep $t$

Unrolling recursively back to x0x_0:

xt=∏s=1tαs x0+1−∏s=1tαs ϵx_t = \sqrt{\prod_{s=1}^t \alpha_s} \, x_0 + \sqrt{1 - \prod_{s=1}^t \alpha_s} \, \epsilon

With αˉt=∏s=1tαs\bar{\alpha}_t = \prod_{s=1}^t \alpha_s:

xt=αˉtx0+1−αˉtϵ,where ϵ∼N(0,I)x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon, \quad \text{where } \epsilon \sim \mathcal{N}(0, I)

This establishes that q(xt∣x0)=N(xt;αˉtx0,(1−αˉt)I)q(x_t | x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t} x_0, (1 - \bar{\alpha}_t) I), allowing instantaneous noising at arbitrary timesteps during training.

3. Reverse Process & The Simplified Loss Objective#

The true reverse conditional distribution q(xt−1∣xt,x0)q(x_{t-1} | x_t, x_0) is a Gaussian whose mean evaluates via Bayes' Rule to:

μ~t(xt,x0)=αˉt−1βt1−αˉtx0+αt(1−αˉt−1)1−αˉtxt\tilde{\mu}_t(x_t, x_0) = \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1 - \bar{\alpha}_t} x_0 + \frac{\sqrt{\alpha_t}(1 - \bar{\alpha}_{t-1})}{1 - \bar{\alpha}_t} x_t

Substituting x0=xt−1−αˉtϵαˉtx_0 = \frac{x_t - \sqrt{1 - \bar{\alpha}_t}\epsilon}{\sqrt{\bar{\alpha}_t}} eliminates x0x_0, leaving the reverse mean as a function of the injected noise ϵ\epsilon:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,t))\mu_\theta(x_t, t) = \frac{1}{\sqrt{\alpha_t}} \left( x_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}} \epsilon_\theta(x_t, t) \right)

Rather than predicting the clean image x0x_0 directly, the neural network ϵθ(xt,t)\epsilon_\theta(x_t, t) is trained to predict the noise vector ϵ\epsilon added at step tt:

Architecture Diagram
Rendering diagram...

Ho et al. proved that omitting the complex variational weighting coefficients produces superior sample visual quality, yielding the Simplified DDPM Loss:

Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥22]L_{\text{simple}}(\theta) = \mathbb{E}_{t, x_0, \epsilon}\left[ \|\epsilon - \epsilon_\theta(x_t, t)\|_2^2 \right]

4. Classifier-Free Guidance (CFG)#

To direct generation using text conditioning cc without training a separate noisy image classifier, Ho and Salimans (2021) introduced Classifier-Free Guidance (CFG).

During training, condition cc is dropped randomly (replaced by null condition ∅\emptyset) with probability puncond≈10%p_{\text{uncond}} \approx 10\%.

During inference sampling, the predicted noise is computed as an extrapolation between conditional and unconditional estimates:

ϵ~θ(xt,c)=ϵθ(xt,∅)+s⋅(ϵθ(xt,c)−ϵθ(xt,∅))\tilde{\epsilon}_\theta(x_t, c) = \epsilon_\theta(x_t, \emptyset) + s \cdot \Big( \epsilon_\theta(x_t, c) - \epsilon_\theta(x_t, \emptyset) \Big)

Where s≥1.0s \ge 1.0 is the Guidance Scale:

  • When s=1.0s = 1.0: Standard conditional generation.
  • When s>1.0s > 1.0: Amplifies alignment with prompt cc by pushing generation along the gradient direction ϵθ(xt,c)−ϵθ(xt,∅)\epsilon_\theta(x_t, c) - \epsilon_\theta(x_t, \emptyset) 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import 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#

Concept check

What mathematical identity allows DDPM to jump directly to any arbitrary noisy timestep x_t in O(1) time without simulating intermediate steps?

Concept check

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)?