AI Knowledge Tree

Diffusion models

Define the forward Gaussian noising process, derive the reverse-time denoising objective, and train DDPM.

⏱ 32 minPrerequisites:Variational autoencoders

Diffusion models are the dominant generative paradigm of the 2020s. The recipe: add Gaussian noise to data in many small steps (the forward process), then learn to reverse the noising step by step. At inference, start from pure noise and denoise to a clean sample. The math is clean (variational like a VAE, hierarchical with shared parameters), the training is stable, and the samples are state-of-the-art across images, audio, and video.

The forward process

Given a data sample x0x_0, define a sequence x0,x1,…,xTx_0, x_1, \dots, x_T by gradually adding Gaussian noise:

q(xt∣xt−1)=N(xt∣1−βt xt−1, βtI),q(x_t \mid x_{t-1}) = \mathcal{N}(x_t \mid \sqrt{1 - \beta_t}\, x_{t-1},\ \beta_t I),

with noise schedule βt∈(0,1)\beta_t \in (0, 1). The marginals have a closed form:

q(xt∣x0)=N(xt∣αˉt x0, (1−αˉt)I),q(x_t \mid x_0) = \mathcal{N}(x_t \mid \sqrt{\bar\alpha_t}\, x_0,\ (1 - \bar\alpha_t) I),

where αt=1−βt\alpha_t = 1 - \beta_t and αˉt=∏s=1tαs\bar\alpha_t = \prod_{s=1}^t \alpha_s. By T≈1000T \approx 1000 steps with a reasonable schedule, αˉT≈0\bar\alpha_T \approx 0, and xTx_T is nearly pure noise.

The single number αˉt\bar\alpha_t controls the signal-to-noise mix at step tt: the clean sample enters with weight αˉt\sqrt{\bar\alpha_t} and the noise with weight 1−αˉt\sqrt{1 - \bar\alpha_t}. On the standard linear schedule (T=1000T = 1000, βt\beta_t from 10−410^{-4} to 0.020.02) those weights trace the whole range. At t=1t = 1, αˉt≈0.9999\bar\alpha_t \approx 0.9999: signal weight ≈1.000\approx 1.000, noise weight ≈0.010\approx 0.010, the image barely perturbed. At t=500t = 500, αˉt≈0.079\bar\alpha_t \approx 0.079: signal ≈0.28\approx 0.28, noise ≈0.96\approx 0.96, half-melted. At t=1000t = 1000, αˉt≈4×10−5\bar\alpha_t \approx 4 \times 10^{-5}: signal ≈0.006\approx 0.006, noise ≈1.000\approx 1.000, indistinguishable from a draw of N(0,I)\mathcal{N}(0, I). The schedule is just this monotone slide from all-signal to all-noise.

A key reparameterization:

xt=αˉt x0+1−αˉt ϵ,ϵ∼N(0,I).x_t = \sqrt{\bar\alpha_t}\, x_0 + \sqrt{1 - \bar\alpha_t}\, \epsilon, \qquad \epsilon \sim \mathcal{N}(0, I).

So at any timestep, xtx_t is a known interpolation between x0x_0 and random noise.

The reverse process

The forward process is fixed; the reverse process is learned:

pθ(xt−1∣xt)=N(xt−1∣μθ(xt,t), Σθ(xt,t)).p_\theta(x_{t-1} \mid x_t) = \mathcal{N}(x_{t-1} \mid \mu_\theta(x_t, t),\ \Sigma_\theta(x_t, t)).

To sample, start at xT∼N(0,I)x_T \sim \mathcal{N}(0, I) and apply the learned reverse step TT times: xT→xT−1→⋯→x0x_T \to x_{T-1} \to \dots \to x_0.

For Gaussian qq with small steps, the reverse process is also nearly Gaussian. The model just needs to predict the mean (and optionally the variance).

The DDPM objective

Ho et al. (2020). After algebra on the ELBO, the training objective reduces to a stunningly simple form[1]:

LDDPM=Et,x0,ϵ ⁣[∥ϵ−ϵθ(xt,t)∥2].\mathcal{L}_{\text{DDPM}} = \mathbb{E}_{t, x_0, \epsilon}\!\left[\lVert \epsilon - \epsilon_\theta(x_t, t)\rVert^2\right].

The model ϵθ\epsilon_\theta takes a noisy sample xtx_t and the timestep tt and predicts the noise that was added. Training:

  1. Sample a clean data point x0x_0.
  2. Sample a timestep t∼Uniform⁡(1,T)t \sim \operatorname{Uniform}(1, T).
  3. Sample noise ϵ∼N(0,I)\epsilon \sim \mathcal{N}(0, I).
  4. Compute xt=αˉt x0+1−αˉt ϵx_t = \sqrt{\bar\alpha_t}\, x_0 + \sqrt{1 - \bar\alpha_t}\,\epsilon.
  5. Loss: ∥ϵ−ϵθ(xt,t)∥2\lVert \epsilon - \epsilon_\theta(x_t, t)\rVert^2.

Why it works: the noise prediction task is well-posed across timesteps (easy when tt is small, hard when tt is large), and the model gets a diverse curriculum of noise levels by sampling tt uniformly.

Sampling (DDPM)

Given the learned ϵθ\epsilon_\theta:

xt−1=1αt ⁣(xt−βt1−αˉtϵθ(xt,t))+σt z,x_{t-1} = \frac{1}{\sqrt{\alpha_t}}\!\left(x_t - \frac{\beta_t}{\sqrt{1 - \bar\alpha_t}}\epsilon_\theta(x_t, t)\right) + \sigma_t\, z,

with z∼N(0,I)z \sim \mathcal{N}(0, I). Iterate from t=Tt = T down to t=1t = 1.

The reverse step takes the current noisy sample, predicts the noise to remove, and applies that estimate plus a smaller amount of fresh noise (except at t=1t = 1).

A typical TT is 1000 for DDPM, meaning 1000 forward passes per sample. This is slow; the next sections cover faster samplers.

DDIM and other faster samplers

Song et al. (2021). DDIM generalizes the sampling step:

xt−1=αˉt−1 x^0+1−αˉt−1−σt2 ϵθ+σtz,x_{t-1} = \sqrt{\bar\alpha_{t-1}}\, \hat x_0 + \sqrt{1 - \bar\alpha_{t-1} - \sigma_t^2}\, \epsilon_\theta + \sigma_t z,

where x^0=(xt−1−αˉt ϵθ)/αˉt\hat x_0 = (x_t - \sqrt{1 - \bar\alpha_t}\,\epsilon_\theta)/\sqrt{\bar\alpha_t} is the predicted clean sample.

When σt=0\sigma_t = 0, the process becomes deterministic, and you can skip many timesteps without losing quality. DDIM can sample in 10-50 steps instead of 1000.

Newer samplers (DPM-Solver, EDM, consistency distillation) push this further to 1-8 steps per sample.

What the model actually learns

The denoiser ϵθ(xt,t)\epsilon_\theta(x_t, t) is implicitly the score function of the noisy data:

ϵθ(xt,t)≈−1−αˉt ∇xlog⁡pt(xt).\epsilon_\theta(x_t, t) \approx -\sqrt{1 - \bar\alpha_t}\,\nabla_x \log p_t(x_t).

The score is the gradient of the log-density of the noisy data. The reverse process is then gradient ascent on the noisy log-density: at each step, move in the direction the data is more likely.

This connection (diffusion = score-based generative model) was made rigorous by Song & Ermon and gives a unified view of DDPM and the continuous-time score-based formulation[2].

Conditional generation

For class-conditional or text-conditional generation, concatenate the condition cc to the input of the denoiser: ϵθ(xt,t,c)\epsilon_\theta(x_t, t, c).

Classifier-free guidance (CFG) is the dominant technique for strengthening conditioning:

ϵ~(xt,t,c)=(1+w) ϵθ(xt,t,c)−w ϵθ(xt,t,∅),\tilde \epsilon(x_t, t, c) = (1 + w)\, \epsilon_\theta(x_t, t, c) - w\, \epsilon_\theta(x_t, t, \emptyset),

where ∅\emptyset is "no condition" (trained by randomly dropping the condition during training). Higher ww (typically 7-15) produces sharper, more on-prompt samples at the cost of diversity.

What diffusion gave the field

  • Stable training at scale.
  • State-of-the-art sample quality on images, audio, and video.
  • Strong conditional generation via cross-attention.
  • A clean likelihood-bound objective that scales with compute.

The next two lessons cover the productionized form (latent diffusion) and the modern continuous-time generalization (flow matching).

Forward diffusion: the signal-retention factor ᾱ_t decays as the per-step noise β_t ramps up (left), melting the two-mode data distribution into a standard Gaussian (right).
Forward diffusion: the signal-retention factor ᾱ_t decays as the per-step noise β_t ramps up (left), melting the two-mode data distribution into a standard Gaussian (right).

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

T = 100                                                # diffusion timesteps
beta = np.linspace(1e-4, 0.02, T)                      # linear schedule
alpha = 1.0 - beta
alpha_bar = np.cumprod(alpha)

# Forward process: closed form
def forward(x0, t, rng):
    """Sample x_t given x_0 and timestep t (1-indexed)."""
    eps = rng.normal(size=x0.shape)
    return np.sqrt(alpha_bar[t - 1]) * x0 + np.sqrt(1 - alpha_bar[t - 1]) * eps, eps

# 1D toy: data is mixture of two Gaussians
x0 = np.concatenate([rng.normal(-2, 0.3, 500), rng.normal(2, 0.3, 500)])
print("forward process: noise grows with t")
for t in [1, 25, 50, 75, 100]:
    xt, _ = forward(x0, t, rng)
    print(f"  t={t:>3}: x_t mean={xt.mean():+.3f}, std={xt.std():.3f}")

# A perfect denoiser would output the true epsilon; here mock with a learned-fit
# Fit a tiny linear "denoiser" that maps x_t to noise: epsilon_pred = a * x_t + b
A = np.zeros(T); B = np.zeros(T)
for t in range(1, T + 1):
    xt, eps = forward(x0, t, rng)
    # OLS for one-dim eps = a * x + b
    a = np.cov(xt, eps)[0, 1] / np.var(xt)
    b = eps.mean() - a * xt.mean()
    A[t - 1] = a; B[t - 1] = b

# DDPM sampling from this naive denoiser
def sample(n, A, B, rng):
    x = rng.normal(size=n)                              # start from N(0, 1)
    for t in range(T, 0, -1):
        eps_pred = A[t - 1] * x + B[t - 1]
        mu = (x - beta[t - 1] / np.sqrt(1 - alpha_bar[t - 1]) * eps_pred) / np.sqrt(alpha[t - 1])
        if t > 1:
            x = mu + np.sqrt(beta[t - 1]) * rng.normal(size=n)
        else:
            x = mu
    return x

samples = sample(2000, A, B, rng)
print(f"\nsampled distribution: mean={samples.mean():.3f}, std={samples.std():.3f}")
print(f"true data:           mean={x0.mean():.3f}, std={x0.std():.3f}")

The forward process visibly noisifies the data; the naive linear denoiser plus DDPM sampling recovers a sample distribution close to the original.

Exercises

Exercise 1. The forward process is q(xt∣xt−1)=N(1−βt xt−1, βtI)q(x_t \mid x_{t-1}) = \mathcal{N}(\sqrt{1 - \beta_t}\, x_{t-1},\ \beta_t I) with αt=1−βt\alpha_t = 1 - \beta_t. Show that the marginal given x0x_0 is q(xt∣x0)=N(αˉt x0, (1−αˉt)I)q(x_t \mid x_0) = \mathcal{N}(\sqrt{\bar\alpha_t}\, x_0,\ (1 - \bar\alpha_t) I) with αˉt=∏s=1tαs\bar\alpha_t = \prod_{s=1}^t \alpha_s, by composing two steps and inducting.

Solution

Step 1. Write each step in reparameterized form: xt=αt xt−1+1−αt ϵtx_t = \sqrt{\alpha_t}\, x_{t-1} + \sqrt{1 - \alpha_t}\,\epsilon_t with independent ϵt∼N(0,I)\epsilon_t \sim \mathcal{N}(0, I).

Step 2. Compose two steps. Substitute xt−1=αt−1 xt−2+1−αt−1 ϵt−1x_{t-1} = \sqrt{\alpha_{t-1}}\,x_{t-2} + \sqrt{1 - \alpha_{t-1}}\,\epsilon_{t-1}:

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

Step 3. The last two terms are independent zero-mean Gaussians, so they sum to a single Gaussian whose variance is the sum of variances: αt(1−αt−1)+(1−αt)=1−αtαt−1\alpha_t(1 - \alpha_{t-1}) + (1 - \alpha_t) = 1 - \alpha_t\alpha_{t-1}. Hence xt=αtαt−1 xt−2+1−αtαt−1 ϵˉx_t = \sqrt{\alpha_t\alpha_{t-1}}\,x_{t-2} + \sqrt{1 - \alpha_t\alpha_{t-1}}\,\bar\epsilon with ϵˉ∼N(0,I)\bar\epsilon \sim \mathcal{N}(0, I).

Step 4. Induct: each composition multiplies the signal coefficient by the next α\alpha and leaves the total variance as one minus that product. After tt steps, xt=αˉt x0+1−αˉt ϵx_t = \sqrt{\bar\alpha_t}\, x_0 + \sqrt{1 - \bar\alpha_t}\,\epsilon, i.e. q(xt∣x0)=N(αˉt x0, (1−αˉt)I)q(x_t \mid x_0) = \mathcal{N}(\sqrt{\bar\alpha_t}\,x_0,\ (1 - \bar\alpha_t)I). The closed form is why a single draw of ϵ\epsilon gives xtx_t at any tt without simulating the chain.

Exercise 2. On the standard linear schedule (T=1000T = 1000, βt\beta_t from 10−410^{-4} to 0.020.02), compute the signal weight αˉt\sqrt{\bar\alpha_t} and noise weight 1−αˉt\sqrt{1 - \bar\alpha_t} at t=1t = 1 and verify they match the lesson's quoted ≈1.000\approx 1.000 and ≈0.010\approx 0.010.

Solution

Step 1. At t=1t = 1, αˉ1=α1=1−β1\bar\alpha_1 = \alpha_1 = 1 - \beta_1. The schedule starts at β1=10−4\beta_1 = 10^{-4}, so αˉ1=1−10−4=0.9999\bar\alpha_1 = 1 - 10^{-4} = 0.9999.

Step 2. Signal weight αˉ1=0.9999≈0.99995\sqrt{\bar\alpha_1} = \sqrt{0.9999} \approx 0.99995, which rounds to 1.0001.000.

Step 3. Noise weight 1−αˉ1=10−4=0.01\sqrt{1 - \bar\alpha_1} = \sqrt{10^{-4}} = 0.01, the quoted ≈0.010\approx 0.010. At the first step the image is almost pure signal with a 1%1\% noise admixture, the start of the monotone slide the lesson describes.

Exercise 3 (code). Implement one forward noising step in closed form and confirm it empirically. Draw many samples of xt=αˉt x0+1−αˉt ϵx_t = \sqrt{\bar\alpha_t}\, x_0 + \sqrt{1 - \bar\alpha_t}\,\epsilon for a fixed x0x_0 and check the sample mean and variance match αˉt x0\sqrt{\bar\alpha_t}\, x_0 and 1−αˉt1 - \bar\alpha_t.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)

T = 1000
beta = np.linspace(1e-4, 0.02, T)
alpha = 1.0 - beta
alpha_bar = np.cumprod(alpha)

t = 500                                   # 1-indexed timestep
ab = alpha_bar[t - 1]
x0 = 1.3                                   # fixed clean scalar

N = 2_000_000
eps = rng.normal(size=N)
xt = np.sqrt(ab) * x0 + np.sqrt(1 - ab) * eps    # closed-form forward step

mean_emp, var_emp = xt.mean(), xt.var()
mean_true, var_true = np.sqrt(ab) * x0, 1 - ab
print(f"t={t}: alpha_bar={ab:.4f}")
print(f"mean  empirical {mean_emp:.4f}  true {mean_true:.4f}")
print(f"var   empirical {var_emp:.4f}  true {var_true:.4f}")

assert np.isclose(mean_emp, mean_true, atol=2e-3)
assert np.isclose(var_emp, var_true, atol=2e-3)

The sampled mean is αˉt x0\sqrt{\bar\alpha_t}\, x_0 and the sampled variance is 1−αˉt1 - \bar\alpha_t, the Gaussian marginal Exercise 1 derived. At t=500t = 500 the signal has shrunk to about a quarter while the noise dominates.

The next lesson moves to the production form: latent diffusion (Stable Diffusion).


References

  1. [1]Ho, J., Jain, A., & Abbeel, P. (2020). "Denoising diffusion probabilistic models (DDPM)". NeurIPS. arXiv:2006.11239. The noise-prediction objective and DDPM sampling.↩
  2. [2]Song, Y., & Ermon, S. (2019). "Generative modeling by estimating gradients of the data distribution". NeurIPS. arXiv:1907.05600. Score matching across noise levels, the score-based view of denoising.↩