Skip to content
SDB
Generative AI

Chapter 11 · advanced · 42 min

Diffusion Models Deep Dive

DDPM math, DDIM sampling, classifier-free guidance, and LoRA for image generation

Subhendu Datta BhowmikAI Tutorials

The Core Idea: Destruction and Reconstruction

Diffusion models learn to generate data by learning to reverse a noise-adding process.

Forward process: gradually corrupt a real image x0x_0 into pure Gaussian noise over TT steps.

Reverse process: learn a neural network that denoises step-by-step, reconstructing a clean image from noise.

At inference time, sample pure noise and apply the learned reverse process → new image.

Why Diffusion Beat GANs

PropertyGANsDiffusion
Training stabilityProne to mode collapseStable (regression objective)
Sample diversityOften limitedExcellent
Sample qualitySharp but sometimes artifactsState of the art
ControllabilityLimitedCFG, ControlNet, IP-Adapter
LikelihoodNot directly availableTractable (ELBO)

Diffusion models (DALL-E 3, Stable Diffusion, Imagen, Midjourney v6) now dominate image generation.

Forward Process (Noise Schedule)

Define a variance schedule β1<β2<<βT\beta_1 < \beta_2 < \ldots < \beta_T (e.g., linearly from 10410^{-4} to 0.020.02).

The forward process adds noise step by step:

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

Key shortcut: sample xtx_t at any step tt directly from x0x_0 in closed form:

q(xtx0)=N(xt; αˉtx0, (1αˉt)I)q(x_t \mid x_0) = \mathcal{N}(x_t;\ \sqrt{\bar{\alpha}_t}\, x_0,\ (1 - \bar{\alpha}_t) I)

xt=αˉtx0+1αˉtε,εN(0,I)x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \varepsilon, \quad \varepsilon \sim \mathcal{N}(0, I)

where αt=1βt\alpha_t = 1 - \beta_t and αˉt=s=1tαs\bar{\alpha}_t = \prod_{s=1}^t \alpha_s.

As tTt \to T: αˉT0\bar{\alpha}_T \approx 0xTN(0,I)x_T \approx \mathcal{N}(0, I) (pure noise).

Noise Schedules

ScheduleFormulaUsed In
Linearβt=\beta_t = linearDDPM original
Cosineαˉt=cos2 ⁣(t/T+s1+sπ2)\bar{\alpha}_t = \cos^2\!\left(\frac{t/T + s}{1+s} \cdot \frac{\pi}{2}\right)Improved DDPM — avoids sudden noise at end
Sigmoid / EDMContinuous formulationsSDXL, EDM (Karras et al.)

Reverse Process and Training Objective

The reverse process is modeled as:

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

A U-Net (or Diffusion Transformer) εθ(xt,t)\varepsilon_\theta(x_t, t) is trained to predict the noise ε\varepsilon added at step tt.

ELBO → Simplified Loss

The full ELBO objective simplifies to:

Lsimple=Et,x0,ε[εεθ(αˉtx0+1αˉtε, t)2]\mathcal{L}_{\text{simple}} = \mathbb{E}_{t, x_0, \varepsilon}\left[\|\varepsilon - \varepsilon_\theta(\sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \varepsilon,\ t)\|^2\right]

In plain English: pick a random timestep tt, add that much noise to the image, ask the network to predict the noise, minimize prediction error.

This is just denoising score matching — the network learns the gradient of the data log-density.

Inference (Ancestral Sampling)

x_T ~ N(0, I)
for t = T, T-1, ..., 1:
    z ~ N(0, I)  if t > 1 else z = 0
    predicted_noise = ε_θ(x_t, t)
    x_{t-1} = (1/√α_t) * (x_t - β_t/√(1-ᾱ_t) * predicted_noise) + √β_t * z
return x_0

Problem: requires T=1000T = 1000 network forward passes — very slow.

DDIM — Denoising Diffusion Implicit Models

Song et al. (2021) reinterpret the forward process as non-Markovian, allowing much larger step sizes.

DDIM update rule (deterministic, η=0\eta = 0):

xt1=αˉt1(xt1αˉtεθ(xt,t)αˉt)predicted x0+1αˉt1εθ(xt,t)direction to xtx_{t-1} = \sqrt{\bar{\alpha}_{t-1}}\underbrace{\left(\frac{x_t - \sqrt{1-\bar{\alpha}_t}\,\varepsilon_\theta(x_t,t)}{\sqrt{\bar{\alpha}_t}}\right)}_{\text{predicted }x_0} + \underbrace{\sqrt{1-\bar{\alpha}_{t-1}}\,\varepsilon_\theta(x_t,t)}_{\text{direction to }x_t}

Why DDIM Matters

  • 10–50× speedup: generate in 20–50 steps instead of 1000
  • Deterministic: same noise → same image (reproducible)
  • Interpolation in noise space: latent space traversal is meaningful
  • Used by all modern samplers: PLMS, DPM-Solver, DPM-Solver++, UniPC

Modern Sampler Comparison

SamplerSteps NeededQualitySpeed
DDPM1000GoodVery slow
DDIM50GoodFast
DPM-Solver++20ExcellentVery fast
LCM (Latent Consistency)4–8GoodExtremely fast

Classifier-Free Guidance (CFG)

Ho & Salimans (2022) — the technique behind prompt adherence in Stable Diffusion, DALL-E 3, and Imagen.

The Idea

Train a single conditional model εθ(xt,t,c)\varepsilon_\theta(x_t, t, c) that also handles the unconditional case by randomly dropping the condition cc during training (replaced with a null token \emptyset).

At inference, interpolate between conditional and unconditional predictions:

ε~θ(xt,t,c)=εθ(xt,t,)+w(εθ(xt,t,c)εθ(xt,t,))guidance direction\tilde{\varepsilon}_\theta(x_t, t, c) = \varepsilon_\theta(x_t, t, \emptyset) + w \cdot \underbrace{(\varepsilon_\theta(x_t, t, c) - \varepsilon_\theta(x_t, t, \emptyset))}_{\text{guidance direction}}

  • ww = guidance scale (CFG scale) — typically 7–12 for text-to-image
  • w=0w = 0: unconditional (diverse but may ignore prompt)
  • w>1w > 1: trades diversity for prompt adherence
  • ww too high: oversaturation, artifacts

What cc Can Be

  • Text embeddings (CLIP, T5, BERT)
  • Class labels
  • Image embeddings (IP-Adapter)
  • Structural conditions (depth, pose, edge via ControlNet)
Minimal DDPM Training Loop + DDIM Samplingpython
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

# ---- Noise schedule ----
def cosine_beta_schedule(T, s=0.008):
    steps = torch.arange(T + 1, dtype=torch.float64)
    alphas_cumprod = torch.cos(((steps / T) + s) / (1 + s) * np.pi / 2) ** 2
    alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
    betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
    return torch.clamp(betas, 0, 0.999).float()

T = 1000
betas = cosine_beta_schedule(T)
alphas = 1 - betas
alphas_cumprod = torch.cumprod(alphas, dim=0)       # ᾱ_t
sqrt_alphas_cumprod = alphas_cumprod.sqrt()
sqrt_one_minus_alphas_cumprod = (1 - alphas_cumprod).sqrt()

# ---- Forward diffusion: sample x_t given x_0 ----
def q_sample(x0, t, noise=None):
    if noise is None:
        noise = torch.randn_like(x0)
    sa = sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)
    sm = sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1)
    return sa * x0 + sm * noise, noise

# ---- Tiny U-Net (stand-in for real U-Net) ----
class TinyDenoiser(nn.Module):
    def __init__(self, dim=32):
        super().__init__()
        self.time_emb = nn.Embedding(T, dim)
        self.net = nn.Sequential(
            nn.Conv2d(1 + dim, 64, 3, padding=1), nn.SiLU(),
            nn.Conv2d(64, 64, 3, padding=1),       nn.SiLU(),
            nn.Conv2d(64, 1, 1),
        )
    def forward(self, x, t):
        t_emb = self.time_emb(t)[:, :, None, None].expand(-1, -1, x.shape[2], x.shape[3])
        return self.net(torch.cat([x, t_emb], dim=1))

# ---- Training step ----
def train_step(model, optimizer, x0):
    B = x0.shape[0]
    t = torch.randint(0, T, (B,))
    xt, noise = q_sample(x0, t)
    pred_noise = model(xt, t)
    loss = F.mse_loss(pred_noise, noise)
    optimizer.zero_grad(); loss.backward(); optimizer.step()
    return loss.item()

# ---- DDIM Sampling (50 steps) ----
@torch.no_grad()
def ddim_sample(model, shape, steps=50, eta=0.0):
    device = next(model.parameters()).device
    # Select subset of timesteps
    step_seq = torch.linspace(0, T - 1, steps).long()
    step_seq_prev = torch.cat([torch.tensor([-1]), step_seq[:-1]])

    x = torch.randn(shape, device=device)
    for t, t_prev in zip(reversed(step_seq), reversed(step_seq_prev)):
        t_batch = torch.full((shape[0],), t, device=device, dtype=torch.long)
        pred_noise = model(x, t_batch)

        ac = alphas_cumprod[t]
        ac_prev = alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0)

        x0_pred = (x - (1 - ac).sqrt() * pred_noise) / ac.sqrt()
        x0_pred = x0_pred.clamp(-1, 1)

        sigma = eta * ((1 - ac_prev) / (1 - ac) * (1 - ac / ac_prev)).sqrt()
        direction = (1 - ac_prev - sigma**2).clamp(0).sqrt() * pred_noise
        noise = torch.randn_like(x) if eta > 0 else 0
        x = ac_prev.sqrt() * x0_pred + direction + sigma * noise

    return x

print("DDPM training loop and DDIM sampler ready.")
print(f"Forward: T={T} steps | DDIM inference: 50 steps ({T//50}x speedup)")

LoRA for Diffusion Models

Low-Rank Adaptation applied to diffusion models (U-Net attention layers) enables:

  • Style LoRAs: teach the model a new art style from ~20 images
  • Subject LoRAs (DreamBooth + LoRA): teach the model a specific person or object
  • Concept LoRAs: add characters, products, or visual concepts

How It Works

Freeze the base U-Net weights W0W_0. Add a low-rank update:

W=W0+ΔW=W0+BA,BRd×r, ARr×k, rmin(d,k)W = W_0 + \Delta W = W_0 + B \cdot A, \quad B \in \mathbb{R}^{d \times r},\ A \in \mathbb{R}^{r \times k},\ r \ll \min(d,k)

Train only AA and BB on the target images using the standard DDPM loss.

MethodParameters TrainedImages NeededUse Case
Full fine-tuneAll U-Net weights100s–1000sLarge style shifts
DreamBoothAll U-Net + text encoder5–30Specific subject
LoRARank-4 to rank-64 deltas10–100Style, subject, concept
Textual InversionNew text embedding only3–10Simple concept binding

Latent Diffusion (Stable Diffusion)

SD runs diffusion in latent space (not pixel space):

Image → VAE Encoder → Latent z (64×64) → Diffusion → Latent → VAE Decoder → Image
  • 4× compression per dimension → 16× fewer pixels to denoise → faster and cheaper
  • SDXL uses two CLIP text encoders and a larger U-Net (2.6B params)
  • SD3 / FLUX use Diffusion Transformers (DiT) instead of U-Net

Knowledge check

A DDPM is trained with T=1000 steps. At inference, DDIM is used with 20 steps instead. What makes this valid — why doesn't skipping 980 steps break the model?

Summary

  • DDPM: learns to reverse Gaussian noise addition; simplified loss = predict the noise at each step
  • Key insight: xtx_t can be sampled from x0x_0 in closed form using αˉt\bar{\alpha}_t — enables efficient training
  • DDIM: non-Markovian reformulation enabling 20–50 step inference (vs 1000 for DDPM)
  • CFG: train conditional + unconditional jointly, extrapolate at inference to control prompt adherence (ww scale)
  • Latent Diffusion (SD): run diffusion in compressed VAE latent space for speed; SDXL and FLUX extend this
  • LoRA / DreamBooth: fine-tune diffusion models for custom styles, subjects, or concepts in hours on a single GPU

Next: Multimodal AI — DALL-E 3, Gemini, GPT-4V, and image-text retrieval at scale.

Generative AI