← All posts

Foundations

Post 14 of 15

Diffusion and flow models: generating from noise

Posts in this series · 15

In the VAE post we generated by picking a random code and decoding it in one go. In the GAN post a generator also jumped from noise to a sample in one go, trained against a critic. This post is about a third family that takes its time. It starts from pure noise and makes it a little less noisy, again and again, until a sample appears. These are diffusion models and their close cousins, flow models.

They are behind most image generators of the last few years, and they are behind the speech models on this site: F5-TTS writes a spectrogram by flow matching, and the head of Pocket TTS turns noise into each audio frame. This post is the gentle on-ramp. The deeper version, with every equation of the original paper, is my Flow Matching review, and I link into it wherever this post stops.

The whole model at a glance

Here is a whole diffusion model at work, on the toy this post uses throughout: data that are only ever −1 or +1. Training and sampling use the same network in two different loops. Training: take a clean point, add a random amount of noise, ask the network to guess the noise, and score the guess. Sampling: start from pure noise and call the network a few times, each time stepping to a slightly cleaner point. The noise schedule, the network’s job and the sampling step each get a section below; step through both paths once now.

A diffusion model's training step and its sampling loop on a two-point toy
Path
Figure 1. A diffusion model’s two loops. Training: x₀, t and ε make x_t; the network guesses ε; the squared error trains it. Sampling: start from noise and call the same network four times (DDIM steps) to reach data. The network here is the exact best one for the toy, as in figure 3.

The generation problem

We have a pile of examples: photos, or short clips of speech. We want a machine that makes new ones that look like they came from the same pile. In the language of the loss post, the examples come from some distribution, a rule that says how likely each possible example is. We never get to see that rule. We only see samples from it.

Everyday picture: you have a jar of a thousand pebbles from one beach. Nobody gives you a map of the beach. Can you build a machine that hands you a new pebble the beach could plausibly have produced?

Throughout this post the “beach” is tiny so we can do everything by hand:

A good generator must output values near +1 or −1, about half each, and almost never 0. That is surprisingly hard to do with one simple formula, which is why it is a good test.

Noise into data, a little at a time

Turning random noise into a +1 or −1 in one jump is a hard problem. Undoing a tiny bit of blur is an easy one. So the trick is:

  1. Take real data and blur it with noise, a little at a time, until nothing is left but noise. This is easy: it needs no learning at all.
  2. Train a network to undo one small blur.
  3. To generate, start from pure noise and apply the undo step many times.

Everyday picture: a drop of ink spreading in a glass of water. Watching it spread is easy. Running the film backwards, ink gathering into a drop, looks like magic, but each frame of the backward film only differs a little from the one before. If you can learn to predict one frame back, you can play the whole film backwards.

The idea of learning to reverse a slow noising process comes from Sohl-Dickstein, Weiss, Maheswaranathan and Ganguli (2015), who borrowed it from physics (the “diffusion” of the name is the ink spreading). Ho, Jain and Abbeel (2020) made it work very well on images with a much simpler training loss, under the name DDPM (denoising diffusion probabilistic models).

The forward process: blurring on purpose

The formula

In words: at noise level tt, keep a shrunken copy of the clean example and add a scaled dose of fresh random noise.

xt=αˉt x0+1−αˉt εx_t = \sqrt{\bar\alpha_t}\, x_0 + \sqrt{1 - \bar\alpha_t}\, \varepsilon

To keep the arithmetic friendly I use the schedule

αˉt=cos⁡ ⁣(πt2),1−αˉt=sin⁡ ⁣(πt2)\sqrt{\bar\alpha_t} = \cos\!\left(\tfrac{\pi t}{2}\right), \qquad \sqrt{1-\bar\alpha_t} = \sin\!\left(\tfrac{\pi t}{2}\right)

so the “keep” and “noise” weights are the two sides of a quarter-turn. It is a simplified version of the cosine schedule of Nichol and Dhariwal (2021); DDPM itself used a schedule that is linear in a different quantity.

Everyday picture: a radio slowly turned from one station to static. At every moment you hear a mix; the dial sets how much is music and how much is hiss.

Worked by hand

Take the clean point x0=1x_0 = 1 and one fixed noise draw ε=−1.5\varepsilon = -1.5.

ttkeep αˉt\sqrt{\bar\alpha_t}noise 1−αˉt\sqrt{1-\bar\alpha_t}αˉt\bar\alpha_txtx_t
01.0000.0001.0001×1+0×(−1.5)=1 \times 1 + 0 \times (-1.5) = 1.000
0.250.9240.3830.8540.924−0.574=0.924 - 0.574 = 0.350
0.50.7070.7070.5000.707−1.061=0.707 - 1.061 = −0.354
0.750.3830.9240.1460.383−1.386=0.383 - 1.386 = −1.003
10.0001.0000.0000−1.5=0 - 1.5 = −1.500

By t=0.5t = 0.5 the point has crossed zero: you can no longer tell for sure which side it started on. By t=1t = 1 it is just the noise. If ε\varepsilon had been +1.5+1.5 instead, the point would have moved away from zero and ended at +1.5+1.5.

Do this for many points and many noise draws and the two sharp spikes at ±1 spread into two bumps, then merge into one bell curve. Figure 2 shows that, with the formula filled in for one point.

Two spikes of data blurring into one bell curve as noise is added
Figure 2. Blurring the data on purpose. Step through t = 0, 0.25, 0.5, 0.75, 1 or drag the slider. The red point is x₀ = 1 with ε = −1.5, and its row of arithmetic matches the table above. No learning happens here: the forward process is just a formula.

A useful fact: we can jump straight to any tt in one line. We never have to simulate the small steps one by one during training. That is what makes training cheap.

The network’s job: guess the noise

The loss

In words: show the network a blurred example and the noise level, and ask “what noise was added?”; score it by the squared error.

L=∥ε−εθ(xt,t)∥2L = \big\| \varepsilon - \varepsilon_\theta(x_t, t) \big\|^2

One training step: pick a real example, pick a random tt, pick a random ε\varepsilon, build xtx_t with the formula, ask the network, compute the loss, take a gradient step. Ho and colleagues found this plain loss (they call it the “simple” loss) worked better in practice than the weighted version their theory first suggested.

Worked: from the table, at t=0.5t = 0.5 the network sees xt=−0.354x_t = -0.354. Suppose it guesses εθ=−1.0\varepsilon_\theta = -1.0. The loss is (−1.5−(−1.0))2=(−0.5)2=(-1.5 - (-1.0))^2 = (-0.5)^2 = 0.25. Had it guessed −1.5-1.5 exactly, the loss would be 0.

Three things it could predict

Since xt=αˉt x0+1−αˉt εx_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\varepsilon ties the three together, knowing xtx_t and any one of them gives the other:

They are three ways of writing down the same knowledge; they differ in which errors the loss punishes most.

Why guessing the noise is learnable

Here is the worry: the network cannot possibly know the exact ε\varepsilon we drew. At t=0.5t = 0.5, the point −0.354-0.354 could have come from x0=1x_0 = 1 with a big negative noise, or from x0=−1x_0 = -1 with a small positive noise. So what does the loss teach it?

It teaches it the average answer. The prediction with the smallest average squared error is the average of all the noises that could have produced this xtx_t (that is a property of squared error: the best single guess is the mean). And for our toy, that average has an exact formula. The best guess of the clean point is

x^0(xt)=tanh⁡ ⁣(αˉt  xt1−αˉt)\hat x_0(x_t) = \tanh\!\left(\frac{\sqrt{\bar\alpha_t}\; x_t}{1-\bar\alpha_t}\right)

Then the best noise guess follows from the line above: ε^=(xt−αˉt x^0)/1−αˉt\hat\varepsilon = (x_t - \sqrt{\bar\alpha_t}\,\hat x_0)/\sqrt{1-\bar\alpha_t}.

Worked for our point, xt=−0.354x_t = -0.354 at t=0.5t = 0.5:

  1. The fraction: αˉt/(1−αˉt)=0.707/0.5=1.414\sqrt{\bar\alpha_t}/(1-\bar\alpha_t) = 0.707 / 0.5 = 1.414, and 1.414×(−0.354)=−0.5001.414 \times (-0.354) = -0.500.
  2. x^0=tanh⁡(−0.5)=\hat x_0 = \tanh(-0.5) = −0.462. The network’s honest answer is “probably −1, but I am not sure”.
  3. ε^=(−0.354−0.707×(−0.462))/0.707=(−0.354+0.327)/0.707=\hat\varepsilon = (-0.354 - 0.707 \times (-0.462))/0.707 = (-0.354 + 0.327)/0.707 = −0.038.

So the best possible network guesses a noise of −0.038, not the −1.5 we actually used. Its loss on this example is about (−1.5+0.038)2=2.14(-1.5 + 0.038)^2 = 2.14, and no network can do better on average, because the information is simply not there. Training does not need perfect answers; it needs the right average.

Why it is learnable: at each noise level the network only has to solve a small, local problem: “given this blurry thing, which way is the clean version, on average?”. Near t=0t = 0 that is a nudge; near t=1t = 1 it is a rough guess at the overall shape. No single question asks for a whole sample at once. If you move xtx_t to 22 (same tt) the answer becomes x^0=0.993\hat x_0 = 0.993: far from zero the network is nearly sure.

A noisy point, the two blurred data bumps, and the best guess of the clean point
Figure 3. The best possible denoiser, step by step. On the ±1 toy we do not need a trained network: the best answer has a closed form. Default x_t = −0.354 at t = 0.5 gives x̂₀ = −0.462 and ε̂ = −0.038. A real network learns to approximate this function.

Sampling: stepping backwards

The DDPM step in words

To generate, start at t=1t = 1 with pure noise and walk down to t=0t = 0 in NN steps. At each step, from time tt to a slightly earlier time ss:

  1. Ask the network for its noise guess, and turn it into a guess of the clean point x^0\hat x_0.
  2. Work out the average point at time ss that lies “between” where you are and where the clean guess is. Statistically, it is the average of xsx_s given xtx_t and x^0\hat x_0.
  3. Add a little fresh noise, so that you land on a random point near that average rather than exactly on it.

Step 3 is what makes DDPM sampling stochastic (random): run it twice from the same start and you get different samples.

The average and the size of the fresh noise have a fixed formula from the schedule. Writing at=αˉta_t = \sqrt{\bar\alpha_t} and bt=1−αˉtb_t = \sqrt{1-\bar\alpha_t}, and r=at/asr = a_t / a_s for how much the signal shrinks between ss and tt:

mean=r bs2bt2 xt+as (bt2−r2bs2)bt2 x^0,spread2=(bt2−r2bs2) bs2bt2\text{mean} = \frac{r\, b_s^2}{b_t^2}\, x_t + \frac{a_s\,(b_t^2 - r^2 b_s^2)}{b_t^2}\, \hat x_0, \qquad \text{spread}^2 = \frac{(b_t^2 - r^2 b_s^2)\, b_s^2}{b_t^2}

You do not need to memorise this; it is a weighted average of “where I am” and “where the clean guess is”, with weights set by the schedule. Ho and colleagues write the same step in terms of the predicted noise.

One step by hand

From xt=−0.354x_t = -0.354 at t=0.5t = 0.5 to s=0.25s = 0.25, with the fresh noise draw z=0.3z = 0.3:

  1. x^0=−0.462\hat x_0 = -0.462 (from the denoiser).
  2. at=0.707a_t = 0.707, bt2=0.5b_t^2 = 0.5; as=0.924a_s = 0.924, bs2=0.146b_s^2 = 0.146; r=0.707/0.924=0.765r = 0.707/0.924 = 0.765.
  3. bt2−r2bs2=0.5−0.586×0.146=0.414b_t^2 - r^2 b_s^2 = 0.5 - 0.586 \times 0.146 = 0.414.
  4. Weight on xtx_t: 0.765×0.146/0.5=0.2240.765 \times 0.146 / 0.5 = 0.224. Weight on x^0\hat x_0: 0.924×0.414/0.5=0.7650.924 \times 0.414 / 0.5 = 0.765.
  5. Mean =0.224×(−0.354)+0.765×(−0.462)=−0.079−0.354== 0.224 \times (-0.354) + 0.765 \times (-0.462) = -0.079 - 0.354 = −0.433.
  6. Spread =0.414×0.146/0.5=0.121=0.348= \sqrt{0.414 \times 0.146 / 0.5} = \sqrt{0.121} = 0.348.
  7. New point =−0.4329+0.3483×0.3=−0.32845= -0.4329 + 0.3483 \times 0.3 = -0.32845, about −0.328.

The point moved a little toward −1, then the fresh noise pushed it back a bit. Many such steps later it settles on −1 or +1.

Deterministic sampling: DDIM

Song, Meng and Ermon (2021) showed you can drop the fresh noise. Their DDIM (denoising diffusion implicit models) step simply rebuilds xsx_s from the two guesses using the forward formula at the earlier time:

xs=as x^0+bs ε^x_s = a_s\, \hat x_0 + b_s\, \hat\varepsilon

In words: “pretend my clean guess and noise guess are right, and blur the clean guess to level ss with that same noise”.

Same point: 0.924×(−0.462)+0.383×(−0.038)=−0.427−0.015=0.924 \times (-0.462) + 0.383 \times (-0.038) = -0.427 - 0.015 = −0.441. No randomness: the same start always gives the same sample. DDIM also works well with far fewer steps than DDPM, and it uses the same trained network.

Song and colleagues (2021, score SDE paper) gave the bigger picture: the noising process can be written in continuous time, and there is an ordinary differential equation (ODE), the probability-flow ODE, whose paths carry the noise distribution to the data distribution with no randomness at all. DDIM is a way of stepping along it. That ODE is our bridge to flow models.

One step backwards from a noisy point, by DDPM with fresh noise and by DDIM without
Figure 4. One step backwards, two ways. DDPM lands at −0.328 (mean −0.433 plus 0.348 × 0.3 of fresh noise); DDIM lands at −0.441 with no noise. Change z to see only the DDPM point move.

Many particles, many steps

Run the walk for 200 noise draws at once and watch the cloud split into two. Figure 5 compares 5, 20 and 100 steps. On this toy the last step always lands on the denoiser’s clean guess, so with the exact denoiser even 5 steps work fairly well: the 200 samples end on average 0.031 (DDPM) and 0.034 (DDIM) away from the nearest of ±1, and with 20 or 100 steps that falls to below 0.001. Roughly half end on each side. Real networks are not exact, and real data is not two points, which is why real samplers needed hundreds of DDPM steps or tens of DDIM steps.

Particles moving from a bell curve of noise to two points, with 5, 20 and 100 steps
Sampler
Figure 5. From noise to two points. The same 200 seeded noise draws in every panel, with the exact toy denoiser standing in for a network. DDPM paths jitter because of the fresh noise; DDIM paths are smooth. Readouts give the mean distance to the nearest data point and the share ending above zero.

The flow view: a velocity field

Watch the time direction

Flow papers count time the other way round, and this trips everyone up once. From here on, t=0t = 0 is noise and t=1t = 1 is data, x0x_0 is the noise and x1x_1 is the data point. This matches the Flow Matching review, F5-TTS and Pocket TTS (which writes τ).

Following arrows

A flow model learns a velocity field vt(x)v_t(x): for every position xx and time tt, which way to move and how fast. To generate, drop a noise point and follow the arrows from t=0t = 0 to t=1t = 1. Picture a leaf on a river: the current at each spot is the velocity, and the leaf’s path is the flow.

Computers follow arrows in steps. Euler’s method says: look at the arrow here, move along it for one step, repeat. With NN steps of size h=1/Nh = 1/N:

xk+1=xk+h vtk(xk),tk=k hx_{k+1} = x_k + h\, v_{t_k}(x_k), \qquad t_k = k\,h

The review works Euler on three roads in its Euler figure; here we do it on our toy.

Straight paths: flow matching and rectified flow

The path for one pair

Pick one noise point x0x_0 and one data point x1x_1 and join them with a straight line:

xt=(1−t) x0+t x1,velocity=x1−x0x_t = (1 - t)\, x_0 + t\, x_1, \qquad \text{velocity} = x_1 - x_0

Training is regression again: pick a pair and a random tt, build xtx_t, and score the network with ∥(x1−x0)−vθ(xt,t)∥2\| (x_1 - x_0) - v_\theta(x_t, t) \|^2. This is the conditional flow matching loss of Lipman, Chen, Ben-Hamu, Nickel and Le (2023); Liu, Gong and Liu (2023) arrived at the same straight-line recipe under the name rectified flow, and added “reflow”, retraining on the model’s own noise and sample pairs to make paths even straighter.

Worked: noise x0=0.5x_0 = 0.5, data x1=1x_1 = 1, t=0.5t = 0.5. Then xt=0.5×0.5+0.5×1=x_t = 0.5 \times 0.5 + 0.5 \times 1 = 0.75 and the target velocity is 1−0.5=1 - 0.5 = 0.5. If the network says 0.3, the loss is (0.5−0.3)2=0.04(0.5 - 0.3)^2 = 0.04.

What the network actually learns

The same worry as before: many lines, from many noise points to both +1 and −1, pass through x=0.75x = 0.75 at t=0.5t = 0.5. The network learns their average velocity. On our toy that average again has a closed form:

vt(x)=x^1(x)−x1−t,x^1(x)=tanh⁡ ⁣(t x(1−t)2)v_t(x) = \frac{\hat x_1(x) - x}{1 - t}, \qquad \hat x_1(x) = \tanh\!\left(\frac{t\, x}{(1-t)^2}\right)

In words: “guess the destination, then head there at the speed that arrives on time”. Why the average is the right target, and why matching single pairs still trains it, is the heart of the review: see Build the big path out of tiny ones and Conditional flow matching, with the MarginalAverage and CfmGradient figures there.

Euler steps by hand

Start from the noise point x0=0.5x_0 = 0.5 with N=2N = 2 steps, h=0.5h = 0.5:

  1. At t=0t = 0: x^1=tanh⁡(0)=0\hat x_1 = \tanh(0) = 0 (no idea yet), v=(0−0.5)/1=−0.5v = (0 - 0.5)/1 = -0.5. Move: 0.5+0.5×(−0.5)=0.5 + 0.5 \times (-0.5) = 0.25.
  2. At t=0.5t = 0.5: the fraction is 0.5×0.25/0.25=0.50.5 \times 0.25 / 0.25 = 0.5, so x^1=tanh⁡(0.5)=0.462\hat x_1 = \tanh(0.5) = 0.462; v=(0.462−0.25)/0.5=0.424v = (0.462 - 0.25)/0.5 = 0.424. Move: 0.25+0.5×0.424=0.25 + 0.5 \times 0.424 = 0.462.

Two steps end at 0.462, far from +1. With N=4N = 4 the stops are 0.5, 0.375, 0.305, 0.425 and then 1.000. Notice the first steps move toward zero: early on, the average of “go to +1” and “go to −1” points at the middle. Each pair’s path is straight, but the averaged field bends. The review calls this “straight per example, curved in aggregate” (Optimal transport).

A straight noise-to-data line, and Euler steps through the averaged velocity field
Euler steps N
Figure 6. One straight pair, one bent average. Left: the training target for a single pair. Right: the field the network learns on the ±1 toy, and Euler with N = 2 (ends at 0.462) or N = 4 (ends at 1.000). Use the N buttons and step through.

Why straighter paths need fewer steps

Euler assumes the arrow stays the same for a whole step. On a straight road that is exactly right; on a bend a big step overshoots. So the less your paths curve, the fewer steps you need.

To see it on the toy, compare two paths from noise to the same ±1 data:

For the same 200 seeded start points I measured the mean distance between Euler’s end and the exact end:

Euler steps24681220
Straight0.4360.0460.0080.0000.0000.000
Curved0.3750.1160.0450.0220.0090.003

To get the mean miss under 0.01, the straight path needs 6 steps and the curved one 12. Same data, same exact field, half the network calls. On real images the review reports the same direction of effect at much larger scale (sampling with few steps), and its DiffusionVsOt and StepBudget figures show it in 2-D.

Euler paths on a straight and a curved flow, and the mean miss against the number of steps
Figure 7. The step-count race. Raise N until the miss drops under 0.01: 6 for the straight path, 12 for the curved one, as in the table above.

Arrows in two dimensions

Everything works the same in more numbers. Put four data points at the corners (±1,±1)(\pm1, \pm1). Then each coordinate behaves like our 1-D toy, and the field is a picture of arrows that start out pointing at the middle and later swing toward the nearest corner. A particle starting at (0.5,−0.3)(0.5, -0.3) with N=4N = 4 Euler steps visits (0.375,−0.225)(0.375, -0.225), (0.305,−0.183)(0.305, -0.183), (0.425,−0.267)(0.425, -0.267) and ends at (1.000, −0.997), the corner (1,−1)(1, -1).

A grid of velocity arrows and a particle following them to a corner
Euler steps N (click the square to move the start)
Figure 8. Following the arrows. Click the square to move the start point, or change N; each step evaluates the field once and moves. For a 2-D flow along one straight pair, see Pocket TTS’s FlowSlider.

Conditioning and classifier-free guidance

So far the model makes “any pebble from the beach”. To make this sentence in this voice, we give the network a condition cc (text, a speaker clip) as an extra input. Classifier-free guidance (CFG), from Ho and Salimans (2022), trains the same network sometimes with the condition and sometimes with it hidden. At sampling time it asks both and pushes further in the direction the condition points: guided velocity = told + strength × (told − untold). With told 2, untold 1 and strength 2, that is 2+2×1=42 + 2 \times 1 = 4. The F5-TTS review works this through with its GuidanceArrows figure (classifier-free guidance), and the DSM review applies the same trick to token scores with GuidanceMixer (voices, turns and guidance). Each guided step costs two network calls.

Diffusion and flow in speech

The 32 numbers Pocket TTS generates are the code of a VAE, which ties this post back to the previous one but one: a VAE makes a smooth space, and a flow model generates points in it.

What to remember

Try it yourself

Sources