Chapter 17: BWM: Tongji Boundless Team's Action Condition World Model
Detailed Explanation and Code Implementation of DDPM
This tutorial starts from the limitations of VAE, explains why DDPM changes “single-step generation” to “multi-step denoising”, and then derives from forward diffusion, reverse generation, ELBO, true posterior, and noise prediction objectives to the final training and sampling formulas.
0. Learning Path
The main process of DDPM can be compressed into five steps:
VAE generates images in one step using latent variables z, but the assumptions of compression and reconstruction often lead to detail loss.
DDPM does not immediately generate a complete image; instead, it defines a fixed noise addition process to gradually transform the real image into Gaussian noise.
During generation, it starts from Gaussian noise, and the neural network is trained to remove a little bit of noise at each step.
The derivation strictly comes from maximum likelihood and ELBO, but it can ultimately be simplified into a straightforward supervision task: predicting the noise added to the image.
The key to implementation is not the complex loss function, but properly handling the noise schedule, time steps, U-Net input/output, sampling formulas, and numerical ranges.
DDPM first gradually adds noise to the image until it is close to pure noise, and then trains the model to gradually remove the noise in reverse. It breaks the difficult “one-step image generation” process into many simple “predict and remove noise” tasks.
The most common training objective for DDPM is:
Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2]
Among them:
xt=αˉtx0+1−αˉtϵ,ϵ∼N(0,I)
In other words, during training, we know the true noise ϵ, and the network only needs to predict it based on the noisy image xt and the time step t.
1. Why DDPM is needed
1.1 Several Issues with VAE
VAE is important, but it also has several common issues.
First, VAE typically compresses images into a low-dimensional latent variable:
x→z→x^
If the capacity of z is insufficient, details will be lost, and the generated image will be blurry.
Second, the reconstruction terms of VAE often correspond to pixel-level likelihoods. Taking Gaussian likelihood as an example, maximizing logpθ(x∣z) is often equivalent to minimizing mean squared error. Mean squared error encourages the model to output a “average answer,” and an average image usually looks dull.
Third, VAE generates x directly from z in one step:
z→x
This requires the Decoder to generate from abstract semantics to all pixel details during a single forward propagation. For complex images, this mapping burden is quite high.
Therefore, the core contradiction of VAE can be understood as:
It wants to generate images using a structured, sampleable latent space, but this compression and one-step decoding process may lose high-frequency details.
1.2 How DDPM Changes Its Approach
DDPM no longer views generation as:
z→x
Instead, consider it as:
xT→xT−1→⋯→x1→x0
Among them:
x0 is a real and clean image;
x1,…,xT is an image with noise added gradually;
xT is close to standard Gaussian noise.
Sample first during generation:
xT∼N(0,I)
Then, for each step, only a small task is done:
Predict the noise based on the current noisy image, and remove a little bit of it.
The benefits of DDPM are:
The training objective is stable, eliminating the need for adversarial training like that of GANs.
Each task step is simple, requiring only local denoising learning.
Generation occurs gradually in the image space, without forcing all details into a very small latent variable.
U-Net can be used to preserve multi-scale spatial details.
Mathematically, it is possible to derive a simple noise prediction MSE from ELBO.
2. Basic Settings of DDPM
DDPM has two processes:
Forward diffusion process: Fixed rules, no learning required; gradually add noise to the real image.
Reverse generation process: Requires learning; gradually remove noise to generate the image.
2.1 Forward Diffusion Process
The forward process starts from the real image x0:
x0→x1→x2→⋯→xT
DDPM defines it as a Markov chain:
q(x1:T∣x0)=t=1∏Tq(xt∣xt−1)
A Markov chain means: if xt−1 is already known, then when generating xt, there is no need to directly refer to earlier states.
Each step of noise addition is defined as:
q(xt∣xt−1)=N(xt;1−βtxt−1,βtI)
Here:
βt is the noise intensity at step t;
βtI indicates that the variance of independent Gaussian noise of βt is added to each dimension;
1−βtxt−1 means retaining a portion of the image signal from the previous moment.
General definition:
αt=1−βt
So:
q(xt∣xt−1)=N(xt;αtxt−1,(1−αt)I)
2.2 Writing the sampling formula from distribution
If:
x∼N(μ,σ2I)
It can be written as:
x=μ+σϵ,ϵ∼N(0,I)
Apply this rule to the one-step forward distribution of DDPM:
xt∣xt−1∼N(αtxt−1,(1−αt)I)
Obtained:
xt=αtxt−1+1−αtϵt
Among them:
ϵt∼N(0,I)
This is the step-plus-noise formula.
2.3 Why the coefficient uses a square root
Many beginners ask: Why is it 1−βt, rather than 1−βt?
Assume that the variance of xt−1 is approximately I, and the variance of noise ϵt is also I. If:
xt=1−βtxt−1+βtϵt
Then the variance is approximately:
Var(xt)=(1−βt)I+βtI=I
In other words, after adding noise to each step, the overall scale does not suddenly increase or decrease.
If changed to:
xt=(1−βt)xt−1+βtϵt
Then the variance will become:
(1−βt)2I+βtI
It is no longer exactly equal to I. Therefore, DDPM uses a square root coefficient to maintain a stable ratio between the variance of the signal and the noise.
3. From one-step noise addition to noise addition at any time
When training DDPM, it is very slow to add noise step by step from x0 to xt every time. Fortunately, the forward process has a closed-form formula, allowing us to directly obtain xt at any time step from x0.
The final formula is:
q(xt∣x0)=N(xt;αˉtx0,(1−αˉt)I)
Among them:
αˉt=s=1∏tαs
The corresponding sampling formula is:
xt=αˉtx0+1−αˉtϵ
Among them:
ϵ∼N(0,I)
This formula is very important. It indicates that during training, a random time step t can be selected, and then the corresponding noisy image xt can be constructed in one go.
3.1 Two-step expansion to understand closed-form formulas
One-step noise addition is:
xt=αtxt−1+1−αtϵt
Write down the previous moment again:
xt−1=αt−1xt−2+1−αt−1ϵt−1
Substitute the first equation:
xt=αt(αt−1xt−2+1−αt−1ϵt−1)+1−αtϵt
Expand:
xt=αtαt−1xt−2+αt(1−αt−1)ϵt−1+1−αtϵt
The last two items are linear combinations of independent Gaussian noise, so they are still Gaussian noise. Its variance is:
αt(1−αt−1)+(1−αt)=1−αtαt−1
Therefore, it can be merged into a new standard Gaussian noise ϵ:
xt=αtαt−1xt−2+1−αtαt−1ϵ
Continuing the recursion to x0, we get:
xt=αtαt−1⋯α1x0+1−αtαt−1⋯α1ϵ
Definition:
αˉt=αtαt−1⋯α1
We get the formula at any given moment:
xt=αˉtx0+1−αˉtϵ
3.2 Why xT approaches standard Gaussian noise
Because:
αˉt=s=1∏tαs
And:
αs=1−βs<1
When t is large enough:
αˉt→0
Insert:
xt=αˉtx0+1−αˉtϵ
The first item will disappear:
αˉtx0→0
The second item will become:
1−αˉtϵ→ϵ
So:
xT≈ϵ∼N(0,I)
This explains why generation can start from pure Gaussian noise.
4. Diffusion Process in Reverse
The forward process is artificially fixed and does not require learning. What actually needs to be learned is the reverse process:
xT→xT−1→⋯→x0
The model joint distribution is written as:
pθ(x0:T)=p(xT)t=1∏Tpθ(xt−1∣xt)
Among them:
p(xT)=N(0,I)
The reverse step is usually also set as a Gaussian distribution:
pθ(xt−1∣xt)=N(xt−1;μθ(xt,t),σt2I)
Here:
μθ(xt,t) is the reverse mean predicted by the neural network;
σt2I is the reverse variance, which is not learned in many basic implementations;
The network input is the noisy image xt and the time step t;
The network output can be designed to predict the mean, predict the original image x0, or predict the noise ϵ.
Intuitively, the reverse step is done as follows:
Given the current noise map xt, guess where the slightly cleaner image xt−1 from the previous step should be located.
5. Training objective: From maximum likelihood to ELBO
As a generative model, DDPM aims to maximize the likelihood of real images:
logpθ(x0)
But:
pθ(x0)=∫pθ(x0:T)dx1:T
This integration requires integrating all intermediate variables x1,…,xT, and direct calculation is difficult.
Therefore, DDPM uses the variational lower bound. Minimize the negative ELBO during training:
It indicates that the final noise distribution should be close to standard Gaussian. When T is large enough, q(xT∣x0)≈N(0,I), this term is usually close to a constant.
Intermediate item:
DKL(q(xt−1∣xt,x0)∥pθ(xt−1∣xt))
It is the core of training. It requires the model to learn a reverse step distribution pθ(xt−1∣xt) that is close to the true posterior q(xt−1∣xt,x0).
Last item:
−logpθ(x0∣x1)
Indicates the reconstruction error of restoring x0 from x1.
So the key issue of DDPM becomes:
How to make a neural network approximate the true posterior q(xt−1∣xt,x0)?
6. Real posterior and mean matching
True a posteriori is:
q(xt−1∣xt,x0)
It indicates: If the original image x0 and the noisy current image xt are known, what distribution should the state at the previous moment xt−1 have been?
By substituting the mean-matching term corresponding to KL, a weighted noise prediction MSE is obtained:
Lt−1=E[2σt2αt(1−αˉt)βt2∥ϵ−ϵθ(xt,t)∥2]+C
In DDPM practice, simplified objectives are often used, and complex weights are removed:
Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2]
Among them:
xt=αˉtx0+1−αˉtϵ
This is the most common training objective for DDPM.
8.2 Training Process
One training iteration can be understood as:
Sample real images from the dataset:
x0∼qdata(x0)
Random sampling time steps:
t∼Uniform({1,…,T})
Sampling Gaussian noise:
ϵ∼N(0,I)
Construct noisy images:
xt=αˉtx0+1−αˉtϵ
Introduce network prediction noise:
ϵθ(xt,t)
Minimize noise prediction error:
∥ϵ−ϵθ(xt,t)∥2
8.3 Sampling Process
After training is completed, start from pure noise:
xT∼N(0,I)
Then perform it step by step from t=T to t=1:
xt−1=μθ(xt,t)+σtz
Among them:
z∼N(0,I)
Insert the mean value in the noise prediction form:
xt−1=αt1(xt−1−αˉtβtϵθ(xt,t))+σtz
Usually, no random noise is added in the last step t=1, that is:
z=0
This can prevent the final output from being further disturbed.
9. DDPM Code Implementation
This section explains how DDPM is implemented in PyTorch from the STL10 code. The complete code is available at:
code/train_stl10_ddpm.py
code/generate_stl10_ddpm.py
The dataset uses STL10. split="train" is used in the code, and the image is adjusted to 96×96 by default. The dataset will be automatically downloaded during the first run. torch and torchvision must be installed in advance.
This implementation uses the most basic DDPM configuration: a linear beta schedule, noise-prediction objective, MSE loss, and a small U-Net noise predictor. Its goal is not to generate the strongest diffusion model in one go, but to first achieve a closed loop where noise is added during training, the model predicts noise, and noise is gradually removed during sampling.
9.1 Dataset and Image Scope
STL10 is a RGB image, so the model input shape is:
x0∈R3×96×96
In the training script, first resize the images to the specified size, and then normalize the pixels to [−1,1]:
Here, the Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)) will convert the original [0,1] pixels into [−1,1]:
xnorm=2x−1
The reason for this is that DDPM gradually brings the image closer to standard Gaussian noise. When the image range approaches [−1,1], the data scale and noise scale become more consistent. When saving the generated image, the result is mapped back to [0,1]:
The DDPM network cannot only consider the noisy image xt, but also must know the current time step t. The same image has a completely different noise level in t=50 and t=900. Without time information, the network doesn’t know how much noise to remove.
In the code, sine time embedding is used to convert integer time steps into continuous vectors:
In this way, each time step has a condition vector available for the network to use.
9.3 How the residual block injects time information
Each residual block in U-Net receives two inputs: image features x and temporal features time_emb. The temporal features are first projected through a linear layer to the current number of channels, and then added to the convolutional feature map:
During forward propagation, the up-sampling stage combines the deep features with the features saved during the down-sampling stage:
pythonx1 = self.down1(self.in_conv(x), time_emb)
x2 = self.down2(self.pool(x1), time_emb)
x3 = self.down3(self.pool(x2), time_emb)
x4 = self.down4(self.pool(x3), time_emb)
h = self.mid(x4, time_emb)
h = F.interpolate(h, scale_factor=2, mode="nearest")
h = self.up3(torch.cat([h, x3], dim=1), time_emb)
h = F.interpolate(h, scale_factor=2, mode="nearest")
h = self.up2(torch.cat([h, x2], dim=1), time_emb)
h = F.interpolate(h, scale_factor=2, mode="nearest")
h = self.up1(torch.cat([h, x1], dim=1), time_emb)
returnself.out(h)
The “skip connection” here is very important. What DDPM needs to do is predict pixel-by-pixel noise, which requires both global semantics and local texture positions. The downsampling path expands the field of view, while the upsampling path restores resolution; the skip connection sends back the shallow details.
9.5 Diffusion Tool Classes
The Diffusion class is responsible for two things:
Precompute the coefficients required for forward diffusion and reverse sampling;
Provide two core functions: q_sample and sample.
Define the linear beta schedule during initialization:
Since these quantities are only related to time steps, they should be calculated once during initialization, rather than being recalculated for each batch.
_extract is used to extract the time-step coefficient for the current batch from the coefficient table with a length of timesteps:
For example, if a batch contains 32 images, and each image is randomly assigned to a different time step, _extract can retrieve 32 corresponding αˉt, which are then reshaped into [B,1,1,1] to facilitate multiplication with the image tensor.
9.6 How to Construct xt During Training
Training DDPM does not require adding noise step by step from x0 to xt, but instead uses a closed-form formula directly:
xt=αˉtx0+1−αˉtϵ
The q_sample in the code is exactly implementing this formula:
Here, the noise is the training supervision signal ϵ. Since the noise is sampled by us, the training labels are naturally known and do not require manual annotation.
9.7 Training Objectives
One training iteration can be written in five steps:
Extract the real image from the dataset x0;
Randomly sample time steps t;
Randomly add noise ϵ;
Obtain the result using a closed-form formula xt;
Let the model predict ϵ based on (xt,t).
The core code in the training script is:
pythonfor x0, _ in train_loader:
x0 = x0.to(device)
t = torch.randint(0, args.timesteps, (x0.size(0),), device=device)
noise = torch.randn_like(x0)
xt = diffusion.q_sample(x0, t, noise)
loss = F.mse_loss(model(xt, t), noise)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
This corresponds to the final simplification goal:
Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2]
Note that the model does not directly predict clean images x0, but predicts the added noise ϵ. After predicting the noise, the sampling phase can construct the reverse mean step by step using the DDPM formula.
9.8 Output of the training script
train_stl10_ddpm.py will be saved:
checkpoints/stl10_ddpm.pt: Model weights and necessary configurations;
outputs/stl10_sample_epoch_XXX.png: STL10-style images sampled from noise during training.
These configurations will be reused in the generated script. Especially for image_size, since STL10 defaults to using 96×96, the generation phase must be consistent with the training phase.
The running results are as follows. It can be seen that there was a lot of noise during the initial training. By the 200th iteration, some outlines could be reconstructed, but the performance was still average. To help everyone understand better, the tutorial did not include many improvement techniques, such as attention mechanisms or class conditional generation. After completing this course, you can continue to learn more~
9.9 Sampling Generation Process
After training ends, the generation phase starts with pure Gaussian noise:
xT∼N(0,I)
Then, perform a reverse loop according to the time step:
xT→xT−1→⋯→x0
The sample function in the code implements this process:
python@torch.no_grad()defsample(self, model: nn.Module, shape: tuple[int, int, int, int]) -> torch.Tensor:
model.eval()
x = torch.randn(shape, device=self.device)
for step inreversed(range(self.timesteps)):
t = torch.full((shape[0],), step, device=self.device, dtype=torch.long)
pred_noise = model(x, t)
beta_t = self._extract(self.betas, t, x)
alpha_t = self._extract(self.alphas, t, x)
alpha_bar_t = self._extract(self.alpha_bars, t, x)
mean = (1.0 / torch.sqrt(alpha_t)) * (
x - beta_t / torch.sqrt(1.0 - alpha_bar_t) * pred_noise
)
x = mean if step == 0else mean + torch.sqrt(beta_t) * torch.randn_like(x)
return x.clamp(-1, 1)
The reverse mean is:
μθ(xt,t)=αt1(xt−1−αˉtβtϵθ(xt,t))
This line in the code:
pythonx = mean if step == 0else mean + torch.sqrt(beta_t) * torch.randn_like(x)
Indicates that no additional random noise is added in the last step. Otherwise, the generated x0 will be affected by the last noise perturbation.
9.10 Test Generation Script
generate_stl10_ddpm.py only generates content and no longer reads training data. Its process is as follows:
Load checkpoints/stl10_ddpm.pt;
Rebuild UNet96 based on base and image_size in the checkpoint;
Only one sheet is generated by default. 96×96 Images are helpful to first check whether the model can actually generate visible structures from noise. If you want to see multiple images, you can increase the scale. --num-samples。
The execution result is as follows:
10. Common Questions
10.1 Is the forward process of DDPM learned?
No. Forward process:
q(xt∣xt−1)=N(xt;αtxt−1,βtI)
It is a manually defined noise addition rule. What needs to be learned is the reverse process:
pθ(xt−1∣xt)
10.2 Why No Step-by-Step Noise Addition During Training
Because there is a closed formula:
xt=αˉtx0+1−αˉtϵ
It can directly obtain x0 to get xt at any time step.
10.3 Why the goal is to predict noise, not the image
Since the noise during training ϵ is sampled by us, it naturally serves as a supervised label. After predicting the noise, x0 can be restored using a formula or the reverse mean can be constructed:
μθ(xt,t)=αt1(xt−1−αˉtβtϵθ(xt,t))
Therefore, the prediction noise is not weak; it is equivalent to learning the reverse denoising step.
10.4 What is the biggest difference between DDPM and VAE?
VAE is:
z∼N(0,I),x∼pθ(x∣z)
That is, generating an image in one step from latent variables.
DDPM is:
xT∼N(0,I)xT→xT−1→⋯→x0
That is, starting from noise, multi-step denoising is performed to generate an image.
10.5 What are the disadvantages of DDPM?
The main drawback is the slow sampling process. It usually requires a step-by-step reverse calculation from T to 1, and the basic version may require hundreds to thousands of steps. Later, DDIM, an acceleration sampler, or distillation methods can be used to reduce the number of sampling steps.
11. Summary
The core logic of DDPM is:
Fixed forward noise addition:
q(xt∣xt−1)=N(xt;αtxt−1,βtI)
Introduce noise at any time:
xt=αˉtx0+1−αˉtϵ
Learn reverse denoising:
pθ(xt−1∣xt)=N(xt−1;μθ(xt,t),σt2I)
Obtain the true posterior match using ELBO:
DKL(q(xt−1∣xt,x0)∥pθ(xt−1∣xt))
Convert the Gaussian KL to mean MSE, and then to noise prediction MSE:
Lsimple(θ)=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2]
Compared to VAE, DDPM does not compress all image information into a low-dimensional latent variable, but instead removes noise gradually in the image space. Its training objective comes from probability derivation, but it ultimately reduces to a very simple supervised learning problem.
12. After-class Exercises
Why is VAE prone to generating blurred images?
Why doesn’t DDPM directly generate images from latent variables in one step?
What does βt control in the forward process?
Why is a square root coefficient used in the one-step noise addition formula?
How can xt at any time step be sampled directly from x0?
Why does xT approach standard Gaussian noise?
Why is the reverse process of DDPM modeled as a Gaussian distribution?
What constraint does the intermediate KL term in ELBO decomposition impose?
Why does the true posterior q(xt−1∣xt,x0) have an analytical solution?
Why is the predicted noise ϵ equivalent to learning reverse denoising?
Why is no noise added in the last step during sampling?
What problems will occur for DDPM networks without time embedding?
Appendix A: Detailed Derivation of DDPM ELBO Decomposition
This expression indicates that the core of training DDPM is to make the model’s reverse step distribution match the true posterior derived from the forward process.
Appendix B: Detailed Derivation of the Real Posterior q(xt−1∣xt,x0)