Skip to content
AILinkDeepTech
Go back
Generative Models Advanced

Denoising Diffusion Probabilistic Model (DDPM) Implementation in PyTorch

Abstract

A PyTorch implementation of a Denoising Diffusion Probabilistic Model (DDPM): a UNet denoiser with sinusoidal-free linear time conditioning, a linear-beta forward diffusion, an iterative reverse sampling loop, and a smoke test that verifies noise addition, model output, and sampling shapes.

Denoising Diffusion Probabilistic Model (DDPM) Implementation in PyTorch

This implementation builds a minimal Denoising Diffusion Probabilistic Model (DDPM) from scratch. It includes a UNet denoiser with linear time conditioning, a linear-beta forward diffusion utility, an iterative reverse sampling loop, and a test script that verifies noise addition, model output, and sampling shapes.

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from tqdm import tqdm

class UNet(nn.Module):
    """
    A UNet for the denoising model.
    """
    def __init__(self, input_channels=1, hidden_dims=64):
        super().__init__()

        self.init_conv = nn.Conv2d(input_channels, hidden_dims, 3, padding=1)

        # Downsampling 
        self.down1 = nn.Conv2d(hidden_dims, hidden_dims*2, 4, stride=2, padding=1)
        self.down2 = nn.Conv2d(hidden_dims*2, hidden_dims*4, 4, stride=2, padding=1)

        # Time embedding
        self.time_mlp = nn.Sequential(
            nn.Linear(1, hidden_dims*4),
            nn.ReLU(),
            nn.Linear(hidden_dims*4, hidden_dims*4)
        )

        # Upsampling 
        self.up1 = nn.ConvTranspose2d(hidden_dims*4, hidden_dims*2, 4, stride=2, padding=1)
        self.up2 = nn.ConvTranspose2d(hidden_dims*2, hidden_dims, 4, stride=2, padding=1)

        self.final_conv = nn.Conv2d(hidden_dims, input_channels, 3, padding=1)

    def forward(self, x, t):
        x = self.init_conv(x)
        x1 = F.relu(x)
        
        x2 = F.relu(self.down1(x1))
        x3 = F.relu(self.down2(x2))

        t = t.float().unsqueeze(-1)
        t = self.time_mlp(t)
        t = t.view(-1, t.shape[1], 1, 1).expand(-1, -1, x3.shape[2], x3.shape[3])
        x3 = x3 + t

        x = F.relu(self.up1(x3))
        x = F.relu(self.up2(x))
        
        return self.final_conv(x)
    
class DDPM:
    def __init__(self, n_steps=1000, beta_start=1e-4, beta_end=0.02):
        """   
        Args:
            n_steps: Number of diffusion steps
            beta_start: Starting value for noise schedule
            beta_end: Ending value for noise schedule
        """
        self.n_steps = n_steps
        self.beta_start = beta_start
        self.beta_end = beta_end

        # noise schedule
        self.betas = torch.linspace(beta_start, beta_end, n_steps)
        self.alphas = 1 - self.betas
        self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)

        # Pre-calculate values for inference
        self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod)
        self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1 - self.alphas_cumprod)

    def add_noise(self, x_0, t):
        noise = torch.randn_like(x_0)
        sqrt_alpha_cumprod = self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)
        sqrt_one_minus_alpha_cumprod = self.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1)
        
        noisy_image = sqrt_alpha_cumprod * x_0 + sqrt_one_minus_alpha_cumprod * noise
        return noisy_image, noise
    
    def sample(self, model, n_samples, img_size, device):
        model.eval()
        with torch.no_grad():
            x = torch.randn(n_samples, 1, img_size, img_size).to(device)

            # denoise
            for t in tqdm(reversed(range(self.n_steps)), desc='Sampling'):
                t_batch = torch.ones(n_samples, dtype=torch.long).to(device) * t
                predicted_noise = model(x, t_batch)
                
                alpha = self.alphas[t]
                alpha_cumprod = self.alphas_cumprod[t]
                beta = self.betas[t]
                
                if t > 0:
                    noise = torch.randn_like(x)
                else:
                    noise = torch.zeros_like(x)
                    
                x = (1 / torch.sqrt(alpha)) * (
                    x - ((1 - alpha) / torch.sqrt(1 - alpha_cumprod)) * predicted_noise
                ) + torch.sqrt(beta) * noise

        model.train()
        return x
    
def test_ddpm():
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = UNet().to(device)
    ddpm = DDPM(n_steps=1000)

    print("Testing forward noise addition")
    x_0 = torch.randn(4, 1, 32, 32).to(device)  
    t = torch.randint(0, 1000, (4,)).to(device)  
    noisy_images, noise = ddpm.add_noise(x_0, t)
    assert noisy_images.shape == x_0.shape, f"Shape mismatch: {noisy_images.shape} vs {x_0.shape}"
    print("✓ Forward noise addition test passed")

    print("\nTTesting model forward pass")
    predicted_noise = model(noisy_images, t)
    assert predicted_noise.shape == noise.shape, f"Shape mismatch: {predicted_noise.shape} vs {noise.shape}"
    print("✓ Model forward pass test passed")

    print("\nTesting sampling process")
    samples = ddpm.sample(model, n_samples=2, img_size=32, device=device)
    assert samples.shape == (2, 1, 32, 32), f"Wrong sample shape: {samples.shape}"
    assert not torch.isnan(samples).any(), "Samples contain NaN values"
    print("✓ Sampling test passed")

    print("\nAll tests passed successfully!")

if __name__ == "__main__":
    test_ddpm()


Cite this Explanation

@article{ailinkdeeptech2025ddpmalgo,
  title={Denoising Diffusion Probabilistic Model (DDPM) Implementation in PyTorch},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/ddpm_algo}
}

Related Explanations