This section is about the score of a probability distribution, the gradient of its log density with respect to the data. Picture an arrow at every point of space. It points in the direction in which the data become more likely, and its length says how fast the log density rises that way. Three facts make this field of arrows the center of diffusion models. A network can learn it from samples alone, samples can be drawn using nothing else, and the noise predictor that §33.2 trains is the score of the noised data multiplied by a known number.
Here is the problem the score solves. Suppose you want a model that can put probability on data of any shape. A flexible way is to take any function Eθ(x), for example a neural network with one output, call it the energy, and declare
pθ(x)=Zθe−Eθ(x),Zθ=∫Rde−Eθ(x)dx.
Low energy means high probability. The number Zθ, the normalizing constant, makes the density integrate to one. Here d is the number of values in one data point, not a model width. For a 256×256 color image d=196,608, and Zθ is an integral over every possible image, which nobody can compute. Maximum likelihood picks θ to make the data as probable as possible (§0.4). It needs logZθ and its gradient, so it is stuck before it starts.
Now differentiate the log density with respect to the data x instead of the parameters θ,
∇xlogpθ(x)=−∇xEθ(x)−∇xlogZθ=−∇xEθ(x).
Zθ is one number that does not change with x, so its gradient in x is zero. The normalizer is gone.
The section follows that observation to the end. We define the score and compute it by hand. We learn it from data, first with a classical trick that needs no normalizer, then by adding noise, which turns learning into ordinary regression. A classical identity, Tweedie's formula, then shows that the best noise predictor, the score, and the best denoiser are one function written three ways. Next we draw samples with the score, using gradient ascent with added noise (Langevin dynamics) and a schedule of falling noise. Finally we let the noise be added continuously. Diffusion then becomes a stochastic differential equation, a differential equation with a random term. That equation has a deterministic twin, the probability flow ODE, with exactly the same distribution at every time. The fast samplers and flow matching of the rest of the chapter are built on that twin.
The score is a field of arrows that ignores the normalizer
Start with the standard normal in one dimension. Its log density is −x2/2 plus a constant, so its derivative is −x. At x=2 the score is −2, an arrow of length 2 pointing back toward 0. At x=−1 it is +1, again pointing toward 0. At the mean it is 0. The arrows always point at the mean, and they grow with the distance from it.
In plain words, the score is an arrow at every point of space. It points where the density increases fastest, and its length says how fast the log density changes. Following the arrows walks you uphill toward the data.
The name collides with one you may have met. The score-function estimator of §0.4 estimates the gradient of an expected value using ∇θlogpθ(x), and the policy-gradient method of §15.3, called REINFORCE, is built on it. That is a gradient with respect to the parameters, with one entry per parameter. The score of this chapter is a gradient with respect to the data, with one entry per pixel. They share a name and nothing else.
Three cases build the intuition.
A Gaussian. For N(μ,s2) in one dimension, logp(x)=−(x−μ)2/(2s2) plus a constant, so the score is −(x−μ)/s2. It points back toward the mean, and a narrow Gaussian pulls harder. In d dimensions, N(μ,Σ) has score −Σ−1(x−μ), a field that is linear in x.
A mixture. Let p(x)=∑kπkpk(x), a weighted sum of component densities pk with weights πk≥0 that add up to 1. Using ∇pk=pk∇logpk,
The weights rk(x) are the responsibilities. By Bayes rule, rk(x) is the probability that the point x came from component k. In plain words, the score of a mixture is the average of the components' scores, weighted by how likely each component is to have produced the point.
An energy-based model. A model defined by an energy, as in the opening, is called an energy-based model. Its score is −∇xEθ, as we saw. The energy-based view of learning studies these models in their own right later in the book. Here they are only the reason the normalizer matters.
Mixtures of Gaussians are the workhorse of this chapter, because adding Gaussian noise keeps them mixtures of Gaussians. Recall the forward process of §33.1. It shrinks a clean point x0 by a signal scaleαt and adds Gaussian noise with a noise scaleσt, so xt=αtx0+σtϵ with ϵ∼N(0,I) independent of x0. We write pt for the density of xt. If x0∼∑kπkN(μk,Σk), then component k becomes a Gaussian with mean αtμk and covariance αt2Σk+σt2I, and the weights stay the same (the theorem on noised mixtures in §33.1). So pt and its score have closed forms at every t.
That gives us an oracle, a function that returns the true score of every noised distribution. Every algorithm in this section can be tested against it before any network is trained. The cell below implements the oracle for components with covariance sk2I and checks it against finite differences of logpt. Then it draws the arrows for the chapter's 8-Gaussian ring (8 modes on a circle of radius 2, each with standard deviation 0.1) at three noise levels, with αt=1 so that only the noise changes.
The responsibilities need care. Far from every mode, each logπkpk is a large negative number, and exponentiating them directly gives zero divided by zero. The cell subtracts the largest of them before exponentiating, the stable log-sum-exp of §0.4.
Python · cell 1
The finite-difference check agrees to better than 10−9. The far point (50,0) gets a finite score of 184.6, which is 48/0.26. That is its distance to the nearest noised mode, at (2,0), divided by that mode's variance 0.12+0.52.
The arrows are drawn with squashed lengths and colored by size, and they tell the story of the whole section. At σ=0.05 the field is enormous. The median arrow on the grid has length 76 and the largest 179, and every arrow points straight at the nearest mode, which says nothing about the ring as a whole. At σ=2 the field is gentle (no arrow is longer than 0.7) and points at the center of the ring, as if the data were one blob. The middle level has both, a pull toward the ring and a pull toward each mode.
So small noise shows the local detail, and large noise shows the global shape. A sampler needs both, which is why diffusion uses many noise levels. An interviewer who asks why diffusion models need a whole range of noise levels wants this picture.
Interview
The exact score of a noised Gaussian mixture
Write the oracle for full covariance matrices. The clean data are ∑kπkN(μk,Σk), and the noised data are xt=αx0+σϵ with ϵ∼N(0,I) independent of x0. Implement gmm_score(x, weights, means, covs, alpha, sigma), which returns the score ∇logpt at each row of x. The array weights holds the πk.
Shapes are x(n,d), weights(K,), means(K,d), and covs(K,d,d). The output is (n,d).
Use no loops over points.
Keep the result finite far from every mode, with no NaN at ∥x∥=50.
Later sections test their samplers against exact mixture scores like this one. It is good interview practice, because it combines the noised-mixture formula, batched linear algebra, and a stable log-sum-exp.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Learning a score you never see
Suppose we have samples x1,…,xN from an unknown density p, and we want a network whose arrows match the true arrows, sθ≈∇logp. The natural loss is the squared distance between the two sets of arrows, averaged over where the data are.
We cannot compute this loss, because its target ∇logp is exactly what we do not know. There is a way around it, found by Hyvärinen (2005). Expand the square into three terms, 21∥sθ∥2−sθ⊤∇logp+21∥∇logp∥2. The last term has no θ in it, so it does not affect training. The first needs only the model. Only the middle term contains the unknown score. Integration by parts moves the derivative off logp and onto the model, and the unknown disappears.
The step from p′/p times p to p′ is the one that removes the unknown. The boundary term vanishes because psθ→0 at infinity. So LESM=Ep[21sθ2]+Ep[sθ′]+21Ep[((logp)′)2], which is the claim for d=1.
In d dimensions the cross term is ∑iEp[sθ,i∂ilogp]. Integrate each summand by parts in the coordinate xi, holding the other coordinates fixed. Each becomes −Ep[∂isθ,i], and the sum over i is −Ep[tr∇xsθ]. ∎
The identity is easiest to believe on a model with one parameter, where both losses can be written down by hand.
The cell checks the example with 200,000 samples.
Python · cell 2
The difference is 0.50 at every θ, up to sampling noise, and both columns are smallest at θ=1, as the theorem says.
Why the implicit form does not scale
The trace is the problem. Reverse-mode automatic differentiation gives one row of a Jacobian per backward pass (§0.3). The exact tr∇xsθ needs every diagonal entry, so it costs d backward passes. For a 256×256×3 image that is 196,608 backward passes for every training example.
A random estimate of the trace is cheaper but noisy (the box below). And there is a deeper problem that no trick for the trace fixes.
Real data lie near a low-dimensional set inside Rd. Natural images, for example, fill only a thin sliver of all possible pixel arrays. This belief is called the manifold hypothesis. Off that set the density is zero or nearly so. The log density is then undefined or extremely steep, and the score is not a smooth field.
Song and Ermon (2019) saw this directly. Sliced score matching on raw CIFAR-10 images, a standard set of 32×32 color images, gave a loss that fell and then fluctuated without converging. Adding Gaussian noise of variance 10−4, invisible on pixels scaled to [0,1], made it converge. Noise spreads the data over the whole space, so the density is positive everywhere and the score is well defined. So we stop fighting the noise and use it.
Denoising score matching
Add noise on purpose. Draw a clean point x0∼p and noise ϵ∼N(0,I), and set xt=αx0+σϵ. This is the forward process at one fixed time. We drop the subscript t on α and σ until we need several noise levels. The noised data have density pt, and we cannot compute its score. But if we know which clean point a noisy point came from, the score of that one Gaussian is easy.
A tiny example first. Take one dimension, α=1, σ=0.5, a clean point x0=1, and a noise draw ϵ=0.4. Then xt=1.2. Given x0, the noisy point has density N(1,0.25), whose score at 1.2 is −(1.2−1)/0.25=−0.8. That is −ϵ/σ=−0.4/0.5. The arrow points back at the clean point, and it is the added noise, reversed and divided by σ.
In general, write q(xt∣x0)=N(xt;αx0,σ2I) for the density of a noisy point given its clean point. The noised density is the average of these over the data, pt(x)=∫q(x∣x0)p(x0)dx0. The score of one of them is
∇xtlogq(xt∣x0)=−σ2xt−αx0=−σϵ.
This conditional score is a target we can compute for every training pair. Vincent (2011) proved that regressing on it is as good as regressing on the unknown score of pt.
∂ Proof of denoising score matching
Expand both squares. Each contains the same 21E∥s(xt)∥2 and a term without s. So it is enough to show that the cross terms agree, E[s(xt)⊤ϵ/σ]=−Ept[s⊤∇logpt].
Work on the right side. Differentiate pt(x)=∫q(x∣x0)p(x0)dx0 under the integral and write ∇q=q∇logq,
The double integral is an expectation over the joint distribution of (x0,xt), and inside it the conditional score equals −ϵ/σ. That is the claim. Using E∥ϵ∥2=d, the constant is C=2σ2d−21Ept∥∇logpt∥2. ∎
There is a second way to see the minimizer, using a fact you already know. LDSM is a squared-error regression of the target −ϵ/σ on the input xt. §32.1 proved that the best squared-error predictor is the conditional mean of the target given the input. So s⋆(xt)=−E[ϵ∣xt]/σ. Tweedie's formula, proved below without any score matching, shows that this conditional mean is exactly ∇logpt. Together the two facts give a second proof of the theorem.
Notice what was never needed. There is no trace, no Jacobian, and no density. Each training example costs one forward pass and has one regression target. The price is that we learn the score of pt, not of p. That is a feature, because pt is smooth and positive everywhere.
The noisy targets are why DSM needs many samples. At σ=0.5 each target −ϵ/σ has standard deviation 2 in every coordinate, while the field we want is smooth. Regression averages the noise away.
The next cell shows this on a 1D two-mode mixture that the rest of the section reuses, p=0.2N(−2,0.25)+0.8N(2,0.25). The model is a weighted sum of 33 fixed bumps, sθ(x)=∑jθjϕj(x), where each ϕj is a Gaussian bump centered on a grid point. The model is linear in θ, so DSM is ordinary least squares, solved in one line.
Python · cell 3
Single targets scatter with standard deviation 2.0. Yet the fitted field is within about 0.2 of the exact score of pt wherever the noised data have appreciable density, and within 3 percent on average. Where pt is tiny the fit is poor, because there are no training points there. This low-density problem will matter when we sample, because a sampler started from noise begins exactly where the data are rare.
In an interview, the one-line summary is that DSM turns an impossible target into a computable one, the added noise, and that regression on that noisy target still recovers the exact score of the noised data.
Many noise levels in one network
One noise level is a compromise. A small σ gives a score close to the data's but badly behaved, and a large σ gives a smooth but blurry one. Song and Ermon (2019) used many levels at once. They trained one network sθ(x,σ) that takes the noise level as an input, and called it a noise conditional score network (NCSN). It is trained on a ladder of levels σ1>σ2>⋯>σL, with a weight w(σ) for each level,
Their paper writes the weight as λ(σ). We write w, because in this chapter λ is the log signal-to-noise ratio of §33.1.
Which weight? The target ϵ/σ has squared norm about d/σ2. Without a weight, the smallest σ dominates the sum. With levels from 1 down to 0.01 it outweighs the largest by a factor of 104. Their choice is w(σ)=σ2, which puts every level on the same scale. To see why, use the identity
σ2s+σϵ2=∥σs+ϵ∥2,
and note that σs⋆=−E[ϵ∣xt] is of order one in each coordinate at every level. Look at the right side once more. It regresses a network output on the noise ϵ. That is the loss of the denoising diffusion model of §33.2 (DDPM) in another form. Tweedie's formula, next, makes that exact.
Core
The multi-level denoising score matching loss
Implement dsm_loss(score_fn, x0, sigmas, rng) for a batch x0 of shape (B,d) and an array sigmas of noise levels.
Give each example its own level, drawn uniformly from sigmas, then its own noise. Draw them in exactly this order so the tests can reproduce your draws, first idx = rng.integers(len(sigmas), size=B) and then eps = rng.standard_normal(x0.shape).
Form x~i=x0,i+σiϵi.
Call score_fn(x_noisy, sig) once, with sig of shape (B,).
Return the scalar
B1i=1∑B21σi2s(x~i,σi)+σiϵi2,
which sums over coordinates inside the norm and averages over the batch.
import numpy as np
def dsm_loss(score_fn, x0, sigmas, rng):
sig = np.full(len(x0), float(np.mean(sigmas))) # placeholder: one level for everyone
eps = rng.standard_normal(x0.shape)
s = score_fn(x0 + sig[:, None] * eps, sig)
return float(0.5 * np.mean(np.sum((s + eps / sig[:, None]) ** 2, axis=1)))
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Tweedie's formula, and why noise, score, and denoiser are one function
A denoiser takes a noisy point and guesses the clean point it came from. Under squared error the best guess is the posterior meanE[x0∣xt], the average of all clean points that could have produced this noisy point, each weighted by how likely it is. That average looks like it needs the unknown data distribution. It needs only the score of the noisy data. Move the noisy point uphill by σ2 times the score, then divide by α to undo the shrinking.
§32.1 checked this for Gaussian data with noise added and no shrinking, the case α=1. Here is the general case, for any data distribution and any signal scale α.
Proof. Differentiate pt(x)=∫N(x;αx0,σ2I)p(x0)dx0 under the integral. The gradient of the Gaussian in x is the Gaussian times −(x−αx0)/σ2, so
By Bayes rule, the fraction inside the last integral is the posterior density of x0 given xt=x, so the integral is E[x0∣xt=x]. Dividing by pt gives ∇logpt(x)=−(x−αE[x0∣xt=x])/σ2. Solving for the expectation gives the formula. ∎
In plain words, to denoise, step uphill on the log density of noisy data by σ2, then undo the shrinking by dividing by α. The formula is named after the statistician Maurice Tweedie. Efron (2011) made it widely known in empirical Bayes, the branch of statistics that estimates a prior from the data themselves. It is exact for any data distribution, and it is the reason diffusion works. A network that knows the score of noisy data knows the best denoiser.
For the two-mode mixture the posterior mean is not linear. The next cell checks Tweedie there by brute force. It noises two million samples. Then, for a few chosen values of xt, it averages the clean x0 of the pairs whose xt lands within 0.02 of that value.
Python · cell 4
The two columns agree to about 0.01, the Monte Carlo error of averaging over a window of width 0.04. Look at the shape. Between xt=−1 and xt=0 the posterior mean swings from −1.5 to +1.05. A point there could have come from either mode, and the denoiser hedges between them. That hedging is the blur of a one-step denoiser, the problem of §32.4. Diffusion avoids it by taking many small denoising steps instead of one big one.
Tweedie's formula also gives a dictionary between three things a network might output. Write the noise as ϵ=(xt−αx0)/σ and take conditional expectations given xt.
Proof. Taking E[⋅∣xt] of ϵ=(xt−αx0)/σ gives E[ϵ∣xt]=(xt−αE[x0∣xt])/σ, which is the second identity. Substituting Tweedie's αE[x0∣xt]=xt+σ2∇logpt gives E[ϵ∣xt]=−σ∇logpt, the first. ∎
Now look again at the denoising diffusion model of §33.2, DDPM. It trains a network ϵθ(xt,t) to predict the added noise with the simple loss Et,x0,ϵ∥ϵ−ϵθ(xt,t)∥2, with the step t drawn uniformly. Write the network as a score, ϵθ=−σtsθ. Then
∥ϵ−ϵθ∥2=∥ϵ+σtsθ∥2=σt2sθ(xt,t)+σtϵ2.
That is twice the DSM loss at level t with weight w=σt2, the same weight the noise conditional network chose. In plain words, DDPM is denoising score matching over all noise levels at once, with time drawn uniformly and the weight that makes all levels equally loud. Ho et al. (2020) pointed out this connection in the DDPM paper. Two lines of research, one from variational bounds and one from score matching, had built the same model. Interviewers ask for exactly this derivation.
Core
The identity triangle
Implement the three conversions between score, noise, and data predictions. Each takes a batch of shape (n,d) and must work when alpha and sigma are scalars or arrays of shape (n,1).
tweedie_denoise(x_t, score, alpha, sigma) returns E[x0∣xt] from the score.
score_to_eps(score, sigma) returns the noise prediction that corresponds to a score.
eps_to_x0(x_t, eps, alpha, sigma) returns the data prediction that corresponds to a noise prediction.
The setup provides mixture_score(x, alpha, sigma), the exact score of a noised 2D mixture whose modes have covariance sk2I. The tests compare your denoiser with the exact posterior mean of that mixture.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Tweedie's formula has a second-order version. It gives the posterior covariance of the clean data from the Hessian of logpt, so it says how uncertain the best denoiser is. §33.6 and Exercise 33.3f use it.
∂ Tweedie's second-order formula
Write m(x)=E[x0∣xt=x] for the posterior mean and p(x0∣x)=q(x∣x0)p(x0)/pt(x) for the posterior density. The first-order proof gave ∇logpt(x)=(αm(x)−x)/σ2. Differentiate once more in x,
∇2logpt(x)=σ2α∇m(x)−I,
where ∇m is the Jacobian of the posterior mean, with entries ∂mi/∂xj. To compute it, differentiate m(x)=∫x0p(x0∣x)dx0. That needs the gradient of the log posterior in x,
Then ∂mi/∂xj=∫x0,ip(x0∣x)∂jlogp(x0∣x)dx0=σ2αE[x0,i(x0,j−mj)∣x]=σ2αCov[x0∣x]ij. Substitute ∇m=(α/σ2)Cov[x0∣xt] into the Hessian and solve,
Cov[x0∣xt]=α2σ2(I+σ2∇2logpt(xt)).
Check it on Gaussian data N(0,s2) in one dimension, where ∇2logpt=−1/(α2s2+σ2). The formula gives α2σ2⋅α2s2+σ2α2s2=α2s2+σ2s2σ2. Direct Bayes gives the posterior precision 1/s2+α2/σ2, whose inverse is the same number.
A covariance is positive semidefinite, so the identity also shows that ∇2logpt⪰−I/σ2 always. The log of a noised density can never curve down more sharply than the log of the noise itself.
Langevin dynamics, sampling with nothing but the score
We can learn the score. Can we sample with it? Gradient ascent on logp (§0.6) climbs to a peak of the density and stops there. That gives the most likely point, not a sample. Add exactly the right amount of random noise at each step, and the climb becomes a random walk whose long-run distribution is p itself.
One step by hand first. Take p=N(0,1), whose score at x is −x, a step size η=0.1, and a current point x=2. The uphill move is η⋅(−2)=−0.2. The noise has standard deviation 2η=0.45. So the next point is 1.8+0.45z for a fresh standard normal z. Far from the mean the pull dominates and the point drifts back. Near the mean the pull is weak, and the noise keeps the point moving.
Why 2η and not some other amount? The uphill step contracts the cloud of points by a fraction of order η per step. The noise spreads it by a variance of order η per step. The factor 2 balances the two exactly when the cloud is distributed as p. We prove this with the Fokker-Planck equation later in the section. The Gaussian case can be done by hand now, and it also shows what a finite step costs.
The cell starts each chain exactly at the target and runs it at several step sizes.
Python · cell 5
The chain starts at the right answer and walks away from it, to exactly the biased variance the formula predicts. Score-based samplers live with this bias and keep η small, for the reason in the box below. An interviewer who asks why diffusion samplers do not use a Metropolis correction wants that reason.
Why plain Langevin fails on real data
Run Langevin with the exact score of a mixture whose modes are far apart, and it gets the proportions wrong. Song and Ermon (2019) give the cleanest argument. Let p=wp1+(1−w)p2, and suppose the two components never overlap. On the region where p1 lives, logp=logw+logp1, so the score is ∇logp1. The weight w has disappeared.
A sampler that sees only the score therefore cannot tell how much mass each mode should get. When the modes overlap slightly, the weight shows up only in the thin valley between them, where the density is tiny. A chain has to cross that valley many times to learn the weight. The expected time to cross grows exponentially with the depth of the valley, measured in log density.
Three problems compound on real data.
The score is undefined off the data manifold.
Scores are poorly estimated where data are rare, as the least-squares fit above showed, and a chain started from noise begins exactly there.
Separated modes mix slowly, so the relative weights come out wrong.
Noise fixes all three at once. A large noise level fills the space. It makes the score well defined and well estimated everywhere, and it merges the modes so that the chain can move between them. A small noise level stays close to the data. So sample at a large level first, then lower it step by step.
Why ηi∝σi2? The score at level σ has size of order 1/σ (the table "Score, noise, and denoiser, at a glance" above). So the uphill move ηs has size η/σ, and the noise 2ηz has size η. Their ratio is η/σ, which is the same at every level exactly when η∝σ2. With this rule, the balance between moving and diffusing does not depend on the noise level.
The next cell repeats Song and Ermon's toy experiment with the exact score. The data are 0.2N((−5,−5),I)+0.8N((5,5),I), and 2,000 particles start uniformly in the square [−8,8]2. Plain Langevin runs 1,000 steps at η=0.05. Annealed Langevin uses 10 geometric levels from σ=20 to σ=1, with M=100 and ηL=0.05. These are the paper's settings, converted to our η.
Python · cell 6
Plain Langevin ends with almost exactly half the particles in each mode, the split of the uniform start. Each particle slides into the basin it started in and never leaves. Annealed Langevin ends at about 0.79, close to the true 0.8 (the standard error with 2,000 particles is about 0.009). At σ=20 the two modes are one blob, and the particles spread across it in the right proportions. As σ falls the blob splits, and each part keeps its share.
The last level here is σ=1. So these samples come from the data noised once more with unit variance, and they are visibly wider than the data. Real samplers end at a tiny σ. Many finish with one Tweedie denoising step, x←x+σL2sθ(x,σL), which jumps to the posterior mean of the clean data (here α=1).
The widget runs both samplers on a smaller copy of this mixture, with the same weights 0.2 and 0.8. The arrows are the exact score at the current noise level, the dots are the particles, and the dashed circles mark the two data modes. Press Sample to run 1,500 Langevin steps from a uniform start, which takes five seconds. The annealed run spends its first 1,000 steps on 10 levels of 100 steps each, from σ=3 down to the noise level you set, and the rest at that level. The big number is the share of particles in the left mode.
How does a field of score arrows turn noise into samples, and why does it need many noise levels?
Data with two modes, weights 0.2 and 0.8. Arrows show the exact score at noise level σ, dots are particles.
Loading interactive figure…
Once you have run both samplers, raise the step size ε toward its maximum and watch the cluster std below the big number. It is measured across the line through the two modes, where both noised modes have the same spread. The widget sets η=ε(s2+σ2), where s2 is the variance of one data mode. So the step is ε in units of one noised mode's variance, which follows the rule η∝σ2. By the 2/(2−η) bias, each cluster's variance then grows by the factor 2/(2−ε). At the top of the slider that is about 1.5, so the measured std comes out about 1.24 times the exact one.
The levels sigmas come in decreasing order. score_fn(x, sigma) returns the score of pσ at the rows of x (shape (n,d)) for a scalar sigma.
At level i the step is ηi=step_scale⋅σi2/σL2, where σL is the last and smallest level.
Run n_steps Langevin updates per level, and draw each step's noise with rng.standard_normal(x.shape).
Return the final samples, and do not modify x_init in place.
The setup provides mix_score, the exact score of 0.2N(−4,0.25)+0.8N(4,0.25) noised to level σ. The tests check the mode weights and the spread of your samples.
import numpy as np
def annealed_langevin(score_fn, sigmas, n_steps, step_scale, x_init, rng):
x = np.array(x_init, dtype=float)
s = sigmas[-1] # placeholder: plain Langevin at the last level
for _ in range(n_steps * len(sigmas)):
x = x + step_scale * score_fn(x, s) + np.sqrt(2 * step_scale) * rng.standard_normal(x.shape)
return x
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Stochastic differential equations from first principles
DDPM uses T=1000 discrete steps. The first noise conditional network used 10 levels with 100 steps each. Both numbers look arbitrary. Song et al. (2021) took the limit in which noise is added continuously. Then the forward process is a stochastic differential equation (SDE), a differential equation with a random term. Every sampler becomes a numerical method for solving an equation. The number of steps becomes a choice made at sampling time rather than a property of the model.
We need one new object, and we define it through the simulation anyone would write. Take a small time step Δt. At each step, move by a deterministic amount proportional to Δt, then add Gaussian noise whose variance is proportional to Δt. For example, take no deterministic move, a noise strength of 1, and Δt=0.01. Each step adds noise of standard deviation 0.01=0.1. After 100 steps, one unit of time, the variances add up to 100×0.01=1.
In plain words, each tiny step moves by the drift and then jiggles by a Gaussian whose variance is proportional to the step length. The Δt is the whole subtlety. Over a fixed time t there are t/Δt independent jiggles, so their variances add up to g2t, a finite amount. Had the jiggle been proportional to Δt, the total variance would be g2tΔt, which goes to zero, and the noise would vanish in the limit. Had it been larger than Δt, the variance would explode.
Brownian paths are continuous but nowhere differentiable, because their increments are of size Δt, far larger than Δt. That is why we write dwt and never dwt/dt. The rigorous theory, Itô calculus, proves that the limit exists. Everything below can be checked at the level of the recursion.
DDPM and the noise conditional network are two SDEs
Take DDPM's chain, xi=1−βixi−1+βizi−1 for i=1,…,N, where βi is the variance added at step i. Put the steps on a time grid ti=i/N with step Δt=1/N. Write each step's variance as a rate times the step, βi=β(ti)Δt, for a continuous noise rate β(t). For small Δt, 1−β(t)Δt=1−21β(t)Δt+O(Δt2), so
xt+Δt=xt−21β(t)xtΔt+β(t)Δtzt+O(Δt2).
That is Euler-Maruyama for the SDE dx=−21β(t)xdt+β(t)dwt. It is called the variance preserving (VP) SDE, because, as in §33.1, it keeps αt2+σt2=1.
The noise conditional network's chain keeps the data unscaled and adds independent noise to reach each level, xi=xi−1+σi2−σi−12zi−1. With σi=σ(ti), the added variance is σi2−σi−12≈dtdσ2Δt. That is Euler-Maruyama for an SDE with no drift, called the variance exploding (VE) SDE because the variance of xt keeps growing instead of staying at 1.
Both rows are special cases of one rule that converts any Gaussian schedule (αt,σt) into an SDE and back.
∂ Proof for linear SDEs
One Euler-Maruyama step is xt+Δt=(1+fΔt)xt+gΔtzt, linear in xt plus independent Gaussian noise. So by induction every xt is Gaussian given x0, with some mean mt and covariance vtI.
Take the mean. mt+Δt=(1+fΔt)mt, so (mt+Δt−mt)/Δt=fmt, and in the limit mt=αtx0 with α′=fα. Take the variance. vt+Δt=(1+fΔt)2vt+g2Δt=vt+(2fvt+g2)Δt+O(Δt2), so v′=2fv+g2. Rearranging the two equations gives the converse. ∎
Check the rule on VP. With f=−21β we get αt=e−21∫0tβ. The variance equation v′=−βv+β has the solution v=1−αt2, so αt2+σt2=1 for all t. Going the other way, dtdσt2=−dtdαt2=βαt2, and g2=βαt2+β(1−αt2)=β. For VE, f=0 gives g2=dσt2/dt.
The rule also builds new processes. Keep VP's drift but ask for the smaller noise variance σt2=(1−αt2)2. The converse formula gives g2=β(t)(1−αt4), which Song et al. (2021) call the sub-VP SDE. Its variance is below VP's at every time, and it gave their best likelihoods.
In practice this rule means that every schedule of §33.1 has an SDE, so every sampler in the rest of this section works for it. §33.4 and §33.6 use the two formulas for f and g directly.
The Fokker-Planck equation, how a density flows
An SDE moves individual points. We care about the whole cloud, the density pt of xt when the start x0 is drawn from p0. How does pt change in time? Two things happen. The drift carries probability along, like a current carrying dye. The noise spreads probability out, from places where it is dense to places where it is sparse.
Two pieces of notation come first. The divergence of a vector field u is ∇⋅u=∑i∂ui/∂xi, the net rate at which the field flows out of a point. The Laplacian of a function is ∇⋅∇p=∑i∂2p/∂xi2. It is negative at a peak of p and positive where p is lower than its surroundings.
In plain words, the drift moves probability like a current, and the noise spreads it from high density toward low density, at a rate set by the curvature of the density.
The proof watches averages. A test functionφ is a smooth function that is zero outside a bounded region. If we know the average ∫φpt for every test function, we know pt. So we track how such averages change over one Euler-Maruyama step.
∂ Deriving the Fokker-Planck equation from one step
Work in one dimension. Write one step as x′=x+Δx with Δx=f(x,t)Δt+gΔtz, and Taylor expand the test function,
Take expectations over x∼pt and the independent z, using Ez=0 and Ez2=1. The first-order term gives ΔtE[φ′f], since the part with z averages to zero. In the second-order term, Δx2=f2Δt2+2fgΔt3/2z+g2Δtz2, and only the last piece is of order Δt. It contributes 21g2ΔtE[φ′′]. The remainder has E∣Δx∣3=O(Δt3/2). Divide by Δt and let it go to zero,
The second equality integrates by parts once in the first term and twice in the second. There are no boundary terms, because φ vanishes outside a bounded region. The left side is ∫φ∂tpt. Both sides agree for every test function, so the integrands agree, which is the Fokker-Planck equation in one dimension.
In d dimensions the same expansion uses the gradient and Hessian of φ. The cross terms E[zizj]=0 for i=j remove every mixed second derivative, and each coordinate integrates by parts separately. ∎
Langevin dynamics is a one-line application. Its drift is f=∇logp and g=2. Put pt=p into the right side and use p∇logp=∇p,
−∇⋅(p∇logp)+21⋅2∇⋅∇p=−∇⋅∇p+∇⋅∇p=0.
So p does not change under the Langevin diffusion, the stationarity we promised. The factor 2 in 2η is exactly what makes the two Laplacians cancel.
That the chain also reaches p from any start needs more than stationarity. Under mild conditions on a smooth positive p it does, but the speed varies enormously. Convergence is fast when p is log-concave, meaning logp is a concave function, a single bump with no valleys. How many steps it takes is the chain's mixing time, and the two-mode experiment showed that it can be enormous.
Running time backward
Two results turn diffusion into a generative model, and both come from the Fokker-Planck equation. The trick is to rewrite its noise term with the score. Since p∇logp=∇p,
21g2∇⋅∇pt=∇⋅(21g2pt∇logpt).
Substituting this into the Fokker-Planck equation leaves a single divergence,
∂t∂pt=−∇⋅(pt[f−21g2∇logpt]).
That is a continuity equation, the density equation of an ODE with no noise at all.
Proof. The density of the ODE's state obeys the continuity equation with velocity u=f−21g2∇logpt. The rewriting above shows that the SDE's marginals obey that same equation. Both start from p0, and a continuity equation with a smooth velocity field has a unique solution for a given start, so the two densities are equal for all t. The ODE can also be run backward, so starting from p1 and integrating down to t=0 produces p0. ∎
In plain words, the SDE moves each point randomly, and the ODE moves each point deterministically along the current of probability. Someone who looks only at the cloud of points at each time cannot tell the two apart. The individual paths are completely different.
For the VE SDE the ODE has a clean form. There f=0 and g2=dσ2/dt=2σdσ/dt, so dx/dt=−σ(dσ/dt)∇logpσ. Written with the noise level σ as the clock, it reads
dσdx=−σ∇logpσ(x),
whatever the schedule σ(t).
The second result keeps the noise and reverses it.
The theorem is about whole paths, and its proof needs stochastic calculus. That the marginals come out right follows from the Fokker-Planck equation in a few lines.
∂ Why the reverse SDE has the right marginals
Let τ=1−t be the reverse time. An SDE in τ with drift b(x,τ) and diffusion g moves its density qτ by ∂τqτ=−∇⋅(qτb)+21g2∇⋅∇qτ, and we want qτ=p1−τ. The target satisfies ∂τp1−τ=−∂tpt=∇⋅(ptf)−21g2∇⋅∇pt. Set the two equal with qτ=pt and move the Laplacian over,
−∇⋅(ptb)=∇⋅(ptf)−g2∇⋅∇pt=∇⋅(pt[f−g2∇logpt]).
This holds with b=−(f−g2∇logpt). A drift of b per unit of τ is a drift of f−g2∇logpt per unit of t when t runs backward, which is the theorem's drift.
Compare the two theorems. The reverse SDE's drift is the ODE's drift with a second −21g2∇logpt added. So split one reverse step in two. One part is the probability flow ODE. The other part, written in the reverse time τ, is 21g2∇logptdτ+gdwˉ. That is Langevin dynamics for pt with its clock sped up by 21g2. (Substitute dτ′=21g2dτ into ∇logptdτ′+2dw.)
To sample, replace the unknown ∇logpt by a trained sθ(x,t). Start from the prior p1≈N(0,I), which is accurate for VP since α12=4.3×10−5. Integrate either equation from t=1 down to a small tmin.
The next cell does this with the exact score of the 1D mixture 0.2N(−2,0.25)+0.8N(2,0.25) and the VP SDE with β(t)=0.1+19.9t. It runs Euler-Maruyama on the reverse SDE and Euler on the ODE, at three step counts, from the same starting noise. It reports the fraction of samples in the right mode, their mean, and their variance. The data have weight 0.8, mean 1.2, and variance 0.25+4−1.44=2.81.
Python · cell 7
With 500 and with 50 steps both samplers reproduce the data within sampling error. With 4,000 samples the weight has a standard error of about 0.006. The variance is mostly the spread between the two modes, 0.25+16w(1−w), so a weight off by 0.006 moves it by about 0.06. The SDE's variance of 2.97 at 50 steps follows mostly from its slightly low weight of 0.789, since 0.25+16×0.789×0.211=2.91.
With 10 steps both samplers are off, and the way they are off is telling. The reverse SDE's variance jumps to about 3.3, too wide. Each big Euler-Maruyama step injects noise that the coarse drift does not fully remove. The ODE's error is smaller. Errors at few steps are the subject of §33.4.
The trajectories show what the intuition box described. The ODE paths are smooth curves that pick their mode early and then glide in, and each one is a fixed function of its starting noise. The SDE paths wander, and some change their minds late. The histograms at the end agree.
The widget runs the same experiment with 5 to 200 steps, from the same noise draws for both samplers. The left panel shows a dozen paths of each sampler over the shaded true density, and the right panel shows the histogram of all samples against the dashed true density. Press Sample to watch the paths grow and the histograms follow the truth.
Do the reverse SDE and the probability flow ODE really give the same distribution, and how do their paths differ?
Data 0.2 N(−2, 0.25) + 0.8 N(2, 0.25), exact score. Both samplers start from the same 3,000 noise draws.
Loading interactive figure…
Two more connections tie this back to the earlier sections. First, DDPM's sampler of §33.2 draws each step from the learned reverse Gaussian, which is called ancestral sampling. It is itself a discretization of the reverse VP SDE (Song et al. 2021, Appendix E). The 1,000 steps of DDPM were solving this equation all along.
Second, ODE paths cannot cross, because two solutions of the same smooth ODE that meet at one time coincide at all times. So the probability flow ODE defines a one-to-one map between noise and data, and it gives every image a unique noise code. It gives exact likelihoods through the change of variables for ODEs (§33.6). And an adaptive ODE solver picks its own step sizes. With one, Song et al. (2021) cut the number of network calls by over 90 percent without visible loss of quality.
In an interview, expect to write all three equations, the forward SDE, the reverse SDE with the full g2, and the ODE with half of it, and to say why the last two share marginals.
Predictor-corrector samplers
We have a score at every time, so we can mix the two ideas. A predictor-corrector (PC) sampler alternates two moves. The predictor takes one step of a solver for the reverse SDE or the ODE, from t to t−Δt. The corrector then runs a few Langevin steps at the new time. These pull the samples back toward pt−Δt without changing t. Annealed Langevin is the special case with no predictor, and DDPM's sampler is the special case with no corrector.
Song et al. (2021) choose the corrector's step as η=2(r∥z∥/∥sθ∥)2. Here r is a target ratio of the uphill move to the injected noise, which they call a "signal-to-noise ratio" (0.16 for their VE models on CIFAR-10). This sets the balance between moving and diffusing directly, as the rule η∝σ2 did.
Core
The reverse SDE and the probability flow ODE
Implement two samplers for the VP SDE dx=−21β(t)xdt+β(t)dwt, given an exact score.
reverse_sde_sample(score_fn, beta_fn, x_T, ts, rng) runs Euler-Maruyama on the reverse SDE.
pf_ode_sample(score_fn, beta_fn, x_T, ts) runs Euler on the probability flow ODE.
The inputs are as follows. x_T has shape (n,d), ts is a decreasing grid from 1 to a small tmin, and score_fn(x, t) returns ∇logpt at the rows of x. Each step goes from ts[i] to ts[i+1], evaluates everything at ts[i], and uses the positive step Δt=ts[i] - ts[i+1]. Draw the SDE's noise with rng.standard_normal(x.shape). Return the samples at tmin.
The tests run both samplers with the exact score of a 2D two-component mixture and compare the sample mean and covariance with the data's.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Research
Tweedie's second-order formula, checked
Check the second-order Tweedie formula from the derivation above on a 2D Gaussian mixture, by computing the posterior covariance in two ways. Implement
exact_posterior_cov(x, weights, means, covs, alpha, sigma), the exact Cov[x0∣xt] of the mixture at each row of x, with shape (n,d,d), and
tweedie_cov(logp_fn, x, alpha, sigma, h=1e-3), the same quantity from α2σ2(I+σ2∇2logpt). Compute the Hessian by central finite differences of logp_fn, a function from (n,d) points to (n,) log densities.
The setup provides gmm_logp(x, weights, means, covs, alpha, sigma), the exact logpt. The tests compare both functions with a reference posterior covariance, so the two ways must agree.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
In PyTorch
Everything so far used an exact score. The block below learns one. It trains a score network sθ(x,σ)=Fθ(x,logσ)/σ on the two-moons dataset, two interleaved half circles of points in the plane. It uses the multi-level DSM loss with the weight w(σ)=σ2, on 12 geometric levels from 3 to 0.02. Then it samples in two ways. The first is annealed Langevin, ending with one Tweedie denoising step. The second is Euler steps of the VE probability flow ODE in the form dx/dσ=−σ∇logpσ(x) derived above.
As a quality check, the block measures each sample's distance to the nearest training point. Fresh points from the true two-moons distribution have a median distance of about 0.015, and nearly all of them lie within 0.1.
🔥 In PyTorchfor a machine with PyTorch
Both samplers put most of their samples on the moons. In our run the median distance to the data is about 0.025 for annealed Langevin and 0.017 for the ODE, and 78 and 87 percent of samples land within 0.1 of a training point. The final Tweedie step removes the leftover noise of the last level. That matters little here but a lot for images. Song et al. (2021), following an observation of Jolicoeur-Martineau et al. (2020), note that FID can be much worse without it, so they end every sampler with one denoising step.
This toy runs on a CPU in seconds. At scale the network is a U-Net or a Transformer (the denoising U-Net and DiT), and it usually predicts ϵ, v, or a preconditioned denoiser rather than a raw score, for the reasons in the table "Score, noise, and denoiser, at a glance".
Choosing a sampler, and where it breaks
sampler
randomness
steps it tolerates
strengths
weaknesses
annealed Langevin
yes
many (levels times steps per level)
corrects its own errors at each level
biased for any finite step, slow, the step needs tuning
reverse SDE, Euler-Maruyama (DDPM-like)
yes
hundreds
robust to score errors, because the noise erases mistakes
needs many steps, a fresh path for every seed
probability flow ODE, Euler or a higher-order solver
discretization error accumulates and is never corrected
predictor-corrector
yes
hundreds of predictor steps plus correctors
best quality at equal compute in the score SDE paper
twice the score evaluations per time step, another step size to tune
The weak points are the ones the rest of the chapter attacks. A score has norm about d/σ, so as σ→0 a network that outputs the score directly must produce enormous, precise vectors. Networks therefore predict ϵ or a preconditioned denoiser, and samplers stop at a small tmin instead of at 0. Stochastic samplers need hundreds of steps. The ODE needs fewer, but its error is never corrected, and how few steps it can get away with depends on how curved its paths are. §33.4 and §33.6 are about exactly that.
🎬 Diffusion and Score-Based Generative Models · MITCBMMYang Song's own lecture on score-based models, from score matching and annealed Langevin to the SDE view and the probability flow ODE.🎬 MIT 6.S184: Flow Matching and Diffusion Models - Lecture 03A - Score Functions (2026) · Peter HolderriethA careful lecture on score functions, denoising score matching, and the SDE and ODE samplers built on them, matching this section's math.🎬 Score-based Diffusion Models | Generative AI Animated · DeepiaAnimations of score fields, Langevin sampling, and noise annealing that build the picture before the equations.