Generative Paradigms and Non-Equilibrium Diffusion
Generative modeling addresses the fundamental task of modeling a high-dimensional probability distribution given empirical samples. Prior frameworks exhibit distinct trade-offs:
- Generative Adversarial Networks (GANs): Train a minimax game . While achieving fast single-step inference, adversarial optimization suffers from mode collapse, gradient vanishing, and non-convergent limit cycles.
- Variational Autoencoders (VAEs): Maximize the Evidence Lower Bound (ELBO) using a single-step encoder and decoder . Single-step decoders average multimodal distributions, causing visual blurriness.
- Normalizing Flows: Learn bijective mappings via exact change-of-variables . Enforcing tractable Jacobian determinants severely restricts model expressivity.
Diffusion Probabilistic Models (Sohl-Dickstein et al., ICML 2015; Ho et al., NeurIPS 2020) resolve these limitations by casting synthesis as the time-reversal of a continuous Gaussian noise perturbation process. By decomposing the generative task across discrete timesteps (or continuous time ), the model learns a sequence of tractable, locally Gaussian denoising transitions.
Architectural Comparison
| Dimension / Metric | GAN (Goodfellow et al.) | VAE (Kingma & Welling) | Normalizing Flow (Dinh et al.) | Autoregressive (PixelCNN) | Diffusion Models (DDPM/Score-SDE) |
|---|---|---|---|---|---|
| Density Estimation | Implicit (No Density) | Tractable Lower Bound (ELBO) | Exact Analytical Likelihood | Exact via Chain Rule | Tractable Bound / Exact ODE Likelihood |
| Training Stability | Unstable (Minimax Game) | Stable (ELBO Optimization) | Stable (Max-Likelihood) | Stable (Cross-Entropy) | Extremely Stable (L2 Noise Regression) |
| Mode Coverage | Susceptible to Mode Collapse | Full Support Coverage | Full Support Coverage | Full Support Coverage | Complete Mode Coverage (No Collapse) |
| Sampling Latency | Single Step | Single Step | Single Step | Sequential Pixels | Iterative Steps ( |
| Architectural Design | Discriminator-Coupled | Encoder-Decoder Bottleneck | Strictly Invertible Layers | Causal Masked Convolutions | Unconstrained (U-Net / Transformer DiT) |
Mathematical Foundations
Figure 1: Forward perturbation Markov chain and learned reverse generative denoising chain .
1. Forward Gaussian Perturbation Chain
Given data , the forward diffusion process is defined as a fixed Gaussian Markov chain parameterized by a variance schedule :
Let and . Evaluating recursive Gaussian convolutions yields the Closed-Form Marginal Distribution at any timestep :
As , , and the distribution asymptotically converges to standard isotropic Gaussian noise .
2. Reverse Process and Posterior Derivation
The reverse trajectory parameters Gaussian transitions:
When conditioned on the clean data , the true reverse step is analytically solvable via Bayes’ expansion:
where:
3. Variational Lower Bound (ELBO) and Objective
Optimizing negative log-likelihood yields the variational bound:
Expressing clean data allows parameterizing the neural network mean via a noise predictor :
Substituting into the KL divergence term produces the Simplified Noise-Prediction Loss:
where . Discarding the analytical weighting factor places higher emphasis on intermediate perceptual scales, substantially improving sample fidelity.
4. Continuous-Time SDEs and Score Matching Duality
In continuous time (), the forward perturbation converges to an Itô Stochastic Differential Equation (SDE):
where is standard Brownian motion. For Variance Preserving (VP) diffusion (DDPM equivalent):
By Anderson’s Theorem (1982) and Song et al. (ICLR 2021), the Reverse-Time SDE is:
where is reverse Brownian motion and is the Stein Score Function. By Tweedie’s Identity, the noise prediction directly parameterizes the score:
Associated with every SDE is a deterministic Probability-Flow ODE sharing identical marginal probability densities :
Discretizing this deterministic ODE yields modern accelerated samplers: DDIM, DPM-Solver++, and EDM Heun solvers.
5. Classifier-Free Guidance (CFG)
To direct conditional generation without a separate classifier, Classifier-Free Guidance (Ho & Salimans, 2022) randomly replaces conditioning tokens with an empty token during training (). At inference, the guided noise prediction extrapolates away from the unconditional estimate:
where is the guidance scale, trading off sample diversity for sharp alignment with conditioning constraints.
Production PyTorch Implementation
Below is a complete, modular PyTorch implementation of the Gaussian Diffusion engine, U-Net with FiLM Time-Modulation & Spatial Self-Attention, and accelerated DDIM deterministic sampling.
Step 1: Forward/Reverse Gaussian Diffusion Engine
from __future__ import annotations
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class GaussianDiffusionEngine(nn.Module):
"""Core mathematical engine for Gaussian forward perturbation and reverse sampling."""
def __init__(self, timesteps: int = 1000, beta_schedule: str = "cosine") -> None:
super().__init__()
self.timesteps = timesteps
if beta_schedule == "linear":
betas = torch.linspace(1e-4, 0.02, 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 implemented.")
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 buffers for device auto-transfer
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))
# Analytical posterior variance \tilde{\beta}_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 ddim_sample(
self,
model: nn.Module,
shape: tuple[int, ...],
num_inference_steps: int = 50,
eta: float = 0.0,
device: str = "cuda",
) -> torch.Tensor:
"""Deterministic ODE / DDIM reverse sampling over subsampled timesteps."""
step_ratio = self.timesteps // num_inference_steps
timesteps = (torch.arange(0, num_inference_steps) * step_ratio).round().flip(0).long().to(device)
x = torch.randn(shape, device=device)
for i, t in enumerate(timesteps):
t_prev = timesteps[i + 1] if i + 1 < len(timesteps) else -1
t_tensor = torch.full((shape[0],), t, device=device, dtype=torch.long)
eps_pred = model(x, t_tensor)
alpha_bar_t = self.alphas_cumprod[t]
alpha_bar_t_prev = self.alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0, device=device)
# Predict original sample x_0
x_0_pred = (x - torch.sqrt(1.0 - alpha_bar_t) * eps_pred) / torch.sqrt(alpha_bar_t)
# Variance calculation
sigma_t = eta * torch.sqrt((1.0 - alpha_bar_t_prev) / (1.0 - alpha_bar_t) * (1.0 - alpha_bar_t / alpha_bar_t_prev))
dir_xt = torch.sqrt((1.0 - alpha_bar_t_prev - sigma_t**2).clamp(min=0.0)) * eps_pred
x = torch.sqrt(alpha_bar_t_prev) * x_0_pred + dir_xt
if eta > 0.0:
x = x + sigma_t * torch.randn_like(x)
return x
Step 2: U-Net Backbone with FiLM Conditioning & Self-Attention
from __future__ import annotations
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class SinusoidalPositionalEmbedding(nn.Module):
def __init__(self, dim: int) -> None:
super().__init__()
self.dim = dim
def forward(self, t: torch.Tensor) -> torch.Tensor:
half_dim = self.dim // 2
embeddings = math.log(10000) / (half_dim - 1)
embeddings = torch.exp(torch.arange(half_dim, device=t.device) * -embeddings)
embeddings = t[:, None].float() * embeddings[None, :]
return torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
class ResBlockFiLM(nn.Module):
"""Residual Block with Feature-wise Linear Modulation (FiLM)."""
def __init__(self, in_ch: int, out_ch: int, time_dim: int, groups: int = 8) -> None:
super().__init__()
self.norm1 = nn.GroupNorm(groups, in_ch)
self.conv1 = nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1)
self.norm2 = nn.GroupNorm(groups, out_ch)
self.conv2 = nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1)
self.time_proj = nn.Linear(time_dim, out_ch * 2) # Outputs Scale & Shift
self.shortcut = nn.Conv2d(in_ch, out_ch, kernel_size=1) if in_ch != out_ch else nn.Identity()
def forward(self, x: torch.Tensor, t_emb: torch.Tensor) -> torch.Tensor:
h = self.conv1(F.silu(self.norm1(x)))
# FiLM Scale & Shift
scale, shift = self.time_proj(F.silu(t_emb)).chunk(2, dim=-1)
h = h * (1.0 + scale[:, :, None, None]) + shift[:, :, None, None]
h = self.conv2(F.silu(self.norm2(h)))
return h + self.shortcut(x)
class SpatialSelfAttention(nn.Module):
def __init__(self, channels: int, num_heads: int = 4) -> None:
super().__init__()
self.norm = nn.GroupNorm(8, channels)
self.qkv = nn.Conv2d(channels, channels * 3, kernel_size=1)
self.proj = nn.Conv2d(channels, channels, kernel_size=1)
self.num_heads = num_heads
def forward(self, x: torch.Tensor) -> torch.Tensor:
b, c, h, w = x.shape
norm_x = self.norm(x)
qkv = self.qkv(norm_x).reshape(b, 3, self.num_heads, c // self.num_heads, h * w)
q, k, v = qkv[:, 0], qkv[:, 1], qkv[:, 2] # [B, num_heads, head_dim, H*W]
# Scaled dot-product attention
q = q.transpose(-1, -2) # [B, num_heads, H*W, head_dim]
k = k.transpose(-1, -2)
v = v.transpose(-1, -2)
attn = F.scaled_dot_product_attention(q, k, v)
attn = attn.transpose(-1, -2).reshape(b, c, h, w)
return x + self.proj(attn)
class DiffusionUNet(nn.Module):
def __init__(self, in_channels: int = 3, out_channels: int = 3, base_dim: int = 64) -> None:
super().__init__()
time_dim = base_dim * 4
self.time_embed = nn.Sequential(
SinusoidalPositionalEmbedding(base_dim),
nn.Linear(base_dim, time_dim),
nn.SiLU(),
nn.Linear(time_dim, time_dim),
)
# Encoder
self.conv_in = nn.Conv2d(in_channels, base_dim, kernel_size=3, padding=1)
self.down1 = ResBlockFiLM(base_dim, base_dim, time_dim)
self.pool1 = nn.Conv2d(base_dim, base_dim, kernel_size=4, stride=2, padding=1)
self.down2 = ResBlockFiLM(base_dim, base_dim * 2, time_dim)
self.pool2 = nn.Conv2d(base_dim * 2, base_dim * 2, kernel_size=4, stride=2, padding=1)
# Bottleneck with Self-Attention
self.mid1 = ResBlockFiLM(base_dim * 2, base_dim * 2, time_dim)
self.mid_attn = SpatialSelfAttention(base_dim * 2)
self.mid2 = ResBlockFiLM(base_dim * 2, base_dim * 2, time_dim)
# Decoder with Skip Connections
self.up2 = ResBlockFiLM(base_dim * 4, base_dim, time_dim)
self.up1 = ResBlockFiLM(base_dim * 2, base_dim, time_dim)
self.conv_out = nn.Conv2d(base_dim, out_channels, kernel_size=3, padding=1)
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
t_emb = self.time_embed(t)
x_in = self.conv_in(x)
d1 = self.down1(x_in, t_emb)
p1 = self.pool1(d1)
d2 = self.down2(p1, t_emb)
p2 = self.pool2(d2)
m = self.mid2(self.mid_attn(self.mid1(p2, t_emb)), t_emb)
u2 = F.interpolate(m, scale_factor=2.0, mode="nearest")
u2 = self.up2(torch.cat([u2, d2], dim=1), t_emb)
u1 = F.interpolate(u2, scale_factor=2.0, mode="nearest")
u1 = self.up1(torch.cat([u1, d1], dim=1), t_emb)
return self.conv_out(u1)
Step 3: PyTorch Diffusion Training Step
from __future__ import annotations
import torch
import torch.nn.functional as F
from diffusion_engine import GaussianDiffusionEngine
from unet_architecture import DiffusionUNet
def train_step(
model: DiffusionUNet,
diffusion: GaussianDiffusionEngine,
optimizer: torch.optim.Optimizer,
images: torch.Tensor,
) -> float:
model.train()
optimizer.zero_grad(set_to_none=True)
b, *_, device = *images.shape, images.device
# 1. Sample uniform timesteps t
t = torch.randint(0, diffusion.timesteps, (b,), device=device).long()
# 2. Add Gaussian noise via forward marginal q(x_t | x_0)
noise = torch.randn_like(images)
x_t, ground_truth_noise = diffusion.q_sample(x_0=images, t=t, noise=noise)
# 3. Model predicts the injected noise
predicted_noise = model(x_t, t)
# 4. Compute L2 Loss || epsilon - epsilon_theta ||^2
loss = F.mse_loss(predicted_noise, ground_truth_noise)
loss.backward()
# Gradient clipping prevents divergence
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 / Benchmark | Model Topology | Training Steps | FID () | Inception Score () | Negative Log-Likelihood () |
|---|---|---|---|---|---|
| CIFAR-10 () | StyleGAN2-ADA | N/A (Implicit) | |||
| VAE (Hierarchical) | |||||
| DDPM (Linear Schedule) | |||||
| Improved DDPM (Cosine) | |||||
| ImageNet | BigGAN-deep | N/A | |||
| ADM (U-Net Baseline) | N/A | ||||
| ADM-G (+ Classifier Guidance) | |||||
| DiT-XL/2 (+ CFG) | |||||
| FFHQ () | StyleGAN3 | N/A | N/A | ||
| EDM (Heun 2nd-Order) | N/A | N/A |
Troubleshooting Common Diffusion Faults
1. Contrast Burn and Boundary Saturation
- Symptom: Generated images exhibit harsh high-contrast lines, color banding, or dark edges.
- Root Cause: Training data was normalized to instead of , breaking zero-mean isotropic Gaussian assumptions at .
- Remedy: Normalize datasets via
transforms.Normalize((0.5,), (0.5,))and clamp sampled outputs usingtorch.clamp(sample, -1.0, 1.0).
2. Early Loss Plateau with Structural Blurring
- Symptom: Loss plateaus at high values within the first steps; outputs remain blurred.
- Root Cause: Timestep embedding projection weights disconnected or missing sinusoidal frequency mappings.
- Remedy: Verify sinusoidal positional encodings are injected into every residual block via FiLM modulation, and evaluate model sampling strictly using an Exponential Moving Average (EMA) shadow copy ().
3. Sampling Divergence Under High CFG Scales ()
- Symptom: Samples generated with classifier-free guidance become oversaturated or degenerate into abstract high-frequency patterns.
- Root Cause: Unconditional and conditional vector variance disparity under aggressive linear extrapolation.
- Remedy: Apply CFG Rescaling (Lin et al., 2024): rescale guided noise standard deviation to match conditional vector norm, or reduce guidance scale to .
References
- Ho, J., Jain, A., & Abbeel, P. (2020). Denoising Diffusion Probabilistic Models. Advances in Neural Information Processing Systems (NeurIPS 2020).
- Sohl-Dickstein, J., Weiss, E., Khan, N., & Sompolinsky, H. (2015). Deep Unsupervised Learning using Nonequilibrium Thermodynamics. ICML 2015.
- Song, Y., Sohl-Dickstein, J., Kingma, D. P., Kumar, A., Ermon, S., & Poole, B. (2021). Score-Based Generative Modeling through Stochastic Differential Equations. ICLR 2021.
- Song, J., Meng, C., & Ermon, S. (2021). Denoising Diffusion Implicit Models. ICLR 2021.
- Ho, J., & Salimans, T. (2022). Classifier-Free Diffusion Guidance. NeurIPS Workshop.
- Karras, T., Aittala, M., Aila, T., & Laine, S. (2022). Elucidating the Design Space of Diffusion-Based Generative Models. NeurIPS 2022.