Denoising Diffusion Models (Literally)
— #Diffusion#Probabilistic
Hey everyone! This Saturday, I will be discussing what Denoising Diffusion Probabilistic Models. This blog will cover the math that goges behind how a diffusion model works, how we prove it to be a generative model, and the loss function on how it learns to generate images.
These models are the foundation of generative audio, images, videos, and world models.
There might be couple of mathematical errors. Just wanted to preface with that lol.
Basics
Diffusion models are modesl that noise the image such that it fully becomes Gaussian Noise at time , and then we have a probabilistic model that attempts to reconstruct the image be gradually denoising the model.
Then, we have a neural network that steps backward from to by learning and predicting how to undo the last noise.
Forward Process
We define the forward process as this. Q is the function that is noising the model.
This isn't important for right now, but we can simplify this to
where
Essentially, what this is saying is that at each time step , we add some Gaussian noise to the previous state with a variance of , while also scaling down the previous state by to maintain a stable signal-to-noise ratio. This process gradually converts our original image into pure Gaussian noise.
If we accumulate this across all timesteps, it is just taking the product across all timesteps
where and .
Thus, we get that:
Now, given a noising schedule , we are able to calculate the noised image in one pass using the above equation, without having to iterate through all intermediate steps. This is a key efficiency gain for the forward process.
Reverse Process
Now to learn how to denoise this "image", we have our neural network that we call which learns to predict the mean of the Gaussian distribution for the previous timestep
where is predicted by our neural network. But how do we find this mean?
First, let's rearrange our forward process equation to solve for :
Because our forward process is Gaussian, the posterior is also Gaussian:
with parameters:
(Ho et al. derive this closed form expression for the posterior mean)
Instead of directly predicting , we train our network to predict the noise . Substituting into :
Replacing with our predicted gives us our model mean:
Finally, to sample :
where
Or more explicitly:
People often set to and is just another random noise vector sampled from a standard normal distribution, independent from the original noise used in the forward process. This additional noise term helps maintain stochasticity in the reverse process.
Now, we directly predict this noise vector and since we know the true used to form , we can train our model by minimizing the mean squared error:
Evidence Lower Bound (ELBO) and Loss Function Derivation
Now, we are going to prove ELBO which guarantees that we approximating the true data distribution and gives us a good way to choose .
KL Divergence
A quick discussion about KL divergence. This tells us the number of extra bits requried to encode samples from Q to P. We always want to minimize this divergence between our model distribution and the true data distribution. Mathematically, KL divergence is defined as:
Deriving log p_θ(x_0)
Let's derive the log likelihood of our model. We can write:
Then, Jensen's Equality says that for any concave function (like ) and random variable :
Applying this to our equation:
This is called the Evidence Lower BOund (ELBO) because it provides a lower bound on the evidence (log likelihood) of our model. In other words, it gives us a guaranteed minimum value for , which represents how well our model can explain the observed data (the evidence).
Now, we know the following from the diffusion process:
where:
- is the prior noise distribution
- is our learned reverse process
- is the forward diffusion process we defined earlier
- Obviously, when we take the log of this, we get summation instead of product
Substituting these into our ELBO equation:
We want to maximize this ELBO expression as it lower bounds our model's log likelihood. Maximizing this bound helps our model better explain the observed data by improving how well it fits the true data distribution. In practice, we typically minimize the negative ELBO since optimization algorithms are designed to minimize loss functions.
Now, for KL simplification we use Bayes's rule to rewrite :
Substituting this back into our ELBO equation:
This can be rearranged as:
- The first term is the log probability of the final noisy sample. Since , this equals where n is the dimensionality.
- The second term gives us a sum of KL divergences comparing our learned reverse process to the true posterior:
- The third term telescopes (cancels out) to:
Now, we get the final equation that we are trying to minimize:
This is our final loss function that we minimize during training. Let's break down what each term represents:
- : The log probability of the final noise (ignoring constants)
- : KL divergence between true and predicted reverse processes
- : Log likelihood of initial data point
- : Log likelihood of final noise given initial data
The only -dependent part that we need to optimize under is the KL term. Each KL term in the summation becomes a quadratic. We won't go into too much depth regarding proving this. However, we get that we are trying to minimize the MSE error as shown previously.
where is the original noise we sampled and is our model's prediction of that noise. This simple MSE objective is what we actually minimize during training, rather than working with the full ELBO loss directly.
Psuedocode
GPT has generated this psueodcode. It may not be correct.
Training
# Pseudocode for DDPM Training
# 1. Hyperparameters
T = 1000 # total diffusion steps
beta_start, beta_end = 1e-4, 0.02
num_epochs = 50
batch_size = 64
learning_rate = 1e-4
# 2. Precompute noise schedule
# β_t linearly spaced from beta_start to beta_end
betas = linspace(beta_start, beta_end, T) # shape: (T,)
alphas = 1.0 - betas # α_t = 1 - β_t
alpha_bars = cumprod(alphas) # \barα_t = ∏_{s=1}^t α_s
# 3. Model setup
θ = initialize_model_parameters() # parameters of ε_θ(x, t)
optimizer = Adam(θ, lr=learning_rate)
# 4. Training loop
for epoch in range(num_epochs):
for x_0 in DataLoader(dataset, batch_size): # x_0: clean images, shape (B, C, H, W)
# a) Sample random timesteps t for each image in the batch
t = randint(1, T, size=B) # t ∈ {1,…,T}
# b) Draw fresh Gaussian noise ε ∼ N(0, I)
ε = randn_like(x_0) # same shape as x_0
# c) Compute noisy images x_t using closed‐form forward process
# x_t = √(ᾱ_t) * x_0 + √(1 − ᾱ_t) * ε
sqrt_alpha_bar = sqrt(alpha_bars[t]) # shape: (B,)
sqrt_one_minus_alpha_bar = sqrt(1 - alpha_bars[t])
x_t = sqrt_alpha_bar * x_0 + sqrt_one_minus_alpha_bar * ε
# d) Predict noise with the model
# ε_θ(x_t, t) — network takes noisy image and timestep embedding
ε_pred = model(x_t, t)
# e) Compute the mean‐squared error loss
# L = ||ε − ε_pred||^2
loss = MSE(ε, ε_pred)
# f) Backpropagate and update parameters
optimizer.zero_grad()
loss.backward()
optimizer.step()
# (Optional) Log progress, sample images, etc.
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {loss.item():.4f}")
# End of training
# The network θ has learned to predict the noise added at any timestep t.
Inference/Sampling
# Pseudocode for DDPM Sampling (Image Generation)
T = 1000 # total diffusion steps
betas = linspace(beta_start, beta_end, T)
alphas = 1.0 - betas
alpha_bars = cumprod(alphas)
# Often precompute:
# sqrt_recip_alphas = sqrt(1/alphas)
# sqrt_one_minus_alpha_bars = sqrt(1 - alpha_bars)
# 1. Start from pure noise
x_T = randn(shape=(C, H, W)) # sample from N(0, I)
# 2. Reverse loop: from t=T down to 1
for t in reversed(range(1, T+1)):
# a) Predict ε at this timestep
ε_pred = model(x_T, t) # ε_θ(x_t, t)
# b) Compute the “denoised” mean μ_θ(x_t, t)
mu = (1.0 / sqrt(alphas[t])) * (
x_T
- (betas[t] / sqrt(1 - alpha_bars[t])) * ε_pred
)
# c) Compute σ_t (often = sqrt(betas[t]) or a clipped version of it)
sigma_t = sqrt(betas[t])
# d) Sample noise for stochasticity, except at the last step
if t > 1:
z = randn_like(x_T)
else:
z = 0
# e) Step to x_{t-1}
x_T = mu + sigma_t * z
# 3. x_0 is now your generated image
generated_image = x_T