Generative Expert Lab
After this lesson, you will be able to:
- Write the forward noising jump, the DDPM reverse mean, and the DDIM step with its eta dial, and say which quantities each one uses
- Combine two noise guesses with classifier-free guidance in the track convention, and translate the Ho and Salimans form into it
- Convert between noise, clean image and velocity predictions using only the signal fraction
- Write the denoising score matching loss and check a true score against a slope found by finite differences
- Write the flow matching target and an Euler sampler, and say which way time runs in this track and in SD3 and FLUX
- Write a confidence-based unmasking step and a Frechet distance, and know the three traps that break most implementations: sign, time direction, and alpha versus alpha bar
Before You Start
#How This Lab Works
TODO marks, a Run button, a Check my work button, and a Show solution button.Check my work runs your code, then runs a list of tests, one at a time. It tells you how many passed and shows the message of the first one that failed. The starter is built to fail on purpose, so a fresh cell always says some checks failed. When you finish the TODO lines, all of them pass.
Three things about the tests are worth knowing before you start.
- They are tolerance based. A result close enough to the right answer passes, so a harmless rounding difference never fails you.
- They are deterministic. Any random numbers come from a fixed seed, so a test that fails today fails the same way tomorrow.
- They check more than the one example in the hand trace. Some compare your answer with a second route to the same number, such as a slope found by finite differences or a closed form for a simple case. If your code only matches the trace, it will still fail those.
The cells use numpy only. The browser has no torch, so none of this needs a graphics card, and each cell runs in well under a second.
The share of the original picture left at step t. It starts near 1 and falls towards 0 as noise is added.
The noise that was mixed in, a draw from a bell curve with mean 0 and spread 1. The network is often trained to guess it.
The network's noise guess with no prompt, and with the prompt. Guidance uses the gap between them.
The direction in which the probability of the data rises fastest, at a given point and noise level.
How fast, and in which direction, a point should move at a given time. Flow matching trains a network to predict it.
The simplest way to follow a velocity: move by velocity times a small time step, then look again.
Replacing a hidden placeholder in a token list with a real token, a few positions at a time.
A gap between two bell curves that counts both the gap in centre and the gap in spread. FID is this distance between fitted curves for real and generated image features.
#The Conventions Used Here
Most mistakes in this area are not about maths. They are about which of two conventions a sentence uses. Fix them before writing code. This table is the single source for the whole lab, and the tests assume it.
| Quantity | Meaning in this lab |
|---|---|
| Step index t | An array index from 0 to 999. t = 0 is the first, lightest noising step. t = 999 is almost pure noise. |
| beta_t | 1000 values from 0.0001 to 0.02, evenly spaced (the linear schedule). |
| alpha_t | 1 minus beta_t. |
| alpha bar_t | The running product of alpha_0 up to alpha_t. The signal fraction. |
| Guidance | eps_guided = eps_u + s * (eps_c - eps_u). s = 1 is the plain conditional model, s = 0 the unconditional one. The guidance_scale of the diffusers library is this s. |
| Flow time | t = 0 is pure noise and t = 1 is data. The point is x_t = (1 - t) x0 + t x1, with x0 the noise and x1 the data. The target velocity is x1 - x0. |
| SD3 and FLUX time | The other way round: t = 0 is data and t = 1 is noise, so their target is noise minus data. |
| Masked token | The number -1 in a list of token ids. |
Two notes. First, the diffusion lesson writes steps from 1 to T in places, and its code indexes arrays from 0. Here the code indexes from 0 throughout, so "t = 499" means the 500th step. Second, the flow matching lesson and the SD3 paper disagree about which end is t = 0. Neither is wrong. Exercise 7 makes you write both.
#Exercise 1: Jump to Any Noise Level
You write two functions. One builds the signal fraction alpha bar for all 1000 steps. The other makes the jump: x_t = sqrt(alpha bar_t) * x0 + sqrt(1 - alpha bar_t) * eps.
Jump to any noise level
Write make_alpha_bar, which builds the signal fraction for all 1000 steps, and q_sample, which jumps straight from clean x0 to the noisy x_t. Press Check my work to run the hidden tests.
The test that most often catches a wrong answer averages many draws. The mean of x_t must be sqrt(alpha bar) times x0, and the variance must be 1 - alpha bar. If you used the plain alpha instead of the running product, the mean stays near x0 even at the middle of the chain, which is wrong by a factor of more than three.
#Exercise 2: One Reverse Step (DDPM)
The mean is (x_t - beta_t / sqrt(1 - alpha bar_t) * eps_pred) / sqrt(alpha_t). The added noise is sqrt(beta_t) times a standard bell-curve draw z. The caller passes z in, which keeps the function deterministic and testable.
One reverse step (DDPM)
Write ddpm_mean, the mean of the slightly cleaner picture x_{t-1} given a noise guess, and ddpm_step, which adds fresh noise on every step except the last. Press Check my work.
A strong test feeds in the true noise. With the true eps, your mean must equal the exact posterior mean, which the test computes by a different formula that uses x0 directly. The two formulas agree only if you divided by the right square root.
#Exercise 3: A DDIM Step
Write the estimate first: x0_hat = (x_t - sqrt(1 - ab_t) * eps) / sqrt(ab_t). Then sigma = eta * sqrt((1 - ab_prev) / (1 - ab_t)) * sqrt(1 - ab_t / ab_prev). Then x_prev = sqrt(ab_prev) * x0_hat + sqrt(1 - ab_prev - sigma^2) * eps + sigma * z. Here ab_prev is the signal fraction you are going to, and it is larger than ab_t.
A DDIM step
Write predict_x0, the clean picture implied by a noise guess, and ddim_step, which jumps from noise level ab_t to a cleaner level ab_prev with a dial eta for fresh noise. Press Check my work.
Two tests deserve a note. With eta = 0 and the true noise, DDIM must land exactly on the forward formula at the earlier level, because the same noise is reused. With eta = 1 and neighbouring steps, the part that has no randomness must equal the DDPM mean from exercise 2. That ties the two exercises together.
#Exercise 4: Classifier-Free Guidance
In this track the formula is eps_guided = eps_u + s * (eps_c - eps_u). The original paper by Ho and Salimans writes (1 + w) * eps_c - w * eps_u. They are the same thing with s = 1 + w. You write the combination, the conversion, and a two-pass wrapper that calls a model with and without a prompt.
Classifier-free guidance
Write cfg_combine, which pushes the unconditional noise guess towards the conditional one by a factor s, s_from_ho_salimans, which converts the paper's w into s, and two_pass_guided, which calls the model with and without a prompt. Press Check my work.
The tests check the two edge values, an order swap (the gap flips sign if you swap the two guesses), that the inputs are not changed in place, and that the wrapper calls the model exactly twice.
#Exercise 5: Velocity Prediction
Once you have a v guess you can recover the others: x0 = sqrt(ab) * x_t - sqrt(1 - ab) * v and eps = sqrt(1 - ab) * x_t + sqrt(ab) * v. Notice that the two roles swap: one formula is the mirror of the other.
Velocity prediction
Write v_target, which builds the velocity from x0 and eps, and x0_from_v and eps_from_v, which recover the clean picture and the noise from x_t and v. Press Check my work.
The round trip tests use two extreme levels, ab = 0.99 and ab = 0.01. A formula with a wrong sign can still pass at one level by luck, but not at both.
#Exercise 6: Denoising Score Matching
To test it we need a case where the true score is known. Take two bumps at -2 and +2, each with spread 0.5. After adding noise of size sigma, each bump has variance 0.25 + sigma^2, and the score is a weighted pull towards the bumps: each bump pulls with strength -(x - mu) / variance, weighted by how much that bump owns the point. You write that score, then the loss.
Denoising score matching
Write mixture_score, the exact score of two noisy bumps, and dsm_loss, the sigma-weighted denoising score matching loss. Press Check my work.
Look at how the tests avoid trusting your formula. They find the slope of log p by a finite difference, which is a completely separate route, and require your score to match it. They also check that scaling the true score by 0.8 or 1.2 makes the loss worse, since the true score is the best possible guess, and that a flipped sign is worse than predicting zero.
#Exercise 7: Flow Matching and Euler
In this track t = 0 is noise and t = 1 is data. SD3 and FLUX flip it: t = 0 is data, t = 1 is noise, and so their target is noise minus data. You write the interpolation, the target in this track's convention, the SD3 target, and the sampler.
Flow matching and Euler
Write fm_interpolate, fm_target, sd3_target and euler_sample for flow matching in this track's convention (t = 0 is noise, t = 1 is data). Press Check my work.
The sharpest test records the times at which your sampler calls the network. With 4 steps they must be 0, 0.25, 0.5 and 0.75: step i is taken at time i times dt, and the network is never asked about t = 1. Another test uses a curved velocity, 1 + 2t, where Euler really is inexact, and expects the known four-step answer of 1.75 against the exact 2.
#Exercise 8: Unmasking Tokens
You write two functions. One splits the positions over the rounds as evenly as possible, giving the first rounds the leftovers. The other does one round: pick the k most confident hidden positions, fill them, and leave everything already filled alone.
Unmasking tokens
Write tokens_per_step, which splits positions over rounds as evenly as possible, and unmask_step, which fills the k most confident hidden positions. Press Check my work.
One test is built so that the largest raw logit and the largest probability point to different places. A position with logits [9, 9] has a huge logit but only a 50 percent best choice. Ranking by raw logits picks it, which is wrong.
#Exercise 9: FID from Features
np.linalg.eigh, take the square root of each strength, and put it back together.The product S1 S2 is not symmetric, so its root is awkward. A neat fix: with R the square root of S1, the matrix R S2 R is symmetric and has the same eigenvalues as S1 S2. So tr((S1 S2)^(1/2)) equals tr((R S2 R)^(1/2)), and you can use your symmetric square root twice.
FID from features
Write psd_sqrt, a symmetric matrix square root, frechet_distance between two Gaussians, and fid_from_features, which fits the Gaussians to two feature sets. Press Check my work.
Real FID uses 2048 features and tens of thousands of images, so the covariance is large and the square root is slower, which is why libraries use dedicated routines. The formula you wrote is the same. The dimension is only a size.
#Where the Traps Are
Every exercise has one common wrong answer that looks right. They fall into three families.
| Symptom | Likely cause |
|---|---|
| Sampled pictures are washed-out, or all noise, or burned in | The guidance gap was reversed, or s was set far too high, or alpha was used where alpha bar belongs |
| A sampler runs without error but ends as pure noise | Time direction reversed: it walked from clean to noisy |
| Training loss never moves from about 1 | The target sign is wrong, so the best the network can do is predict zero |
| A deterministic sampler is not deterministic | A noise draw is still being added when eta is 0, or the same z is reused in the wrong place |
| Last step looks grainy | Noise was added at t = 0, the final step |
| Masked sampler overwrites a token it already chose | The update did not skip already-filled positions |
#Capstone Checklist
Use this list to check that you are ready to read and write real diffusion and flow code. Tick each item only if you can do it without looking.
- I can write the forward jump and say which of alpha and alpha bar it uses, and why
- I can write the DDPM mean from a noise guess, and say when no noise is added
- I can write a DDIM step, set eta to get a deterministic or a random sampler, and say what happens at ab_prev = 1
- I can write classifier-free guidance, state what s = 0 and s = 1 give, and turn a Ho and Salimans w into s
- I can convert between noise, clean image and velocity predictions in both directions
- I can write the score matching loss and name the target the network is matched to
- I can write a flow matching target and an Euler sampler, and say which end is data in this track and in SD3
- I can write a confidence-based unmasking round that never overwrites a chosen token
- I can write the Frechet distance with an eigen-decomposition square root
- Given a sampler I have never seen, I can find the sign, the time direction and the alpha-or-alpha-bar choice in under a minute
If you can tick every line, you can read the sampler code of a library and spot a convention mismatch. If you cannot, go back to the exercise for that line, open the solution, and write the function again without it.
Denoising Diffusion Probabilistic Models
Jonathan Ho, Ajay Jain, Pieter Abbeel (2020)
The forward jump and the reverse mean from exercises 1 and 2.
Denoising Diffusion Implicit Models
Jiaming Song, Chenlin Meng, Stefano Ermon (2021)
The deterministic step with the eta dial from exercise 3.
Progressive Distillation for Fast Sampling of Diffusion Models
Tim Salimans, Jonathan Ho (2022)
Introduces the velocity prediction of exercise 5.
Flow Matching for Generative Modeling
Yaron Lipman and others (2023)
The straight-line target and Euler sampling of exercise 7.
#Key Takeaways
- The jump to any noise level uses alpha bar: x_t = sqrt(alpha bar) x0 + sqrt(1 - alpha bar) eps. The single-step alpha is a different number
- The DDPM mean is (x_t - beta / sqrt(1 - alpha bar) * eps) / sqrt(alpha), with noise added on every step except the last. DDIM goes through an x0 estimate and an eta dial, and with eta = 1 it matches DDPM
- Guidance in this track is eps_u + s * (eps_c - eps_u). Ho and Salimans' w gives s = 1 + w
- Velocity v = sqrt(ab) eps - sqrt(1 - ab) x0 lets you recover x0 and eps with two mirror formulas
- The denoising score matching loss is the mean of (sigma s + eps) squared, and a true score beats every scaled copy of itself
- Flow matching here runs from noise at t = 0 to data at t = 1 with target x1 - x0. SD3 and FLUX run the other way. A straight path needs one Euler step
- Unmasking ranks by softmax probability, never by raw logit, and leaves chosen tokens alone. FID is a Frechet distance whose matrix square root comes from an eigen-decomposition
#Quick Check
In the forward jump, which number scales the clean picture x0?