The diffusion noising process: from data to noise
Read these first if they are new to you
- Generative models: making new examples Intermediate
- The bell curve (normal distribution) Basic
The idea in one paragraph
A diffusion model learns to turn noise into examples by learning to undo a fixed noising process. Each step of that process shrinks the example a little and adds a little bell-curve noise. The noise of many steps adds up to one bell-curve draw, so the noisy version at any step can be made in one jump. The noise schedule, a list of step sizes, sets how fast the example fades. After the last step, the result looks almost like a draw from the standard bell curve. To generate, a trained network starts from such a draw and runs the steps backward.
Step t runs from 0, the clean example x0, to T, the noisiest step, as in the DDPM paper. The flow-matching unit runs time the other way. Here α and β belong to the noise schedule; the time-conditioning unit uses them for a gate and a shift. Each formula acts on one number; an image gets the same formula for every pixel, each with its own noise draw.
Each step shrinks the example a little and adds a little noise
A diffusion model is a generative model that learns from the opposite direction: a fixed process that turns examples into noise. This is the noising process, also called the forward process. This page follows the DDPM paper (Ho, Jain and Abbeel, 2020), which uses T = 1000 steps. Step t makes xt from xt−1:
xt = √(1 − βt) × xt−1 + √βt × εt
Here εt (epsilon) is a fresh draw from the standard normal, the bell curve with mean 0 and standard deviation 1. βt (beta), between 0 and 1, sets the step's size, and the list β1, …, βT is the noise schedule. So the step shrinks the value by the factor √(1 − βt), then adds bell-curve noise with standard deviation √βt.
The shrink keeps the variance at 1
The reason uses the variance, the square of the standard deviation, and two standard rules of probability:
- Scaling. Multiplying a random value by c multiplies its variance by c².
- Adding. Two random values are independent when knowing one tells you nothing about the other. A sum of independent values has the sum of their variances. If both are normal, so is the sum.
Each noise draw is fresh, so it is independent of the value it joins. If xt−1 has variance 1, the rules give xt the variance
(1 − βt) × 1 + βt × 1 = 1.
Without the shrink, the variance would grow by βt at every step, and the example would stay at full size. Song and others (2021) call this form variance preserving. The variance stays exactly 1 only for data with variance 1; other data move toward it.
The noise of many steps adds up to one draw, so any step is one jump away
Put the formula for x1 into the formula for x2:
x2 = √(1 − β2) √(1 − β1) x0 + [ √(1 − β2) √β1 ε1 + √β2 ε2 ]
The part in brackets is a sum of two independent normal draws with mean 0. By the rules, it is one normal draw with variance (1 − β2) β1 + β2, which multiplies out to 1 − (1 − β1)(1 − β2). The same argument works for any number of steps. Write αt = 1 − βt (alpha), and let ᾱt (alpha bar) be the product α1 × α2 × … × αt. Then
xt = √ᾱt × x0 + √(1 − ᾱt) × ε,
where ε is one standard normal draw. √ᾱt is the signal scale, the factor left on the example. √(1 − ᾱt) is the standard deviation of the noise. Every αt is below 1, so ᾱt falls at every step, and the example fades.
Take one pixel, x0 = 0.8 (DDPM scales pixels to the range −1 to 1). Use β1 = β2 = 0.36, far larger than real steps, so that √(1 − 0.36) = 0.8 and √0.36 = 0.6. Let the draws be ε1 = −0.5 and ε2 = 1.0.
- Step 1: x1 = 0.8 × 0.8 + 0.6 × (−0.5) = 0.64 − 0.30 = 0.34.
- Step 2: x2 = 0.8 × 0.34 + 0.6 × 1.0 = 0.272 + 0.600 = 0.872.
- For the jump, ᾱ2 = 0.64 × 0.64 = 0.4096, so √ᾱ2 = 0.64. The signal part is 0.64 × 0.8 = 0.512.
- The bracket is 0.8 × 0.6 × ε1 + 0.6 × ε2 = 0.48 × (−0.5) + 0.6 × 1.0 = 0.36. And 0.512 + 0.36 = 0.872, as the two steps gave.
- The bracket's variance is 0.48² + 0.6² = 0.2304 + 0.36 = 0.5904. That is 1 − ᾱ2, as the jump formula says.
Your turn. With the same β values, start from x0 = −0.5, with the draws ε1 = 1.0 and ε2 = −1.0. Find x2 both ways.
Answer
Steps: x1 = −0.4 + 0.6 = 0.2, and x2 = 0.16 − 0.6 = −0.44. Jump: 0.64 × (−0.5) + (0.48 × 1.0 + 0.6 × (−1.0)) = −0.32 − 0.12 = −0.44.
With one fresh draw ε, the jump has the same distribution as t single steps. It says nothing about the steps in between; a whole path needs the single steps.
The jump makes training cheap. DDPM picks a random step t from 1 to T, makes xt in one jump, and trains a network to predict ε from xt and t. The network needs t to know how noisy its input is (see time conditioning).
noising.py: 500 single steps against one jump, with the DDPM schedule
import numpy as np
beta = np.linspace(0.0001, 0.02, 1000) # DDPM: beta_1 ... beta_1000
alpha_bar = np.cumprod(1 - beta) # step t is at index t - 1
a = alpha_bar[499] # step 500
rng = np.random.default_rng(0)
x = np.full(100_000, 0.8) # 100,000 copies of x0
for t in range(1, 501): # t = 1, 2, ..., 500
x = np.sqrt(1 - beta[t - 1]) * x + np.sqrt(beta[t - 1]) * rng.standard_normal(100_000)
jump = np.sqrt(a) * 0.8 + np.sqrt(1 - a) * rng.standard_normal(100_000)
print("500 steps: mean", round(x.mean(), 3), "std", round(x.std(), 3))
print("one jump: mean", round(jump.mean(), 3), "std", round(jump.std(), 3))
print("formula: mean", round(np.sqrt(a) * 0.8, 3), "std", round(np.sqrt(1 - a), 3))
500 steps: mean 0.227 std 0.964 one jump: mean 0.226 std 0.954 formula: mean 0.224 std 0.96
np.linspace(a, b, n) gives n evenly spaced numbers from a to
b, and np.cumprod gives running products. Both ways agree with the formula to within
0.01. The small differences come from using a finite number of draws.
The schedule sets how fast the example fades
The DDPM schedule is linear: βt rises in a straight line from 0.0001 to 0.02 over 1000 steps. Each βt is small, but ᾱT, a product of 1000 numbers below 1, is about 0.00004. So √ᾱT ≈ 0.0064: less than 1% of the example's scale is left.
Nichol and Dhariwal (2021) found that, for images of 64 × 64 and 32 × 32 pixels, the linear schedule destroys the example faster than needed. Their cosine schedule builds ᾱt from a cosine curve. It falls slowly at the start and the end, and almost in a straight line in between.
After the last step, every example has become almost pure noise
With the linear schedule, xT ≈ 0.0064 × x0 + 1.0 × ε: nearly a standard normal draw, whatever the example was. So generation can start from fresh noise.
Standard deviation of the dots' x coordinates: .
Dots inside the dashed circle: of 400. Pure noise would put about 47 there.
What you should find
No. With the linear schedule, noise fills much of the empty middle by step 200, and by step 500 the dashed circle holds 44 dots, about what pure noise gives. The cosine schedule's step 500 looks like the linear step 260. The standard deviation stays between 1.00 and 1.03.
None of the common schedules that Lin and others (2023) examined makes ᾱT exactly 0. They give √ᾱT ≈ 0.068 for the schedule of the image model Stable Diffusion. Pure noise lacks that leftover signal, which carries each image's overall brightness. They found that this mismatch kept the model to images of medium brightness.
It is DDPM's form of noising, with either schedule. Some diffusion models skip the shrink, so the variance grows ("variance exploding" in Song and others, 2021), and some noise in continuous time. DDPM fixes the βt, though it notes that they could be learned.
Check yourself
-
Why does each step multiply the value by √(1 − βt) before it adds the noise?
Answer
It keeps the variance at 1: (1 − βt) × 1 + βt = 1. Without it, the linear schedule's β values, which add up to 10.05, would raise the variance to 11.05, and the full example would remain in the last step.
-
One image has every pixel at −1; another has every pixel at +1. After the last step of the linear schedule, how far apart are the means of a noisy pixel from each? Compare that with the noise's standard deviation.
Answer
The means are √ᾱT × (±1) ≈ ±0.0064, about 0.013 apart. The noise's standard deviation is about 1.0, almost 80 times as large, so one noisy pixel barely tells the images apart.
Go further
-
Derives the jump by merging two bell curves at a time, as here, but numbers the draws εt−1.
-
The Annotated Diffusion Model: Defining the forward diffusion process Niels Rogge and Kashif Rasul, Hugging Face Article Advanced
PyTorch code for four schedules and the jump, applied to a photo of cats, with 300 steps.
-
Section 3.2 introduces the cosine schedule; Figure 3 shows images noised under both.
Where this shows up
No unit or explanation builds on this one yet.