Introduction
How do you turn random noise into an image?
Consider starting with an image filled with completely random pixels and somehow moving those pixels, little by little, until they form a handwritten digit, a face, or a photograph. Generative modeling is, in many ways, about learning how to perform such a transformation.
Given a dataset, our goal is to learn its underlying structure well enough to produce new objects that look as though they could have belonged to the dataset, without simply copying the examples we have already seen.
There are several approaches to generative modeling. In this post, we will focus on flow models and build up the ideas behind them from the ground up.
To make the ideas easier to visualize, we will focus on image generation. The same concepts extend to many other modalities.
From noise to images
Imagine a dataset containing thousands of handwritten digits.
An image can be thought of as a point in a very high-dimensional space. For example, a grayscale image can be represented by numbers, and therefore corresponds to a point in .
Our dataset gives us many such points. But these points are not scattered uniformly throughout the space. Images that look like handwritten digits occupy some regions much more densely than others.
We can describe this using a probability distribution.
A probability distribution tells us how probability is spread over the space of possible images. Regions containing plausible handwritten digits should have high probability, while regions containing meaningless images should have very little probability.
We call the distribution underlying our dataset
We do not actually know explicitly. We only have samples from it (the images in our dataset). If we somehow learned this distribution, generation would become conceptually simple:
Draw a new sample, and we get a new image.
The problem is that sampling directly from this complicated, high-dimensional distribution is difficult. We choose a simple distribution that we already know how to sample from, usually a standard Gaussian,
The central idea is then to learn a transformation that takes samples from this simple noise distribution and transport them into the complicated data distribution:
Once we know how to perform this transformation, generation becomes:
- sample some noise
- transform it
- obtain an image
The interesting question is therefore no longer how to generate an image but rather how do we continuously move an entire probability distribution from noise to data?
Optional: what does Gaussian noise mean?
A Gaussian, also called a Normal distribution, has a mean that sets its centre and a standard deviation that sets its spread. The variance is .
The notation means “draw a noise array whose coordinates are independent Gaussians with mean 0 and variance 1.” The symbol means “is sampled from”; specifies the independent, equal-variance coordinates.
For image generation, we draw a whole array at once. That array is one noise sample, just as a whole image is one data sample.
What to train?
Choose a training image and an independent noise array (pronounced “epsilon”) of the same shape. Now choose a number between 0 and 1 and blend the two:
At , we have only noise.
At , we have the image.
Halfway through, is an equal mixture of both. We call time, but it is simply a progress value along this path. As moves from 0 to 1, traces a straight-line trajectory from the noise sample to the image . Because this trajectory is a straight line, its velocity is constant:
If we can ask the model to learn the velocity, we can go from noise to our data point easily!
This gives us everything needed to construct a training example:
| Give the model | Ask it to predict |
|---|---|
| The blended image and time | The velocity |
The model sees neither the clean image nor the original noise separately. We use them to construct the blended image and its velocity target, then ask the model to predict that velocity from and alone.
The animation above uses a known image to construct a training path. During generation, however, we will have only noise and the velocity predicted by the trained model.
Optional: watch a whole noise cloud move toward one image
So far we followed one noise sample toward one image. Now keep the destination image fixed and draw many different noise samples.
Each noise sample follows its own straight-line trajectory:
At every value of , these points form a distribution. The centre of the noise cloud moves toward , while its spread gradually shrinks until every trajectory reaches the same image at .
Because we have fixed the destination image , this evolving distribution is called a conditional probability path. Tt describes how the distribution changes while conditioning on one particular data sample.
Papers often write such a path more generally as
The functions and form a schedule that controls how much data and noise are present at each time.
For this article, we deliberately choose the simplest possible schedule:
This gives us a straight path from noise to data and keeps the underlying ideas easy to see. Flow matching itself is not restricted to this particular choice.
What to infer?
Suppose we already have a model that can predict velocity. How would we use it to generate an image?
Imagine a map of wind. At every location, an arrow tells you both the direction and speed in which something should move. A collection of such arrows is called a vector field.
Our model plays exactly this role. Given the current position and time, it predicts the velocity:
velocity = model(x, t)
Once we know the velocity, we can move a small amount in that direction. For a small time step ,
This procedure is called Euler’s method.
After taking one step, we ask the model for another velocity. The answer may be different because both our position and the time have changed.
With a trained model, generation becomes a small loop:
def generate(model, shape, steps=40):
x = torch.randn(shape) # start with Gaussian noise
h = 1.0 / steps
for i in range(steps):
t = i * h
x = x + h * model(x, t) # predict a velocity, then move
return x
We begin with fresh Gaussian noise and repeatedly follow the velocity predicted by the model. Each run starts from a different noise sample, so the same trained model can produce different images. The model itself does not change during generation.
We now know how to generate an image if we have the right vector field. The remaining question is how to train a model to produce those arrows.
Optional: ODEs, trajectories, and flows
Euler’s method is only a discrete approximation of an underlying continuous motion.
If the velocity field is written as , then continuously following its arrows is described by an ordinary differential equation, or ODE:
You can read this equation as:
The rate at which the current point moves is given by the velocity field at its current position and time.
Starting from one point and following this rule traces out a trajectory.
Euler’s method approximates that trajectory using a sequence of small jumps.
But we can start from many different points and apply the same velocity field to all of them. Each point traces its own trajectory.
The motion of all these points together is called a flow.
If denotes the flow map, then
means that tells us where a starting point has moved after time .
So the terminology fits together naturally:
a trajectory is the path followed by one point, a vector field tells every point how to move, an ODE describes how a point follows that field continuously, and a flow describes what happens to all starting points under that same motion.
Train the velocity field
We already know how to construct a training example.
Take an image , pair it with random noise , choose a random time , and interpolate between the two. This gives us a point somewhere along the path from noise to data .
More importantly, because we created the path ourselves (the straight line), we also know the velocity that should take us along it.
So training is simply:
- Create a random point somewhere between noise and data, .
- Ask the model which direction it would move from there.
- Compare its prediction with the velocity we already know ().
- Update the model and repeat.
for z in data_loader:
eps = torch.randn_like(z)
t = torch.rand(z.shape[0], 1, device=z.device)
x_t = (1 - t) * eps + t * z
target = z - eps
prediction = model(x_t, t)
loss = ((prediction - target) ** 2).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
Each training step shows the model only a small piece of the overall problem. By repeating this across many images, noise samples, and times, the model gradually learns what the velocity should look like throughout the space.
training does not require following the entire trajectory. We can jump directly to any time , construct the corresponding point, and know its training target immediately. The ODE solver only becomes necessary during generation, when we start from noise and must repeatedly follow the model’s predictions without knowing the final image.
After enough training, the model gives us a vector field that we can follow from noise toward the data distribution.
Optional: the training objective
If we denote the learned velocity field by , the training objective can be written as
Here,
The expectation simply means that we average this error over many randomly chosen images, noise samples, and times.
Try it on handwritten digits
We can now use the same recipe on images. To keep training fast in the browser, this demo represents each 16×16 digit with 24 numbers, using a compression method called PCA. The model moves those 24 coordinates and a decoder turns them back into pixels for display.
We also give the model a digit label , so we can request a particular digit:
velocity = model(x_t, t, c)
During training, is the label of the example image. During generation, we choose it. This is class conditioning. Unlike the hidden destination image , the label remains available to the model.
Let it rip!
Press train and allow a few thousand updates. Pause before comparing sampler settings so the model stays fixed.
Try these two experiments:
- Choose a digit and press new noise: The label stays fixed while the starting point changes. Look for different handwriting styles.
- Compare 16, 4, and 2 steps with the same noise. Fewer steps cost fewer model evaluations, but can change the result or reduce quality. Straight training paths do not guarantee accurate generation in two steps.
The learned model may produce plausible variations, but this small demo does not establish generalization. That requires evaluation on held-out data and checks for memorization.
What to take away
Flow matching turns a distribution-learning problem into a velocity-prediction problem. Mix data with noise to make an input, subtract them to make a target, and train with squared error. To generate, start from fresh noise and follow the learned field.
You can now read the core of a flow-matching implementation:
- identify its path
- its velocity target
- and its sampler
Different schedules, architectures, and conditions build on those same pieces.
Reference: the notation in one place
| Symbol | Meaning in this article |
|---|---|
| Progress from noise at 0 to data at 1 | |
| One Gaussian noise sample | |
| One training data sample | |
| A random blended point; is one realization | |
| Distribution of points at time | |
| Distribution when the destination is known | |
| Probabilities of destinations given the current point | |
| Velocity target for one training pair | |
| Velocity field for a known destination | |
| Marginal field, averaging over plausible destinations | |
| Neural network approximating that field | |
| The network’s trainable parameters | |
| Flow map: where a starting point ends up at time | |
| Data and noise weights; here and | |
| A condition, such as a digit label or text representation | |
| NFE | Number of model evaluations; one per Euler step here |
These lecture series helped shape this article:
- Stanford CME296: Diffusion & Large Vision Models, by Afshine and Shervine Amidi (Amidi & Amidi, 2026) — diffusion, flow matching, and modern image-generation architectures.
- MIT 6.S184: Flow Matching and Diffusion Models, by Peter Holderrieth (Holderrieth, 2026) — the mathematical foundations of flows and diffusion, with practical examples.
- Stanford CS109: Introduction to Probability for Computer Scientists, by Chris Piech (Piech, 2022) — probability foundations for the ideas used here.
The blog post was polished using an LLM. This in no way means that I have let an agent run in the background and let it generate the blog. I am a non-english speaker and think LLMs (which are mostly trained in the English Language) can rectify silly grammar mistakes or rephrase sentences that sound less intimidating and cleaner. Hope this helps with the idea of “why should I read, if this was LLM generated”. 🤗 The embeds used are completely LLM generated with major human in the loop feedback. If you find any issues, feel free to send a PR my way.