April 21, 2026
We’ve built a complete text-to-image pipeline:
This is DALL-E 1. It works. But it has fundamental limitations that prevented it from scaling to the quality we expect from modern image generators.
Today: the alternative paradigm that replaced it. Diffusion models.
By the end of today, you’ll understand why diffusion won — and it comes down to fixing a problem we’ve been fighting all semester.
Discrete codebooks bottleneck generation in two ways:
1. Finite vocabulary for an infinite visual world. A codebook with 512 entries divides the continuous visual manifold into 512 bins. Fine details between bins are lost.
2. No interpolation between concepts. In a discrete space, there’s no “halfway between code 237 and code 238.” The space between codebook entries is empty.
Continuous VAEs solve this — they map images to a dense, low-dimensional manifold. No discretization, no codebook limits. With perceptual and adversarial losses (L19), reconstructions can be sharp and detailed.
So why didn’t we just use a continuous VAE all along?
The fatal flaw of continuous VAEs: the KL penalty.
The VAE loss has two terms fighting each other:
\[\mathcal{L}_{\text{VAE}} = \underbrace{\text{Reconstruction}}_{\text{wants complex, detailed latents}} + \beta \cdot \underbrace{D_{KL}(q(\mathbf{z}|\mathbf{x}) \| \mathcal{N}(\mathbf{0}, \mathbf{I}))}_{\text{wants simple Gaussian latents}}\]
Reconstruction wants the encoder to preserve every detail — spread the latents out, use all dimensions, be expressive.
The KL term wants the encoder to collapse everything onto a unit Gaussian — simple, smooth, undifferentiated.
These goals are fundamentally in conflict.
The true posterior \(q(\mathbf{z}|\mathbf{x})\) looks like Swiss cheese — dense regions where training data maps to, surrounded by empty gaps where nothing lives.
The problem appears at generation time. When we sample from \(\mathcal{N}(\mathbf{0}, \mathbf{I})\), we frequently land in the holes — regions of the latent space that the decoder has never seen during training.
The result: blurry, incoherent, artifacted generations. The decoder doesn’t know what to do with latent codes from the holes. It guesses, and it guesses badly.
We want:
The KL penalty was our tool for making the prior matchable. But it fights reconstruction quality.
What if we dropped the strict KL penalty entirely and let the latent space be as complex as it needs to be?
New problem: how do we sample from an unknown, highly complex continuous distribution?
This is the question diffusion answers.
Diffusion doesn’t assume a shape for the prior. It learns it.
Instead of forcing the encoder to produce \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) directly — which creates the prior hole — diffusion takes a completely different approach:
Gradually transform ANY distribution into \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) through a fixed noising process. Then learn to reverse it.
The prior is still Gaussian at \(t = T\) — we haven’t abandoned that. We just don’t force the encoder to get there in one impossible step. Instead, we spread that work across \(T\) tiny, manageable steps.
The VAE asks the encoder to teleport the data to a Gaussian in one jump.
Diffusion says: walk there slowly, one small step at a time, and learn to retrace your steps.
Two processes:
Forward process (fixed, no learning): gradually add noise to data until it becomes pure Gaussian noise. This is the “walk to the Gaussian.”
Reverse process (learned): gradually remove noise from Gaussian noise until it becomes data. This is “retracing your steps.”
If we can learn the reverse process, we can:
No prior holes. The Gaussian at \(t = T\) is reached by actually walking there, so the reverse path exists and is learnable.
Visualize what the forward process does to the data distribution:
The manifold gets smeared from a concentrated surface into a diffuse cloud that fills the entire space. Every point in the space is now reachable.
Diffusion seems to come out of left field. Why would “add noise then learn to remove it” be a good generative model?
The idea comes from three independent intellectual threads that converged around 2020:
1. Physics — non-equilibrium thermodynamics.
A drop of ink in water spreads until it’s uniformly dissolved. This is thermodynamic diffusion — a structured state dissolving into maximum entropy equilibrium. The process is gradual, Markovian, and well-understood mathematically.
Sohl-Dickstein et al. (2015) asked: if the forward process (structure \(\to\) noise) follows known physics, can we learn the reverse (noise \(\to\) structure)?
This is why it’s called “diffusion.” It’s not a metaphor — the forward process is literally thermodynamic diffusion applied to data.
2. Statistics — score matching and Langevin dynamics.
The score function \(\nabla_\mathbf{x} \log p(\mathbf{x})\) is the gradient of the log-density at any point. Think of it as a vector field that points “uphill” toward higher-density regions of the data.
If you know the score everywhere, you can generate samples by following the gradient uphill — this is Langevin dynamics, a random walk biased toward high-density regions.
Song & Ermon (2019) showed: train a neural network to estimate the score at multiple noise levels.
The deep connection we’ll see later: the noise prediction \(\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\) that diffusion learns IS the score function, up to a scaling factor. Predicting noise and estimating the gradient of the log-density are mathematically equivalent.
3. Representation learning — denoising autoencoders.
Vincent et al. (2008) showed that training a network to denoise corrupted inputs implicitly learns the data distribution. The optimal denoising function points toward the data manifold. You saw this in your own autoencoders early in the semester!
Diffusion extends this from one noise level to a continuum of noise levels — it’s a denoising autoencoder trained at every scale simultaneously.
Ho et al. (2020) — the DDPM paper — unified all three threads into one clean framework. That’s the formulation we’re learning today.
Define the forward process as a Markov chain — each step only depends on the previous step:
\[q(\mathbf{x}_t | \mathbf{x}_{t-1}) = \mathcal{N}(\mathbf{x}_t; \sqrt{1 - \beta_t}\,\mathbf{x}_{t-1},\; \beta_t \mathbf{I})\]
Why shrink?
Without it, the image would get brighter and brighter as we add noise.
The shrinking keeps the overall magnitude constant while the noise gradually overwhelms the signal.
\[q(\mathbf{x}_t | \mathbf{x}_{t-1}) = \mathcal{N}(\mathbf{x}_t; \sqrt{1 - \beta_t}\,\mathbf{x}_{t-1},\; \beta_t \mathbf{I})\]
\(\beta_t\) is the noise schedule — it controls how much noise we add at each step.
Typical values: \(\beta_1 = 0.0001\), \(\beta_T = 0.02\), linearly spaced. Tiny amounts of noise, applied 1000 times.
The key trick: we don’t need to iterate \(T\) steps to get to timestep \(t\).
Define: \(\alpha_t = 1 - \beta_t\) (the “signal retention” at step \(t\))
Define: \(\bar{\alpha}_t = \prod_{s=1}^{t} \alpha_s\) (the cumulative signal survival rate — how much of the original image survives after \(t\) steps)
Then we can jump directly to any timestep in one shot:
\[q(\mathbf{x}_t | \mathbf{x}_0) = \mathcal{N}(\mathbf{x}_t;\; \sqrt{\bar{\alpha}_t}\,\mathbf{x}_0,\; (1 - \bar{\alpha}_t)\mathbf{I})\]
Or equivalently, using the reparameterization trick from VAEs:
\[\mathbf{x}_t = \sqrt{\bar{\alpha}_t}\,\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\,\boldsymbol{\epsilon}, \quad \boldsymbol{\epsilon} \sim \mathcal{N}(\mathbf{0}, \mathbf{I})\]
In plain English: the noisy image at step \(t\) is a weighted mix of the clean image and pure noise.
\(\sqrt{\bar{\alpha}_t}\) controls how much signal survives; \(\sqrt{1 - \bar{\alpha}_t}\) controls how much noise is added.
The weights are designed so the total variance stays constant — we’re not making the image “louder,” we’re replacing signal with noise.
Why this matters for training: to create a noisy image at step 500, we don’t have to add noise 500 times sequentially. We just plug in \(\bar{\alpha}_{500}\), generate one noise sample, and mix. One multiplication, one addition, done.
The forward process is fixed and known. The reverse process is what we need to learn.
In plain English: given a noisy image, make it slightly less noisy. Do this 1000 times in a row, and you go from static to a photograph.
The true reverse step \(q(\mathbf{x}_{t-1} | \mathbf{x}_t)\) is intractable — computing it requires knowing the entire data distribution \(p(\mathbf{x}_0)\), which is the thing we’re trying to learn. So we approximate it with a neural network.
The question: what should the neural network predict?
Three equivalent framings:
All three are mathematically equivalent — you can derive any one from the others (because knowing any two of \(\mathbf{x}_0\), \(\mathbf{x}_t\), \(\boldsymbol{\epsilon}\) gives you the third). In practice, noise prediction (option 2) works best.
The network \(\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\) takes in a noisy image and the timestep, and predicts the noise that was added.
Note: option 3 connects directly to the score matching thread — predicting noise IS estimating the gradient of the log-density, up to a scaling factor. The compass pointing toward the data manifold.
Just like VAEs, we can derive a variational lower bound on \(\log p(\mathbf{x}_0)\). The ELBO decomposes into a sum of terms, one per timestep:
\[\log p(\mathbf{x}_0) \geq -\underbrace{L_T}_{\text{prior matching}} - \sum_{t=2}^{T} \underbrace{L_{t-1}}_{\text{denoising at scale } t} - \underbrace{L_0}_{\text{reconstruction}}\]
In words, this bound says: the quality of your generative model is determined by three things:
1. Does your noising process actually reach pure Gaussian noise by step \(T\)? (\(L_T\) — yes, by construction, so this term is basically zero.)
2. At each intermediate step, can your network correctly undo one step of noising? (\(L_{t-1}\) — this is where ALL the learning happens.)
3. Can you reconstruct the final clean image from the almost-clean version at \(t = 1\)? (\(L_0\) — yes, this is easy because \(t = 1\) is barely noisy.)
The hard work is entirely in the middle: learn to denoise at every scale.
\[\log p(\mathbf{x}_0) \geq -\underbrace{L_T}_{\text{prior matching}} - \sum_{t=2}^{T} \underbrace{L_{t-1}}_{\text{denoising at scale } t} - \underbrace{L_0}_{\text{reconstruction}}\]
This ELBO has the exact same structure as the VAE ELBO:
The VAE asks the encoder to match \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) in one giant step \(\to\) prior holes.
Diffusion distributes this work across \(T\) tiny steps \(\to\) each step is easy, no holes.
The \(L_T\) term is \(\approx 0\) by construction — the forward process ensures \(q(\mathbf{x}_T | \mathbf{x}_0) \approx \mathcal{N}(\mathbf{0}, \mathbf{I})\) by step \(T\). The real learning is in \(L_1\) through \(L_{T-1}\).
A crucial fact: \(q(\mathbf{x}_{t-1} | \mathbf{x}_t, \mathbf{x}_0)\) — the true reverse step when you know the clean image — is a Gaussian with a known mean and variance.
Why?
The posterior mean:
\[\tilde{\mu}_t(\mathbf{x}_t, \mathbf{x}_0) = \frac{\sqrt{\bar{\alpha}_{t-1}}\,\beta_t}{1 - \bar{\alpha}_t}\,\mathbf{x}_0 + \frac{\sqrt{\alpha_t}(1 - \bar{\alpha}_{t-1})}{1 - \bar{\alpha}_t}\,\mathbf{x}_t\]
the best guess for the slightly-less-noisy image \(\mathbf{x}_{t-1}\) is a weighted average of two things you know: the clean original \(\mathbf{x}_0\) and the current noisy version \(\mathbf{x}_t\).
Early in the process (\(t\) small, image is mostly clean): lean heavily on \(\mathbf{x}_t\) — it’s already almost right.
Late in the process (\(t\) large, image is mostly noise): lean more on \(\mathbf{x}_0\) — \(\mathbf{x}_t\) is garbage.
The problem: during generation, we don’t HAVE \(\mathbf{x}_0\). That’s the whole point — we’re trying to create it. So we need to eliminate \(\mathbf{x}_0\) from this formula.
Here’s the key move. We can’t use \(\mathbf{x}_0\) during generation because we don’t have it. But we know the relationship:
\[\mathbf{x}_t = \sqrt{\bar{\alpha}_t}\,\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\,\boldsymbol{\epsilon}\]
Solve for \(\mathbf{x}_0\):
\[\mathbf{x}_0 = \frac{1}{\sqrt{\bar{\alpha}_t}}\left(\mathbf{x}_t - \sqrt{1 - \bar{\alpha}_t}\,\boldsymbol{\epsilon}\right)\]
Plug this into the posterior mean and simplify. Everything rearranges so that the only unknown is \(\boldsymbol{\epsilon}\) — the noise that was added:
\[\tilde{\mu}_t = \frac{1}{\sqrt{\alpha_t}}\left(\mathbf{x}_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\,\boldsymbol{\epsilon}\right)\]
The model doesn’t know \(\boldsymbol{\epsilon}\) — so it predicts it. Replace \(\boldsymbol{\epsilon}\) with the network’s prediction \(\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\):
\[\mu_\theta(\mathbf{x}_t, t) = \frac{1}{\sqrt{\alpha_t}}\left(\mathbf{x}_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\,\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\right)\]
After pages of algebra — grinding through per-timestep KL divergences between two Gaussians, substituting the reparameterized posterior, canceling schedule coefficients — the complex ELBO simplifies to something almost embarrassingly simple:
\[L_{\text{simple}} = \mathbb{E}_{t,\, \mathbf{x}_0,\, \boldsymbol{\epsilon}}\left[\left\|\boldsymbol{\epsilon} - \boldsymbol{\epsilon}_\theta\!\left(\sqrt{\bar{\alpha}_t}\,\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\,\boldsymbol{\epsilon},\; t\right)\right\|^2\right]\]
That’s it. The training recipe in one sentence: pick a random image, add random noise at a random scale, and train the network to predict what noise you added.
No adversarial training. No carefully balanced loss terms. No mode collapse to worry about. Just MSE on noise prediction. The simplicity is shocking given how good the results are.
repeat:
x_0 ~ dataset # sample a clean image
t ~ Uniform(1, T) # pick a random timestep
ε ~ N(0, I) # sample random noise
x_t = sqrt(ᾱ_t) * x_0 + sqrt(1 - ᾱ_t) * ε # create noisy image
loss = || ε - ε_θ(x_t, t) ||² # predict the noise
gradient step on loss
until converged
Compare this to the other training loops we’ve built this semester:
Diffusion: just MSE on noise. One loss, one network, stable convergence. The simplest training loop of any generative model we’ve studied — and it produces the best results.
The network \(\boldsymbol{\epsilon}_\theta\) acts as a compass pointing back toward the clean data manifold.
At any point in the noisy space, the predicted noise \(\hat{\boldsymbol{\epsilon}}\) tells you: “the clean data is in THIS direction, THIS far away.”
Subtracting the predicted noise from the noisy image takes one step back toward the manifold.
Training at every noise scale forces the compass to work everywhere — not just near the data manifold, but deep in the noise, far from any training example.
There are no holes. The model has learned the density of the space everywhere.
Remember the score matching connection: this compass IS the score function \(\nabla_\mathbf{x} \log p(\mathbf{x})\) — the gradient of the log-density, pointing uphill toward the data. The network has learned to estimate this gradient at every noise level.
Compare:
VAE: learns the data distribution only at the surface of the manifold. The prior is assumed (\(\mathcal{N}(\mathbf{0}, \mathbf{I})\)), not learned. Holes in the prior \(\to\) bad samples.
Diffusion: learns the data distribution at EVERY noise scale. The model must correctly denoise from any level of corruption. This forces it to understand the density of the space everywhere.
By training the model to remove noise at every scale, we explicitly force it to learn the density of the space everywhere. There are no more holes. Sampling works because the model has seen — and learned to navigate from — every part of the noisy space.
This is denoising as explicit regularization.
The “prior hole” problem is solved not by assuming a better prior, but by learning the data distribution so thoroughly that no holes can exist.
The forward process guarantees we reach \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) at \(t = T\). The reverse process learns to undo every step. The result: a fully learned, nonparametric path from noise to data, with no assumptions about the shape of the data distribution.
Start with \(\mathbf{x}_T \sim \mathcal{N}(\mathbf{0}, \mathbf{I})\) — pure noise. Then iterate backward:
For \(t = T, T-1, \ldots, 1\):
\[\mathbf{x}_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(\mathbf{x}_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\,\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\right) + \sigma_t \mathbf{z}\]
where \(\mathbf{z} \sim \mathcal{N}(\mathbf{0}, \mathbf{I})\) and \(\sigma_t = \sqrt{\beta_t}\) for DDPM.
At \(t = 1\), omit the noise (\(\mathbf{z} = \mathbf{0}\)).
a little randomness helps explore the space and avoid getting stuck
After \(T\) steps, \(\mathbf{x}_0\) is a clean sample from the learned distribution.
The DDPM sampling algorithm has a quirk: the fresh noise \(\sigma_t \mathbf{z}\) added at each step makes generation stochastic. The same starting noise \(\mathbf{x}_T\) produces a different image every time.
Song et al. (2021) noticed something remarkable: you can derive a family of reverse processes that all correspond to the same forward process and the same trained model. DDPM is just one member of this family — the fully stochastic one.
At the other extreme is DDIM (Denoising Diffusion Implicit Models): a fully deterministic sampler.
The DDIM update rule:
\[\mathbf{x}_{t-1} = \sqrt{\bar{\alpha}_{t-1}}\underbrace{\left(\frac{\mathbf{x}_t - \sqrt{1 - \bar{\alpha}_t}\,\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)}{\sqrt{\bar{\alpha}_t}}\right)}_{\text{predicted } \mathbf{x}_0} + \sqrt{1 - \bar{\alpha}_{t-1}}\;\underbrace{\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)}_{\text{predicted noise direction}}\]
Instead of “remove some noise and add fresh randomness,” DDIM says “estimate the clean image, then re-noise it to the level appropriate for step \(t-1\).” No fresh randomness at all.
The predicted \(\mathbf{x}_0\) term is just the reparameterization we derived earlier — use the network’s noise prediction to estimate what the clean image looks like. Then blend that estimate with the predicted noise direction to land at the right noise level for the next step.
DDIM gives us two things DDPM doesn’t:
1. Deterministic generation. Same starting noise \(\to\) same output image, every time. This means each point in the Gaussian noise space maps to a specific image. The noise becomes a “seed” — a coordinate system for the image manifold.
DDIM gives us two things DDPM doesn’t:
2. Fewer steps without retraining. DDPM needs all \(T\) steps — skip steps and quality degrades badly because the stochastic noise compounds errors. DDIM’s deterministic updates are much more tolerant of skipped steps.
With DDIM, you can subsample the timesteps — use every 10th step, or every 20th — and still get good results. 50 steps instead of 1000, using the exact same trained model.
DDPM is a wandering hike — random detours at every step, must take all 1000 steps. DDIM is a straight-line walk — no detours, can take shortcuts.
Why 1000 tiny steps instead of 1 big step?
The noise prediction \(\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\) is a local approximation. It estimates the direction to the manifold from the current noisy position.
Taking one giant step from \(t = 1000\) to \(t = 0\) would be like following a compass bearing for 1000 miles without rechecking — the terrain curves, and you’d end up far from your destination.
Each small step recalculates the local direction. After 1000 recalculations, you arrive at the manifold precisely.
This is the fundamental tradeoff of diffusion: quality requires iteration. This computational cost — the “denoising tax” — is the price we pay for generation quality that surpasses everything else. We’ll discuss how to reduce this tax next lecture.
The network takes in a noisy image \(\mathbf{x}_t\) and outputs predicted noise \(\hat{\boldsymbol{\epsilon}}\). These must have exactly the same dimensions.
This rules out standard architectures:
We need an architecture that preserves spatial resolution exactly while processing global context.
Picture in, picture out. Map an image to an image of the same size, where each pixel’s value is informed by the entire image.
Guesses? We’ve seen it before.
You’ve seen U-Nets before (segmentation). Same architecture, different purpose.
class DiffusionUNet(nn.Module):
def __init__(self):
# Downsampling blocks
self.down1 = DownBlock(3, 64) # (3, H, W) → (64, H/2, W/2)
self.down2 = DownBlock(64, 128) # → (128, H/4, W/4)
self.down3 = DownBlock(128, 256) # → (256, H/8, W/8)
# Bottleneck
self.mid = MidBlock(256)
# Upsampling blocks (with skip connections)
self.up3 = UpBlock(256 + 256, 128) # concat skip from down3
self.up2 = UpBlock(128 + 128, 64) # concat skip from down2
self.up1 = UpBlock(64 + 64, 3) # concat skip from down1
# Timestep embedding
self.time_mlp = nn.Sequential(
SinusoidalPosEmbed(256),
nn.Linear(256, 256), nn.GELU(),
)
def forward(self, x_t, t):
t_emb = self.time_mlp(t) # timestep injected at every layer
# ... down, mid, up with skip connections and t_emb
return predicted_noise # same shape as x_tThe input and output are the same shape. The network predicts a noise map exactly the same size as the noisy input. That’s the whole architectural constraint, and the U-Net satisfies it elegantly.
How does diffusion compare to every generative approach we’ve studied this semester?
| Training | Generation | Quality | Mode collapse? | |
|---|---|---|---|---|
| GAN | Adversarial (unstable) | One forward pass | Sharp but artifacts | Yes |
| VAE | ELBO (stable) | One forward pass | Blurry | No |
| VQ-VAE + GPT | CE + autoregressive | Sequential tokens | Good but discrete | No |
| Diffusion | MSE on noise (very stable) | \(T\) sequential steps | Excellent | No |
So, what’s the weakness?
Diffusion trades generation speed for everything else: stable training, no mode collapse, best sample quality, and a principled probabilistic framework.
We have the perfect mathematical framework to step from pure noise to a clean image.
But two critical questions remain:
1. How do we control what the model generates? We want text-to-image, not random-image. How does CLIP conditioning enter the U-Net? And how do we make the conditioning actually strong enough to steer generation?
2. How do we make this practical? Running a U-Net 1000 times on a 512×512 image is absurdly expensive. How does Stable Diffusion make this fast enough for consumer GPUs?
Next lecture: the engineering reality. Cross-attention conditioning, classifier-free guidance, latent diffusion, and the path to Stable Diffusion. Plus: the omnimodal frontier — where everything we’ve built this semester converges into the AI systems that define 2026.