← Master Index
Vol. 17 Module 17.1 Lecture

Denoising

Diffusion Foundations

How This Lesson Fits the Module & Volume

Noise destroys structure. Denoising is the learned inverse: a network that, given xt and t (and often text), estimates how to take a step toward x0. This is the heart of every Vol. 16 generator you ran—SD, SDXL, SVD—and of DDPM / DDIM samplers. Latent space only changes where you denoise (z instead of pixels), not the idea.

You already know U-shaped CNNs from Vol. 07 and attention from Vol. 10. The denoiser is those blocks plus a time embedding. CFG is a sampling-time trick on top, not a different network family.

Learning Objectives

By the end of this lesson, students should be able to:

  • State the reverse-step goal: parameterize pθ(xt−1 | xt) using a neural net.
  • Compare ε-prediction, x0-prediction, v-prediction, and score prediction.
  • Describe a UNet denoiser: encoder–decoder skips, time embedding, optional cross-attention.
  • Implement one reverse update from a predicted ε (DDPM-style) in PyTorch.
  • Explain classifier-free guidance as an extrapolation between cond and uncond ε.
  • Relate “steps” and “guidance” sliders in SD UIs to this lecture, not to training epochs.
Definition

Denoising in diffusion is estimating, from a noisy sample xt (or latent zt) and timestep t, a quantity that lets you construct a slightly cleaner xt−1. The most common quantity is the noise εθ(xt, t) itself. Equivalently you can predict the clean image x̂0, the velocity v, or the Stein score ∇x log pt(x). A sampler is just a rule that turns that prediction into the next tensor and repeats until t = 0.

What the Network Is Allowed to Predict

TargetIntuitionWhere you meet it
ε-predictionGuess the Gaussian that was mixed into x0DDPM, SD 1.x default
x0-predictionGuess the clean image/latent directlySome samplers; easy to visualize
v-predictionPredict a velocity mixing x0 and εSD 2.x, some XL/zero-SNR setups
Score ∇ log ptPoint uphill on the noisy densityNCSN / score-SDE papers

These targets are algebraically convertible for a Gaussian forward process. If you know ε̂ and ᾱt, then x̂0 = (xt − √(1−ᾱt) ε̂) / √ᾱt. Samplers in diffusers expose prediction_type; mismatching it with the checkpoint is a silent quality killer.

The Denoiser Architecture

UNet backbone

  • Downsample → mid → upsample.
  • Skip connections (Vol. 07 spatial hierarchy).
  • Self-attention at coarser maps.

Time embedding

  • Sinusoidal t, like Transformer positions.
  • Added or FiLM-ed into residual blocks.
  • Tells the net “which SNR am I at?”

Conditioning

  • Cross-attention to CLIP/T5 tokens.
  • Concat extra maps (mask, pose) later.
  • DiT / FLUX: transformer instead of UNet.

One Reverse Step from Predicted Noise

DDPM’s ancestral step (you will derive the variances in the DDPM lecture) looks like this once εθ is known. The extra z ~ N(0,I) is why two DDPM runs with the same seed still differ if you change step count; DDIM can drop that stochasticity.

import torch from torch import nn class TinyDenoiser(nn.Module): """Didactic CNN stand-in. Real SD uses a UNet + time + cross-attn.""" def __init__(self, channels=3): super().__init__() self.net = nn.Sequential( nn.Conv2d(channels + 1, 64, 3, padding=1), nn.SiLU(), nn.Conv2d(64, 64, 3, padding=1), nn.SiLU(), nn.Conv2d(64, channels, 3, padding=1), ) def forward(self, x, t_norm): # t_norm: (N,) in [0, 1], broadcast as an extra channel t_map = t_norm[:, None, None, None].expand(-1, 1, x.size(2), x.size(3)) return self.net(torch.cat([x, t_map], dim=1)) @torch.no_grad() def ddpm_step(x, t, eps_pred, alphas, alphas_cumprod, add_noise=True): alpha_t = alphas[t] a_bar_t = alphas_cumprod[t] beta_t = 1.0 - alpha_t mean = (1.0 / alpha_t.sqrt()) * (x - beta_t / (1.0 - a_bar_t).sqrt() * eps_pred) if t == 0 or not add_noise: return mean sigma = beta_t.sqrt() return mean + sigma * torch.randn_like(x) # toy loop (pixel space, unconditioned) T = 1000 betas = torch.linspace(1e-4, 0.02, T) alphas = 1.0 - betas alphas_cumprod = torch.cumprod(alphas, dim=0) model = TinyDenoiser() x = torch.randn(1, 3, 32, 32) for t in range(T - 1, -1, -1): t_norm = torch.tensor([t / T]) eps = model(x, t_norm) x = ddpm_step(x, t, eps, alphas, alphas_cumprod) # x is a crude sample; swap TinyDenoiser for a trained UNet in production

Classifier-Free Guidance (CFG)

Ho & Salimans dropped the extra classifier. During training, randomly replace the text condition with a null token (~10% of the time). At sample time run the denoiser twice and set

ε̂ = εuncond + s · (εcond − εuncond)

s = 1 is the conditional model; s > 1 extrapolates past it (typical SD 1.5: ~7–8; SDXL often lower; FLUX schnell often ~1). Too high s burns contrast, drops diversity, and can ignore the prior—the “CFG look.” Negative prompts are just a different cond embedding in the uncond (or a second cond) slot.

Training vs Sampling Denoising

Training

  • Sample x0, t, ε; build xt.
  • Loss ≈ ||ε − εθ(xt, t, c)||2.
  • One net eval per example (plus cond dropout).

Sampling

  • Start from xT ~ N(0, I).
  • Many net evals (steps × CFG doubles).
  • Sampler choice (DDPM/DDIM/DPM) changes the path, not the weights.
Common Misconception

“The UNet outputs the finished image in one forward pass, like a GAN generator.” One forward pass outputs a noise (or x0) estimate at that t. A finished image is dozens to thousands of passes. Also: more steps are not monotonically better once you leave the stochastic DDPM regime—a good 20-step DPM++ run can beat a sloppy 100-step run. Match prediction_type and scheduler to the checkpoint.

Knowledge Check

  1. Short Answer: Name two valid prediction targets for a diffusion denoiser. Answer: Any two of ε, x0, v, score.
  2. True/False: CFG trains a separate classifier on noisy images. Answer: False—classifier-free guidance drops the classifier and uses cond vs null.
  3. Multiple Choice: Time embeddings exist so the UNet: (a) knows the batch size, (b) knows the noise level / SNR, (c) replaces the VAE. Answer: (b).
  4. Short Answer: Given ε̂ and ᾱt, how do you recover x̂0? Answer: x̂0 = (xt − √(1−ᾱt) ε̂) / √ᾱt.
  5. True/False: SD 1.5 and SD 2.x always share the same prediction type. Answer: False—2.x often uses v-prediction.
  6. Multiple Choice: Raising CFG scale s typically: (a) always increases diversity, (b) pushes toward the prompt and can oversaturate, (c) shortens training. Answer: (b).
  7. Short Answer: Why does CFG roughly double inference cost? Answer: You evaluate the denoiser twice per step (conditioned and unconditioned).
  8. Short Answer: What do UNet skip connections carry? Answer: High-resolution spatial detail from the encoder to the decoder (same idea as segmentation UNets).
  9. Multiple Choice: A “steps” slider changes: (a) dataset epochs, (b) reverse-chain / ODE evaluations, (c) VAE channel count. Answer: (b).
  10. True/False: DDPM reverse steps are deterministic if you still add z ~ N(0,I) at each t > 0. Answer: False—that extra z makes them stochastic; DDIM η=0 is the deterministic cousin.

Key Takeaways

  • Denoising is iterative: predict ε (or an equivalent), take a scheduler step, repeat.
  • Prediction type is part of the checkpoint contract; do not mix ε and v blindly.
  • The denoiser is a time-conditioned UNet or transformer, optionally cross-attending to text.
  • CFG extrapolates between null and text conditions; it is a sampler knob with real failure modes.
  • Next: Latent Space—why we denoise z, not RGB, in Stable Diffusion.
Trainer’s Guide

Hands-on idea: Freeze a tiny trained MNIST DDPM (or a diffusers pipeline on CPU with 5 steps). Visualize xt and x̂0 every few reverse steps. Students should see x̂0 sharpen long before xt looks clean.

Discussion prompt: At s = 1 vs s = 15, which images look more like “the training prior” and which look like “the prompt police”? When would you want each?

Recap: Denoising is a time-conditioned network plus a sampler rule—CFG steers it. Continue with Latent Space.