Lecture 1 gave us a sampler — but the arrows were hand-drawn. Flow Matching is how you TRAIN the vector field so that pure noise actually flows into your data. The single algorithm behind Stable Diffusion 3 and Movie Gen, derived from zero.
Recall the flow model from Lecture 1. Start at a random Gaussian point, follow a neural-network vector field uθt(x) for unit time, and you want the endpoint to be a fresh data sample:
Here θ are the network weights and pinit is the easy standard Gaussian N(0, Id). An untrained network produces garbage — random arrows transport noise to more noise. Training means finding weights θ so the endpoint distribution actually equals the data distribution pdata.
The construction has a beautiful skeleton called the Flow Matching matrix — a 2×3 grid we will fill in cell by cell over the next chapters:
| Probability Path | Vector Field | FM Loss | |
|---|---|---|---|
| Conditional per single data point | pt(x | z) — Ch 2 | ut(x | z) — Ch 3 | LCFM — Ch 5 |
| Marginal across all data | pt(x) — Ch 1 | ut(x) — Ch 4 | LFM — Ch 5 |
One sentence of vocabulary now, because it organizes everything. "Conditional" means "for one fixed data point z" — e.g. build a path that ends exactly at this one cat photo. "Marginal" means "averaged over the whole data distribution" — the thing that ends at any plausible cat photo. Conditional objects are simple because they target a single point; marginal objects are what generate real samples but require summing over all data.
The flow model only pins down the two endpoints: at t = 0 we have noise, at t = 1 we have data. But what should the distribution look like at t = 0.5? Lecture 1 never said. Flow Matching's first move is to choose what happens in between — to lay down a road through the space of distributions. That road is a probability path.
An analogy that will pay off: imagine a weather map of fog density over a landscape, filmed over a day. At dawn (t = 0) the fog is a single smooth Gaussian hill. By dusk (t = 1) it has gathered into the exact shapes of your data — five tight clusters, say. The probability path is that whole time-lapse film.
Because the endpoints alone are a hopeless target. "Find arrows that move this Gaussian to that data cloud" is a global, tangled constraint — you'd have to reason about the entire transport at once. But if you also dictate every intermediate snapshot, the problem becomes local in time: at each instant you only need arrows that nudge the current fog into the next instant's fog. Local problems are the ones we can solve with calculus and closed forms.
So the recipe is now:
One catch we will confront head-on in Chapter 2: we still cannot directly write down a path that ends at the full data distribution pdata, because we don't know pdata — we only have samples. The fix is to first build an easy path that ends at a single data point, then average. That is the conditional/marginal split, next chapter.
Each frame is a snapshot pt of the distribution (a 1-D density curve here for clarity). Drag t from 0 to 1: at t = 0 the density is a single broad Gaussian (noise); as t grows it morphs into the structured target (a two-bump "data" density). This is the marginal path — what we eventually want. Notice it specifies only the shape at each time, not how any one particle travels.
We hit a wall at the end of Chapter 1: we can't write a path ending at the full data distribution, because we don't know it. Flow Matching's escape is almost too simple. Pick a single data point z from the training set, and build an easy path that ends exactly at z. That is a conditional probability path, written pt(x | z) — "the distribution at time t, given that we are heading toward the data point z."
Why is this so much easier? Because the destination is a single point, not a complicated cloud. There's an obvious, hand-buildable path: start with a wide Gaussian centered near the origin, and shrink it down while sliding its center onto z. That's the Gaussian conditional path, and it is the path used by essentially every state-of-the-art model.
Read the symbols carefully — αt is the data-mixing dial (how much of z is in the mean) and βt is the noise level (the spread). Check the endpoints by plugging in:
This is the workhorse formula of the whole lecture. A Gaussian with mean αt z and standard deviation βt can be sampled by taking standard noise ε (drawn from N(0, Id)) and shifting/scaling it:
In words: a noisy data point is a blend of the clean data z (weighted αt) and pure noise ε (weighted βt). At t = 0 it's all noise (α = 0, β = 1 ⇒ x = ε); at t = 1 it's all data (α = 1, β = 0 ⇒ x = z). In between it's a interpolation. This single line is what we run millions of times during training.
Now the conditional path for a single z. To get the path for all data, average over z — that's Chapter 4. For now, here is a single conditional path, animated.
The orange star is the chosen data point z. The cloud is a sample from pt(·|z) = N(αt z, βt2 I). Drag t: at t = 0 it's a broad Gaussian centered at the origin (all noise, doesn't know z); as t grows the cloud slides toward z (mean αt z) and tightens (std βt), collapsing to a spike at z by t = 1. Schedule used: αt = t, βt = 1−t.
Now implement the sampling formula yourself and confirm both endpoints — this is Code Lab 1.
We have a conditional path: a movie of the fog collapsing onto z. Chapter 1 warned us that a path only says what distribution at each time, not how particles move. Now we find the arrows. We want a conditional vector field ut(x | z) such that if you start a particle at a noise point and follow these arrows, the particle's distribution at time t is exactly pt(· | z).
Recall from Lecture 1 that the flow ψt(x0) is the map "where does a particle starting at x0 end up at time t," and that the vector field is the time-derivative of the flow along its own trajectory. For the Gaussian path we can guess the flow directly. We want a map that takes a standard-noise start x0 and produces a sample of pt(·|z) = N(αt z, βt2 I). But that's exactly the reparameterization from Chapter 2 with x0 playing the role of ε:
Check it: if X0 ∼ N(0, I), then αt z + βt X0 is Gaussian with mean αt z and variance βt2 — precisely pt(·|z). ✓ So this map carries the noise distribution onto the conditional path. Now extract the vector field by differentiating the flow in time.
By the definition of a flow, the velocity of a trajectory equals the field evaluated at the trajectory's location. Write x = ψt(x0 | z) for the current location. We need two pieces: the time-derivative of the flow, and a way to express x0 in terms of the current x.
That is the closed-form conditional Gaussian vector field. Stare at its two pieces — the meaning is the whole point:
Let's confirm the formula is right the way an engineer should: numerically. If ut(x|z) really transports the path, then the time-derivative of a sample xt = ψt(x0|z) (computed by finite differences) must equal ut evaluated at that sample. That's Code Lab 2.
We have, for each data point z, a tractable conditional field ut(x | z) that aims a particle straight at z. Individually useless (every trajectory lands on the same z). Now the master stroke: average the conditional fields over the whole dataset to get a field that generates the real data distribution. This is the marginal vector field, and the theorem that it works is the marginalization trick.
The marginal probability path pt(x) is what you get by first picking a random data point z, then sampling its conditional path:
As a density, that is a weighted average of the conditional densities — an integral over all data points weighted by how likely each is:
Check the endpoints: since every conditional path starts at pinit and ends at δz, the average starts at pinit (Chapter 1's noise) and ends at the average of spikes over the data — which is exactly pdata. ✓ So pt is a genuine noise-to-data path. We can sample from it (pick z, add noise) but we cannot evaluate its density — that integral is intractable. Remember this asymmetry; it drives everything.
The bracket is the secret sauce. By Bayes' rule, the quantity pt(x | z) pdata(z) ⁄ pt(x) is exactly the posterior pt(z | x) — "given that I observe a noisy point x at time t, how likely is it that it came from data point z?" So the marginal field is a posterior-weighted average of velocities:
An analogy: x is a blurry photo. Each clean training image z "claims" the blurry photo could be its corrupted version, and offers a direction to sharpen toward itself. The marginal field is the consensus direction, weighting each claim by how believable it is. Early (t near 0) the photo is almost pure noise, so many images claim it weakly — the arrow is gentle and broad. Late (t near 1) the noise is small, one image dominates the posterior, and the arrow points sharply at it.
Three data points (orange stars). Conditional shows the field for ONE chosen z — every arrow points straight at it (a single pull). Marginal shows the posterior-weighted average over all three — arrows point toward whichever data point is most plausible from that location, so the field splits the plane into basins. Toggle the mode and drag t: early t gives soft, broad arrows; late t snaps each region toward its nearest data point.
We want our network uθt(x) to equal the marginal field ut(x). The obvious loss is mean-squared error: penalize the squared distance between the network's arrow and the target arrow, averaged over random times and random points on the path.
The fix is audacious: replace the intractable target with the tractable conditional field — and also sample x by first picking a data point z. Define the Conditional Flow Matching loss:
But here's the puzzle that stops everyone: we want the network to match the marginal field, yet we are training it to match the conditional field. The conditional field is "useless" — it points at one fixed data point. How can copying it possibly give us the marginal field? The answer is a small miracle.
It's just algebra on the squared norm. Expand ‖a − b‖2 = ‖a‖2 − 2 a·b + ‖b‖2 with a = uθt(x) and b the target field. Do it for both losses and compare the three pieces:
Time to make everything numeric. We pick concrete noise schedules αt and βt, plug them into the conditional field, and watch the famously simple Flow Matching loss fall out.
Drop the schedule derivatives into Chapter 3's formula. With α̇t and β̇t the time-derivatives:
This is fully closed-form — given the schedules and their derivatives (both known analytically), you can evaluate it for any x, z, t with elementary arithmetic. That tractability is the engine of the conditional loss.
Here's the trick that makes the loss beautiful. Instead of sampling x and then computing ut(x|z), substitute the reparameterization x = αt z + βt ε directly into the conditional field and watch it collapse. Plug x = αt z + βt ε into the formula above:
So when you generate the training point as a noise+data blend, the target velocity is just the same blend of schedule derivatives: how fast the data dial is opening times z, plus how fast the noise dial is closing times ε. The Conditional Flow Matching loss becomes:
Top: the two schedules αt (data dial, teal) and βt (noise dial, orange) over time — toggle linear vs. square-root. Bottom: for fixed z and ε, the conditional velocity target α̇t z + β̇t ε over time (purple). For the linear schedule it's the constant z − ε; for the square-root schedule it blows up near t = 1 (because β̇t diverges) — a real practical wrinkle.
The linear schedule deserves its own chapter because it is the cleanest possible path and the basis of rectified flow — the formulation behind Stable Diffusion 3. It also generalizes beautifully to interpolating between any source and target, not just a Gaussian and a data point.
This is just the reparameterization with the linear schedule, rewritten to emphasize the geometry: each particle travels in a perfectly straight line from its noise start to its data target, at constant speed.
Differentiate the straight segment xt = (1 − t) ε + t z in time:
The velocity does not depend on t — it is the constant difference between data and noise. So the conditional Flow Matching loss for the straight path is the simplest objective in all of generative modeling:
Nothing forced the source to be a Gaussian. Replace ε ∼ pinit with a sample from any easy-to-sample source distribution, and z from the target. The straight-line path and its constant velocity z − (source) carry on unchanged:
This is how Flow Matching becomes a general distribution-to-distribution transport learner — image-to-image translation, paired data, and more, not just noise-to-data. The recipe never changes: interpolate, regress on the difference vector.
Blue dots (left) are noise samples ε; orange stars (right) are data points z, paired up. Drag t: each particle slides along the straight segment xt = (1−t)ε + t z from its noise start to its data target, at constant velocity z − ε. The thin gray lines are the full straight paths. This is the CondOT / rectified-flow geometry — the reason such fields need few Euler steps.
Build the general transport yourself — not noise-to-data, but one arbitrary distribution to another, with the same two-line recipe: interpolate, regress on the difference vector.
Everything assembles here. We will train a tiny vector field by minimizing the Conditional Flow Matching loss on a 2D toy — noise to a ring of five Gaussian clusters, exactly the lab notebook's target — and watch the loss fall and samples land on the target. This is the full Flow Matching pipeline in pure numpy.
The lab notebook uses a multi-layer perceptron with weights tuned by Adam over thousands of steps. To keep the in-browser lab fast and transparent, we train a compact featurized linear field: the network's velocity is a linear function of a fixed set of features of (x, t) — position, time, and a few radial-basis "bumps" in space. It's a real learned field (it has weights fit by gradient descent on the CFM loss), just small enough to train in a second.
Below: first, the interactive showcase widget — watch a field train live and pull noise onto the ring. Then Code Lab 3, where you implement the loss and the training step yourself.
Press Train: a small featurized field is fit by gradient descent on the CFM loss (target ring = five clusters, orange). The loss curve falls and plateaus at a healthy nonzero value (as Chapter 6 warned). Then press Sample to Euler-integrate fresh Gaussian noise through the learned field — the blue cloud should land on the orange ring. Reset to start over.
You just derived — and implemented — the training algorithm behind Stable Diffusion 3 and Movie Gen, from a hand-drawn-arrows problem to a one-line regression. Here is the whole lecture on a page.