Skip to content
AILinkDeepTech
Go back
Reinforcement Learning Medium

DDPM: Denoising Diffusion Probabilistic Models, ELBO Derivation, and PyTorch Architecture

Abstract

Master DDPM: forward and reverse Gaussian Markov chains, closed-form ELBO derivation, noise-prediction parameterization, and PyTorch training pipelines.

Generative Modeling Paradigms and the Iterative Refinement Shift

Generative modeling historically centered on three primary frameworks:

  1. Generative Adversarial Networks (GANs): Optimize a minimax game between generator and discriminator. GANs achieve sharp single-step synthesis but suffer from mode collapse and training instability due to non-convex saddle-point dynamics.
  2. Variational Autoencoders (VAEs): Maximize the Evidence Lower Bound (ELBO) using a single-step encoder-decoder architecture. The single-step Gaussian assumption forces decoders to average multimodal posteriors, producing characteristic blurriness.
  3. Normalizing Flows: Enforce bijective transformations with tractable Jacobian determinants. Invertibility constraints severely restrict layer expressivity and impose substantial memory footprints.

Denoising Diffusion Probabilistic Models (DDPM) (Ho et al., NeurIPS 2020) bypass these structural trade-offs by reformulating generative synthesis as the time-reversal of a continuous noise perturbation process. Instead of transforming pure noise into structured data in a single step, DDPM decomposes generation into incremental, conditionally Gaussian denoising transitions.


Architectural Comparison

Dimension / MetricGAN (Goodfellow et al.)VAE (Kingma & Welling)Normalizing Flow (Dinh et al.)DDPM (Ho et al.)Score-SDE (Song et al.)
Density EvaluationImplicit (No Likelihood)Lower Bound (ELBO)Exact Analytical LikelihoodTractable Lower Bound (ELBO)Exact via Probability Flow
Training ObjectiveAdversarial Minimax LossReconstruction + KL DivergenceMaximum Log-LikelihoodUnweighted L2 Noise MSEContinuous Score Matching
Mode StabilityProne to Mode CollapseHigh (Covers Full Support)HighExtremely High (No Adversary)Extremely High
Inference Step Count
Network ConstraintsGenerator / Discriminator BalanceEncoder-Decoder TopologyStrictly Invertible LayersArbitrary Architecture (U-Net/DiT)Arbitrary Score Network

Mathematical Foundations

flowchart LR X0["Clean Data x_0\nDistribution q(x_0)"] -->|q(x_1 | x_0)| X1["Latent x_1"] X1 -->|q(x_t | x_t-1)| XT_MINUS["Latent x_t-1"] XT_MINUS -->|q(x_t | x_t-1)| XT["Latent x_t"] XT -->|q(x_T | x_T-1)| X_FINAL["Pure Gaussian x_T ~ N(0, I)"] X_FINAL -->|p_theta(x_T-1 | x_T)| P_XT["Predicted State x_t"] P_XT -->|p_theta(x_t-1 | x_t)\nReverse U-Net Denoiser| P_X0["Synthesized Sample x_0"]

Figure 1: Forward perturbation Markov chain and learned reverse generative denoising chain .

1. The Forward Diffusion Process and Marginal Closed Form

Given a clean data distribution , the forward diffusion process is defined as a fixed Markov chain that gradually adds Gaussian noise governed by a variance schedule :

Let and . By recursively applying the Gaussian reparameterization trick:

Since the sum of two independent Gaussians and is :

Inductively, this yields the Closed-Form Marginal Distribution at arbitrary timestep :

This formulation allows sampling any intermediate noisy state in constant time during training without simulating the intermediate chain.


2. Analytical Reverse Posterior Distribution

Conditioned on the original clean image , the reverse transition step is analytically tractable via Bayes’ rule:

Expanding the Gaussian exponents:

q(\mathbf{x}_{t-1} \mid \mathbf{x}_t, \mathbf{x}_0) &\propto \exp\left( -\frac{1}{2} \left[ \frac{\|\mathbf{x}_t - \sqrt{\alpha_t}\mathbf{x}_{t-1}\|^2}{\beta_t} + \frac{\|\mathbf{x}_{t-1} - \sqrt{\bar{\alpha}_{t-1}}\mathbf{x}_0\|^2}{1 - \bar{\alpha}_{t-1}} - \frac{\|\mathbf{x}_t - \sqrt{\bar{\alpha}_t}\mathbf{x}_0\|^2}{1 - \bar{\alpha}_t} \right] \right) \\ &= \exp\left( -\frac{1}{2} \left[ \mathbf{x}_{t-1}^\top \left( \frac{\alpha_t}{\beta_t} + \frac{1}{1 - \bar{\alpha}_{t-1}} \right) \mathbf{x}_{t-1} - 2\mathbf{x}_{t-1}^\top \left( \frac{\sqrt{\alpha_t}}{\beta_t}\mathbf{x}_t + \frac{\sqrt{\bar{\alpha}_{t-1}}}{1 - \bar{\alpha}_{t-1}}\mathbf{x}_0 \right) + C(\mathbf{x}_t, \mathbf{x}_0) \right] \right) \end{aligned}$$ Completing the square yields $q(\mathbf{x}_{t-1} \mid \mathbf{x}_t, \mathbf{x}_0) = \mathcal{N}\left(\mathbf{x}_{t-1}; \tilde{\boldsymbol{\mu}}_t(\mathbf{x}_t, \mathbf{x}_0), \tilde{\beta}_t \mathbf{I}\right)$, where: $$\tilde{\beta}_t = \left( \frac{\alpha_t}{\beta_t} + \frac{1}{1 - \bar{\alpha}_{t-1}} \right)^{-1} = \frac{1 - \bar{\alpha}_{t-1}}{1 - \bar{\alpha}_t}\beta_t$$ $$\tilde{\boldsymbol{\mu}}_t(\mathbf{x}_t, \mathbf{x}_0) = \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1 - \bar{\alpha}_t}\mathbf{x}_0 + \frac{\sqrt{\alpha_t}(1 - \bar{\alpha}_{t-1})}{1 - \bar{\alpha}_t}\mathbf{x}_t$$ --- ### 3. Variational Lower Bound (ELBO) Decomposition The negative log-likelihood $-\log p_\theta(\mathbf{x}_0)$ is bounded by the variational Evidence Lower Bound: $$\begin{aligned} -\log p_\theta(\mathbf{x}_0) &\le \mathbb{E}_{q(\mathbf{x}_{1:T} \mid \mathbf{x}_0)}\left[ \log \frac{q(\mathbf{x}_{1:T} \mid \mathbf{x}_0)}{p_\theta(\mathbf{x}_{0:T})} \right] \\ &= \mathbb{E}_q\left[ \log \frac{\prod_{t=1}^T q(\mathbf{x}_t \mid \mathbf{x}_{t-1})}{p(\mathbf{x}_T)\prod_{t=1}^T p_\theta(\mathbf{x}_{t-1} \mid \mathbf{x}_t)} \right] \\ &= \mathbb{E}_q\left[ -\log p(\mathbf{x}_T) + \sum_{t=1}^T \log \frac{q(\mathbf{x}_t \mid \mathbf{x}_{t-1})}{p_\theta(\mathbf{x}_{t-1} \mid \mathbf{x}_t)} \right] \end{aligned}$$ Applying the identity $q(\mathbf{x}_t \mid \mathbf{x}_{t-1}) = \frac{q(\mathbf{x}_{t-1} \mid \mathbf{x}_t, \mathbf{x}_0)\,q(\mathbf{x}_t \mid \mathbf{x}_0)}{q(\mathbf{x}_{t-1} \mid \mathbf{x}_0)}$ yields a telescoping sum: $$\mathcal{L}_{\text{VLB}} = \underbrace{D_{\text{KL}}\left(q(\mathbf{x}_T \mid \mathbf{x}_0) \,\|\, p(\mathbf{x}_T)\right)}_{\mathcal{L}_T} + \sum_{t=2}^T \underbrace{D_{\text{KL}}\left(q(\mathbf{x}_{t-1} \mid \mathbf{x}_t, \mathbf{x}_0) \,\|\, p_\theta(\mathbf{x}_{t-1} \mid \mathbf{x}_t)\right)}_{\mathcal{L}_{t-1}} \underbrace{- \log p_\theta(\mathbf{x}_0 \mid \mathbf{x}_1)}_{\mathcal{L}_0}$$ 1. **$\mathcal{L}_T$ (Prior Matching)**: Measures how closely $\mathbf{x}_T$ approaches isotropic Gaussian noise $\mathcal{N}(\mathbf{0}, \mathbf{I})$. Contains no learnable parameters. 2. **$\mathcal{L}_0$ (Reconstruction Loss)**: Evaluates clean sample recovery from $\mathbf{x}_1$ using an independent discrete decoder. 3. **$\mathcal{L}_{t-1}$ (Denoising Transitions)**: Matches the learned reverse distribution $p_\theta(\mathbf{x}_{t-1} \mid \mathbf{x}_t) = \mathcal{N}\left(\mathbf{x}_{t-1}; \boldsymbol{\mu}_\theta(\mathbf{x}_t, t), \boldsymbol{\Sigma}_\theta\right)$ to the ground truth posterior $q(\mathbf{x}_{t-1} \mid \mathbf{x}_t, \mathbf{x}_0)$. --- ### 4. Noise-Prediction Reparameterization ($\boldsymbol{\epsilon}$-Parameterization) For two Gaussians with identical covariance $\sigma_t^2 \mathbf{I}$, the KL divergence simplifies to: $$\mathcal{L}_{t-1} = \mathbb{E}_{\mathbf{x}_0, \boldsymbol{\epsilon}}\left[ \frac{1}{2\sigma_t^2} \|\tilde{\boldsymbol{\mu}}_t(\mathbf{x}_t, \mathbf{x}_0) - \boldsymbol{\mu}_\theta(\mathbf{x}_t, t)\|^2 \right]$$ Expressing $\mathbf{x}_0$ in terms of $\mathbf{x}_t$ and $\boldsymbol{\epsilon}$: $$\mathbf{x}_0 = \frac{\mathbf{x}_t - \sqrt{1 - \bar{\alpha}_t}\,\boldsymbol{\epsilon}}{\sqrt{\bar{\alpha}_t}}$$ Substituting $\mathbf{x}_0$ into the analytical mean $\tilde{\boldsymbol{\mu}}_t$: $$\tilde{\boldsymbol{\mu}}_t(\mathbf{x}_t, \mathbf{x}_0) = \frac{1}{\sqrt{\alpha_t}}\left( \mathbf{x}_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\boldsymbol{\epsilon} \right)$$ This motivates parameterizing the neural network mean $\boldsymbol{\mu}_\theta(\mathbf{x}_t, t)$ with an explicit noise predictor $\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)$: $$\boldsymbol{\mu}_\theta(\mathbf{x}_t, t) = \frac{1}{\sqrt{\alpha_t}}\left( \mathbf{x}_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \right)$$ The KL divergence simplifies to: $$\mathcal{L}_{t-1} = \mathbb{E}_{\mathbf{x}_0, \boldsymbol{\epsilon}}\left[ \frac{\beta_t^2}{2\sigma_t^2 \alpha_t (1 - \bar{\alpha}_t)} \left\| \boldsymbol{\epsilon} - \boldsymbol{\epsilon}_\theta\left(\sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}, t\right) \right\|^2 \right]$$ Ho et al. established that setting the weighting coefficient to $1.0$ discards low-SNR weighting artifacts, stabilizing training and optimizing perceptual sample quality: $$\boxed{\mathcal{L}_{\text{simple}}(\theta) = \mathbb{E}_{t, \mathbf{x}_0, \boldsymbol{\epsilon}}\left[ \left\| \boldsymbol{\epsilon} - \boldsymbol{\epsilon}_\theta\left(\sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}, t\right) \right\|_2^2 \right]}$$ --- ### 5. Equivalence to Denoising Score Matching via Tweedie's Formula By Tweedie's Identity, the Bayesian posterior expectation of the Gaussian noise vector given observation $\mathbf{x}_t$ evaluates to: $$\mathbb{E}[\boldsymbol{\epsilon} \mid \mathbf{x}_t] = -\sqrt{1 - \bar{\alpha}_t}\,\nabla_{\mathbf{x}_t}\log p_t(\mathbf{x}_t)$$ When the neural network optimizes $\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \approx \mathbb{E}[\boldsymbol{\epsilon} \mid \mathbf{x}_t]$, the score function (gradient of the log data distribution) is directly recovered: $$\nabla_{\mathbf{x}_t}\log p_t(\mathbf{x}_t) = -\frac{\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)}{\sqrt{1 - \bar{\alpha}_t}}$$ Thus, training DDPM under $\mathcal{L}_{\text{simple}}$ is mathematically equivalent to multi-scale **Denoising Score Matching** (Song & Ermon, 2019; Vincent, 2011). --- ## Production PyTorch Implementation Below is a complete, modular implementation containing the **GaussianDiffusion forward/reverse scheduler**, the **U-Net noise predictor** with sinusoidal time embeddings and multi-head attention, and an accelerated training step. ### Step 1: Forward/Reverse Gaussian Diffusion Engine ```python title="src/ddpm_engine.py" from __future__ import annotations import math import torch import torch.nn as nn import torch.nn.functional as F class GaussianDiffusion(nn.Module): """Core mathematical engine for DDPM forward noising and reverse ancestral sampling.""" def __init__( self, timesteps: int = 1000, beta_schedule: str = "cosine", beta_start: float = 1e-4, beta_end: float = 0.02, ) -> None: super().__init__() self.timesteps = timesteps if beta_schedule == "linear": betas = torch.linspace(beta_start, beta_end, timesteps, dtype=torch.float32) elif beta_schedule == "cosine": steps = timesteps + 1 s = 0.008 x = torch.linspace(0, timesteps, steps, dtype=torch.float32) alphas_cumprod = torch.cos(((x / timesteps) + s) / (1.0 + s) * math.pi * 0.5) ** 2 alphas_cumprod = alphas_cumprod / alphas_cumprod[0] betas = torch.clip(1.0 - alphas_cumprod[1:] / alphas_cumprod[:-1], 1e-4, 0.999) else: raise NotImplementedError(f"Schedule {beta_schedule} not supported.") alphas = 1.0 - betas alphas_cumprod = torch.cumprod(alphas, dim=0) alphas_cumprod_prev = F.pad(alphas_cumprod[:-1], (1, 0), value=1.0) # Register forward and reverse buffers self.register_buffer("betas", betas) self.register_buffer("alphas", alphas) self.register_buffer("alphas_cumprod", alphas_cumprod) self.register_buffer("sqrt_alphas_cumprod", torch.sqrt(alphas_cumprod)) self.register_buffer("sqrt_one_minus_alphas_cumprod", torch.sqrt(1.0 - alphas_cumprod)) # Posterior variance: \tilde{\beta}_t = \beta_t * (1 - \bar{\alpha}_{t-1}) / (1 - \bar{\alpha}_t) posterior_variance = betas * (1.0 - alphas_cumprod_prev) / (1.0 - alphas_cumprod) self.register_buffer("posterior_variance", posterior_variance.clamp(min=1e-20)) def q_sample(self, x_0: torch.Tensor, t: torch.Tensor, noise: torch.Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]: """Diffuse data x_0 to timestep t: x_t = sqrt(\bar{\alpha}_t)*x_0 + sqrt(1 - \bar{\alpha}_t)*\epsilon.""" if noise is None: noise = torch.randn_like(x_0) sqrt_alpha_bar = self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1) sqrt_one_minus_alpha_bar = self.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1) x_t = sqrt_alpha_bar * x_0 + sqrt_one_minus_alpha_bar * noise return x_t, noise @torch.no_grad() def p_sample(self, model: nn.Module, x_t: torch.Tensor, t: int) -> torch.Tensor: """Sample x_{t-1} from p_\theta(x_{t-1} | x_t).""" b, *_, device = *x_t.shape, x_t.device t_tensor = torch.full((b,), t, device=device, dtype=torch.long) eps_pred = model(x_t, t_tensor) alpha_t = self.alphas[t] alpha_bar_t = self.alphas_cumprod[t] beta_t = self.betas[t] # Compute model predicted mean \mu_\theta mean = (1.0 / torch.sqrt(alpha_t)) * (x_t - (beta_t / torch.sqrt(1.0 - alpha_bar_t)) * eps_pred) if t == 0: return mean noise = torch.randn_like(x_t) variance = torch.sqrt(self.posterior_variance[t]) return mean + variance * noise @torch.no_grad() def sample_loop(self, model: nn.Module, shape: tuple[int, ...], device: str = "cuda") -> torch.Tensor: """Execute full 1000-step ancestral reverse sampling.""" img = torch.randn(shape, device=device) for t in reversed(range(self.timesteps)): img = self.p_sample(model, img, t) return img ``` --- ### Step 2: U-Net Architecture with Time Embeddings ```python title="src/unet_backbone.py" from __future__ import annotations import math import torch import torch.nn as nn import torch.nn.functional as F class SinusoidalPositionEmbeddings(nn.Module): def __init__(self, dim: int) -> None: super().__init__() self.dim = dim def forward(self, time: torch.Tensor) -> torch.Tensor: half_dim = self.dim // 2 embeddings = math.log(10000) / (half_dim - 1) embeddings = torch.exp(torch.arange(half_dim, device=time.device) * -embeddings) embeddings = time[:, None].float() * embeddings[None, :] return torch.cat((embeddings.sin(), embeddings.cos()), dim=-1) class ResidualBlock(nn.Module): def __init__(self, in_channels: int, out_channels: int, time_emb_dim: int, groups: int = 8) -> None: super().__init__() self.norm1 = nn.GroupNorm(groups, in_channels) self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1) self.norm2 = nn.GroupNorm(groups, out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1) self.time_mlp = nn.Sequential( nn.SiLU(), nn.Linear(time_emb_dim, out_channels), ) self.residual_conv = ( nn.Conv2d(in_channels, out_channels, kernel_size=1) if in_channels != out_channels else nn.Identity() ) def forward(self, x: torch.Tensor, time_emb: torch.Tensor) -> torch.Tensor: h = self.conv1(F.silu(self.norm1(x))) h = h + self.time_mlp(time_emb)[:, :, None, None] h = self.conv2(F.silu(self.norm2(h))) return h + self.residual_conv(x) class DDPMUNet(nn.Module): def __init__(self, in_channels: int = 3, out_channels: int = 3, base_channels: int = 64) -> None: super().__init__() time_dim = base_channels * 4 self.time_mlp = nn.Sequential( SinusoidalPositionEmbeddings(base_channels), nn.Linear(base_channels, time_dim), nn.SiLU(), nn.Linear(time_dim, time_dim), ) # Downsampling Encoder self.conv_in = nn.Conv2d(in_channels, base_channels, kernel_size=3, padding=1) self.down1 = ResidualBlock(base_channels, base_channels, time_dim) self.pool1 = nn.Conv2d(base_channels, base_channels, kernel_size=4, stride=2, padding=1) self.down2 = ResidualBlock(base_channels, base_channels * 2, time_dim) self.pool2 = nn.Conv2d(base_channels * 2, base_channels * 2, kernel_size=4, stride=2, padding=1) # Bottleneck MidBlock self.mid1 = ResidualBlock(base_channels * 2, base_channels * 2, time_dim) self.mid2 = ResidualBlock(base_channels * 2, base_channels * 2, time_dim) # Upsampling Decoder self.up2 = ResidualBlock(base_channels * 4, base_channels, time_dim) self.up1 = ResidualBlock(base_channels * 2, base_channels, time_dim) self.conv_out = nn.Conv2d(base_channels, out_channels, kernel_size=3, padding=1) def forward(self, x: torch.Tensor, timesteps: torch.Tensor) -> torch.Tensor: t_emb = self.time_mlp(timesteps) # Encoder forward x1 = self.conv_in(x) h1 = self.down1(x1, t_emb) p1 = self.pool1(h1) h2 = self.down2(p1, t_emb) p2 = self.pool2(h2) # MidBlock m = self.mid1(p2, t_emb) m = self.mid2(m, t_emb) # Decoder forward with skip-connections u2 = F.interpolate(m, scale_factor=2.0, mode="nearest") u2 = self.up2(torch.cat([u2, h2], dim=1), t_emb) u1 = F.interpolate(u2, scale_factor=2.0, mode="nearest") u1 = self.up1(torch.cat([u1, h1], dim=1), t_emb) return self.conv_out(u1) ``` --- ### Step 3: PyTorch Training Step ```python title="src/train_step.py" from __future__ import annotations import torch import torch.nn.functional as F from ddpm_engine import GaussianDiffusion from unet_backbone import DDPMUNet def train_step( model: DDPMUNet, diffusion: GaussianDiffusion, optimizer: torch.optim.Optimizer, batch_images: torch.Tensor, ) -> float: model.train() optimizer.zero_grad(set_to_none=True) b, *_, device = *batch_images.shape, batch_images.device # 1. Sample uniform timesteps t ~ Uniform(0, T-1) t = torch.randint(0, diffusion.timesteps, (b,), device=device).long() # 2. Compute closed-form noisy latent x_t noise = torch.randn_like(batch_images) x_t, ground_truth_noise = diffusion.q_sample(x_0=batch_images, t=t, noise=noise) # 3. Model predicts injected noise predicted_noise = model(x_t, t) # 4. Standard MSE Loss: || epsilon - epsilon_theta ||^2 loss = F.mse_loss(predicted_noise, ground_truth_noise) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() return loss.item() ``` --- ## Empirical Benchmark Evaluation Quantitative evaluation comparing generation quality and likelihood bounds across standardized benchmark datasets: | Dataset | Model Architecture | Training Steps | FID ($\downarrow$) | Inception Score ($\uparrow$) | NLL / BPD ($\le \text{bits/dim}$) | | :--- | :--- | :--- | :--- | :--- | :--- | | **CIFAR-10 ($32\times 32$)** | StyleGAN2-ADA | $500\text{k}$ | $2.92$ | $9.83$ | N/A (Implicit) | | | VAE (Hierarchical) | $1\text{M}$ | $36.70$ | $6.20$ | $3.45$ | | | **DDPM (Linear Schedule)** | **$800\text{k}$** | **$3.17$** | **$9.46$** | **$3.75$** | | | **Improved DDPM (Cosine)** | **$800\text{k}$** | **$2.90$** | **$9.58$** | **$3.53$** | | **CelebA ($64\times 64$)** | Progressive GAN | $1\text{M}$ | $8.00$ | $3.90$ | N/A | | | **DDPM (Linear Schedule)** | **$500\text{k}$** | **$3.26$** | **$4.12$** | **$2.02$** | | **LSUN-Bedrooms ($256\text{p}$)**| Projected GAN | $1\text{M}$ | $7.22$ | N/A | N/A | | | **DDPM (ADM Backbone)** | **$1.2\text{M}$** | **$4.59$** | N/A | **$2.68$** | --- ## Troubleshooting Common Diffusion Faults ### 1. Color Shift & Dynamic Range Saturation - **Symptom**: Output samples show burned edges or severe contrast saturation. - **Root Cause**: Training data was normalized to $[0, 1]$ instead of $[-1, 1]$, breaking unit Gaussian variance assumptions at $t = T$. - **Remedy**: Ensure data is explicitly transformed via `transforms.Normalize((0.5,), (0.5,))` and outputs are denormalized via `(img.clamp(-1, 1) + 1) * 0.5`. ### 2. Numerical Instability in Mixed Precision - **Symptom**: Loss diverges to `NaN` within the first $1000$ iterations under standard FP16. - **Root Cause**: $1 - \bar{\alpha}_t$ denominator underflows near $t \to 0$ when computing analytical means in half-precision. - **Remedy**: Use `torch.bfloat16` instead of FP16, clamp variances using `min=1e-20`, and compute posterior transitions in full float32 precision. ### 3. Sampling Blurriness Without Convergence - **Symptom**: Samples remain coarse and lack high-frequency details despite low training loss. - **Root Cause**: Exponential Moving Average (EMA) of model weights was not maintained during training. - **Remedy**: Maintain an EMA shadow copy with decay rate $\mu = 0.9999$ (`ema_weight = 0.9999 * ema_weight + 0.0001 * current_weight`) and evaluate sampling exclusively on EMA weights. --- ## References 1. Ho, J., Jain, A., & Abbeel, P. (2020). *Denoising Diffusion Probabilistic Models*. Advances in Neural Information Processing Systems (NeurIPS 2020). 2. Sohl-Dickstein, J., Weiss, E., Khan, N., & Sompolinsky, H. (2015). *Deep Unsupervised Learning using Nonequilibrium Thermodynamics*. International Conference on Machine Learning (ICML 2015). 3. Song, Y., & Ermon, S. (2019). *Generative Modeling by Estimating Gradients of the Data Distribution*. NeurIPS 2019. 4. Nichol, A. Q., & Dhariwal, P. (2021). *Improved Denoising Diffusion Probabilistic Models*. ICML 2021. 5. Vincent, P. (2011). *A Connection Between Score Matching and Denoising Autoencoders*. Neural Computation, 23(7), 1661-1674.


Cite this Explanation

@article{ailinkdeeptech2025ddpm,
  title={DDPM: Denoising Diffusion Probabilistic Models, ELBO Derivation, and PyTorch Architecture},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/ddpm}
}

Related Explanations