Stable Diffusion, Sora, AlphaFold3, Movie Gen — the same idea underneath all of them. Turn pure noise into a sample of your data by following a differential equation. This lecture builds the machine: ODEs, SDEs, and how to simulate them.
Type "a photo of a dog" into an image generator and out comes a dog that has never existed. The model did not retrieve it from a database. It generated it — it produced a new object. That is the deceptively simple thing this entire course is about: algorithms that generate new objects.
To engineer something, you must first say precisely what "generate a good dog photo" means. Vague taste won't compile. So Lecture 1 has two jobs, and they map exactly onto the two sections of the original MIT slides:
By the end you will be able to take a standard Gaussian blob, push it through a hand-written numerical integrator, and watch it become a target shape — the literal skeleton of how Stable Diffusion samples an image. We have no neural network yet (that arrives in Lectures 2–4, where we learn how to train the arrows). Today we build and drive the engine.
Step one is purely about representation. A computer cannot store "a dog" — it stores numbers. So every object we ever want to generate, we first flatten into a vector of real numbers, a point in some space Rd (d-dimensional space):
So our target object is a point z ∈ Rd. Now: what makes a good z? The slides line up four dog images — "useless," "bad," "wrong animal," "great!" — and ask the key question: those are subjective words. Can we make "good" objective?
Here is the move. Imagine the gigantic distribution of all images on the internet. A crisp, well-lit dog photo is very likely to appear there; a smear of random static is essentially impossible. So we declare:
With that definition, generation becomes sampling: to "generate an object," draw a fresh sample from the data distribution.
Read that as: "z is sampled from p-data." A great dog photo is a high-probability draw; we want our algorithm to land in those high-density regions.
Spell out the dataset precisely: it is a finite collection
That is the only grip we have on pdata. Training will mean: squeeze as much information as we can out of these samples so that, at the end, we can produce more samples that look like they came from the same source.
"A photo of a dog" vs. "a photo of a cat" vs. "a landscape" are different distributions. So we generalize: a condition variable y (the prompt, a class label, a text caption) selects a conditional data distribution pdata(· | y). Generating from a prompt means sampling that conditional distribution:
We can't sample pdata directly. But there is one distribution we can always sample, trivially, as many times as we like: a standard Gaussian. So here is the entire architecture of every flow and diffusion model:
Read it left to right: start at a random Gaussian point X0 at time t = 0; transform it; arrive at a data sample X1 at time t = 1. The whole game is to build that arrow in the middle. The defining choice of flow/diffusion models: the transformation is not one big jump — it is a continuous journey through time, governed by a differential equation.
A cloud of points starts as a standard Gaussian (left, t = 0). Press Generate to transport every point continuously over time t = 0 → 1 toward a target distribution (a ring of clusters — our toy "data"). This is exactly what a trained flow model does to make images; here the arrows are hand-designed so you can see the motion. Notice: every output point lands in a high-density region of the target.
Two questions now stand between us and a working generator, and they are the rest of this lecture:
"Transport a point continuously through time" is the job of an ordinary differential equation (ODE). Three objects describe the same idea from three angles. Get all three and the rest of the course is downhill.
A trajectory is the path a single point takes. It is a function of time:
For each time t between 0 and 1, Xt is where the point is. (We use the time window [0,1] by convention — t = 0 is noise, t = 1 is data.)
A vector field u assigns, to every location x and every time t, a velocity — an arrow saying "if you are here at this time, move this way, this fast":
Think of ut(x) as a wind map, or the current in a river that changes over the day. It is the only thing we get to design — later, a neural network is this vector field.
The ODE is the rule connecting the two: at every instant, the trajectory's velocity must equal the arrow at its current location. "Velocity" is the time-derivative, so:
The first line says "go where the arrow at your feet points." The second pins down where you start. Without a starting point the path is not determined — the same wind map produces a different journey from a different origin.
So: vector fields define ODEs, whose solutions are flows. Three names, one underlying object. The vector field is the wind; the ODE is the law "drift with the wind"; the flow is the resulting map that warps all of space, like a fluid carrying a grid of dye.
Blue arrows are the vector field ut(x). The orange dot starts where you click (or press Drop point) and follows the arrows — that traced path is the ODE trajectory. Switch fields to feel how the arrows dictate the motion. The "contracting" field is u(x) = −θx, the running example we solve by hand next.
A careful person asks: given a vector field, is there always a trajectory, and is it the only one? The Picard–Lindelöf theorem answers yes, under mild conditions:
Why you can stop worrying: we will parameterize u with a neural network, and neural networks always have bounded derivatives. So in every case we care about, the flow exists and is unique. This theorem is good news you can file away, not an obstacle.
Pick the simplest non-trivial field, a contraction toward the origin: ut(x) = −θx for some θ > 0. Claim: the flow is
Let's prove it by checking the two defining properties of a flow — every step shown:
Plug in numbers (we'll reuse these in the code lab): θ = 1, x0 = 2. Then ψt(2) = 2·exp(−t). At t = 1: 2·exp(−1) = 2⁄e ≈ 0.7358. That exact number is our ground truth for testing a numerical integrator.
The linear field had a closed-form flow. Almost nothing else does — a neural-network vector field has no formula for its flow. So we simulate: approximate the continuous journey by many tiny straight steps. The simplest, most intuitive scheme is the Euler method.
The idea is one line of common sense. You are at Xt. The arrow there says "move in direction ut(Xt)." Take a small step of size h in that direction:
Here h = 1/n is the step size and n is the number of steps. Repeat from t = 0 up to t = 1. That's it — "small step in the direction of the arrow," over and over.
Field u(x) = −x (so θ = 1), start x0 = 2, integrate to t = 1. The exact answer (Chapter 3) is 0.7358. Watch Euler approach it as we halve the step:
Halving h roughly halved the error — Euler is a first-order method (error shrinks proportionally to h). That trade-off — accuracy vs. number of steps — is the central practical knob in diffusion sampling, where each step is an expensive neural-network call.
The smooth teal curve is the exact flow 2·exp(−t). The orange polyline is the Euler approximation. Drag the slider to change the number of steps n (step size h = 1/n). Few steps → jagged, large endpoint error; many steps → the polyline hugs the curve. The readout shows the endpoint error.
Now build the integrator yourself. This is the literal engine of every sampler in the course — the step function you write here is reused, unchanged, all the way to Lecture 4.
We now assemble the first real generative model. Take the ODE machinery and make two changes that turn it from "a wind map" into "an image generator."
Putting it together, a flow model is the ODE:
and the goal of training is to make the endpoint land on the data distribution:
This is the entire inference procedure. Memorize its shape; everything in the course either trains the network inside it or makes its steps cheaper.
# Sample from a flow model with the Euler method # Input: trained vector-field network u_theta(x, t); number of steps n t = 0.0 h = 1.0 / n # step size x = sample_gaussian() # X_0 ~ p_init = N(0, I) for i in range(n): x = x + h * u_theta(x, t) # one Euler step along the learned arrows t = t + h return x # X_1, a sample of the data distribution
Read it against Algorithm 1 in the slides: draw a Gaussian, then loop n Euler steps, return the endpoint. The only difference from Chapter 4's hand example is that u_theta is a trained network instead of −θx. The "Generate" button in Chapter 2 ran exactly this loop with a toy field.
u_theta(x, t) takes that vector plus a scalar time and returns another Rd vector — same shape as x, because it's a velocity for each coordinate. Each Euler step adds h·velocity, nudging all d numbers at once. After n steps the pixel-vector has been transported from "Gaussian static" to "a dog." n is typically 20–1000 network evaluations — that is the inference cost.
Flow models are deterministic after the start. Diffusion models add randomness at every step. To build them we need the canonical source of continuous randomness: Brownian motion (also called the Wiener process, hence the symbol W — after Norbert Wiener, who taught at MIT).
Picture a stochastic process: a trajectory that is random. Run it twice from the same start and you get two different paths, because the dynamics themselves roll dice. Brownian motion is the continuous-time limit of a random walk — the path a speck of pollen traces as water molecules kick it.
How do we simulate it? Discretize time into steps of size h. Each step, add a Gaussian kick whose variance is h (matching "variance = elapsed time"). Since standard normal noise ε has variance 1, scaling by √h gives variance h:
Each thin line is one simulated path of 1-D Brownian motion, all starting at 0. They fan out over time; the dashed envelope is ±√t (one standard deviation), the theoretical spread. Press Resimulate for fresh randomness. The readout reports the empirical variance at the final time — it should sit near t.
Now simulate it yourself and confirm the signature law Var[Wt] = t — the property that defines Brownian motion.
A stochastic differential equation (SDE) is just an ODE with Brownian noise stirred in at every step. To see exactly where the noise slots in, we first rewrite the ODE without derivatives (because for a random, jagged path the derivative no longer exists in the usual sense).
Start from d⁄dt Xt = ut(Xt). Use the definition of the derivative as a difference quotient and rearrange — every step shown:
where Rt(h) is an error term that vanishes as h → 0. This just restates Euler: "each instant, take a small step of size h in direction ut." Nothing new — but now there are no derivatives, so we can add randomness.
Amend the update: take the deterministic step and a Brownian kick, scaled by a diffusion coefficient σt ≥ 0 that controls how much noise to inject:
This is the Euler–Maruyama update — the SDE analogue of Euler. Since the Brownian increment Wt+h − Wt = √h·ε (Chapter 6), in code it reads:
Mathematicians write the same thing in the compact symbolic form (just shorthand for the limit of the update above):
Here ut is the drift (the deterministic pull) and σt is the diffusion (the noise strength). Two facts make SDEs easy to live with:
You already wrote the deterministic x + h*drift step (Chapter 4) and the √h Brownian kick (Chapter 6). The next code lab (Chapter 8) snaps them together into Euler–Maruyama — the complete diffusion sampler.
A diffusion model is a generative model built from an SDE: random Gaussian start, a learned drift uθt, a noise schedule σt, simulated with Euler–Maruyama. To build intuition for how drift and noise interact, we study the cleanest non-trivial SDE — the Ornstein–Uhlenbeck (OU) process.
The remarkable result: the OU process converges to a Gaussian whose variance is set by the tug-of-war between pull and push:
Sanity-check the extremes. More noise σ → wider stationary spread (σ² on top). Stronger pull θ → tighter spread (2θ on the bottom). And if σ = 0 (no noise), the variance is 0 — every path collapses deterministically to the origin, recovering the contracting flow from Chapter 3. Same equation, dial in between.
Walkers start spread out; the drift pulls them toward 0 while the noise jostles them. Watch them settle into a steady band whose half-width is the theoretical std √(σ²/2θ) (dashed lines). Drag θ (pull strength) and σ (noise). Set σ = 0 to see the deterministic flow collapse to the origin — an ODE is just an SDE with the noise off.
Now assemble everything: implement the OU drift and the full Euler–Maruyama step, then verify the stationary variance σ²/(2θ).
The same target can often be reached by an ODE (deterministic, σ = 0) or an SDE (stochastic, σ > 0). ODE sampling is reproducible and usually needs fewer steps; SDE sampling injects fresh noise each step, which can improve sample diversity and quality and "self-correct" errors. Lecture 3 shows the two are deeply linked through the score function. For now: flow = ODE, diffusion = SDE, and ODE is the σ = 0 corner of the SDE world.
You built the entire engine of modern generative AI from first principles — with no neural network yet. Here is the whole lecture on one page.