From Noise to Images: Flow Matching Explained

See how a model learns to turn random noise into an image, then train a small digit generator in your browser.

Affiliation

Hugging Face

Published

Sep. 13, 2026

PDF

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.

Note

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 28×2828 \times 28 image can be represented by 784784 numbers, and therefore corresponds to a point in R784\mathbb{R}^{784}.

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

pdata(x)p_{\text{data}}(x)

We do not actually know pdatap_{\text{data}} explicitly. We only have samples from it (the images in our dataset). If we somehow learned this distribution, generation would become conceptually simple:

xpdatax \sim p_{\text{data}}

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,

zN(0,I)z \sim \mathcal{N}(0, I)

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:

pnoisepdatap_{\text{noise}} \quad\longrightarrow\quad p_{\text{data}}

Once we know how to perform this transformation, generation becomes:

  1. sample some noise
  2. transform it
  3. obtain an image
Food for thought

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 μ\mu that sets its centre and a standard deviation σ\sigma that sets its spread. The variance is σ2\sigma^2.

Where do the random numbers land?
Press Draw 50, then increase the variance and draw again. A wider bell produces more widely spread samples.

The notation εN(0,I)\varepsilon\sim\mathcal{N}(0,I) means “draw a noise array whose coordinates are independent Gaussians with mean 0 and variance 1.” The symbol \sim means “is sampled from”; II 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 zz and an independent noise array ε\varepsilon (pronounced “epsilon”) of the same shape. Now choose a number tt between 0 and 1 and blend the two:

xt=(1t)ε+tzx_t=(1-t)\varepsilon+t z

At t=0t=0, we have only noise.

x0=εx_0=\varepsilon

At t=1t=1, we have the image.

x1=zx_1=z

Halfway through, xtx_t is an equal mixture of both. We call tt time, but it is simply a progress value along this path. As tt moves from 0 to 1, xtx_t traces a straight-line trajectory from the noise sample ε\varepsilon to the image zz. Because this trajectory is a straight line, its velocity is constant:

target velocity=ddtxt=ddt[tz+(1t)ε]=zε\text{target velocity} = \frac{d}{dt} x_t = \frac{d}{dt}\big[t z + (1-t)\varepsilon\big] = z - \varepsilon

If we can ask the model to learn the velocity, we can go from noise to our data point easily!

A changing image, a constant training target
Move time from 0 to 1. The noisy image changes, but the velocity on the right stays fixed. New noise creates a different path and target.

This gives us everything needed to construct a training example:

Give the modelAsk it to predict
The blended image xtx_t and time ttThe velocity zεz-\varepsilon

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 xtx_t and tt 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 zz fixed and draw many different noise samples.

Each noise sample follows its own straight-line trajectory:

Xt=(1t)ε+tz.X_t=(1-t)\varepsilon+t z.

At every value of tt, these points form a distribution. The centre of the noise cloud moves toward zz, while its spread gradually shrinks until every trajectory reaches the same image at t=1t=1.

Data increases as noise decreases
Drag z, then move time. Every point follows its own straight line toward the same destination.

Because we have fixed the destination image zz, 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

Xt=αtz+βtε.X_t=\alpha_t z+\beta_t\varepsilon.

The functions αt\alpha_t and βt\beta_t 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:

αt=t,βt=1t.\alpha_t=t, \qquad \beta_t=1-t.

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 hh,

xt+hxt+h,model(xt,t).x_{t+h}\approx x_t+h,\text{model}(x_t,t).

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.

How many steps does it take to follow a curve?
Compare 4 Euler steps with 40. Smaller steps usually follow the smooth reference more closely, but require more evaluations of the field.

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 ut(x)u_t(x), then continuously following its arrows is described by an ordinary differential equation, or ODE:

dxtdt=ut(xt).\frac{\mathrm{d}x_t}{\mathrm{d}t}=u_t(x_t).

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 ψt\psi_t denotes the flow map, then

xt=ψt(x0)x_t=\psi_t(x_0)

means that ψt\psi_t tells us where a starting point x0x_0 has moved after time tt.

One motion rule, many starting points
Switch from one trajectory to the flow. Each point follows the same field, but its starting position gives it a different route.

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 zz, pair it with random noise ε\varepsilon, choose a random time tt, and interpolate between the two. This gives us a point somewhere along the path from noise to data xtx_t.

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:

  1. Create a random point somewhere between noise and data, xtx_t.
  2. Ask the model which direction it would move from there.
  3. Compare its prediction with the velocity we already know (zεz-\varepsilon).
  4. 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.

Note

training does not require following the entire trajectory. We can jump directly to any time tt, 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.

Watch a network learn where the arrows should point
Choose a target shape and press train. Compare the generated points with the faint target points as the loss changes.

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 utθu_t^\theta, the training objective can be written as

Et,z,ε[utθ(Xt)(zε)2].\mathbb{E}_{t,z,\varepsilon} \left[ \left| u_t^\theta(X_t) - (z-\varepsilon) \right|^2 \right].

Here,

Xt=(1t)ε+tz.X_t=(1-t)\varepsilon+t z.

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 cc, so we can request a particular digit:

velocity = model(x_t, t, c)

During training, cc is the label of the example image. During generation, we choose it. This is class conditioning. Unlike the hidden destination image zz, 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.

Turn fresh noise into handwritten digits
The grid shows all ten digits; the filmstrip follows one sample from noise to an image.

Try these two experiments:

  1. Choose a digit and press new noise: The label stays fixed while the starting point changes. Look for different handwriting styles.
  2. 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:

Different schedules, architectures, and conditions build on those same pieces.

Reference: the notation in one place
SymbolMeaning in this article
t[0,1]t\in[0,1]Progress from noise at 0 to data at 1
εN(0,I)\varepsilon\sim\mathcal{N}(0,I)One Gaussian noise sample
zpdataz\sim p_{\mathrm{data}}One training data sample
Xt=(1t)ε+tzX_t=(1-t)\varepsilon+t zA random blended point; xtx_t is one realization
ptp_tDistribution of points at time tt
pt(xz)p_t(x\mid z)Distribution when the destination is known
pt(zx)p_t(z\mid x)Probabilities of destinations given the current point
zεz-\varepsilonVelocity target for one training pair
ut(xz)u_t(x\mid z)Velocity field for a known destination
ut(x)u_t(x)Marginal field, averaging over plausible destinations
uθ(x,t)u_\theta(x,t)Neural network approximating that field
θ\thetaThe network’s trainable parameters
ψt(x0)\psi_t(x_0)Flow map: where a starting point ends up at time tt
αt,βt\alpha_t,\beta_tData and noise weights; here tt and 1t1-t
ccA condition, such as a digit label or text representation
NFENumber of model evaluations; one per Euler step here

    These lecture series helped shape this article:

    Generated Content Disclaimer

    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.

    Amidi, A., & Amidi, S. (2026). Stanford CME296: Diffusion & Large Vision Models. Stanford University. https://www.youtube.com/playlist?list=PLoROMvodv4rNdy8rt2rZ4T2xM0OjADnfu
    Holderrieth, P. (2026). MIT 6.S184: Flow Matching and Diffusion Models. Massachusetts Institute of Technology. https://www.youtube.com/playlist?list=PL57nT7tSGAAXwjhDYcxEycx5W7YoSrZyt
    Piech, C. (2022). Stanford CS109: Introduction to Probability for Computer Scientists. Stanford University. https://www.youtube.com/playlist?list=PLoROMvodv4rOpr_A7B9SriE_iZmkanvUg