ES 667 · Deep learning · Generative Modelling II

From noise to imagesDenoising diffusion, from one noisy pixel to text-to-image models

t = 1000
600
400
200
50
0

One sample from the small model we train in this lecture: 814,145 parameters, about 50 minutes on a laptop GPU.

our model

Start with a question

This digit was pure noise 1,000 steps ago

t = 1000
800
600
400
200
50
0

What happened at each step: one network looked at the current image and removed a little of what it thought was noise.

What never happened: nobody gave it a digit to copy. It started from random numbers.

How can learning to remove noise let us create an image from nothing but noise?

our model

The same idea, conditioned on different things

What would each model produce?

a sentence

“A basket full of cherries”
→
?

a face with a region missing

→
?

a crude painting

→
?

a blurry, low-resolution face

→
?

an edge map + “bird”

→
?

boxes: planet, sun

→
?

4 photos of one dog + “at the Acropolis”

→
?

a 10 m satellite image

→
?

Eight different inputs, one recipe. By the end you will be able to say, for each, what the network was trained to predict.

published

LDM (Rombach et al. 2022) · RePaint (Lugmayr et al. 2022) · SDEdit (Meng et al. 2022) · SR3 (Saharia et al. 2022) · ControlNet (Zhang et al. 2023) · GLIGEN (Li et al. 2023) · DreamBooth (Ruiz et al. 2023) · DiffusionSat (Khanna et al. 2024)

Text to image · the prompt we will follow later

One prompt, several valid images

“A basket full of cherries”

sample 1
sample 2
sample 3
sample 4

Look closely: two samples carry stock-photo watermarks. The model was trained on web images with captions, and it learned the watermarks too.

Which one is correct? None and all: a prompt describes a distribution of images, and a generator samples from it.

published

Latent Diffusion, Rombach et al. 2022, project-page Figure 17 (model trained on Conceptual Captions). Seeds are not reported.

Completion · we will do this ourselves in Part 6

Fill a missing region

input · lavender = missing
→
five completions?

The visible pixels constrain the answer; they do not decide it. Smile or no smile is still the model's choice.

published

RePaint, Lugmayr et al. 2022 (project figure): one input, five independent samples.

Super-resolution · keep this picture in mind for Part 2

Is the sharp detail real?

bicubic

regression

SR3 sample

real photo

Bicubic = the low-resolution input, enlarged smoothly. Regression = one network trained with squared error.

Regression blurs, a generator invents. Compare the wrinkles under the eye: plausible, and different from the real photo.

published

SR3, Saharia et al. 2022 (project figure). The lower row enlarges the boxed eye region.

A question for our lab

Would you count brick kilns in a generated image?

Sentinel-2, 10 m per pixel

the input

DiffusionSat sample

conditioned on the input

real high-resolution image

withheld reference

Our lab maps brick kilns across South Asia from this kind of 10 m imagery. A kiln is only a few pixels wide.

Plausible is not measured. Before using generated detail, we must check it against held-out observations.

published

DiffusionSat, Khanna et al. 2024, super-resolution figure (left panel, row 5). Columns: Sentinel-2 RGB, DiffusionSat output, high-resolution reference.

Where Lecture I left us

Two generators we already trained, and their problems

VAE samples (16-number code)

One decoder pass from a random code. Smooth to train, but the digits come out blurry.

GAN samples

Sharper, but training is a two-player game that can collapse onto a few modes.

Diffusion's bet: train with a plain regression loss, like the VAE, and generate in many small steps instead of one big jump.

our model Lecture I

The plan

Four questions answer the core of the lecture

  1. What noisy input do we train on?
    Part 1 · corrupt a digit
  2. What does the network learn to predict?
    Part 2 · training
  3. How do predictions become a new image?
    Part 3 · sampling
  4. Why is that the right loss?
    Part 4 · Lecture I's ELBO

Then: inside the network (Part 5), steering it with a prompt (Part 6), and text instead of pixels (Part 7).

One running example throughout: a single 7 from the MNIST test set, and one pixel of it.

Part 1 of 7

Destroy a digit, one small step at a time

Before any learning, we fix a recipe that turns an image into noise. Goal of this part: a formula for the noisy image at any step t, checked by hand on one pixel. No neural network yet.

  1. 1Corrupt a digit
  2. 2Train: predict the noise
  3. 3Sample: run it backwards
  4. 4Why this loss: the ELBO
  5. 5Inside the network
  6. 6Steer with a condition
  7. 7Text by denoising

The running example

Our 7, and one pixel of it

Pixels are stored as 0 … 255. We rescale them:

$$x = \dfrac{v}{127.5} - 1$$

0 (paper) → −1,   255 (ink) → +1

our pixel row 12, column 19: value 255, so $x_0 = +1$.

One image is a tensor $[1, 28, 28]$; a batch is $[B, 1, 28, 28]$.

We will follow this one pixel, with value +1, through every equation in Parts 1–3.

MNIST test image 0

The forward step

One step: shrink the image a little, then add fresh noise

$x_{t-1}$
current state× 0.894
$\sqrt{1-\beta}$
fresh noise $\eta$, × 0.447 = $\sqrt{\beta}$
+
$x_t$
a little less 7, a little more noise
our pixel0.894 × (+1)+ 0.447 × (−0.5)= 0.671
$$x_t = \sqrt{1-\beta_t}\;x_{t-1} + \sqrt{\beta_t}\;\eta_t, \qquad \eta_t \sim \mathcal N(0, I)$$

With β = 0.2, our pixel goes from 1 to 0.671. Each step keeps most of the image and mixes in a little noise.

computed β = 0.2 so you can see it

Why the square roots?

Variances add, so scale by square roots

a unit-variance signal

signal 1.0

after one step, β = 0.2

$(\sqrt{0.8})^2 = 0.8$
0.2

total 1.0

without the roots: 0.8·x + 0.2·η

0.64

total 0.68, and shrinking every step

$$\mathrm{Var}(x_t) = (1-\beta_t)\,\mathrm{Var}(x_{t-1}) + \beta_t = 1 \quad\text{if}\quad \mathrm{Var}(x_{t-1}) = 1$$

The square roots keep the total variance at 1, so after many steps we land exactly on a standard Gaussian, $\mathcal N(0, I)$.

computed

Repeat on the real schedule

A thousand small steps dissolve the 7

t = 0signal 1.00
t = 100signal 0.947
t = 300signal 0.63
t = 500signal 0.28
t = 700signal 0.083
t = 1000signal 0.006

One fresh noise image per step; β grows linearly from 0.0001 to 0.02 over T = 1000 steps. “Signal” is the number that multiplies the original 7 after t steps.

After about t = 600 every panel looks the same: almost nothing of the 7 is left.

computed no model

Keeping score

How much signal is left after t steps: $\bar\alpha_t$

t = 0
1
t = 1
0.8
0.2
t = 2
0.64
0.36
$$\alpha_t = 1-\beta_t \quad(\text{here } 0.8)$$
$$\bar\alpha_t = \alpha_1\alpha_2\cdots\alpha_t$$
$$\bar\alpha_2 = 0.8\times0.8 = 0.64$$

The real schedule: the dots are the panels on the previous slide.

Blue is signal variance $\bar\alpha_t$, amber is noise variance $1-\bar\alpha_t$; they always add to 1. Signal falls below half at t = 260.

computed

A shortcut we will use in training

Skip the loop: jump straight to step t

$x_1$$=$$\sqrt{\alpha_1}\,x_0 + \sqrt{\beta_1}\,\eta_1$ $x_2$$=$$\sqrt{\alpha_2}\,x_1 + \sqrt{\beta_2}\,\eta_2$ $=$$\cx{\sqrt{\alpha_1\alpha_2}\,x_0} + \cz{\sqrt{\alpha_2\beta_1}\,\eta_1 + \sqrt{\beta_2}\,\eta_2}$ two independent noises; their variances add: $\alpha_2\beta_1 + \beta_2 = 0.16 + 0.20 = 0.36 = 1 - \bar\alpha_2$ $x_2$$=$$\cx{0.8\,x_0} + \cz{0.6\,\epsilon}, \quad \epsilon\sim\mathcal N(0,1)$

The sum of the two scaled noises is one Gaussian with variance 0.36.

$$x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon, \qquad \epsilon\sim\mathcal N(0, I)$$

Any step t in one line: no loop. This is the formula training uses.

computed

Our pixel at step 2

Signal and noise can cancel

0.8 × (+1)signal: our ink pixel+ 0.6 × (−0.5)noise: $\epsilon = -0.5$= 0.5noisy pixel x₂

Check against the loop: with η₁ = −0.5 and η₂ = −0.224, two single steps give $x_1 = 0.671$ and then $x_2 = 0.5$. Same number.

0.8 × (−1)signal: a paper pixel+ 0.6 × (+2.17)a large noise draw= 0.5the same x₂

From 0.5 alone you cannot tell whether the pixel was ink or paper. Undoing noise needs more than this one number.

computed

Try it

Slide t and watch our 7

t100
signal scale $\sqrt{\bar\alpha_t}$–
noise scale $\sqrt{1-\bar\alpha_t}$–
our pixel's noisy value–
signal-to-noise $\bar\alpha_t/(1-\bar\alpha_t)$–

Same noise image $\epsilon$ at every t (at our pixel, $\epsilon$ = −2.33).

The same formula covers every noise level: at t = 920 the image is 99.99% noise by variance.

computed

The forward process in code

Everything in Part 1 fits in a few lines of PyTorch

beta = torch.cat([torch.zeros(1),        # beta[0] = 0
                  torch.linspace(1e-4, 0.02, 1000)])
alpha = 1 - beta
alpha_bar = torch.cumprod(alpha, 0)      # [0] = 1

def corrupt(x0, t):       # x0 [B,1,28,28], t [B]
    eps = torch.randn_like(x0)
    ab = alpha_bar[t].reshape(-1, 1, 1, 1)
    return ab.sqrt() * x0 + (1 - ab).sqrt() * eps, eps
the schedule: how much noise each step adds
running product $\bar\alpha_t$; index t matches the maths
a fresh noise image the shape of the batch
the jump formula: no loop over steps

No learning anywhere in this code. It returns the noisy input and the noise; Part 2 uses both.

The view from a whole dataset

Noise turns any dataset into the same Gaussian

Eight tight clusters at t = 0 become one round blob, $\mathcal N(0, I)$, by t = 1000. Generation will start from that blob.

computed 800 points, 2-D

Can we undo it?

A 7 has ink at our pixel; a 2 has paper

7: pixel = +1 2: pixel = −1

At step 2 an observed 0.5 says “ink” with probability 0.90; at step 500 only 0.58. Going backwards needs knowledge of digits, so it must be learned.

computed

Answer to question 1, and the setup for question 2

Forward is a fixed recipe; reverse is a model we must learn

$x_0$
$x_1$
$x_2$
…
$x_T$
data
≈ pure noise $\mathcal N(0, I)$
forward $q(x_t\mid x_{t-1})$: a fixed recipe, no parameters
reverse $p_\theta(x_{t-1}\mid x_t)$: a network we must learn
$$q(x_t\mid x_{t-1}) = \mathcal N\big(\sqrt{1-\beta_t}\,x_{t-1},\ \beta_t I\big)$$
$$p_\theta(x_{t-1}\mid x_t) = \mathcal N\big(\mu_\theta(x_t,t),\ \sigma_t^2 I\big)$$

Each reverse step only has to undo a small step, which is why a Gaussian with a learned mean is a good model of it.

End of Part 1

What we did, and what comes next

What we did

  1. One step: shrink by $\sqrt{1-\beta_t}$, add $\sqrt{\beta_t}$ fresh noise; the square roots keep the variance at 1.
  2. $\bar\alpha_t$ is the signal left after t steps: 1 at the start, 0.00004 at t = 1000.
  3. Jump to any step in one line: $x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon$. Our pixel: 0.5.
  4. One noisy value cannot tell ink from paper, so going back needs a learned model.

Part 2 of 7

Training: a regression problem we build ourselves

Part 1 gave us a way to make noisy images. Goal of this part: turn that into (input, label) pairs and a loss, so that a network learns to predict the noise, and see what it can and cannot learn.

  1. ✓Corrupt a digit
  2. 2Train: predict the noise
  3. 3Sample: run it backwards
  4. 4Why this loss: the ELBO
  5. 5Inside the network
  6. 6Steer with a condition
  7. 7Text by denoising

One training example, built from scratch

Make an input and its label; ask the network for the noise

$x_0$
a training image
$\epsilon\sim\mathcal N(0, I)$
t = 350random step
0.534 · $x_0$
+ 0.845 · $\epsilon$
$\sqrt{\bar\alpha_{350}}$, $\sqrt{1-\bar\alpha_{350}}$
$x_t$
network input
network
$\hat\epsilon$
its prediction
compare $\hat\epsilon$ with $\epsilon$: mean squared error over 784 pixels = 0.015

Everything except $\hat\epsilon$ was made by us. The noise we drew is the label: we get inputs and labels for free.

our model prediction

Which label? Three equivalent choices

From one noisy input we could ask for $\epsilon$, for $x_0$, or for a mix

input $x_t$, t = 350
→
predict the noise $\epsilon$DDPM (Ho et al. 2020)
predict the clean image $x_0$
predict $v = \sqrt{\bar\alpha_t}\,\epsilon - \sqrt{1-\bar\alpha_t}\,x_0$used by many later models
Since $x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon$, knowing $x_t$ and any one of them gives the others.

We follow DDPM and predict $\epsilon$: whatever t is, the target is always a standard Gaussian image, so the regression stays well scaled.

computed

The loss

Squared error, pixel by pixel, then averaged

pixel (row, col)target $\epsilon$prediction $\hat\epsilon$$(\hat\epsilon-\epsilon)^2$
ours (12,19)−2.33−2.340.000
(8,10)1.641.820.032
(20,15)−0.18−0.300.017
(3,3)−0.58−0.580.000
mean over all 784 pixels0.015

squared error at every pixel: the largest errors sit on the stroke of the 7

loss = F.mse_loss(eps_hat, eps)

That is the whole objective: an ordinary regression on 784 numbers.

our model

Back to our toy pixel, x₂ = 0.5

Can the network recover the exact noise? No, so it averages

if the pixel wasthen $\epsilon$ wasprobability*
ink, x₀ = +1−0.50.90
paper, x₀ = −1+2.170.10
$$\hat\epsilon^\star = \mathbb E[\epsilon\mid x_2] = 0.90(-0.5) + 0.10(2.17) = \mathbf{-0.24}$$

Squared error is smallest for the average of everything that could have happened, not for the true −0.5.

* if ink and paper were equally likely before we looked

$\mathbb E[x_0\mid x_2] = 0.90(+1) + 0.10(-1) = 0.80$: a grey pixel.

This is why regression blurs (the SR3 slide): it returns an average. Diffusion uses that average to take one small step, not as the final image.

computed

The same effect, in our trained network

What the network believes the clean 7 is, at each noise level

t = 920

t = 700

t = 500

t = 350

t = 100

input $x_t$ guess $\hat x_0$

guess = the jump formula solved for $x_0$, with the predicted noise: $\hat x_0 = (x_t - \sqrt{1-\bar\alpha_t}\,\hat\epsilon)/\sqrt{\bar\alpha_t}$

At t = 920 the guess is a blurry average of digits; by t = 350 it is our 7. The average sharpens as evidence accumulates.

our model

One network, every noise level

One network must handle nearly clean and nearly pure noise

t = 7$\bar\alpha_t$ = 0.999: remove a whisper
t = 350$\bar\alpha_t$ = 0.285: mostly noise
t = 920$\bar\alpha_t$ = 0.00019: almost all noise

How much noise to remove depends on the level, so t is a second input: the network computes $\hat\epsilon = \epsilon_\theta(x_t, t)$.

One model for all 1000 levels, told which level it is at. Part 5 shows how t enters the network.

computed inputs

Shapes, in a batch

A batch mixes noise levels: one $\bar\alpha$ per image

t [B]7350920look up
$\bar\alpha[t]$ [B]
0.9990.2850.0002
reshape[B, 1, 1, 1]× broadcastt = 7t = 350t = 920
$x_t$ [B, 1, 28, 28]
each image gets its own noise level
ab = alpha_bar[t].reshape(B, 1, 1, 1)        # [B] -> [B, 1, 1, 1]
xt = ab.sqrt() * x0 + (1 - ab).sqrt() * eps   # [B, 1, 28, 28]

Random t per image means every batch trains the network at many noise levels at once.

computed

The complete training step

The whole training step in PyTorch

beta = torch.cat([torch.zeros(1), torch.linspace(1e-4, 0.02, 1000)])
alpha_bar = torch.cumprod(1 - beta, 0)     # once, as in Part 1

t   = torch.randint(1, T + 1, (B,))        # a step per image
eps = torch.randn_like(x0)                 # the label
ab  = alpha_bar[t].reshape(B, 1, 1, 1)
xt  = ab.sqrt() * x0 + (1 - ab).sqrt() * eps
eps_hat = model(xt, t)
loss = F.mse_loss(eps_hat, eps)
opt.zero_grad(); loss.backward(); opt.step()
the $\bar\alpha$ table; t runs from 1 to 1000
draw t and $\epsilon$
build the noisy input
predict the noise
regress, update $\theta$

No reverse chain, no sampling loop: one random step per image, one forward pass, one MSE.

The computation graph

Gradients flow only into the network's weights

$x_0$
data
$\epsilon$
random drawtrandom step
$x_t$
no parametersnetworktrainable weights
$\hat\epsilon$
the same $\epsilon$ is also the label
MSEa single number
$\partial\,\mathrm{loss}/\partial\theta$: the only gradient we need
nothing to train here

$x_0$, t and $\epsilon$ are constants made before the forward pass; only $\theta$ gets a gradient.

Our training run

Our training run, and where the network still struggles

noise MSE during training (smoothed)

noise MSE on 512 test digits, by step t

814,145 parameters · batch 128 · 30000 steps (about 64 passes over MNIST) · about 50 minutes on an Apple-silicon GPU

Predicting the noise is hardest at small t (0.16 at t = 1) and easy near pure noise (0.000038 at t = 1000), where the input is almost exactly $\epsilon$.

our model

End of Part 2

What we did, and what comes next

What we did

  1. A training example is (noisy image, step t) as input and the noise $\epsilon$ as the label.
  2. The loss is plain squared error over 784 pixels; one random t per image, no loop.
  3. Even a perfect network cannot recover $\epsilon$ exactly; it predicts the average of what is possible: −0.24 for our pixel.
  4. Our network: 814,145 parameters, 30,000 steps, hardest at small t.

Part 3 of 7

Sampling: run the chain backwards with the network

We have a trained noise predictor. Goal of this part: start from pure noise and use the network to take one small backward step at a time, until a digit appears. Same toy pixel, same numbers.

  1. ✓Corrupt a digit
  2. ✓Train: predict the noise
  3. 3Sample: run it backwards
  4. 4Why this loss: the ELBO
  5. 5Inside the network
  6. 6Steer with a condition
  7. 7Text by denoising

The first step of generation

Start from noise; all we have is the network's guess

$x_{1000} \sim \mathcal N(0, I)$
→
networkt = 1000
→
its guess $\hat x_0$ of a clean image

The first guess is a featureless average. The trick is to move only a little way towards it, then ask again.

our model

Our toy pixel again: $x_2 = 0.5$, and suppose $\hat\epsilon = -0.4$

Invert the jump formula to guess the clean value

$$x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon$$
$$\Rightarrow\quad \hat x_0 = \dfrac{x_t - \sqrt{1-\bar\alpha_t}\,\hat\epsilon}{\sqrt{\bar\alpha_t}}$$
0.5 − 0.6 × (−0.4)= 0.74÷ 0.8 = 0.925guess of x₀

The true value was 1. A noise error of 0.1 became an error of 0.075 = (0.6 / 0.8) × 0.1.

error in $\hat x_0$ per unit error in $\hat\epsilon$, real schedule

At high noise a small error in $\hat\epsilon$ is magnified up to 72 times, which is why early guesses are poor and we take small steps.

computed

If we knew the clean pixel

Knowing $x_0$, the step back from $x_2$ is a Gaussian we can write down

$$q(x_{t-1}\mid x_t, x_0) = \mathcal N\big(\tilde\mu_t,\ \tilde\beta_t\big)$$
$$\tilde\mu_t = \cx{a_t}\,x_0 + \cxt{b_t}\,x_t$$

$a_t = \dfrac{\sqrt{\bar\alpha_{t-1}}\,\beta_t}{1-\bar\alpha_t}$,   $b_t = \dfrac{\sqrt{\alpha_t}\,(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}$,   $\tilde\beta_t = \beta_t\,\dfrac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}$

0.497 × 1$a_2\,x_0$: the true clean pixel+ 0.497 × 0.5$b_2\,x_2$: the noisy pixel= 0.745$\tilde\mu_2$

Variance $\tilde\beta_2 = 0.2 \times 0.2/0.36 = 0.111$. A blend of where we are and where the clean image is, plus some spread.

This is the “true” reverse step. It needs $x_0$, which we have during training but not when generating.

computed

We do not know $x_0$, so use the guess

The model's reverse step: the same blend, with $\hat x_0$ in place of $x_0$

0.497 × 0.925$a_2\,\hat x_0$: the guess+ 0.497 × 0.5$b_2\,x_2$= 0.708$\mu_\theta$, the model's mean
$$\mu_\theta(x_t, t) = \cx{a_t}\,\hat x_0 + \cxt{b_t}\,x_t$$
$$p_\theta(x_{t-1}\mid x_t) = \mathcal N\big(\mu_\theta,\ \tilde\beta_t\big)$$

True mean 0.745, model mean 0.708: they differ by 0.037 only because $\hat\epsilon = -0.4$ instead of $-0.5$. Part 4 turns this gap into the loss.

A better noise prediction gives a better clean guess, which gives a reverse step closer to the true one.

computed

One reverse step · part 1

The same mean, written with the predicted noise

$$\mu_\theta(x_t, t) = \dfrac{1}{\sqrt{\alpha_t}}\left(x_t - \dfrac{\beta_t}{\sqrt{1-\bar\alpha_t}}\;\hat\epsilon\right)$$
0.5x₂− (0.2 / 0.6) × (−0.4)$c_2\hat\epsilon = -0.133$= 0.633$\div\sqrt{0.8}$ = 0.708$\mu_\theta$

Substitute $\hat x_0 = (x_t - \sqrt{1-\bar\alpha_t}\,\hat\epsilon)/\sqrt{\bar\alpha_t}$ into the blend and simplify: the same 0.708, computed directly from $\hat\epsilon$.

Read it as: remove a fraction $\beta_t/\sqrt{1-\bar\alpha_t}$ of the predicted noise, then rescale by $1/\sqrt{\alpha_t}$. The code uses this form.

computed

One reverse step · part 2

Then add fresh noise, except at the very last step

$$x_{t-1} = \mu_\theta + \sigma_t\,z, \qquad z\sim\mathcal N(0, I)$$
$$\sigma_t^2 = \tilde\beta_t = \beta_t\,\dfrac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}$$
$$\sigma_2^2 = 0.2 \times \dfrac{0.2}{0.36} = 0.111$$

With $z = 0.3$:   $x_1 = 0.708 + 0.333 \times 0.3 = \mathbf{ 0.808 }$

At t = 1 we return the mean with no noise: the output is the network's final estimate.

$\hat\epsilon$−0.400
z0.300
mean $\mu_\theta$–
fresh noise $\sigma z$–
next state $x_1$–
clean guess $\hat x_0$–

The mean says where to go; the noise keeps each step a sample from a distribution rather than a single best guess.

computed

Do not confuse these

Four numbers on one line

0.40.50.60.70.80.91.01.1
shaded: the reverse step $\mathcal N(\mu_\theta,\ \sigma^2)$, $\sigma = 0.333$
$x_2 = 0.5$
noisy input
$\mu_\theta = 0.708$
mean
$x_1 = 0.808$
sampled next state
$\hat x_0 = 0.925$
guess of the clean value
true value 1

The sampler keeps x₁ = 0.808 and feeds it back in. The guess x̂₀ = 0.925 is only for looking at.

computed

Why add noise back at every step?

Run our model with and without the fresh noise

normal sampler
mean + $\sigma_t z$
mean only
same starting noise, $\sigma_t = 0$

Average distance between the 8 images: 21.9 with noise, 0.6 without.

Without fresh noise, all eight starting points end on the same thin “1”. Following only the mean leads every start to the same average answer.

our model

The complete sampler

The loop from the first slide, in ten lines

x = torch.randn(B, 1, 28, 28)               # x_T
for t in range(T, 0, -1):                   # 1000, 999, ..., 1
    tt = torch.full((B,), t)
    eps_hat = model(x, tt)
    c = beta[t] / (1 - alpha_bar[t]).sqrt()
    mean = (x - c * eps_hat) / alpha[t].sqrt()
    if t > 1:
        x = mean + post_var[t].sqrt() * torch.randn_like(x)
    else:
        x = mean                            # last step: no noise
start from pure noise
one network call per step, same weights
the mean formula
fresh noise with the shape of x

1000 network calls for one batch of images. Sampling is slow; training was one call per example.

What we watch during generation

The state, and the network's guess at every stage

1000

800

600

400

200

100

50

1

state $x_t$ guess $\hat x_0$

The guess commits to a digit long before the state looks clean: the overall shape is decided at high noise, details at low noise.

our model

Try it

Scrub through a sample as it forms

state $x_t$
the network's guess $\hat x_0$
step1000
seed

Recorded from our model every 20 steps; nothing is computed in the browser.

Drag slowly through t = 600 … 300: that is where each seed decides which digit it will be.

our model

Evidence, not a showcase

32 samples from our model, in the order they came out

top: our first six samples · bottom: the closest of the 60,000 training images

The samples resemble training digits without copying them. Judge a generator by an unselected batch, and check for memorisation.

our model seed 667, unselected

End of Part 3

What we did, and what comes next

What we did

  1. Invert the jump formula to guess the clean image: $\hat x_0 = 0.925$ (true 1).
  2. Knowing $x_0$, the backward step is a Gaussian with mean 0.745; with the guess instead, 0.708.
  3. Add fresh noise $\sigma_t z$ at every step but the last: 0.708 → 0.808. Without it, every sample is the same “1”.
  4. Repeat 1000 times from pure noise: the loop on the first slide.

Part 4 of 7

Why is noise MSE the right loss?

Part 2 chose “predict the noise” because it is easy to set up. Now that Part 3 has given us both reverse means, the true one and the model's, we can show it is what the ELBO from Lecture I asks for, up to a weighting.

  1. ✓Corrupt a digit
  2. ✓Train: predict the noise
  3. ✓Sample: run it backwards
  4. 4Why this loss: the ELBO
  5. 5Inside the network
  6. 6Steer with a condition
  7. 7Text by denoising

Lecture I, side by side with today

A VAE has one latent; diffusion has a chain of them

Lecture I · VAE
$x$
encoder $q_\phi(z\mid x)$, learned
decoder $p_\theta(x\mid z)$
0.33−1.99
$z$
prior $\mathcal N(0, I)$
Diffusion
$x_0$
$x_1$
$x_2$
$x_T$
…
fixed noising $q$: no encoder to learn
prior $\mathcal N(0, I)$
the same three ingredients
latent: $z$ ↔ $x_1, \dots, x_T$
decoder: one pass ↔ $p_\theta(x_{t-1}\mid x_t)$, T passes
encoder: learned ↔ fixed

If diffusion is a VAE with a fixed encoder, the VAE's training objective, the ELBO, should apply to it.

Lecture I's ELBO, again

Diffusion is a VAE with a fixed encoder

Lecture I: VAEDiffusion
latentone code $z$, 2–16 numbers$x_1, \dots, x_T$: each the size of the image
encoder$q_\phi(z\mid x)$, learned$q(x_{1:T}\mid x_0)$: fixed noising, nothing to learn
decoder$p_\theta(x\mid z)$, one network pass$p_\theta(x_{t-1}\mid x_t)$, the same network used T times
prior$p(z) = \mathcal N(0, I)$$p(x_T) = \mathcal N(0, I)$
bound$\log p(x) \ge$ reconstruction $-$ KL$\log p_\theta(x_0) \ge \mathbb E_q\!\left[\log \dfrac{p_\theta(x_{0:T})}{q(x_{1:T}\mid x_0)}\right]$, the same Jensen step

Same ELBO derivation as Lecture I. The new part is how it splits up, one term per step.

Taking the bound apart

The bound splits into one comparison per step

$x_0$
$x_1$
$x_{t-1}$
$x_t$
$x_T$
……
$L_T$
end matches $\mathcal N(0, I)$
$L_{t-1}\ \text{ for } t = 2,\dots,T$
each reverse step matches the true one
$L_0$
last step reproduces the pixels
$$-\text{ELBO} = \cz{L_T} + \sum_{t=2}^{T}\cdec{L_{t-1}} + \cx{L_0}$$
$$\cdec{L_{t-1}} = \mathbb E_{q}\, D_{KL}\big(\,q(x_{t-1}\mid x_t, x_0)\;\|\;p_\theta(x_{t-1}\mid x_t)\,\big)$$

$L_T$ has no parameters; $L_0$ is a small pixel-level term. Training is really about the middle terms.

One middle term, for our toy pixel at t = 2

Equal variances: KL is the squared gap between the means

$$D_{KL} = \dfrac{(\tilde\mu - \mu_\theta)^2}{2\sigma^2}$$

true reverse mean $\tilde\mu = 0.745$ (we know $x_0 = 1$)
model's mean $\mu_\theta = 0.708$ (uses $\hat\epsilon = -0.4$)
both variances $\sigma^2 = 0.111$

$$D_{KL} = \dfrac{0.0373^2}{2 \times 0.111} = 0.00625$$

Training pushes the model's reverse mean onto the true one. Next: rewrite both means in terms of noise.

computed

The one substitution that explains the loss

Write both means with noise: the KL becomes noise MSE

model:$\mu_\theta$$= \dfrac{1}{\sqrt{\alpha_t}}\Big(x_t - c_t\,\cdec{\hat\epsilon}\Big)$$c_t = \beta_t/\sqrt{1-\bar\alpha_t}$, from Part 3 true:$\tilde\mu$$= \dfrac{1}{\sqrt{\alpha_t}}\Big(x_t - c_t\,\cz{\epsilon}\Big)$the same formula with the true noise (it is the posterior mean of Part 3) gap:$\tilde\mu - \mu_\theta$$= \dfrac{c_t}{\sqrt{\alpha_t}}\big(\cdec{\hat\epsilon} - \cz{\epsilon}\big)$$x_t$ cancels so:$D_{KL}$$= \underbrace{\dfrac{\beta_t^2}{2\sigma_t^2\,\alpha_t\,(1-\bar\alpha_t)}}_{w_t}\;\closs{(\hat\epsilon - \epsilon)^2}$a weighted squared error on the noise
$w_2 = 0.625$toy weight at t = 2× $(-0.4 + 0.5)^2 = 0.01$our pixel's noise error= 0.00625the KL from the last slide

Each ELBO term is a weighted MSE between predicted and true noise, which is why we trained on noise.

computed

The qualification

DDPM keeps the squared error and drops the weights

$$L_{\text{simple}} = \mathbb E_{x_0, t, \epsilon}\,\|\epsilon - \epsilon_\theta(x_t, t)\|^2$$

The bound's weight falls about 109× from t = 2 to t = 500. Setting every weight to 1 gives relatively more weight to noisy steps than the bound does.

It also drops $L_T$ (no parameters) and the discrete pixel term $L_0$.

The training loss is not the ELBO; it is a re-weighted sum of its per-step terms, chosen because it gave better samples (Ho et al. 2020).

computed

End of Part 4

What we showed, and what comes next

What we did

  1. Diffusion is a VAE whose encoder is the fixed noising chain.
  2. Its ELBO is one KL per reverse step.
  3. Each KL is a weighted squared error on the noise: $0.625 \times 0.1^2$ for our pixel.
  4. DDPM trains with weight 1 at every step instead.

Part 5 of 7

Inside the denoiser: networks you already know

So far the network was a box. Goal of this part: open it: a U-Net for pixels, a Transformer for patches, and why big models denoise a compressed image.

  1. ✓Corrupt a digit
  2. ✓Train: predict the noise
  3. ✓Sample: run it backwards
  4. ✓Why this loss: the ELBO
  5. 5Inside the network
  6. 6Steer with a condition
  7. 7Text by denoising

What kind of prediction is this?

Same shape in, same shape out: a dense prediction

$x_t$: [B, 1, 28, 28]
→network+ t→
$\hat\epsilon$: [B, 1, 28, 28]
⇒
implied $\hat x_0$

Like segmentation, there is one output per pixel. Unlike segmentation, each output is a real number (noise), not a class.

So the architectures that work for dense prediction (encoder–decoders with skips) work here.

our model

The network that made every sample in this lecture

A small U-Net: shrink to see context, grow back for detail

input1×28×28enc 132×28×28enc 264×14×14halvemiddle128×7×7halvedec 164×14×14dec 232×28×28output1×28×28doubledouble1×1skip: concatenate, 32 + 32 = 64 channels at 28 × 28skip: 64 + 64 = 128 at 14 × 14two channels per level for our noisy 7 at t = 350: the trained network's activations

814,145 parameters. The skips carry fine detail across; the middle sees the whole digit at 7 × 7.

our model feature maps

Telling the network which step it is at

t becomes a vector, then a per-channel offset in every block

t = 350sin / cos64 featuresMLPsharedLinear64 → C, per block[B, C, 1, 1]broadcast over H × W+ feature map [B, C, H, W]in every block

sin and cos of t at 32 frequencies, like a Transformer's position encoding

Every block can change its behaviour with the noise level, at almost no cost in parameters.

Why do large models denoise a compressed image?

Run 1000 steps on fewer numbers

imagelatentpositionsnumbers
Stable Diffusion v13 × 512 × 5124 × 64 × 6464× fewer48× fewer
our MNIST autoencoder1 × 28 × 284 × 7 × 716× fewer4× fewer

786,432 numbers → 16,384. Positions shrink by 8 × 8 = 64; the latent keeps 4 channels instead of 3, so numbers shrink by 48.

The latent is still spatial (a small image), so the same U-Net or DiT works on it. The autoencoder handles fine pixel detail once.

published SD v1 shapes

Latent diffusion, trained in two stages

First an autoencoder, then diffusion on its latents

Stage 1 · train an autoencoder (reconstruction loss)encoderz: 4 × 7 × 7 = 196 numbersdecoderreconstructionStage 2 · freeze the autoencoder, train a denoiser on zencoder
$z / s$
s = 5.322the same DDPM training stepon 4 × 7 × 7 latents instead of pixelsthe decoder isnot used in stage 2

The diffusion target is noise in latent coordinates; nothing about the training step changes.

our model

Generation in latent space

Denoise latents for 1000 steps, decode once

$z_T\sim\mathcal N(0, I)$
4 × 7 × 7latent denoiser1,000 reverse steps
$z_0$
decoderoncepixels 1 × 28 × 28

No encoder at generation time: text-to-image starts from latent noise. Image-to-image tasks (SDEdit) can start from an encoded input instead.

our model

ViT returns as a denoiser

A Transformer can be the denoiser too: DiT

noisy latent 4 × 32 × 32cut into 2 × 2 patches256 tokens × 16 numbers…Transformer blocksfull self-attentionover all 256 tokensstep t and class labelset a scale and shift in every blockback to 4 × 32 × 32a prediction per patch

Patches in, a prediction for every patch out: no class token, because the output is dense. t and the label set each block's scale and shift.

published Peebles & Xie 2023 shapes

DiT: Peebles & Xie, Scalable Diffusion Models with Transformers, 2023 (class-conditional, latent space, 256 × 256 images).

Part 6 of 7

Steering the denoiser with a prompt, a label or known pixels

Everything so far generates a random digit. Goal of this part: make it generate what we ask for. Add the condition c as another input, $\epsilon_\theta(x_t, t, c)$; the training step is unchanged.

  1. ✓Corrupt a digit
  2. ✓Train: predict the noise
  3. ✓Sample: run it backwards
  4. ✓Why this loss: the ELBO
  5. ✓Inside the network
  6. 6Steer with a condition
  7. 7Text by denoising

Back to “A basket full of cherries”

What the network receives from a prompt

<start>abasketfullofcherries<end>+ 70 padding494063201033814765392724849407= 77 IDstext encoder (frozen)Stable Diffusion v1: CLIP ViT-L/14 text towerC: [1, 77, 768]one vector per tokent = 1000t = 999…t = 1the same C at every step

The prompt becomes 77 vectors, computed once and reused at every one of the denoising steps.

computed real CLIP tokenizer

Cross-attention, reversed from the VLM lecture

Image positions ask questions; words supply the answers

image features HN = H × W positions
$Q = HW_Q$
a query per position
abasketfullofcherriestext features C (only the 5 words drawn)
$K = CW_K$
keys
$V = CW_V$
values
one row of A: softmax over the wordsa0.06basket0.31full0.08of0.05cherries0.50illustrative weights, sum to 1
$AV$
a text summary per positionadded back to that position's features

In a VLM, text queried image features. Here image features query the text: each position pulls in the words relevant to it.

illustrative weights

One image query, three words, worked by hand

Scores, softmax over the words, weighted sum of values

wordkey kvalue v
golden(1, 0)(1, 0)
dog(0, 1)(0, 2)
grass(1, 1)(1, 1)

query 1 = (1, 0)

scores $q\cdot k/\sqrt2$:   (0.707, 0, 0.707)

softmax:   (0.401, 0.198, 0.401)   sums to 1

output:   0.401·(1,0) + 0.198·(0,2) + 0.401·(1,1) = (0.802, 0.797)

query 2 = (0, 1): softmax (0.198, 0.401, 0.401), output (0.599, 1.203)

Softmax runs over the words, separately for each image position: every position gets its own mixture of the prompt.

illustrative hand-chosen numbers

Our class-conditional model

Train one network with the label, and sometimes without it

$x_t,\ t$
label c = 7
$c=\varnothing$
label hidden10% of the time
one networklearns labels 0–9 and “none”
$\hat\epsilon$
MSE with $\epsilon$
same target either way
c = torch.where(torch.rand(B) < 0.1, torch.full_like(c, 10), c)   # 10 means "no label"
eps_hat = model(xt, t, c)

The same weights now give two predictions for any noisy image: $\epsilon_c$ with the label and $\epsilon_u$ without.

our model

Classifier-free guidance

Guidance: step past the conditional prediction

$\epsilon_u=(0.2,-0.3)$, s = 0
$\epsilon_c=(0.6, 0.1)$, s = 1
s = 3: (1.4, 0.9)
$3(\epsilon_c-\epsilon_u)$ from $\epsilon_u$
illustrative 2-D vectors, equal axis scales
$$\epsilon_{\text{cfg}} = \epsilon_u + s\,(\epsilon_c - \epsilon_u)$$

$s = 0$: ignore the label · $s = 1$: ordinary conditional · $s > 1$: exaggerate the label's direction

guess, s = 0
guess, s = 1
guess, s = 3

Our model, label 7, one noisy blank page at t = 600: the implied clean image for each prediction.

Two network calls per step, combined by one line of arithmetic. No separate classifier is trained.

our model bottom row

Same 64 starting noises, label 7, four guidance scales

Turning up guidance trades variety for fidelity

s = 09% look like 7 · spread 19.6 s = 195% · spread 16.4 s = 3100% · spread 15.3 s = 7100% · spread 15.7

“Look like a 7”: a separate MNIST classifier's verdict on all 64 samples. Spread: average pixel distance between samples. 8 of 64 shown.

At s = 1, 95% already look like a 7. Larger s gives 100%, with bolder and more alike strokes: spread 16.4 → 15.7.

our model

Back to the opener: filling a missing region

Inpainting with our model: paste the known pixels back in

known: top halfunknown: shadedknown pixelsthe clean pixels noised to step t − 1unknown pixelsone reverse step of our modelpaste together
$x_{t-1}$
repeat fort = 1000 … 1
six samples:

No retraining: known pixels come from the forward process, unknown ones from the reverse step. Different seeds give different digits that agree with the top half.

our model

Back to the opener: a crude painting becomes a picture

Start the reverse chain from a noised sketch

blocky sketch

start at t = 200

t = 400

t = 600

t = 800

top: the noised sketch $\sqrt{\bar\alpha_{t_0}}\,x_{\text{sketch}} + \sqrt{1-\bar\alpha_{t_0}}\,\epsilon$ · below: three outputs each

Little noise keeps the sketch's blocks; a lot of noise gives a realistic digit that may drift from it. The starting t sets the trade-off.

our model

The same sampler loop from Part 3, with conditions added

Guidance and inpainting are a few changed lines

x = torch.randn(B, 1, 28, 28)
for t in range(T, 0, -1):
    tt = torch.full((B,), t)
    eps_u = model(x, tt, none)                 # label hidden
    eps_c = model(x, tt, label)                # label 7
    eps_hat = eps_u + s * (eps_c - eps_u)      # guidance
    c = beta[t] / (1 - alpha_bar[t]).sqrt()
    mean = (x - c * eps_hat) / alpha[t].sqrt()
    x = mean + post_var[t].sqrt() * torch.randn_like(x) if t > 1 else mean
    known_t, _ = corrupt(x_known, torch.full((B,), t - 1))
    x = mask * known_t + (1 - mask) * x       # inpainting
guidance: two calls, one line of arithmetic
inpainting: overwrite the known pixels, noised to the next step
SDEdit: change only the first line, start from the noised sketch at $t_0$

The trained network never changes; only how we call it and what we do with its output.

Back to the opener: super-resolution

Super-resolution: feed the low-res image in as extra channels

low-res, enlarged
+
noisy $x_t$
high-res state
→network6 input channels→
$\hat\epsilon$
noise in $x_t$
⇒
SR3 sample

Unlike inpainting, this needs a model trained with the low-res input. And, as with our lab images, the added detail is a sample, not a measurement.

published

SR3, Saharia et al. 2022. Diagram: the conditioning scheme described in the paper (concatenation of the upsampled input).

Part 7 of 7

Can text be generated by denoising?

Pixels are numbers, so Gaussian noise makes sense; words are not. Goal of this part: a different kind of corruption for tokens, with the rest of the recipe unchanged.

  1. ✓Corrupt a digit
  2. ✓Train: predict the noise
  3. ✓Sample: run it backwards
  4. ✓Why this loss: the ELBO
  5. ✓Inside the network
  6. ✓Steer with a condition
  7. 7Text by denoising

Why not add Gaussian noise to token IDs?

Token IDs are arbitrary labels; arithmetic on them means nothing

sentenceThedogchasedtheball
vocabulary A8243911
vocabulary B501283174same sentence, IDs reshuffled

Add the same noise, +1.6, to “dog” and round: in A, 24 → 25.6 → 26, some unrelated word; in B, 2 → 3.6 → 4 = “ball”.

corrupt by maskingThe?chased?ball

Numbers near 24 are not words near “dog”. For discrete data, corrupt by hiding tokens instead.

illustrative

Training, the discrete version

Hide a random fraction t of tokens; predict the hidden ones

clean $x_0$Thedogchasedtheball
t = 0.4: hide each with prob. 0.4Thedog?the?
slot 3: model's probabilities
chased0.60bit0.20saw0.15
slot 5: model's probabilities
ball0.70cat0.20car0.06

loss $= \tfrac{1}{t}\big(-\log 0.60 - \log 0.70\big) = \tfrac{1}{0.4}(0.511 + 0.357) = \mathbf{2.17}$   (visible tokens contribute nothing)

Same pattern as images: corrupt by a random amount, predict what was removed. The model sees the whole sequence in both directions.

illustrative probabilities

Generation, the discrete version

Start masked; fix the most confident slots first, in any order

startThedog???prompt fixed · 3 response slots
step 1
slot 3
chased0.45ate0.25
slot 4
the0.40a0.33
slot 5
ball0.71bone0.13
Thedog??ball
step 2
slot 3
chased0.78ate0.07
slot 4
the0.62a0.26
Thedogchasedtheball

“ball” was chosen first, then two words at once. Every step predicts all hidden slots, and seeing “ball” made the others easier.

illustrative a scripted trace

Same prompt, same three slots

Two ways to write the same sentence

autoregressive (GPT)masked diffusion (LLaDA)
one pass predictsthe next token onlyevery masked token
context usedtokens to the left (causal mask)all visible tokens, both sides
orderleft to right, fixedany order; set by confidence
passes for our 3 slots3here 2; in general a chosen number of steps
lengthstop when an end token appearsresponse length chosen up front

Fewer passes is not automatically faster: each masked-diffusion pass processes the whole sequence, and autoregressive models reuse cached work.

illustrative

Back to the opening slide

Now we can say what each model is trained and run to do

noisy objectcondition cwhat changes in our recipe
text → imageimage latent77 text vectorscross-attention to the prompt; classifier-free guidance
fill a regionpixelsthe known pixelspaste noised known pixels back at every step (no retraining)
painting → photopixelsthe paintingstart the reverse chain at $t_0$ from the noised painting
super-resolutionhigh-res pixelslow-res imageextra input channels; trained on image pairs
edges → imageimage latentedge map + prompta trainable copy of the encoder reads the edges (ControlNet)
one particular dogimage latentprompt with a rare wordfine-tune the denoiser on 3–5 photos first (DreamBooth)
The dog ? ? texttokensthe promptmask instead of Gaussian noise; cross-entropy instead of MSE

One idea throughout: corrupt, learn to undo one step, then repeat from noise, with the condition as an extra input.

The four questions, answered

The whole method on one slide

  1. Training input: $x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon$ for a random t
    our pixel: 0.8·1 + 0.6·(−0.5) = 0.5
  2. Target: the noise $\epsilon$, with squared error
    best possible guess: an average, −0.24
  3. Sampling: from $x_T \sim \mathcal N(0, I)$, repeat mean + $\sigma_t z$ down to t = 1
    one step: 0.5 → 0.708 → 0.808
  4. Why: each ELBO term is a weighted noise MSE; DDPM drops the weights
    our pixel: $0.625 \times 0.1^2 = 0.00625$

Removing noise creates an image because each small step is a sample from the learned reverse distribution, and the steps add up.

Check yourself

Three questions to answer before next class

  1. Does training require running the reverse chain? No. One random t, one jump-formula input, one network call, one MSE per example.
  2. Why is the guess $\hat x_0$ different from the next state $x_{t-1}$? $\hat x_0$ is a full jump to a clean estimate, unreliable at high noise; $x_{t-1}$ moves only a little towards it, adds fresh noise, and is the state fed back in.
  3. What changes when the data are tokens? Corruption becomes masking and the loss becomes cross-entropy on masked positions; the sampler unmasks confident slots over several passes.

Appendix

Derivations and extensions

A1 adding Gaussians · A2 the training posterior · A3 the bound · A4 $\epsilon$, $x_0$ and $v$ · A5 schedules · A6 DDIM · A7 the score view · A8 flow matching · A9 masked-diffusion objective · A10 attention heads · A11 reporting a generated example

A1 · the jump formula for every t

Independent Gaussians add: the step from t − 1 to t

assume$x_{t-1} = \sqrt{\bar\alpha_{t-1}}\,x_0 + \sqrt{1-\bar\alpha_{t-1}}\,\epsilon'$ one step$x_t$$= \sqrt{\alpha_t}\,x_{t-1} + \sqrt{\beta_t}\,\eta_t = \sqrt{\alpha_t\bar\alpha_{t-1}}\,x_0 + \sqrt{\alpha_t(1-\bar\alpha_{t-1})}\,\epsilon' + \sqrt{\beta_t}\,\eta_t$ variances$\alpha_t(1-\bar\alpha_{t-1}) + (1-\alpha_t) = 1 - \alpha_t\bar\alpha_{t-1} = 1-\bar\alpha_t$ so$x_t$$= \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon$,   true for t = 1, hence for all t

Toy check at t = 2: 0.8 × 0.2 + 0.2 = 0.36 = 1 − 0.64 (Part 1).

computed

A2 · where Part 3's posterior comes from

Knowing $x_0$ makes the reverse step a known Gaussian

$$q(x_{t-1}\mid x_t, x_0) = \mathcal N(\tilde\mu_t,\ \tilde\beta_t I), \qquad \tilde\beta_t = \beta_t\,\dfrac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}$$
$$\tilde\mu_t = \dfrac{\sqrt{\bar\alpha_{t-1}}\,\beta_t}{1-\bar\alpha_t}\,x_0 + \dfrac{\sqrt{\alpha_t}\,(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}\,x_t$$
0.497 × 1toy: true x₀+ 0.497 × 0.5x₂= 0.745$\tilde\mu$
0.497 × 0.925the guess x̂₀ instead+ 0.497 × 0.5= 0.708$\mu_\theta$

The model's reverse mean is the true posterior mean with $\hat x_0$ plugged in; rewriting $\hat x_0$ in terms of $\hat\epsilon$ gives the formula of Part 3.

computed

A3 · from the ELBO to one term per step

Bayes on each forward step makes the bound telescope

$-\log p_\theta(x_0)$$\le$$\mathbb E_q\Big[-\log p(x_T) - \textstyle\sum_{t\ge1}\log\dfrac{p_\theta(x_{t-1}\mid x_t)}{q(x_t\mid x_{t-1})}\Big]$ Jensen, as in Lecture I, with $p_\theta(x_{0:T}) = p(x_T)\prod_t p_\theta(x_{t-1}\mid x_t)$ and $q(x_{1:T}\mid x_0)=\prod_t q(x_t\mid x_{t-1})$ for $t\ge2$:$q(x_t\mid x_{t-1}) = \dfrac{q(x_{t-1}\mid x_t, x_0)\;q(x_t\mid x_0)}{q(x_{t-1}\mid x_0)}$   (Bayes, using the Markov property) so$=$$\underbrace{D_{KL}(q(x_T\mid x_0)\,\|\,p(x_T))}_{L_T} \;-\; \underbrace{\mathbb E_q\log p_\theta(x_0\mid x_1)}_{L_0}$ $+$$\sum_{t=2}^T \underbrace{\mathbb E_q D_{KL}(q(x_{t-1}\mid x_t,x_0)\,\|\,p_\theta(x_{t-1}\mid x_t))}_{L_{t-1}}$

The ratios $q(x_t\mid x_0)/q(x_{t-1}\mid x_0)$ cancel in a chain, leaving only $q(x_T\mid x_0)$: this is the decomposition used in Part 2.

A4 · three targets, one rotation

$x_t$ and $v$ are $x_0$ and $\epsilon$ rotated by an angle that grows with t

$x_0$
$\epsilon$
$x_t$
$\phi$
$v$
$\cos\phi=\sqrt{\bar\alpha_t},\ \ \sin\phi=\sqrt{1-\bar\alpha_t}$
$$x_t = \cos\phi\,x_0 + \sin\phi\,\epsilon$$
$$v = \cos\phi\,\epsilon - \sin\phi\,x_0$$
$$x_0 = \cos\phi\,x_t - \sin\phi\,v, \quad \epsilon = \sin\phi\,x_t + \cos\phi\,v$$

Given $x_t$, predicting any one of $\epsilon$, $x_0$, $v$ determines the others; the sampler must use the matching conversion.

computed

A5 · noise schedules

The cosine schedule spends fewer steps on near-pure noise

With the linear schedule, ᾱ is nearly 0 for the last ~40% of steps (Part 1's identical panels). The cosine schedule removes signal more evenly.

computed

Cosine schedule: Nichol & Dhariwal 2021, $\bar\alpha_t = f(t)/f(0)$, $f(t) = \cos^2\!\big(\tfrac{t/T + 0.008}{1.008}\cdot\tfrac{\pi}{2}\big)$.

A6 · a faster sampler with the same trained network

DDIM: jump between chosen steps using the clean guess

$$x_s = \sqrt{\bar\alpha_s}\,\hat x_0 + \sqrt{1-\bar\alpha_s}\,\hat\epsilon, \qquad s < t$$

no fresh noise: re-noise the clean guess with the predicted noise

DDPM, 1000 calls
DDIM, 50 calls

Same weights, same starting noise, 20× fewer calls. Deleting steps from DDPM's loop would not work: DDIM defines a different transition.

our model

A7 · the score view, on the eight clusters

Predicting noise is learning which way density increases

$$\nabla_x \log q_t(x) = -\dfrac{\mathbb E[\epsilon\mid x_t = x]}{\sqrt{1-\bar\alpha_t}}$$

For the cluster data this score is known exactly, so we can run the sampler with the exact $\epsilon$ instead of a network.

Arrows: the score at t = 150. Lines: 8 reverse trajectories (deterministic DDIM steps) from noise; dots: 400 final samples, which land on the clusters.

computed exact score, no network

A8 · flow matching

Learn a velocity along straight lines from noise to data

$$x_\tau = (1-\tau)\,x_{\text{noise}} + \tau\,x_{\text{data}}$$
$$u = x_{\text{data}} - x_{\text{noise}}$$
$\min_\theta\ \mathbb E\,\|v_\theta(x_\tau, \tau) - u\|^2$,   then integrate $\dfrac{dx}{d\tau} = v_\theta$

Where straight lines cross, the network can only learn their average direction, so the learned paths curve.

computed

A9 · the masked-diffusion objective and sampler

LLaDA's loss and its low-confidence remasking

$$\mathcal L(\theta) = -\,\mathbb E_{t, x_0, x_t}\Big[\dfrac{1}{t}\sum_{i=1}^{L}\mathbf 1[x_t^i = \texttt{MASK}]\,\log p_\theta(x_0^i\mid x_t)\Big]$$

$t\sim U(0, 1]$: each token masked independently with probability $t$

  1. Choose a response length and a number of steps; mask every response slot.
  2. Each step: predict all masked slots in one pass.
  3. Keep the most confident predictions (the schedule says how many); re-mask the rest.
  4. Prompt tokens are never masked during fine-tuning or generation.

Nie et al., Large Language Diffusion Models (LLaDA), 2025.

A10 · cross-attention with heads

Each head attends separately; then concatenate and project

H: [B, N, d]N = H·Wh headsA: [B, h, N, L]concat heads[B, N, h·d_v]
$W_O$
project+ Hback to H × W
$$Q: [B, h, N, d_k], \quad K: [B, h, L, d_k], \quad V: [B, h, L, d_v]$$
$$A = \mathrm{softmax}_L\big(QK^\top/\sqrt{d_k}\big): [B, h, N, L]$$

Softmax over the L text positions, for every image position in every head; then $W_O$ maps $h\,d_v$ back to the image feature width.

A11 · reporting a generated example

Say where every image came from

badgewhat it meanswhat to report
publisheda figure from a paper or project pagesource, figure, what the authors say was supplied (prompt, input)
our modelproduced by a model trained for this lecturecheckpoint, steps, seed, sampler, whether selected
computedexact arithmetic or the fixed forward processthe formula and the inputs
illustrativehand-chosen numbers to show a mechanismthat it is not a measured result

Our models: pixel DDPM 814,145 parameters, 30000 steps, batch 128, seed 667, EMA 0.999, linear β ∈ [10⁻⁴, 0.02], T = 1000, posterior variance; class-conditional copy with label dropout 0.1; 4 × 7 × 7 autoencoder + latent DDPM. Scripts: scripts/train_models.py, scripts/make_figures.py.