Flow matching: turning noise into shapes
Read these first if they are new to you
- Generative models: making new examples Intermediate
- Euler's method: small steps along a field Intermediate
- The bell curve (normal distribution) Basic
- Loss: one number for how wrong Intermediate
- Gradient descent Intermediate
- The multilayer perceptron (MLP) Intermediate
The idea in one paragraph
Flow matching trains a network to turn random noise into examples, such as shapes made of points. In training, it pairs a noise draw with a real example and joins them with a straight path. The network learns to predict how fast, and in which direction, a point moves along that path, given only where the point is and the time. Many paths pass through each spot at the same time, so the network learns their average velocity there. Following that averaged velocity field carries the whole cloud of noise onto the whole collection of examples. To generate, draw fresh noise and follow the field in small steps.
Time t runs from 0 (pure noise) to 1 (data). x0 is a noise draw, x1 is a real example, and the point on the path between them at time t is xt = (1 − t) x0 + t x1. Some papers run time the other way, with t = 0 as data, so check the convention before you compare formulas.
Watch the whole method first, then take it apart
The video shows the method on a flat shape in under two minutes. The rest of the page explains each part, with numbers you can check.
Transcript
Flow matching turns random noise into shapes. On the left is noise: random points from a bell-shaped distribution. On the right is a branching, neuron-like shape, made of points. Both live in the same plane, so we overlay them. Join each noise point to a random shape point with a straight line. As time t runs from 0 to 1, each point slides from noise to the shape.
Freeze at t = 0.5. Each point moves at a steady velocity. Take one pair: noise point x₀ and shape point y. It travels from x₀ to y in one unit of time, so its velocity is y − x₀. Training teaches a neural network to predict this velocity from the position and the time. Many paths pass each spot at the same time, so it can only learn their average velocity. Here we can compute that average velocity exactly, at every spot. A real model trains a network to approximate it.
To generate, start from fresh noise and take small steps along the field. After 40 steps, the noise has become the shape. This exact field only rebuilds these points. A trained network's smoother field can make new shapes. One step lands every point on the average. Three steps blur the shape. Forty make it sharp.
The same idea works for 3D point clouds, with three coordinates per point. Learn the velocity that carries noise to data. Then follow it in small steps.
A straight path joins each noise draw to an example
The noise is a cloud of random points. Each coordinate of a noise draw x0 is an independent draw from the standard bell curve, with mean 0 and spread 1. In two dimensions the cloud is round and densest at the center. The examples are points on a shape. A 3D point cloud works the same way, with three coordinates per point.
Pair a noise draw x0 with an example x1. The straight path between them is
xt = (1 − t) x0 + t x1, 0 ≤ t ≤ 1.
At t = 0 the point is at the noise draw; at t = 1 it is at the example. Its velocity, the rate of change of xt with time, is the same all along the path:
velocity = x1 − x0.
So the point moves at a steady speed and covers the whole path in one unit of time.
Take the noise draw x0 = (−1, 2) and the example x1 = (3, 1).
- At t = 0.25: x0.25 = 0.75 × (−1, 2) + 0.25 × (3, 1) = (−0.75 + 0.75, 1.5 + 0.25) = (0, 1.75).
- The velocity is x1 − x0 = (3 − (−1), 1 − 2) = (4, −1).
- Check: from (0, 1.75), move for the remaining 0.75 units of time at (4, −1). You arrive at (0 + 3, 1.75 − 0.75) = (3, 1), the example.
Your turn. With the same pair, where is the point at t = 0.5, and what is its velocity there?
Answer
(1, 1.5), halfway between the two points. The velocity is still (4, −1), because it is the same everywhere on a straight path.
Training: predict the path's velocity from the position and the time
The network, written vθ(x, t), takes a position and a time and returns a velocity of the same size as the position. θ (theta) stands for all of its adjustable numbers, its weights. The training target is the velocity of the straight path, x1 − x0. The loss is the squared error between prediction and target, averaged over many draws:
L(θ) = average of ‖ vθ(xt, t) − (x1 − x0) ‖2
Here ‖…‖2 is the squared length: the sum of the squared coordinates. The average runs over examples x1 from the training data, noise draws x0 from the standard bell curve, and times t drawn evenly from 0 to 1. One training step does this:
- Take a minibatch of examples x1.
- Draw one noise draw x0 for each example, with the same shape.
- Draw one time t for each example.
- Compute xt = (1 − t) x0 + t x1.
- Run the network to get the predicted velocity vθ(xt, t).
- Compute the loss: the average squared error against x1 − x0.
- Take one gradient-descent step to change θ so the loss goes down.
Continue the pair above: x0.25 = (0, 1.75) and the target velocity is (4, −1). Suppose the network predicts (3, 0) at that position and time.
- Error: (3 − 4, 0 − (−1)) = (−1, 1).
- Squared length: (−1)2 + 12 = 2.
- This pair adds 2 to the sum that the loss averages.
For a point cloud, an example x1 is the whole cloud, say 1,000 points with 3 coordinates each. The noise draw has the same shape, so every point gets its own noise point, and the network returns one velocity per point.
Code often averages over coordinates as well as examples (PyTorch's .mean() does). That divides
the loss by the number of coordinates, so it changes the scale of the loss but not which network is best.
Drawing t evenly from 0 to 1 is the simplest choice. Some models draw middle times more often, which weights those times more in the loss.
This page pairs each noise draw with an example at random. Some models pair them by distance instead: within each minibatch, they match noise draws to examples so that the total squared distance is smallest (see the assignment problem). Then the paths that pass one spot at one time disagree less, and the averaged field is straighter. Everything this page says about t = 0 assumes random pairing.
Many paths pass through each spot at the same time, so the network learns their average
Pick one spot and one time. Many training pairs have a path that passes through that spot at that time, and their velocities differ. The network sees only the spot and the time, so it cannot tell those pairs apart.
Squared error is smallest when a prediction equals the average of its targets (see loss). For the targets 1 and 3, for example, the prediction 2 gives squared errors 1 + 1 = 2, and the prediction 1 gives 0 + 4 = 4. So the best possible network predicts, at each spot and time, the average velocity of all paths through it. This averaged field is what training learns.
Three things follow.
- The loss never reaches zero. The targets at one spot and time disagree with each other, and no single prediction can match them all.
- The field's paths are curved. A particle that follows the averaged field does not follow any single straight path. Its path bends, because two particles can never be at the same place at the same time (see velocity fields).
- The field still carries noise onto data. Start a cloud of particles from the noise at t = 0 and let each follow the averaged field. At every time, the cloud is spread exactly like the points xt of the training pairs. At t = 1, that is the spread of the examples. This is the central result of flow matching.
Training pairs: each noise point moves on a straight line to a randomly chosen point of the shape.
What you should find
When two training dots are near each other at the same moment, the network gets nearly the same input from both. Their paths ask for different velocities, so the network can only predict their average. Lines that cross in the still picture are usually reached at different times. Those do not conflict, because the network also reads t.
The generation paths rarely cross in the picture. When two do, they pass the spot at different times: two moving dots never meet. The generation paths also bend late, near t = 1, as each dot settles onto one part of the shape.
At t = 0, the point x0 is pure noise. It says nothing about which example its pair uses, so every example is equally likely to be at the other end.
- The average target at x0 is the average of x1 − x0 over all examples, which is (average example) − x0.
- Say the examples are the two numbers −2 and 4. Their average is 1. At x0 = 3 the arrow is 1 − 3 = −2; at x0 = −1 it is 1 − (−1) = 2. Both arrows point at 1.
- One Euler step of size 1 from t = 0 moves 3 to 3 + (−2) = 1, and −1 to −1 + 2 = 1. Every noise point lands on the average example, which is not one of the examples.
Generation: start from fresh noise and take Euler steps
To make a new example, draw a fresh noise draw and follow the learned field from t = 0 to t = 1 with Euler's method. With n steps of size h = 1/n, each step is
xt+h = xt + h × vθ(xt, t), t = 0, h, 2h, …, 1 − h.
Each step runs the network once, so n steps cost n network runs. A new noise draw gives a new example. Few steps are cheap but drift off the curved paths; many steps follow them closely.
The program below trains a small network on points of a ring with radius 2, then generates with 1, 4 and 40 steps. Predict first: where do the points land after just one step?
flow_matching_ring.py (PyTorch, CPU, about 5 seconds)
import torch
import torch.nn as nn
torch.manual_seed(0) # the same random draws on every run
def data(n):
# Training examples: points on a ring of radius 2.
a = 2 * torch.pi * torch.rand(n) # n random angles from 0 to 2π
return 2 * torch.stack([a.cos(), a.sin()], dim=1)
# The network reads (x, y, t) and returns a velocity (vx, vy).
net = nn.Sequential(nn.Linear(3, 128), nn.SiLU(), nn.Linear(128, 128), nn.SiLU(), nn.Linear(128, 2))
opt = torch.optim.Adam(net.parameters(), lr=0.001) # Adam, an improved gradient descent
for step in range(3000):
x1 = data(256) # data examples
x0 = torch.randn_like(x1) # noise, one draw per example
t = torch.rand(256, 1) # a random time per example
xt = (1 - t) * x0 + t * x1 # the point on each straight path
# torch.cat puts t beside xt, so each row reads (x, y, t).
v = net(torch.cat([xt, t], dim=1)) # the predicted velocity
loss = ((v - (x1 - x0)) ** 2).mean() # squared error to the target x1 - x0
opt.zero_grad() # clear the old gradients
loss.backward()
opt.step() # one step of Adam
print(f"final loss {loss.item():.2f}")
# @torch.no_grad() works like "with torch.no_grad():" around the whole function.
@torch.no_grad()
def generate(n, steps):
x = torch.randn(n, 2) # fresh noise at t = 0
for k in range(steps):
t = torch.full((n, 1), k / steps) # the time k / steps, once per point
x = x + (1 / steps) * net(torch.cat([x, t], dim=1)) # one Euler step
return x
for steps in [1, 4, 40]:
r = generate(2000, steps).norm(dim=1) # each point's distance from the center
# {steps:2d} pads the step count to 2 characters, so the columns line up.
print(f"{steps:2d} steps: distance from center {r.mean():.2f} on average, spread {r.std():.2f}")
final loss 2.03 1 steps: distance from center 0.18 on average, spread 0.10 4 steps: distance from center 1.91 on average, spread 0.30 40 steps: distance from center 2.00 on average, spread 0.09
One step lands every point near the center, the average example, as the worked example predicted. Four steps reach the ring but blur it. Forty steps give a sharp ring of radius 2. The final loss stays near 2, far from zero, because the targets at each spot disagree. Your numbers may differ slightly on another machine.
The central result holds for the exact averaged field. A trained network only approximates that field, and Euler steps add their own error, so real samples are close to the data's spread but not identical to it. The result also assumes the field changes smoothly from point to point.
Flow matching is a close relative of diffusion models
Diffusion models also start from bell-curve noise and remove it over many steps. The two families differ in four choices. Diffusion models usually blend noise and data along a curved schedule rather than a straight line. Their networks often predict the noise that was added rather than a velocity. They weight the times in the loss differently. And many diffusion samplers add a little fresh noise at every step.
With bell-curve noise, the two are closely linked. A prediction of the noise can be converted into a velocity and back, and the way to sample a diffusion model without fresh noise also follows a velocity field. So the families share most of their mathematics, but a model trained one way is not the same model as one trained the other way. "Rectified flow" is the same straight-path method, published at about the same time under another name.
Check yourself
-
A pair has x0 = (0, 0) and x1 = (2, 4). Find xt at t = 0.5 and the target velocity. If the network predicts (1, 4) there, what is the squared error?
Answer
x0.5 = (1, 2). The target is (2, 4) − (0, 0) = (2, 4). The error is (1 − 2, 4 − 4) = (−1, 0), so the squared error is 1.
-
Your model's training loss stops falling at about 2, far above zero. Is training broken?
Answer
Not necessarily. Many paths with different velocities pass through each spot at each time, and the network can only predict their average. The leftover disagreement keeps the best possible loss above zero. Judge the model by its samples instead.
-
Why does a single Euler step from t = 0 send every noise point to the same place?
Answer
At t = 0 the position is pure noise, so every example is equally likely to be its partner. The averaged velocity is (average example) − x0, and a step of size 1 adds it to x0, which gives the average example for every starting point.
-
Samples made with 2 steps look blurred, and samples made with 40 steps look sharp. Why?
Answer
The averaged field's paths are curved. A large Euler step follows the arrow at its start and drifts off a curved path. With 40 small steps, each step drifts very little.
Go further
Optional background, for readers with more time.
-
Introduction to Flow Matching and Diffusion Models (MIT 6.S184), lecture notes Holderrieth and Erives, MIT Course Advanced
The most complete free course on this topic. The notes derive every claim on this page, and lab 2 builds a flow-matching model in a notebook.
-
An introduction to Flow Matching: conditional flows Cambridge Machine Learning Group blog Article Advanced
Shows, with figures, why training on single straight paths teaches the averaged field.
-
Diffusion Meets Flow Matching: Two Sides of the Same Coin Gao, Hoogeboom, Heek and others Article Advanced
States exactly how flow matching and diffusion models relate. It runs time the other way: t = 0 is data.
-
Figures of straight training paths that cross, and of the flow that the averaged field makes, in which no two particles meet at the same time.
Where this shows up
Units that build on this one
- Diffusion transformers (DiT) Advanced
- Classifier-free guidance Advanced