Weeks
Deep Learning and GenAIWeek 11 · Diffusion Models: DDPM, DDIM, Guidance and Latent Diffusion
Week 11 Notes

Deep Learning,
Week 11

Introduction to DL and GenAI, Week 11: diffusion models. VAE, GAN and diffusion compared, the forward process and its closed form, why DDPM predicts the noise, the U-Net with time embeddings, DDPM and DDIM samplers, DDIM inversion, classifier-free guidance and latent diffusion (Stable Diffusion). A complete toy diffusion model was trained and every number was really run.

8 chapters11 worked examples~49 min read
TrilemmaForward processPredict the noiseU-NetDDPM / DDIMCFGLatent diffusionInterview prep

Chapters

  1. 01Where Diffusion FitsPart 1
  2. 02The Forward ProcessPart 2
  3. 03Learning to Reverse ItPart 3
  4. 04Sampling: DDPM and DDIMPart 4
  5. 05GuidancePart 5
  6. 06Latent Diffusion and Stable DiffusionPart 6
  7. 07Interview Question BankInterview
  8. 08Week 11 Summary SheetSummary
01
Part 1

Where Diffusion Fits

Week 6 introduced GANs. This week a different family takes over the image-generation world: diffusion models, the engine behind Stable Diffusion, DALL-E and Imagen. To see why, put the three families side by side first.

1.1VAE and GAN: two philosophies

  • VAE (variational autoencoder): an encoder compresses an image to a latent zz, a decoder rebuilds the image from zz; training maximises the ELBO (evidence lower bound: a reconstruction term plus a term keeping the latent close to a Gaussian). It gives a smooth, structured latent space you can interpolate in, and trains stably, but its outputs are often blurry.
  • GAN (Week 6): a generator against a discriminator. Sharp images and fast sampling, but unstable training, mode collapse and no structured latent space.

Why are VAE outputs blurry? Not because of anything mysterious in the architecture, but because of the loss. A mean-squared-error reconstruction loss rewards the average of all the sharp images that could fit a description, and an average of sharp images is a blur. A minimal version of the effect: one input, two equally likely correct outputs +1+1 and −1-1.

main.py
import numpy as np
rng = np.random.default_rng(0)
# one input, two equally likely correct outputs: +1 or -1 (think of two different sharp images that both fit the same description)
n = 100000
y = rng.choice([-1.0, 1.0], n)
blur = y.mean()                                           # the single value that minimises mean squared error is the MEAN
coin = rng.choice([-1.0, 1.0], n)                         # a sampler that commits to +1 or -1 at random
print(f"the answer that minimises squared error: {blur:+.3f}, an 'average' of the two correct answers")
print(f"  squared error of the average         : {((y - blur) ** 2).mean():.3f}")
print(f"  squared error of committing randomly: {((y - coin) ** 2).mean():.3f}   <- squared error PREFERS the average")
print(f"distance from the average to the nearest real answer: {min(abs(blur - 1), abs(blur + 1)):.3f} (it is not a valid answer at all)")
print("so a decoder trained with MSE (a plain VAE) learns the mean of everything that could fit, which in an image is a blur;")
print("a GAN or a diffusion model must commit to ONE sharp answer instead")
Output
the answer that minimises squared error: -0.001, an 'average' of the two correct answers
  squared error of the average         : 1.000
  squared error of committing randomly: 2.002   <- squared error PREFERS the average
distance from the average to the nearest real answer: 0.999 (it is not a valid answer at all)
so a decoder trained with MSE (a plain VAE) learns the mean of everything that could fit, which in an image is a blur;
a GAN or a diffusion model must commit to ONE sharp answer instead

The best answer under squared error is the average, 0, which is not a valid answer at all (it is 1.0 from both real ones); and its error (1.0) even beats committing to a random sharp answer (2.0). That is exactly how an MSE-trained decoder ends up blurring: it is doing what it was asked to do. A generative sampler must instead commit to one sharp answer, which is what GANs and diffusion models do.

1.2The generative trilemma

high qualitydiversity (mode coverage)fast samplingGANVAEdiffusiondiffusion: quality + diversity,but slow (many steps)fixed by DDIM and latent diffusionGAN: quality + speed,but mode collapseVAE: diversity + speed, but blurry (MSE regresses to the mean)the "trilemma": each family gives up one corner of the triangle (a useful simplification, not a law)
Figure 1. Three wishes, and each family gives one up

A useful (and simplified) summary of why diffusion models matter: a generator would ideally give high-quality samples, cover all the modes of the data (diversity) and sample fast. GANs give quality and speed but not diversity (mode collapse); VAEs give diversity and speed but not quality (blur); diffusion models give quality and diversity but are slow, because generation takes many network calls. The rest of the week is largely about making that third wish come true (DDIM, latent diffusion) without losing the first two.

1.3Measuring generated images

  • FID (Fréchet Inception Distance, Week 6): compare Gaussians fitted to Inception features of real and generated images, ∥μr−μg∥2+Tr(Σr+Σg−2(ΣrΣg)1/2)\|\mu_r-\mu_g\|^2+\mathrm{Tr}(\Sigma_r+\Sigma_g-2(\Sigma_r\Sigma_g)^{1/2}). Lower is better (0 means identical statistics); sensitive to both quality and diversity.
  • Inception Score (IS): a pre-trained classifier should be confident on each generated image (low-entropy P(y∣x)P(y|x): sharp, recognisable) while the images together span many classes (high-entropy P(y)P(y): diverse). IS=exp⁡(Ex KL(P(y∣x) ∥ P(y)))\mathrm{IS}=\exp\big(\mathbb{E}_x\,\mathrm{KL}(P(y|x)\,\|\,P(y))\big). Higher is better. It never looks at real images.
  • CLIP score (for text-to-image): the cosine similarity between the CLIP embedding of the image and of the prompt. Higher is better: the image matches the text.
main.py
import numpy as np
def inception_score(p_yx):                                   # p_yx: (n images, n classes) = the classifier's P(y | x)
    p_y = p_yx.mean(0, keepdims=True)                        # the marginal P(y)
    kl = (p_yx * (np.log(p_yx + 1e-12) - np.log(p_y + 1e-12))).sum(1)
    return float(np.exp(kl.mean()))
n, k = 1000, 10
rng = np.random.default_rng(0)
sharp_diverse = np.full((n, k), 0.0005); sharp_diverse[np.arange(n), rng.integers(0, k, n)] = 0.9955      # confident, all 10 classes used
sharp_collapsed = np.full((n, k), 0.0005); sharp_collapsed[:, 3] = 0.9955                                   # confident, always class 3
blurry = np.full((n, k), 1.0 / k)                                                                          # classifier cannot decide
print("Inception Score = exp( mean KL( P(y|x) || P(y) ) )   (1 is the worst, the number of classes is the best)")
print(f"  sharp images, all 10 classes   : {inception_score(sharp_diverse):6.2f}   <- realistic AND diverse")
print(f"  sharp images, one class only   : {inception_score(sharp_collapsed):6.2f}   <- mode collapse is caught")
print(f"  blurry images (P(y|x) uniform) : {inception_score(blurry):6.2f}   <- unrealistic is caught")
print("IS never looks at the real images, which is its limitation; FID (Week 6) compares generated features with real features")
Output
Inception Score = exp( mean KL( P(y|x) || P(y) ) )   (1 is the worst, the number of classes is the best)
  sharp images, all 10 classes   :   9.58   <- realistic AND diverse
  sharp images, one class only   :   1.00   <- mode collapse is caught
  blurry images (P(y|x) uniform) :   1.00   <- unrealistic is caught
IS never looks at the real images, which is its limitation; FID (Week 6) compares generated features with real features

IS behaves as described: sharp images over all 10 classes score 9.58 (the maximum is 10), the same sharp images all from one class score 1.00 (mode collapse is punished), and a classifier that cannot decide (a blur) also scores 1.00. Note the direction, a classic slip: FID lower is better, IS and CLIP score higher is better.

1.4Chapter summary

  • VAE: encoder + decoder, ELBO, smooth latent, blurry because MSE regresses to the mean (average of ±1\pm1 is 0). GAN: sharp, but collapses.
  • Trilemma: quality, diversity, speed; diffusion gives quality + diversity at the cost of many steps.
  • FID lower is better; IS and CLIP score higher is better (IS: 9.58 diverse, 1.00 collapsed).
Next · Part 2The Forward Process
02
Part 2

The Forward Process

The idea behind diffusion models is borrowed from physics. Drop ink into water: it spreads until the water is uniformly coloured. That forward process, order into disorder, is easy and needs no learning. The reverse, un-mixing the ink, is impossible to do by hand, but if you take the forward process in tiny steps, undoing one tiny step is a simple, learnable job. A diffusion model destroys an image by adding noise in many small steps, then learns to reverse each one.

x0x1...x(t-1)x(t)...x(T)forward (fixed, add noise): data to pure noise, easy, no trainingreverse (learned): a network eps_theta(x_t, t) predicts the noise, so each step removes a littlesmall steps make the reverse step almost Gaussian, which is why it can be learned (Sohl-Dickstein 2015, Ho 2020)
Figure 2. Forward: fixed, adds noise. Reverse: learned, removes a little noise at a time

2.1One step of noise

xt=1−βt  xt−1+βt  ε,ε∼N(0,I)⟺q(xt∣xt−1)=N(xt;1−βt xt−1, βtI)x_t=\sqrt{1-\beta_t}\;x_{t-1}+\sqrt{\beta_t}\;\varepsilon,\qquad \varepsilon\sim\mathcal{N}(0,I)\qquad\Longleftrightarrow\qquad q(x_t\mid x_{t-1})=\mathcal{N}\big(x_t;\sqrt{1-\beta_t}\,x_{t-1},\ \beta_tI\big)

βt\beta_t is a small number (the noise schedule, for example growing from 10−410^{-4} to 0.02 over T=1000T=1000 steps). Each step shrinks the previous image slightly (1−βt<1\sqrt{1-\beta_t}<1) and adds a little Gaussian noise. The factors are chosen so that the variance stays at 1 (a "variance-preserving" process): if xt−1x_{t-1} has variance 1, so does xtx_t. After TT steps nothing of the image is left and xTx_T is pure Gaussian noise N(0,I)\mathcal{N}(0,I). There is nothing to learn here: the forward process is fixed.

2.2The closed form: jump to any step

Training needs a noisy image at a random step tt, and iterating tt small steps would be slow. Fortunately the repeated steps collapse into one. Define αt=1−βt\alpha_t=1-\beta_t and αˉt=∏s=1tαs\bar\alpha_t=\prod_{s=1}^{t}\alpha_s. A sum of independent Gaussians is a Gaussian, so

q(xt∣x0)=N(xt; αˉt x0, (1−αˉt)I)⟺xt=αˉt  x0+1−αˉt  ε,ε∼N(0,I)q(x_t\mid x_0)=\mathcal{N}\big(x_t;\ \sqrt{\bar\alpha_t}\,x_0,\ (1-\bar\alpha_t)I\big)\qquad\Longleftrightarrow\qquad x_t=\sqrt{\bar\alpha_t}\;x_0+\sqrt{1-\bar\alpha_t}\;\varepsilon,\quad \varepsilon\sim\mathcal{N}(0,I)

The second form is the reparameterisation trick: instead of "sample xtx_t from a distribution", write xtx_t as a deterministic function of the data x0x_0 and an independent noise draw ε\varepsilon. That is what lets us produce xtx_t for any tt in one line (and, in a VAE, lets gradients flow through the sampling). Check it numerically: run 500 small steps on 200,000 copies of a one-pixel image of value 1.5 and compare with one jump.

main.py
import numpy as np
T = 1000
betas = np.linspace(1e-4, 0.02, T)                 # the linear noise schedule of the original DDPM
alphas = 1 - betas
alpha_bar = np.cumprod(alphas)                     # alpha_bar_t = product of (1 - beta_s) for s = 1..t
print("   t    alpha_bar   sqrt(alpha_bar)  sqrt(1-alpha_bar)   signal-to-noise  alpha_bar/(1-alpha_bar)")
for t in (1, 10, 100, 250, 500, 750, 1000):
    ab = alpha_bar[t - 1]
    print(f"{t:5d}   {ab:9.5f}   {np.sqrt(ab):13.3f}   {np.sqrt(1 - ab):15.3f}   {ab / (1 - ab):18.4f}")

# the closed form x_t = sqrt(ab) x_0 + sqrt(1-ab) eps against running the one-step process t times
rng = np.random.default_rng(0)
x0 = 1.5                                           # a one-pixel "image" with value 1.5
t, n = 500, 200000
x = np.full(n, x0)
for s in range(t):                                 # iterate: x_s = sqrt(1-beta_s) x_{s-1} + sqrt(beta_s) eps
    x = np.sqrt(1 - betas[s]) * x + np.sqrt(betas[s]) * rng.normal(size=n)
direct = np.sqrt(alpha_bar[t - 1]) * x0 + np.sqrt(1 - alpha_bar[t - 1]) * rng.normal(size=n)
print(f"\nafter {t} steps: iterated mean {x.mean():.4f}, std {x.std():.4f}   |   closed form mean {direct.mean():.4f}, std {direct.std():.4f}   (theory: mean {np.sqrt(alpha_bar[t - 1]) * x0:.4f}, std {np.sqrt(1 - alpha_bar[t - 1]):.4f})")
print("one jump to any t gives the same distribution as t small steps: training never has to iterate")
Output
   t    alpha_bar   sqrt(alpha_bar)  sqrt(1-alpha_bar)   signal-to-noise  alpha_bar/(1-alpha_bar)
    1     0.99990           1.000             0.010            9999.0000
   10     0.99811           0.999             0.044             526.7615
  100     0.89702           0.947             0.321               8.7104
  250     0.52409           0.724             0.690               1.1012
  500     0.07859           0.280             0.960               0.0853
  750     0.00335           0.058             0.998               0.0034
 1000     0.00004           0.006             1.000               0.0000

after 500 steps: iterated mean 0.4226, std 0.9606   |   closed form mean 0.4245, std 0.9616   (theory: mean 0.4205, std 0.9599)
one jump to any t gives the same distribution as t small steps: training never has to iterate

After 500 small steps the mean and standard deviation (0.4226 and 0.9606) agree with one jump (0.4245 and 0.9616) and with the theory (0.4205 and 0.9599). The table shows how the image dissolves for the linear schedule: αˉt\bar\alpha_t falls from 0.9999 at t=1t=1 to 0.897 at t=100t=100, 0.524 at 250, 0.079 at 500, 0.003 at 750 and 4×10−54\times10^{-5} at 1000, where the image coefficient is αˉ=0.006\sqrt{\bar\alpha}=0.006 and the noise coefficient is 1.000. The signal-to-noise ratio αˉ/(1−αˉ)\bar\alpha/(1-\bar\alpha) crosses 1 around t=250t=250.

x_t = sqrt(alpha_bar) * x_0 + sqrt(1 - alpha_bar) * noise (linear schedule, T = 1000)t = 01.000.00t = 1000.950.32t = 2500.720.69t = 5000.280.96t = 7500.061.00t = 10000.011.00coefficient of the imagecoefficient of the noiseas t grows the image coefficient falls to 0.006 and the noise coefficient rises to 1
Figure 3. The coefficient of the image falls and the coefficient of the noise rises

2.3Reading the notes' example correctly

The notes' worked example takes αˉt=0.1\bar\alpha_t=0.1 and writes xt=0.316 x0+0.949 εx_t=0.316\,x_0+0.949\,\varepsilon, then says "only 31.6% of the original image remains, 94.9% noise". The algebra is right; the sentence is not.

main.py
import numpy as np
ab = 0.1                                           # the notes' example: alpha_bar = 0.1 at t = 500
print("x_t = sqrt(0.1) x_0 + sqrt(0.9) eps =", round(np.sqrt(ab), 3), "x_0 +", round(np.sqrt(1 - ab), 3), "eps")
rng = np.random.default_rng(0)
x0 = rng.normal(0, 1, 100000); eps = rng.normal(0, 1, 100000)
xt = np.sqrt(ab) * x0 + np.sqrt(1 - ab) * eps
print("variance of x_t:", round(xt.var(), 3), "(x_0 and eps each have variance 1, so the variance stays 1: the process is variance-preserving)")
print("share of the VARIANCE that is signal:", round(ab, 3), "  share that is noise:", round(1 - ab, 3))
print("share of the COEFFICIENT that is signal:", round(np.sqrt(ab), 3), "(this is the 0.316; it is a multiplier, not '31.6% of the image')")
print("correlation between x_t and x_0:", round(float(np.corrcoef(xt, x0)[0, 1]), 3), " = sqrt(alpha_bar)")
# for the real linear schedule, what is alpha_bar at t = 500?
alpha_bar = np.cumprod(1 - np.linspace(1e-4, 0.02, 1000))
print("for the linear schedule 1e-4 .. 0.02 the true value at t = 500 is", round(float(alpha_bar[499]), 3), "(the notes' 0.1 is an illustration)")
Output
x_t = sqrt(0.1) x_0 + sqrt(0.9) eps = 0.316 x_0 + 0.949 eps
variance of x_t: 1.005 (x_0 and eps each have variance 1, so the variance stays 1: the process is variance-preserving)
share of the VARIANCE that is signal: 0.1   share that is noise: 0.9
share of the COEFFICIENT that is signal: 0.316 (this is the 0.316; it is a multiplier, not '31.6% of the image')
correlation between x_t and x_0: 0.317  = sqrt(alpha_bar)
for the linear schedule 1e-4 .. 0.02 the true value at t = 500 is 0.079 (the notes' 0.1 is an illustration)

0.316 and 0.949 are multipliers (they do not add to 100%: 0.316+0.949=1.2650.316+0.949=1.265). What adds to 1 are their squares: 10% of the variance is signal and 90% is noise, and the result keeps variance 1.005, confirming the variance-preserving property. Equivalently, xtx_t and x0x_0 have correlation αˉ=0.317\sqrt{\bar\alpha}=0.317. Also note that the example's αˉ500=0.1\bar\alpha_{500}=0.1 is illustrative: for the linear schedule it is 0.079.

2.4Noise schedules

00.5102505007501000step talpha_bar_t (signal left)linearcosineat t = 500:linear 0.079 leftcosine 0.494 left
Figure 4. Signal left against the step: the linear schedule destroys the image early
main.py
import numpy as np
T = 1000
t = np.arange(1, T + 1)
linear = np.cumprod(1 - np.linspace(1e-4, 0.02, T))
s = 0.008
f = lambda u: np.cos((u / T + s) / (1 + s) * np.pi / 2) ** 2           # the cosine schedule of Nichol and Dhariwal
cosine = f(t) / f(0)
print("alpha_bar (how much of the original signal's variance is left)")
print("     t      linear    cosine")
for k in (50, 100, 250, 500, 750, 900, 1000):
    print(f"{k:6d}   {linear[k - 1]:8.4f}  {cosine[k - 1]:8.4f}")
print("linear destroys the image early: by t = 500 only", round(float(linear[499]), 3), "of the signal is left, so the last half of the steps work on almost pure noise")
print("cosine destroys it more gradually: still", round(float(cosine[499]), 3), "left at t = 500")
print("first step alpha_bar:  linear", round(float(linear[0]), 5), " cosine", round(float(cosine[0]), 5))
Output
alpha_bar (how much of the original signal's variance is left)
     t      linear    cosine
    50     0.9710    0.9920
   100     0.8970    0.9721
   250     0.5241    0.8470
   500     0.0786    0.4938
   750     0.0034    0.1443
   900     0.0003    0.0241
  1000     0.0000    0.0000
linear destroys the image early: by t = 500 only 0.079 of the signal is left, so the last half of the steps work on almost pure noise
cosine destroys it more gradually: still 0.494 left at t = 500
first step alpha_bar:  linear 0.9999  cosine 0.99996

The schedule decides how fast the image is destroyed. The linear schedule (β\beta rising linearly) leaves only 7.9% of the signal at t=500t=500, so the second half of the steps work on near-pure noise, which wastes capacity. The cosine schedule (Nichol and Dhariwal) is smoother: 49% is still left at t=500t=500 and it reaches 0 only at the end, which is reported to give better samples. A quadratic schedule grows β\beta as the square of tt, adding noise aggressively early. All three share the same closed form; only αˉt\bar\alpha_t differs.

2.5Chapter summary

  • Forward step xt=1−βtxt−1+βtεx_t=\sqrt{1-\beta_t}x_{t-1}+\sqrt{\beta_t}\varepsilon; xT≈N(0,I)x_T\approx\mathcal{N}(0,I). Fixed, no training.
  • Closed form xt=αˉtx0+1−αˉtεx_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilon (reparameterisation): jump to any tt; verified against 500 iterated steps. Linear αˉ500=0.079\bar\alpha_{500}=0.079, cosine 0.494.
  • 0.316 and 0.949 are coefficients, not percentages: the variance is 10% signal and 90% noise.
Next · Part 3Learning to Reverse It
03
Part 3

Learning to Reverse It

The reverse process is where learning happens. Starting from pure noise xTx_T, we want a network that walks back to a clean image, one small step at a time, by modelling pθ(xt−1∣xt)p_\theta(x_{t-1}\mid x_t). Because each forward step was tiny, the true reverse step is also (very nearly) Gaussian, so the network only has to predict its mean (the variance can be fixed).

3.1What is the right reverse step?

If we knew the clean image x0x_0, the exact reverse step would be a Gaussian with a known mean (the posterior):

q(xt−1∣xt,x0)=N(xt−1; μ~t, β~tI),μ~t=αˉt−1 βt1−αˉt x0+αt (1−αˉt−1)1−αˉt xt,β~t=1−αˉt−11−αˉtβtq(x_{t-1}\mid x_t,x_0)=\mathcal{N}\big(x_{t-1};\,\tilde\mu_t,\,\tilde\beta_tI\big),\qquad \tilde\mu_t=\frac{\sqrt{\bar\alpha_{t-1}}\,\beta_t}{1-\bar\alpha_t}\,x_0+\frac{\sqrt{\alpha_t}\,(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}\,x_t,\quad \tilde\beta_t=\frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\beta_t

The network does not know x0x_0, but it does not need to: since xt=αˉtx0+1−αˉtεx_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilon, knowing xtx_t and the noise ε\varepsilon tells you x0x_0. Substituting, the same mean can be written purely with the noise:

μ~t=1αt(xt−βt1−αˉt  ε)\tilde\mu_t=\frac{1}{\sqrt{\alpha_t}}\Big(x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\;\varepsilon\Big)
main.py
import numpy as np
rng = np.random.default_rng(0)
T = 1000
betas = np.linspace(1e-4, 0.02, T); alphas = 1 - betas; abar = np.cumprod(alphas)
t = 400                                                       # 0-based index of the step
x0 = rng.normal(size=5); eps = rng.normal(size=5)
xt = np.sqrt(abar[t]) * x0 + np.sqrt(1 - abar[t]) * eps       # a noisy point made with a KNOWN noise eps

# (a) the true posterior mean q(x_{t-1} | x_t, x_0), the formula in the notes
mu_post = (np.sqrt(abar[t - 1]) * betas[t] / (1 - abar[t])) * x0 + (np.sqrt(alphas[t]) * (1 - abar[t - 1]) / (1 - abar[t])) * xt
# (b) the same mean written with the noise: this is what the sampler computes once eps_theta predicts eps
mu_eps = (xt - betas[t] / np.sqrt(1 - abar[t]) * eps) / np.sqrt(alphas[t])
print("posterior mean (notes' formula)       :", mu_post.round(4))
print("mean written through the noise epsilon:", mu_eps.round(4))
print("identical:", np.allclose(mu_post, mu_eps), " -> predicting the NOISE is enough to take a denoising step")
print("posterior variance beta_tilde =", f"{betas[t] * (1 - abar[t - 1]) / (1 - abar[t]):.6f}", " (beta_t =", f"{betas[t]:.6f})")
Output
posterior mean (notes' formula)       : [ 0.3783  1.1056  1.1283 -0.5818 -1.3662]
mean written through the noise epsilon: [ 0.3783  1.1056  1.1283 -0.5818 -1.3662]
identical: True  -> predicting the NOISE is enough to take a denoising step
posterior variance beta_tilde = 0.008052  (beta_t = 0.008068)

The two expressions agree to every digit (True). This is the heart of DDPM: a network that predicts the noise ε\varepsilon in xtx_t is enough to take a denoising step. So we train εθ(xt,t)\varepsilon_\theta(x_t,t) to predict the noise, and plug its output into the mean above.

3.2The training objective

Lsimple=Et,  x0,  ε[∥ε−εθ(αˉt x0+1−αˉt ε, t)∥2]L_{\text{simple}}=\mathbb{E}_{t,\;x_0,\;\varepsilon}\Big[\big\|\varepsilon-\varepsilon_\theta\big(\sqrt{\bar\alpha_t}\,x_0+\sqrt{1-\bar\alpha_t}\,\varepsilon,\ t\big)\big\|^2\Big]
  • Sample a clean image x0x_0 from the data.
  • Sample a random step t∼Uniform(1,T)t\sim\mathrm{Uniform}(1,T) and a noise ε∼N(0,I)\varepsilon\sim\mathcal{N}(0,I).
  • Make the noisy image in one jump with the closed form.
  • Predict the noise with the network and minimise the mean squared error between the true and predicted noise.

Three lines of code: pick x0,t,εx_0,t,\varepsilon; add noise in one jump; minimise ∥ε−εθ∥2\|\varepsilon-\varepsilon_\theta\|^2. The network is trained as a plain regression, with none of the instability of a GAN. Why predict the noise rather than the clean image? (1) It is equivalent, as just shown, to predicting the denoising mean. (2) The target ε\varepsilon always has the same, well-behaved scale (mean 0, variance 1) at every tt, unlike the clean image, whose best estimate at high tt is hard to scale. (3) It matches denoising score matching, a well-studied objective. The notes' mistake-list is right: DDPM predicts the noise, not the image.

3.3The network: a U-Net with the step as an extra input

x_tdown 1down 2down 3up 1up 2up 3middleepsU-Net (green dashed: skip connections carry spatial detail across)t -> sinusoidal embedding -> MLPadded to the feature maps at every resolution
Figure 5. A U-Net: the step embedding is injected at every resolution

For images the noise predictor is a U-Net (Week 4): the encoder-decoder with skip connections is a natural fit because the output (a noise image) has the same size as the input and needs fine spatial detail. One new problem: the same network must work for every noise level, so it has to be told the step tt. Feeding the bare integer is poor, because "step 10" and "step 11" should be treated as similar. The fix is borrowed from Chapter 1 of Week 10: a sinusoidal time embedding, the same formula as the Transformer's positional encoding, passed through a small MLP and added to the feature maps at every resolution of the U-Net.

main.py
import math, torch
def time_embedding(t, d=64):
    freq = torch.exp(-math.log(10000) * torch.arange(d // 2) / (d // 2))
    a = t[:, None].float() * freq[None]
    return torch.cat([a.sin(), a.cos()], 1)
t = torch.tensor([10, 11, 12, 20, 50, 500, 900])
E = time_embedding(t); E = E / E.norm(dim=1, keepdim=True)
print("cosine similarity of the embedding of step 10 with others (nearby steps should look alike):")
for j in range(1, len(t)):
    print(f"   step {t[0].item():3d} vs step {t[j].item():3d}: {(E[0] @ E[j]).item():+.3f}")
# how it enters a convolutional U-Net: project, then add to every spatial position of the feature maps
B, C, H, W = 4, 32, 16, 16
features = torch.randn(B, C, H, W)
proj = torch.nn.Sequential(torch.nn.Linear(64, 128), torch.nn.SiLU(), torch.nn.Linear(128, C))
emb = proj(time_embedding(torch.randint(0, 1000, (B,))))                      # (B, C): one vector per image in the batch
out = features + emb[:, :, None, None]                                          # broadcast over H and W
print("\nfeature maps", tuple(features.shape), "+ time embedding", tuple(emb.shape), "-> broadcast to", tuple(out.shape), "(added at every resolution of the U-Net)")
print("same sinusoidal idea as the Transformer's positional encoding, applied to the diffusion step instead of the token position")
Output
cosine similarity of the embedding of step 10 with others (nearby steps should look alike):
   step  10 vs step  11: +0.966
   step  10 vs step  12: +0.884
   step  10 vs step  20: +0.658
   step  10 vs step  50: +0.481
   step  10 vs step 500: +0.216
   step  10 vs step 900: +0.155

feature maps (4, 32, 16, 16) + time embedding (4, 32) -> broadcast to (4, 32, 16, 16) (added at every resolution of the U-Net)
same sinusoidal idea as the Transformer's positional encoding, applied to the diffusion step instead of the token position

The embeddings of nearby steps are similar (cosine 0.966 for steps 10 and 11, 0.884 for 10 and 12) and fall off with distance (0.658 at 20, 0.216 at 500), and projecting it to the channel count and broadcasting over height and width adds one vector per image to every spatial position. This is the "time conditioning" of the notes.

3.4Chapter summary

  • The reverse step is a Gaussian whose mean can be written with the noise: μ~t=1αt(xt−βt1−αˉtε)\tilde\mu_t=\tfrac1{\sqrt{\alpha_t}}(x_t-\tfrac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon) (verified identical to the posterior formula).
  • Train εθ(xt,t)\varepsilon_\theta(x_t,t) to predict the noise: L=E∥ε−εθ(xt,t)∥2L=\mathbb{E}\|\varepsilon-\varepsilon_\theta(x_t,t)\|^2; a plain regression, stable. Predict the noise, not the image.
  • U-Net backbone; the step tt enters through a sinusoidal embedding (neighbouring steps have cosine 0.97) added at every resolution.
Next · Part 4Sampling: DDPM and DDIM
04
Part 4

Sampling: DDPM and DDIM

With a trained noise predictor, generation starts from pure noise xT∼N(0,I)x_T\sim\mathcal{N}(0,I) and removes noise step by step down to x0x_0. The way the steps are taken gives two samplers with very different properties. To see everything run, here is a complete toy diffusion model: the data are 8 tight Gaussian blobs on a circle (a distribution that a model must cover completely), the network is a small MLP with the sinusoidal time embedding, and the schedule is scaled to T=100T=100 steps. The first lines of output are the training loss (Chapter 3's three-line recipe); the rest are the samplers of this chapter.

main.py
import math, torch, torch.nn as nn
torch.set_num_threads(1); torch.manual_seed(0)
# data: 8 tight Gaussian blobs on a circle of radius 2 (a distribution a model must cover completely)
CENTRES = torch.stack([2 * torch.cos(torch.arange(8) * math.pi / 4), 2 * torch.sin(torch.arange(8) * math.pi / 4)], 1)
def data(n):
    return CENTRES[torch.randint(0, 8, (n,))] + 0.1 * torch.randn(n, 2)

T = 100
betas = torch.linspace(1e-3, 0.2, T); alphas = 1 - betas; abar = torch.cumprod(alphas, 0)     # a schedule scaled for 100 steps

def time_embedding(t, d=32):                                      # sinusoidal embedding of the step index, as in the Transformer
    freq = torch.exp(-math.log(10000) * torch.arange(d // 2) / (d // 2))
    a = t[:, None].float() * freq[None]
    return torch.cat([a.sin(), a.cos()], 1)

class NoisePredictor(nn.Module):                                   # eps_theta(x_t, t)
    def __init__(self, h=128):
        super().__init__()
        self.t = nn.Sequential(nn.Linear(32, h), nn.SiLU()); self.x = nn.Linear(2, h)
        self.body = nn.Sequential(nn.SiLU(), nn.Linear(h, h), nn.SiLU(), nn.Linear(h, h), nn.SiLU(), nn.Linear(h, 2))
    def forward(self, x, t):
        return self.body(self.x(x) + self.t(time_embedding(t)))

net = NoisePredictor(); opt = torch.optim.Adam(net.parameters(), lr=2e-3)
for step in range(1, 6001):                                        # TRAINING: the three lines from the notes
    x0 = data(512); t = torch.randint(0, T, (512,)); eps = torch.randn_like(x0)       # 1. pick data and a random step; 2. draw noise
    ab = abar[t][:, None]; xt = ab.sqrt() * x0 + (1 - ab).sqrt() * eps               #    add noise in ONE jump (closed form)
    loss = ((net(xt, t) - eps) ** 2).mean()                                          # 3. predict the noise, mean squared error
    opt.zero_grad(); loss.backward(); opt.step()
    if step in (1, 500, 6000): print(f"step {step:4d}: loss {loss.item():.3f}   (predicting zero noise would give about 1.0)")

@torch.no_grad()
def ddpm(xT, seed):                                                # ancestral sampling: T network calls, fresh noise at every step
    g = torch.Generator().manual_seed(seed); x = xT.clone()
    for t in reversed(range(T)):
        eps = net(x, torch.full((len(x),), t))
        x = (x - (1 - alphas[t]) / (1 - abar[t]).sqrt() * eps) / alphas[t].sqrt()
        if t > 0: x = x + (betas[t] * (1 - abar[t - 1]) / (1 - abar[t])).sqrt() * torch.randn(x.shape, generator=g)
    return x

@torch.no_grad()
def ddim(xT, steps):                                               # deterministic, can use a SUBSET of the steps
    x = xT.clone(); ts = torch.linspace(T - 1, 0, steps).long().tolist()
    for i, t in enumerate(ts):
        eps = net(x, torch.full((len(x),), t)); x0_hat = (x - (1 - abar[t]).sqrt() * eps) / abar[t].sqrt()
        x = x0_hat if i + 1 == len(ts) else abar[ts[i + 1]].sqrt() * x0_hat + (1 - abar[ts[i + 1]]).sqrt() * eps
    return x

def quality(x):
    d = torch.cdist(x, CENTRES); near = d.min(1)
    ok = near.values < 0.4                                          # within 4 standard deviations of a blob centre
    cover = torch.bincount(near.indices[ok], minlength=8).float() / len(x)
    return ok.float().mean().item(), cover.min().item(), cover.max().item()

xT = torch.randn(2000, 2, generator=torch.Generator().manual_seed(1))
print(f"\nreal data: {quality(data(2000))[0]:.3f} of points on a blob (each blob should get 0.125)")
print("sampler        network calls   points on a blob   smallest / largest blob share")
for name, calls, x in (("DDPM", 100, ddpm(xT, 0)), ("DDIM 50 steps", 50, ddim(xT, 50)), ("DDIM 20 steps", 20, ddim(xT, 20)), ("DDIM 10 steps", 10, ddim(xT, 10)), ("DDIM 5 steps", 5, ddim(xT, 5)), ("DDIM 2 steps", 2, ddim(xT, 2))):
    q = quality(x); print(f"{name:14s} {calls:8d}        {q[0]:9.3f}          {q[1]:.2f} / {q[2]:.2f}")

a = ddpm(xT[:5], 0); b = ddpm(xT[:5], 1)
print("\nsame starting noise x_T:")
print("  DDPM run twice with different step noise: mean difference between the two results =", round((a - b).abs().mean().item(), 3), "(stochastic)")
print("  DDIM run twice:                          mean difference between the two results =", round((ddim(xT[:5], 20) - ddim(xT[:5], 20)).abs().mean().item(), 3), "(deterministic)")

@torch.no_grad()
def invert(x0, steps):                                             # DDIM inversion: run the deterministic process FORWARD, data -> noise
    x = x0.clone(); ts = torch.linspace(0, T - 1, steps).long().tolist()
    for i in range(len(ts) - 1):
        t, tn = ts[i], ts[i + 1]; eps = net(x, torch.full((len(x),), t))
        x0_hat = (x - (1 - abar[t]).sqrt() * eps) / abar[t].sqrt()
        x = abar[tn].sqrt() * x0_hat + (1 - abar[tn]).sqrt() * eps
    return x
real = data(1000); z = invert(real, 50); back = ddim(z, 50)
print(f"\nDDIM inversion of 1000 real points: reconstruction error {(back - real).norm(dim=1).mean().item():.3f} (blob width is 0.1*sqrt(2)=0.14); the recovered noise has mean {z.mean().item():+.2f}, std {z.std().item():.2f} (target 0, 1)")
nudged = ddim(z[:200] + 0.5 * torch.randn(200, 2), 50)
print(f"nudging the recovered noise by 0.5 and decoding again: the point stays on a blob in {quality(nudged)[0]:.2f} of cases")
Output
step    1: loss 0.985   (predicting zero noise would give about 1.0)
step  500: loss 0.295   (predicting zero noise would give about 1.0)
step 6000: loss 0.220   (predicting zero noise would give about 1.0)

real data: 1.000 of points on a blob (each blob should get 0.125)
sampler        network calls   points on a blob   smallest / largest blob share
DDPM                100            0.985          0.10 / 0.14
DDIM 50 steps        50            0.956          0.08 / 0.19
DDIM 20 steps        20            0.915          0.06 / 0.20
DDIM 10 steps        10            0.762          0.04 / 0.19
DDIM 5 steps          5            0.273          0.00 / 0.15
DDIM 2 steps          2            0.014          0.00 / 0.01

same starting noise x_T:
  DDPM run twice with different step noise: mean difference between the two results = 2.585 (stochastic)
  DDIM run twice:                          mean difference between the two results = 0.0 (deterministic)

DDIM inversion of 1000 real points: reconstruction error 0.064 (blob width is 0.1*sqrt(2)=0.14); the recovered noise has mean +0.02, std 1.19 (target 0, 1)
nudging the recovered noise by 0.5 and decoding again: the point stays on a blob in 0.95 of cases

4.1DDPM: ancestral sampling

The original sampler repeats the reverse step xt→xt−1x_t\to x_{t-1} for t=T,…,1t=T,\dots,1, each time computing the mean from the predicted noise and adding fresh random noise (of variance β~t\tilde\beta_t). It is stochastic (two runs from the same xTx_T give different images) and Markovian: each step depends only on the previous one, so you cannot skip steps; generation costs TT network calls (1,000 for the original model, about a minute per image). The toy table's first row: 100 calls put 98.5% of the generated points on a real blob, with every blob getting between 10% and 14% of them (ideal: 12.5%).

4.2DDIM: deterministic and fast

fraction of generated points that land on one of the 8 real blobs (real data: 1.000)DDPM, 100 calls0.985DDIM, 500.956DDIM, 200.915DDIM, 100.762DDIM, 50.273DDIM, 20.014DDIM trades steps for quality: 10 steps keep 76%; with 2 steps the model has barely started denoising
Figure 6. Fewer network calls cost quality, gradually

DDIM (Denoising Diffusion Implicit Models, Song et al., 2020) observes that the training objective does not depend on the forward process being a Markov chain: many non-Markovian processes share the same marginals q(xt∣x0)q(x_t\mid x_0) and so the same trained network. Choosing the deterministic one gives a sampler with no random noise added at inference: the predicted noise gives an estimate of the clean image, x^0=(xt−1−αˉt εθ)/αˉt\hat x_0=(x_t-\sqrt{1-\bar\alpha_t}\,\varepsilon_\theta)/\sqrt{\bar\alpha_t}, and the next point is placed on the path to it, xtnext=αˉtnext x^0+1−αˉtnext εθx_{t_{\text{next}}}=\sqrt{\bar\alpha_{t_{\text{next}}}}\,\hat x_0+\sqrt{1-\bar\alpha_{t_{\text{next}}}}\,\varepsilon_\theta. Because nothing depends on the immediately preceding step only, you can jump through a subset of the steps (1000, 980, 960, ... gives 50 steps).

Read the toy table. With the same network and the same starting noise: DDIM with 50 calls still puts 95.6% of points on a blob, 20 calls 91.5%, 10 calls 76.2%; below that it collapses (5 calls: 27%; 2 calls: 1.4%, the model has barely started). So DDIM buys a 10x to 20x speed-up for a modest quality loss, exactly the notes' "1000 →\to 50 steps, 20x faster", and the trade-off is gradual, not free. The determinism is measured too: from the same xTx_T, two DDPM runs differ by 2.585 on average, while two DDIM runs differ by 0.0: same noise in, same image out.

DDPMDDIM
Randomness at inferencefresh noise at every step (stochastic)none: deterministic (same xTx_T, same image)
Skip steps?no (Markov chain)yes (10-50 steps)
Speedslow (TT calls)10-20x faster
Same trained network?yesyes: only the sampler changes
Invertible (image to noise)?noyes: DDIM inversion

4.3DDIM inversion and editing

Because DDIM is deterministic, it can be run backwards: take a real image, apply the DDIM step in the direction of increasing noise, and obtain a noise xTx_T that DDIM will decode back to that very image. This is DDIM inversion. The recovered xTx_T "encodes" the image (its composition and content), so nudging it slightly and decoding gives an edited image, the basis of diffusion-based image editing and style transfer. On the toy model: inverting 1,000 real points and decoding them back reproduces them to an average error of 0.064 (the blob width is 0.14), the recovered noise has mean +0.02 and standard deviation 1.19 (the target is 0 and 1, so inversion is approximate here), and after nudging the recovered noise by 0.5 the decoded point still lands on a blob in 95% of cases: nearby noise decodes to a nearby, valid sample.

4.4Chapter summary

  • DDPM: stochastic, Markovian, TT calls, cannot skip. Toy: 100 calls give 98.5% on-blob.
  • DDIM: deterministic (measured difference 0.0 against 2.585), skips steps with the same network: 50, 20, 10 calls give 95.6%, 91.5%, 76.2%; 2 calls fail.
  • DDIM inversion: image to noise xTx_T and back (error 0.064); edit the noise, decode an edited image.
Next · Part 5Guidance
05
Part 5

Guidance

A plain diffusion model is unconditional: it produces a random sample from the data. Useful systems need control: "a cat", "a beautiful sunset". Guidance steers the denoising toward what we asked for.

5.1Classifier guidance

The first approach (Dhariwal and Nichol, 2021) uses a separate pre-trained classifier p(y∣xt)p(y\mid x_t). The denoising direction is the score ∇log⁡p(xt)\nabla\log p(x_t) (up to scale, the predicted noise); Bayes' rule gives the conditional score as the unconditional one plus the classifier's gradient, ∇log⁡p(xt∣y)=∇log⁡p(xt)+∇log⁡p(y∣xt)\nabla\log p(x_t\mid y)=\nabla\log p(x_t)+\nabla\log p(y\mid x_t), so at each step the sample is pushed in the direction that makes the classifier more sure of the wanted class. The drawback: the classifier must be trained on noisy images of every noise level (an ordinary classifier has never seen them), and it only supports the fixed classes it was trained on, not free text.

5.2Classifier-free guidance (CFG)

Classifier-free guidance (Ho and Salimans, 2021) removes the classifier. Train one network εθ(xt,t,c)\varepsilon_\theta(x_t,t,c) that takes the condition cc (a class or a text embedding) as an extra input, and during training drop the condition a fraction of the time (10 to 20%), replacing cc by a "null" token ∅\varnothing. One model thereby learns both the conditional prediction εθ(xt,c)\varepsilon_\theta(x_t,c) and the unconditional one εθ(xt,∅)\varepsilon_\theta(x_t,\varnothing). At sampling time run both and extrapolate from the unconditional toward the conditional:

ε~(xt,c)=εθ(xt,∅)+w (εθ(xt,c)−εθ(xt,∅))\tilde\varepsilon(x_t,c)=\varepsilon_\theta(x_t,\varnothing)+w\,\big(\varepsilon_\theta(x_t,c)-\varepsilon_\theta(x_t,\varnothing)\big)
  • w=0w=0: the unconditional model; w=1w=1: the plain conditional model; w>1w>1: the difference εc−εu\varepsilon_c-\varepsilon_u, "what the condition adds", is amplified. The notes' example: w=7.5w=7.5 gives ε~=−6.5 εu+7.5 εc\tilde\varepsilon=-6.5\,\varepsilon_u+7.5\,\varepsilon_c (check: εu+7.5εc−7.5εu\varepsilon_u+7.5\varepsilon_c-7.5\varepsilon_u).
  • Typical text-to-image values are 7 to 15 (Stable Diffusion's default is 7.5). Higher ww: more faithful to the prompt but less diverse, and, pushed far, over-saturated and artificial.
  • Notation warning: the original CFG paper writes (1+w)εc−w εu(1+w)\varepsilon_c-w\,\varepsilon_u, so its w=0w=0 is the plain conditional model. The notes (and Stable Diffusion code) use the form above with w=1w=1 conditional. The two differ by a shift of 1; quote the form you use.

5.3Guidance, measured on a toy problem

To see the effect without any images: two overlapping classes in 2-D, class 0 centred at (−1,0)(-1,0) and class 1 at (+1,0)(+1,0), each with standard deviation 1. One network is trained with the condition dropped 20% of the time (the null label is 2). Then sample class 1 with guidance scales from 0 to 8:

main.py
import math, torch, torch.nn as nn
torch.set_num_threads(1); torch.manual_seed(0)
# two OVERLAPPING classes: class 0 ~ N((-1, 0), I) and class 1 ~ N((+1, 0), I)
def data(n):
    c = torch.randint(0, 2, (n,)); mean = torch.stack([(2 * c - 1).float(), torch.zeros(n)], 1)
    return mean + torch.randn(n, 2), c
T = 100; betas = torch.linspace(1e-3, 0.2, T); alphas = 1 - betas; abar = torch.cumprod(alphas, 0)
def time_embedding(t, d=32):
    freq = torch.exp(-math.log(10000) * torch.arange(d // 2) / (d // 2)); a = t[:, None].float() * freq[None]
    return torch.cat([a.sin(), a.cos()], 1)
NULL = 2                                                              # label 2 = "no condition"
class Net(nn.Module):
    def __init__(self, h=128):
        super().__init__()
        self.t = nn.Sequential(nn.Linear(32, h), nn.SiLU()); self.x = nn.Linear(2, h); self.c = nn.Embedding(3, h)
        self.body = nn.Sequential(nn.SiLU(), nn.Linear(h, h), nn.SiLU(), nn.Linear(h, h), nn.SiLU(), nn.Linear(h, 2))
    def forward(self, x, t, c):
        return self.body(self.x(x) + self.t(time_embedding(t)) + self.c(c))
net = Net(); opt = torch.optim.Adam(net.parameters(), lr=2e-3)
for step in range(5000):
    x0, c = data(512)
    c = torch.where(torch.rand(512) < 0.2, torch.full_like(c, NULL), c)           # DROP the condition 20% of the time: ONE model learns both
    t = torch.randint(0, T, (512,)); eps = torch.randn_like(x0); ab = abar[t][:, None]
    loss = ((net(ab.sqrt() * x0 + (1 - ab).sqrt() * eps, t, c) - eps) ** 2).mean()
    opt.zero_grad(); loss.backward(); opt.step()

@torch.no_grad()
def sample(n, cls, w, seed=0):
    g = torch.Generator().manual_seed(seed); x = torch.randn(n, 2, generator=g)
    for t in reversed(range(T)):
        tt = torch.full((n,), t)
        eps_u = net(x, tt, torch.full((n,), NULL)); eps_c = net(x, tt, torch.full((n,), cls))
        eps = eps_u + w * (eps_c - eps_u)                                          # classifier-free guidance: eps_u + w (eps_c - eps_u)
        x = (x - (1 - alphas[t]) / (1 - abar[t]).sqrt() * eps) / alphas[t].sqrt()
        if t > 0: x = x + (betas[t] * (1 - abar[t - 1]) / (1 - abar[t])).sqrt() * torch.randn(n, 2, generator=g)
    return x

xr, cr = data(20000); real = xr[cr == 1]
print(f"real class-1 data: mean x {real[:, 0].mean():+.2f}, std x {real[:, 0].std():.2f}, fraction with x > 0: {(real[:, 0] > 0).float().mean():.3f}")
print("\nguidance scale w   mean x   std x   fraction x > 0   std y")
for w in (0, 1, 2, 4, 8):
    x = sample(3000, 1, w)
    print(f"{w:10d}      {x[:, 0].mean():+7.2f}  {x[:, 0].std():6.2f}   {(x[:, 0] > 0).float().mean():10.3f}      {x[:, 1].std():.2f}")
print("w = 0 is unconditional (both classes); w = 1 reproduces the real class-1 distribution; w > 1 pushes further and further into class 1 and the spread shrinks")
Output
real class-1 data: mean x +0.99, std x 1.00, fraction with x > 0: 0.842

guidance scale w   mean x   std x   fraction x > 0   std y
         0        -0.11    1.39        0.465      1.05
         1        +0.95    0.97        0.832      0.99
         2        +1.43    0.75        0.978      0.95
         4        +1.78    0.60        0.999      0.88
         8        +1.89    0.47        1.000      0.75
w = 0 is unconditional (both classes); w = 1 reproduces the real class-1 distribution; w > 1 pushes further and further into class 1 and the spread shrinks
real data-0.110.951.431.781.89w=0w=1w=2w=4w=8mean x of samples (class 1)real data1.390.970.750.600.47w=0w=1w=2w=4w=8std of x (diversity)w = 1 matches the real conditional distribution; larger w pushes into the class and the spread shrinks
Figure 7. The sample mean moves into the class while the spread shrinks

Four readings. (1) w=0w=0 is the unconditional model: mean −0.11-0.11, standard deviation 1.39, because it draws from both classes at once. (2) w=1w=1 reproduces the real class-1 data (mean 0.95 against 0.99, std 0.97 against 1.00, 83.2% of samples on the right side against 84.2%): the plain conditional model is a faithful sampler, and because the classes overlap about 17% of its samples honestly land on the "wrong" side. (3) Larger ww pushes samples into the class: 97.8% on the right side at w=2w=2, 99.9% at w=4w=4; the mean moves to 1.43, 1.78, 1.89, past the real mean of 0.99, an exaggeration of the class. (4) The cost is diversity: the spread falls from 0.97 to 0.47 at w=8w=8, and even the unrelated yy direction shrinks (0.99 to 0.75). That is the "more faithful, less diverse" of the notes, seen in numbers: guidance trades diversity for fidelity.

Classifier guidanceClassifier-free guidance
Extra modelseparate classifier trained on noisy imagesnone: one network, condition randomly dropped
Conditionsfixed classes onlyanything you can embed: class, text, image
Controlgradient scaleguidance scale ww
Costclassifier gradient per steptwo network evaluations per step
Used inclass-conditional ImageNet modelsStable Diffusion, Imagen, DALL-E 2

5.4Chapter summary

  • Classifier guidance adds ∇log⁡p(y∣xt)\nabla\log p(y|x_t) from a separate noisy-image classifier; CFG trains one model with the condition dropped 10 to 20% of the time.
  • ε~=εu+w(εc−εu)\tilde\varepsilon=\varepsilon_u+w(\varepsilon_c-\varepsilon_u): w=0w=0 unconditional, w=1w=1 conditional, w>1w>1 amplified (typical 7 to 15). Check the convention: the CFG paper uses (1+w)εc−wεu(1+w)\varepsilon_c-w\varepsilon_u.
  • Measured: w=1w=1 matched the data (83.2% against 84.2%); w=8w=8 put 100% in the class but cut the spread from 0.97 to 0.47: fidelity up, diversity down.
Next · Part 6Latent Diffusion and Stable Diffusion
06
Part 6

Latent Diffusion and Stable Diffusion

Everything so far works, but a 512×512512\times512 RGB image has 786,432786{,}432 numbers, and the U-Net must denoise all of them at every step, hundreds of times. Most of that effort is spent on tiny, perceptually invisible detail. Latent Diffusion Models (LDM) (Rombach et al., 2022) move the whole diffusion process to a much smaller space.

6.1Two stages

image512x512x3VAE encoderfrozenlatent z64x64x4denoising U-Netin latent spaceVAE decoderfrozentext -> CLIPcross-attentiongeneration: noise z_T (64x64x4) -> denoise in latent space with the prompt -> decode with the VAEthe expensive loop runs on 16,384 numbers instead of 786,432 (48x fewer; 64x fewer positions)
Figure 8. Compress with a VAE once; do the diffusion in the small latent space; decode at the end
  • Stage 1: train a VAE first to compress images into a small latent and decode them back (for example 512×512×3→64×64×4512\times512\times3\to64\times64\times4, height and width divided by 8). Then freeze it.
  • Stage 2: train the diffusion model in the latent space (the U-Net denoises 64×64×464\times64\times4 latents, with the same noise-prediction loss).
  • Generation: sample zT∼N(0,I)z_T\sim\mathcal{N}(0,I) of size 64×64×464\times64\times4; denoise it to z0z_0 with the text condition; decode z0z_0 with the frozen VAE decoder into the image.
The order is a classic exam trap The VAE is trained first and then frozen; the diffusion model is trained second, on the VAE's latents. Saying "first the diffusion model, then the VAE" is wrong, and the diffusion model cannot be trained before the latent space exists.

6.2How much does it save?

main.py
pixel = 512 * 512 * 3
latent = 64 * 64 * 4
print(f"pixel space : 512 x 512 x 3 = {pixel:,} numbers")
print(f"latent space: 64 x 64 x 4   = {latent:,} numbers   -> {pixel / latent:.0f}x fewer")
print(f"spatial positions: {512 * 512:,} against {64 * 64:,} = {(512 * 512) / (64 * 64):.0f}x fewer (the VAE shrinks height and width by 8)")
print()
print("what this does to the cost of the denoising network:")
print(f"  operations that grow with the number of positions (convolutions, FFN): about {(512 * 512) / (64 * 64):.0f}x cheaper")
print(f"  operations that grow with positions squared (self-attention over all positions): about {((512 * 512) / (64 * 64)) ** 2:.0f}x cheaper")
print("the notes' 2304x is the attention-only figure; a U-Net is mostly convolutions, so the realistic saving is a good deal smaller than 2304x")
print(f"memory of one float32 image: {pixel * 4 / 1e6:.1f} MB in pixel space, {latent * 4 / 1e3:.1f} KB as a latent")
Output
pixel space : 512 x 512 x 3 = 786,432 numbers
latent space: 64 x 64 x 4   = 16,384 numbers   -> 48x fewer
spatial positions: 262,144 against 4,096 = 64x fewer (the VAE shrinks height and width by 8)

what this does to the cost of the denoising network:
  operations that grow with the number of positions (convolutions, FFN): about 64x cheaper
  operations that grow with positions squared (self-attention over all positions): about 4096x cheaper
the notes' 2304x is the attention-only figure; a U-Net is mostly convolutions, so the realistic saving is a good deal smaller than 2304x
memory of one float32 image: 3.1 MB in pixel space, 65.5 KB as a latent

The latent holds 16,384 numbers against 786,432: 48 times fewer (the notes' figure). The notes then claim a "2304 times faster" speed-up, 48248^2. Careful: height and width shrink by 8, so the number of spatial positions falls from 262,144 to 4,096, 64 times, and the channel count goes up (3 to 4), which is why the total numbers fall by 48, not 64. Convolutions and per-position layers scale with the number of positions (about 64 times cheaper); only the self-attention over all positions scales with its square (about 4,096 times). A real U-Net uses attention only at its coarser resolutions and is mostly convolution, so the realistic saving is large (tens of times) but nowhere near 2304. The notes themselves say "approximate"; treat 48 times fewer numbers as the solid fact.

6.3Text conditioning by cross-attention

The prompt is turned into vectors by a text encoder (CLIP in Stable Diffusion). Inside the U-Net, the denoising network uses cross-attention (Week 9): the queries QQ come from the U-Net's image features and the keys and values K,VK,V come from the text encoder's output. So at every attention layer each image position looks at the words of the prompt and takes what is relevant, steering generation at every layer. This is the same cross-attention as in the Week 10 decoder, with the "encoder" being the text. Combined with classifier-free guidance (Chapter 5, where the null condition is the empty prompt), this is the recipe of Stable Diffusion.

ComponentRole in Stable Diffusion
Text encoder (CLIP)prompt to a sequence of vectors
VAE encoder / decoderimage to latent 64×64×464\times64\times4 and back (trained first, frozen)
U-Netpredicts the noise in the latent; time embedding + text via cross-attention
Sampler (DDIM, etc.) + CFG20 to 50 steps with guidance scale about 7.5
Which model for which job (the notes' list)

Text to high-quality image: Stable Diffusion (LDM + CFG + CLIP). Faster generation: DDIM instead of DDPM. Image editing: DDIM inversion. Class-conditional generation: classifier guidance (or CFG). Flexible multi-modal conditioning: CFG.

6.4Chapter summary

  • LDM: train a VAE first and freeze it; train the diffusion model in the latent space; generate by denoising zTz_T and decoding z0z_0.
  • 512×512×3=786,432512\times512\times3=786{,}432 against 64×64×4=16,38464\times64\times4=16{,}384: 48 times fewer numbers, 64 times fewer positions; the notes' "2304 times" holds only for attention.
  • Text conditioning: CLIP embedding feeds cross-attention (QQ from the U-Net, K,VK,V from the text) at every layer; with CFG this is Stable Diffusion.
Next · InterviewInterview Question Bank
07
Interview

Interview Question Bank

What is likely to be asked from Week 11, with 30-second answers, deeper explanations, a lecture-or-extra tag on each question, and code you can run.

Week 11 questions are about how an image is destroyed, how the network learns to undo it, and the tricks (DDIM, guidance, latent space) that make it usable. Each answer starts with a 30-second version. Questions are tagged From the notes or Extra.

1Definition

One clear sentence: what it is.

2Intuition or tiny example

Why it works, in one picture or three lines.

3The failure it prevents

What goes wrong without it.

7.1Interview heat-map: what is most likely

Very likely6 topics
Forward diffusion closed form"Derive the closed form of forward diffusion."Notes 2, 3
The DDPM training objective"What does DDPM train a network to predict, and why the noise?"Notes 2, 3, 7
DDIM versus DDPM"Compare DDIM and DDPM."Notes 2, Ex. 3
Classifier-free guidance"Explain CFG. What does the guidance scale do?"Notes 2, 3, Ex. 2
Latent diffusion"Explain latent diffusion and the Stable Diffusion pipeline."Notes 2, Ex. 4
The generative trilemma"What is the generative trilemma and where do GAN, VAE and diffusion sit?"Notes 2
Likely4 topics
Sinusoidal time embeddings"How does the U-Net know the diffusion step?"Notes 2
Noise schedules"Linear versus cosine noise schedule?"Notes 2
FID, IS and CLIP score"Which metrics measure generative models and which direction is better?"Notes 2
Classifier guidance versus CFG"Why is classifier-free guidance preferred?"Notes 2
Sometimes1 topics
Why diffusion is slow, and the fixes"Why is diffusion sampling slow? Three fixes."Notes 2, 6

7.2Foundations

Interview 1State the generative trilemma and place GANs, VAEs and diffusion models in it. Why are VAE outputs blurry?
Week 11Very likelyNotes 2From the lecture

Quality, diversity, speed: pick two.

Prints
the answer that minimises squared error: -0.001, an 'average' of the two correct answers
  squared error of the average         : 1.000
  squared error of committing randomly: 2.002   <- squared error PREFERS the average
distance from the average to the nearest real answer: 0.999 (it is not a valid answer at all)
so a decoder trained with MSE (a plain VAE) learns the mean of everything that could fit, which in an image is a blur;
a GAN or a diffusion model must commit to ONE sharp answer instead
Answer
Say this first (30 seconds)

Three desirable properties: high-quality samples, mode coverage (diversity), fast sampling. GANs: quality and speed, not diversity (mode collapse). VAEs: diversity and speed, not quality (blurry). Diffusion: quality and diversity, but slow (many steps; mitigated by DDIM and latent diffusion). VAE blur comes from the MSE reconstruction loss, which is minimised by the average of all outputs that could fit, and an average of sharp images is a blur.

Go deeper
  • Measured: for outputs +1 or -1, the MSE-optimal answer is 0 (error 1.0), which beats committing to a random sharp answer (2.0) although 0 is not valid.
Follow-up they may askIs the trilemma a law or a simplification?
Interview 2Write the forward diffusion step, derive the closed form for any tt, and explain the reparameterisation trick.
Week 11Very likelyNotes 2, 3From the lecture

Products of alphas; a sum of Gaussians is a Gaussian.

Prints
after 500 steps: iterated mean 0.4226, std 0.9606   |   closed form mean 0.4245, std 0.9616   (theory: mean 0.4205, std 0.9599)
one jump to any t gives the same distribution as t small steps: training never has to iterate
Answer
Say this first (30 seconds)

One step: xt=1−βt xt−1+βt εx_t=\sqrt{1-\beta_t}\,x_{t-1}+\sqrt{\beta_t}\,\varepsilon. With αt=1−βt\alpha_t=1-\beta_t and αˉt=∏s≤tαs\bar\alpha_t=\prod_{s\le t}\alpha_s, substituting repeatedly and using that a sum of independent Gaussians is Gaussian gives xt=αˉt x0+1−αˉt εx_t=\sqrt{\bar\alpha_t}\,x_0+\sqrt{1-\bar\alpha_t}\,\varepsilon, ε∼N(0,I)\varepsilon\sim\mathcal{N}(0,I). Reparameterisation: write the sample as a deterministic function of x0x_0 and independent noise, so xtx_t is obtained directly at any tt without iterating. As t→Tt\to T, αˉt→0\bar\alpha_t\to0 and xTx_T is pure noise.

Go deeper
  • Verified: 500 iterated steps and one jump gave the same mean and std (0.4226/0.9606 against 0.4245/0.9616).
Follow-up they may askIs αˉt\bar\alpha_t increasing or decreasing in tt?
Interview 3Compare the linear, cosine and quadratic noise schedules.
Week 11LikelyNotes 2Extra

How fast is the signal destroyed?

Prints
alpha_bar (how much of the original signal's variance is left)
     t      linear    cosine
    50     0.9710    0.9920
   100     0.8970    0.9721
   250     0.5241    0.8470
   500     0.0786    0.4938
   750     0.0034    0.1443
   900     0.0003    0.0241
  1000     0.0000    0.0000
linear destroys the image early: by t = 500 only 0.079 of the signal is left, so the last half of the steps work on almost pure noise
Answer
Say this first (30 seconds)

The schedule fixes αˉt\bar\alpha_t, how much signal remains at step tt. Linear β\beta destroys the image early (αˉ500=0.079\bar\alpha_{500}=0.079), wasting later steps on near-pure noise. Cosine destroys it gradually (0.494 at t=500t=500), reported to give better samples. Quadratic adds noise aggressively early. All use the same closed form.

Go deeper
  • Measured at t=50: 0.971 against 0.992; t=500: 0.079 against 0.494.
Follow-up they may askWhat would an ideal schedule look like?
Interview 4Define FID, Inception Score and CLIP score. Which is better, higher or lower?
Week 11LikelyNotes 2From the lecture

FID lower; IS and CLIP higher.

Prints
Inception Score = exp( mean KL( P(y|x) || P(y) ) )   (1 is the worst, the number of classes is the best)
  sharp images, all 10 classes   :   9.58   <- realistic AND diverse
  sharp images, one class only   :   1.00   <- mode collapse is caught
  blurry images (P(y|x) uniform) :   1.00   <- unrealistic is caught
IS never looks at the real images, which is its limitation; FID (Week 6) compares generated features with real features
Answer
Say this first (30 seconds)

FID: Fréchet distance between Gaussians fitted to Inception features of real and generated images, ∥μr−μg∥2+Tr(Σr+Σg−2(ΣrΣg)1/2)\|\mu_r-\mu_g\|^2+\mathrm{Tr}(\Sigma_r+\Sigma_g-2(\Sigma_r\Sigma_g)^{1/2}); lower is better; sees quality and diversity. IS: exp⁡E KL(P(y∣x)∥P(y))\exp\mathbb{E}\,\mathrm{KL}(P(y|x)\|P(y)); rewards confident P(y∣x)P(y|x) (sharp) and spread-out P(y)P(y) (diverse); higher is better; ignores real images. CLIP score: cosine similarity of image and prompt embeddings; higher is better (text-image alignment).

Go deeper
  • Measured IS: 9.58 for sharp diverse images, 1.00 for collapsed or blurry ones.
Follow-up they may askWhy can IS miss mode collapse within a class?

7.3Training and sampling

Interview 5What is the DDPM training objective? Why predict the noise rather than the image?
Week 11Very likelyNotes 2, 3, 7From the lecture

MSE between true and predicted noise.

Prints
posterior mean (notes' formula)       : [ 0.3783  1.1056  1.1283 -0.5818 -1.3662]
mean written through the noise epsilon: [ 0.3783  1.1056  1.1283 -0.5818 -1.3662]
identical: True  -> predicting the NOISE is enough to take a denoising step
posterior variance beta_tilde = 0.008052  (beta_t = 0.008068)
Answer
Say this first (30 seconds)

Lsimple=Et,x0,ε ∥ε−εθ(αˉtx0+1−αˉtε, t)∥2L_{\text{simple}}=\mathbb{E}_{t,x_0,\varepsilon}\,\|\varepsilon-\varepsilon_\theta(\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilon,\,t)\|^2. Algorithm: sample x0x_0, a random tt, noise ε\varepsilon; make xtx_t with the closed form; predict ε\varepsilon; minimise the MSE. Predicting the noise is enough because the reverse-step mean can be written with it, μ~t=1αt(xt−βt1−αˉtε)\tilde\mu_t=\tfrac1{\sqrt{\alpha_t}}(x_t-\tfrac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon); the target always has mean 0 and variance 1; and it equals denoising score matching. DDPM predicts the noise, not the image.

Go deeper
  • Verified: the mean written with the noise equals the posterior mean exactly. In the toy model the loss fell from 0.985 to 0.220.
Follow-up they may askWhat input does the network receive besides xtx_t?
Interview 6How is the timestep given to the U-Net, and why not as a plain integer?
Week 11LikelyNotes 2From the lecture

Same idea as positional encoding.

Prints
cosine similarity of the embedding of step 10 with others (nearby steps should look alike):
   step  10 vs step  11: +0.966
   step  10 vs step  12: +0.884
   step  10 vs step  20: +0.658
   step  10 vs step  50: +0.481
   step  10 vs step 500: +0.216
   step  10 vs step 900: +0.155
Answer
Say this first (30 seconds)

The step tt is mapped to a sinusoidal embedding (the Transformer's positional-encoding formula), passed through a small MLP, and added to the feature maps at every resolution of the U-Net. As a bare integer, "step 10" and "step 11" would be unrelated inputs; sinusoidal embeddings make nearby steps similar (cosine 0.966 for 10 and 11) and far steps different, and give the network a smooth, bounded signal about the noise level.

Go deeper
  • Measured: cosine with step 10 is 0.966 (11), 0.658 (20), 0.216 (500).
Follow-up they may askWhy a U-Net rather than a plain CNN?
Interview 7Compare DDIM and DDPM. What is DDIM inversion used for?
Week 11Very likelyNotes 2, Ex. 3From the lecture

Stochastic and slow versus deterministic and fast.

Prints
DDPM                100            0.985          0.10 / 0.14
DDIM 50 steps        50            0.956          0.08 / 0.19
DDIM 20 steps        20            0.915          0.06 / 0.20
DDIM 10 steps        10            0.762          0.04 / 0.19
DDIM 5 steps          5            0.273          0.00 / 0.15
DDIM 2 steps          2            0.014          0.00 / 0.01

same starting noise x_T:
  DDPM run twice with different step noise: mean difference between the two results = 2.585 (stochastic)
  DDIM run twice:                          mean difference between the two results = 0.0 (deterministic)
Answer
Say this first (30 seconds)

DDPM: a stochastic Markov chain; fresh noise at each step, so the same xTx_T gives different images; it cannot skip steps (about 1,000 calls). DDIM: a non-Markovian, deterministic sampler using the same trained network (same xTx_T, same image); it can skip steps (1000 →\to 50, about 20x faster) for a modest quality loss. DDIM is also invertible: run it forward to map a real image to a noise xTx_T (DDIM inversion), edit that noise or the conditioning, and decode: image editing and style transfer.

Go deeper
  • Measured on the toy model: DDIM with 50, 20, 10 calls kept 95.6%, 91.5%, 76.2% of points on a blob (DDPM with 100: 98.5%); DDIM runs differed by 0.0, DDPM runs by 2.585.
Follow-up they may askDoes DDIM need retraining?
Interview 8Why is diffusion sampling slow, and how can it be sped up?
Week 11SometimesNotes 2, 6Extra

Many sequential network calls on big tensors.

Prints
DDPM                100            0.985          0.10 / 0.14
DDIM 50 steps        50            0.956          0.08 / 0.19
DDIM 20 steps        20            0.915          0.06 / 0.20
DDIM 10 steps        10            0.762          0.04 / 0.19
DDIM 5 steps          5            0.273          0.00 / 0.15
DDIM 2 steps          2            0.014          0.00 / 0.01
Answer
Say this first (30 seconds)

Generation needs about TT sequential denoising calls (1,000 for DDPM), each a full U-Net pass on a large tensor; they cannot be run in parallel because each needs the previous result. Speed-ups: DDIM (skip steps, 20-50 calls), latent diffusion (a 48x smaller tensor), and other better samplers or distilled models. CFG doubles the cost per step.

Go deeper
  • Measured: DDIM 10 calls kept 76% of the quality metric of 100 DDPM calls on the toy.
Follow-up they may askWhich of these changes the trained network?

7.4Control and scale

Interview 9Explain classifier-free guidance, its formula and the effect of the guidance scale ww.
Week 11Very likelyNotes 2, 3, Ex. 2From the lecture

Extrapolate from unconditional toward conditional.

Prints
real class-1 data: mean x +0.99, std x 1.00, fraction with x > 0: 0.842

guidance scale w   mean x   std x   fraction x > 0   std y
         0        -0.11    1.39        0.465      1.05
         1        +0.95    0.97        0.832      0.99
         2        +1.43    0.75        0.978      0.95
         4        +1.78    0.60        0.999      0.88
         8        +1.89    0.47        1.000      0.75
w = 0 is unconditional (both classes); w = 1 reproduces the real class-1 distribution; w > 1 pushes further and further into class 1 and the spread shrinks
Answer
Say this first (30 seconds)

Train one network on both conditional and unconditional prediction by dropping the condition (replacing it by a null token) 10 to 20% of the time. At sampling, ε~=εθ(xt,∅)+w [εθ(xt,c)−εθ(xt,∅)]\tilde\varepsilon=\varepsilon_\theta(x_t,\varnothing)+w\,[\varepsilon_\theta(x_t,c)-\varepsilon_\theta(x_t,\varnothing)]. w=0w=0: unconditional; w=1w=1: conditional; w>1w>1: the condition's effect is amplified (typically 7 to 15 for text-to-image): more faithful to the prompt, less diverse, and over-saturated if too large. For w=7.5w=7.5: −6.5 εu+7.5 εc-6.5\,\varepsilon_u+7.5\,\varepsilon_c. It needs no separate classifier, unlike classifier guidance.

Go deeper
  • Measured on a two-class toy: w=1 matched the data (83.2% against 84.2% on the right side); w=8 gave 100% but cut the spread from 0.97 to 0.47.
  • Convention: the original paper writes (1+w) eps_c - w eps_u.
Follow-up they may askWhy does CFG need two network evaluations per step?
Interview 10Compare classifier guidance with classifier-free guidance.
Week 11LikelyNotes 2From the lecture

A separate noisy-image classifier or none.

Prints
real class-1 data: mean x +0.99, std x 1.00, fraction with x > 0: 0.842

guidance scale w   mean x   std x   fraction x > 0   std y
         0        -0.11    1.39        0.465      1.05
         1        +0.95    0.97        0.832      0.99
Answer
Say this first (30 seconds)

Classifier guidance adds ∇log⁡p(y∣xt)\nabla\log p(y|x_t) from a separate classifier that must be trained on noisy images and handles only fixed classes. CFG trains one network with the condition randomly dropped and extrapolates εu+w(εc−εu)\varepsilon_u+w(\varepsilon_c-\varepsilon_u): no extra model, any embeddable condition (class, text), a tunable scale. Cost: two network evaluations per step. CFG is used in Stable Diffusion and Imagen.

Go deeper
  • Both trade diversity for fidelity when the scale grows.
Follow-up they may askWhat condition is used for the unconditional pass in Stable Diffusion?
Interview 11Explain the two-stage latent diffusion model and the Stable Diffusion pipeline. Why work in latent space?
Week 11Very likelyNotes 2, Ex. 4From the lecture

VAE first, then diffusion in the latent space.

Prints
pixel space : 512 x 512 x 3 = 786,432 numbers
latent space: 64 x 64 x 4   = 16,384 numbers   -> 48x fewer
spatial positions: 262,144 against 4,096 = 64x fewer (the VAE shrinks height and width by 8)

what this does to the cost of the denoising network:
  operations that grow with the number of positions (convolutions, FFN): about 64x cheaper
  operations that grow with positions squared (self-attention over all positions): about 4096x cheaper
the notes' 2304x is the attention-only figure; a U-Net is mostly convolutions, so the realistic saving is a good deal smaller than 2304x
memory of one float32 image: 3.1 MB in pixel space, 65.5 KB as a latent
Answer
Say this first (30 seconds)

Pixel-space diffusion is costly (512×512×3=786,432512\times512\times3=786{,}432 numbers per step). Stage 1: train a VAE to compress images to a latent (64×64×4=16,38464\times64\times4=16{,}384, 48x fewer numbers) and freeze it. Stage 2: train the diffusion U-Net in the latent space. Generation: sample zT∼N(0,I)z_T\sim\mathcal{N}(0,I) (64×64×464\times64\times4), denoise to z0z_0 guided by the text, decode with the VAE decoder. Stable Diffusion: CLIP text encoder, U-Net with cross-attention (QQ from image features, K,VK,V from the text), CFG, latent 64×64×464\times64\times4. The VAE is trained first.

Go deeper
  • The notes' "2304x faster" is the attention-only figure; positions fall 64x, so convolutions get about 64x cheaper.
Follow-up they may askWhich part is trained first?
Next · SummaryWeek 11 Summary Sheet
08
Summary

Week 11 Summary Sheet

Week 11 on one page: chapter glance, every formula, the two samplers side by side, the pipeline of Stable Diffusion, a checklist and corrections to the source notes.

8.1Week 11 at a glance

C1Landscape

VAE blurry (MSE), GAN collapses, diffusion: quality + diversity but slow. FID lower, IS and CLIP higher.

C2Forward

xt=αˉtx0+1−αˉtεx_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilon: jump to any tt. Linear αˉ500=0.079\bar\alpha_{500}=0.079, cosine 0.494.

C3Training

Predict the noise: L=∥ε−εθ(xt,t)∥2L=\|\varepsilon-\varepsilon_\theta(x_t,t)\|^2. U-Net + sinusoidal time embedding.

C4Sampling

DDPM stochastic, no skipping. DDIM deterministic, skips steps (10 calls keep 76%), invertible.

C5CFG

εu+w(εc−εu)\varepsilon_u+w(\varepsilon_c-\varepsilon_u); w=1w=1 matched the data, w=8w=8 spread 0.97 to 0.47.

C6LDM

VAE first (frozen), diffusion in latent 64×64×464\times64\times4 (48x fewer numbers, 64x fewer positions), text by cross-attention.

8.2Every formula

ItemFormulaNote
One stepxt=1−βtxt−1+βtεx_t=\sqrt{1-\beta_t}x_{t-1}+\sqrt{\beta_t}\varepsilonfixed
Closed formxt=αˉtx0+1−αˉtεx_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\varepsilonαˉt=∏(1−βs)\bar\alpha_t=\prod(1-\beta_s)
LossE∥ε−εθ(xt,t)∥2\mathbb{E}\|\varepsilon-\varepsilon_\theta(x_t,t)\|^2predict the noise
Reverse mean1αt(xt−βt1−αˉtεθ)\tfrac1{\sqrt{\alpha_t}}(x_t-\tfrac{\beta_t}{\sqrt{1-\bar\alpha_t}}\varepsilon_\theta)DDPM step
DDIM stepαˉsx^0+1−αˉsεθ\sqrt{\bar\alpha_{s}}\hat x_0+\sqrt{1-\bar\alpha_{s}}\varepsilon_\thetax^0=(xt−1−αˉtεθ)/αˉt\hat x_0=(x_t-\sqrt{1-\bar\alpha_t}\varepsilon_\theta)/\sqrt{\bar\alpha_t}
CFGεu+w(εc−εu)\varepsilon_u+w(\varepsilon_c-\varepsilon_u)w≈7w\approx7-15
FID∥μr−μg∥2+Tr(…)\|\mu_r-\mu_g\|^2+\mathrm{Tr}(\ldots)lower better
ISexp⁡E KL(P(y∣x)∥P(y))\exp\mathbb{E}\,\mathrm{KL}(P(y|x)\|P(y))higher better

8.3DDPM and DDIM side by side

DDPMDDIM
Noise at inferencefresh each stepnone
Same xTx_Tdifferent images (2.585)same image (0.0)
Skip stepsnoyes (20-50 calls)
Image to noisenoinversion

8.4Key terms

C1-C2
ELBO
VAE training objective.
trilemma
quality, diversity, speed.
reparameterisation
sample = function of noise.
schedule
how βt\beta_t grows.
C3-C4
U-Net
noise predictor with skips.
time embedding
sinusoidal tt.
DDIM
deterministic sampler.
inversion
image to xTx_T.
C5-C6
CFG
condition dropped in training.
guidance scale
ww.
LDM
diffusion in a VAE latent.
CLIP
text-image embeddings.

8.5The pairs you must tell apart

DDPMpredicts the noise
VS
"predicts the image"wrong
classifier guidanceseparate noisy classifier
VS
CFGone model, condition dropped
w=0w=0unconditional
VS
w=1w=1conditional; w>1w>1 amplified
FIDlower is better
VS
IS / CLIPhigher is better
VAE stagetrained FIRST, frozen
VS
diffusion stagetrained second, in latent space
linear scheduledestroys early
VS
cosine schedulegradual

8.6Last-minute checklist

C1-C2
  • trilemma: GAN, VAE, diffusion placement
  • closed form from alphas
  • FID lower, IS higher
C3-C4
  • predict the noise
  • time embedding is sinusoidal
  • DDIM deterministic, skips, inverts
C5-C6
  • CFG formula and ww meaning
  • VAE first, then diffusion
  • cross-attention: Q image, K,V text

8.7Corrections and additions to the source notes

WhereThe source saysBetter
Example 1"only 31.6% of the original image remains, 94.9% noise"0.316 and 0.949 are coefficients (they sum to 1.265); the variance is 10% signal, 90% noise; the real linear-schedule αˉ500\bar\alpha_{500} is 0.079, not 0.1
Example 4"(786432/16384)2^2 = 2304x faster"48x fewer numbers is right, but positions fall 64x (not 48) and only attention scales with their square (4,096x); a U-Net is mostly convolution, so the saving is far below 2304x
CFG"w=1w=1 conditional, w=0w=0 unconditional"correct for this form; the original paper uses (1+w)εc−wεu(1+w)\varepsilon_c-w\varepsilon_u where w=0w=0 is conditional: state your form
Example 3 timings"1000 steps ~1 min, 50 steps ~3 s"illustrative; real times depend on hardware and network size. Measured here: quality, not time
Trilemmapresented as fixed pairsa simplification: modern GANs improved diversity and fast diffusion samplers exist
DDIM steps"20x faster"quality drops as steps shrink (toy: 95.6%, 91.5%, 76.2% for 50, 20, 10 calls)
Revision overview
Forwardadd noise
closed form
schedules
Trainingpredict the noise
U-Net
time embedding
SamplingDDPM stochastic
DDIM deterministic
inversion
Week 11Diffusion models
Guidanceclassifier guidance
CFG, scale w
fidelity vs diversity
LDMVAE first
latent 64x64x4
cross-attention text
MetricsFID lower
IS higher
CLIP higher
Revision: all of Week 11 on one page
Back to top