Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

diffusion_code.py

40_diffusion_models/diffusion_code.py  ·  315 lines  ·  view on GitHub

"""
Diffusion Models: Complete Implementation
Simple, interview-writable code for continuous diffusion
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import Optional, Tuple

# ==================== VARIANCE SCHEDULE ====================

def linear_beta_schedule(timesteps: int, beta_start: float = 0.0001, 
                         beta_end: float = 0.02) -> torch.Tensor:
    """
    Linear variance schedule
    
    Args:
        timesteps: Number of diffusion steps
        beta_start: Starting noise level
        beta_end: Ending noise level
    Returns:
        Beta schedule: (timesteps,)
    """
    return torch.linspace(beta_start, beta_end, timesteps)


def cosine_beta_schedule(timesteps: int, s: float = 0.008) -> torch.Tensor:
    """
    Cosine variance schedule (often works better)
    
    Args:
        timesteps: Number of diffusion steps
        s: Small offset to prevent β_t from being too small
    Returns:
        Beta schedule: (timesteps,)
    """
    steps = timesteps + 1
    x = torch.linspace(0, timesteps, steps)
    alphas_cumprod = torch.cos(((x / timesteps) + s) / (1 + s) * np.pi * 0.5) ** 2
    alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
    betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
    return torch.clip(betas, 0.0001, 0.9999)


# ==================== FORWARD DIFFUSION ====================

def q_sample(x_start: torch.Tensor, t: torch.Tensor, sqrt_alphas_cumprod: torch.Tensor,
             sqrt_one_minus_alphas_cumprod: torch.Tensor, noise: Optional[torch.Tensor] = None):
    """
    Forward diffusion: Add noise to data
    
    q(x_t | x_0) = N(x_t; √(ᾱ_t)x_0, (1-ᾱ_t)I)
    
    Args:
        x_start: Clean data, shape (batch_size, ...)
        t: Timesteps, shape (batch_size,)
        sqrt_alphas_cumprod: √(ᾱ_t) for each t
        sqrt_one_minus_alphas_cumprod: √(1-ᾱ_t) for each t
        noise: Optional noise (for reproducibility)
    Returns:
        Noisy data x_t, shape (batch_size, ...)
    """
    if noise is None:
        noise = torch.randn_like(x_start)
    
    # Get values for each sample in batch
    sqrt_alphas_cumprod_t = sqrt_alphas_cumprod[t].reshape(-1, *([1] * (x_start.ndim - 1)))
    sqrt_one_minus_alphas_cumprod_t = sqrt_one_minus_alphas_cumprod[t].reshape(
        -1, *([1] * (x_start.ndim - 1))
    )
    
    # Sample: x_t = √(ᾱ_t)x_0 + √(1-ᾱ_t)ε
    return sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise


# ==================== NOISE PREDICTION MODEL ====================

class TimeEmbedding(nn.Module):
    """
    Sinusoidal time embedding
    
    Encodes timestep t into a vector for conditioning
    """
    def __init__(self, dim: int):
        super().__init__()
        self.dim = dim
    
    def forward(self, time: torch.Tensor) -> torch.Tensor:
        """
        Args:
            time: Timesteps, shape (batch_size,)
        Returns:
            Time embeddings, shape (batch_size, dim)
        """
        device = time.device
        half_dim = self.dim // 2
        emb = np.log(10000) / (half_dim - 1)
        emb = torch.exp(torch.arange(half_dim, device=device) * -emb)
        emb = time[:, None] * emb[None, :]
        emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
        return emb


class SimpleDiffusionModel(nn.Module):
    """
    Simple diffusion model for continuous data
    
    Predicts noise ε given noisy data x_t and timestep t
    """
    def __init__(self, input_dim: int, hidden_dim: int = 128, time_dim: int = 32):
        super().__init__()
        self.time_embedding = TimeEmbedding(time_dim)
        
        # Simple MLP
        self.input_proj = nn.Linear(input_dim, hidden_dim)
        self.time_proj = nn.Linear(time_dim, hidden_dim)
        self.layers = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, input_dim)  # Predict noise (same dim as input)
        )
    
    def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
        """
        Predict noise ε given noisy data x_t and timestep t
        
        Args:
            x: Noisy data, shape (batch_size, input_dim)
            t: Timesteps, shape (batch_size,)
        Returns:
            Predicted noise, shape (batch_size, input_dim)
        """
        # Time embedding
        t_emb = self.time_embedding(t)
        
        # Project inputs
        x_emb = self.input_proj(x)
        t_emb_proj = self.time_proj(t_emb)
        
        # Combine
        h = x_emb + t_emb_proj
        h = self.layers(h)
        
        return h


# ==================== TRAINING ====================

def train_diffusion_model(model: nn.Module, dataloader, num_epochs: int = 10,
                         timesteps: int = 1000, device: str = 'cpu'):
    """
    Train diffusion model
    
    Loss: L = E[||ε - ε_θ(x_t, t)||²]
    """
    # Setup variance schedule
    betas = linear_beta_schedule(timesteps)
    alphas = 1.0 - betas
    alphas_cumprod = torch.cumprod(alphas, dim=0)
    sqrt_alphas_cumprod = torch.sqrt(alphas_cumprod)
    sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - alphas_cumprod)
    
    # Move to device
    sqrt_alphas_cumprod = sqrt_alphas_cumprod.to(device)
    sqrt_one_minus_alphas_cumprod = sqrt_one_minus_alphas_cumprod.to(device)
    
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
    model.train()
    
    for epoch in range(num_epochs):
        total_loss = 0
        
        for batch_idx, x_0 in enumerate(dataloader):
            x_0 = x_0.to(device)
            batch_size = x_0.size(0)
            
            # Sample random timesteps
            t = torch.randint(0, timesteps, (batch_size,), device=device)
            
            # Sample noise
            noise = torch.randn_like(x_0)
            
            # Forward diffusion: create noisy data
            x_t = q_sample(x_0, t, sqrt_alphas_cumprod, sqrt_one_minus_alphas_cumprod, noise)
            
            # Predict noise
            noise_pred = model(x_t, t)
            
            # Loss: MSE between predicted and actual noise
            loss = F.mse_loss(noise_pred, noise)
            
            # Backward
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
        
        avg_loss = total_loss / len(dataloader)
        print(f"Epoch {epoch}, Loss: {avg_loss:.4f}")


# ==================== SAMPLING/GENERATION ====================

@torch.no_grad()
def p_sample(model: nn.Module, x: torch.Tensor, t: int, sqrt_recip_alphas: torch.Tensor,
             sqrt_one_minus_alphas_cumprod: torch.Tensor, betas: torch.Tensor,
             posterior_variance: torch.Tensor) -> torch.Tensor:
    """
    Single reverse diffusion step
    
    p(x_{t-1} | x_t) = N(x_{t-1}; μ_θ(x_t, t), Σ_t)
    
    Args:
        x: Noisy data at step t, shape (batch_size, ...)
        t: Current timestep
        sqrt_recip_alphas: 1/√(α_t)
        sqrt_one_minus_alphas_cumprod: √(1-ᾱ_t)
        betas: β_t
        posterior_variance: Variance for sampling
    Returns:
        Denoised data at step t-1, shape (batch_size, ...)
    """
    # Predict noise
    t_tensor = torch.full((x.size(0),), t, device=x.device, dtype=torch.long)
    noise_pred = model(x, t_tensor)
    
    # Compute predicted mean
    # μ_θ = (1/√(α_t))(x_t - (β_t/√(1-ᾱ_t))ε_θ)
    sqrt_recip_alphas_t = sqrt_recip_alphas[t]
    sqrt_one_minus_alphas_cumprod_t = sqrt_one_minus_alphas_cumprod[t]
    beta_t = betas[t]
    
    # Reshape for broadcasting
    sqrt_recip_alphas_t = sqrt_recip_alphas_t.reshape(-1, *([1] * (x.ndim - 1)))
    sqrt_one_minus_alphas_cumprod_t = sqrt_one_minus_alphas_cumprod_t.reshape(
        -1, *([1] * (x.ndim - 1))
    )
    beta_t = beta_t.reshape(-1, *([1] * (x.ndim - 1)))
    posterior_variance_t = posterior_variance[t].reshape(-1, *([1] * (x.ndim - 1)))
    
    # Predicted mean
    pred_mean = sqrt_recip_alphas_t * (
        x - beta_t * noise_pred / sqrt_one_minus_alphas_cumprod_t
    )
    
    # Sample
    if t == 0:
        return pred_mean  # No noise at last step
    else:
        noise = torch.randn_like(x)
        return pred_mean + torch.sqrt(posterior_variance_t) * noise


@torch.no_grad()
def sample(model: nn.Module, shape: Tuple[int, ...], timesteps: int = 1000,
           device: str = 'cpu') -> torch.Tensor:
    """
    Generate samples by reversing diffusion process
    
    Start from pure noise x_T ~ N(0, I) and iteratively denoise
    
    Args:
        model: Trained diffusion model
        shape: Shape of samples to generate (batch_size, ...)
        timesteps: Number of diffusion steps
        device: Device to run on
    Returns:
        Generated samples, shape (batch_size, ...)
    """
    # Setup variance schedule
    betas = linear_beta_schedule(timesteps).to(device)
    alphas = 1.0 - betas
    alphas_cumprod = torch.cumprod(alphas, dim=0)
    sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - alphas_cumprod)
    sqrt_recip_alphas = torch.sqrt(1.0 / alphas)
    posterior_variance = betas * (1.0 - alphas_cumprod) / (1.0 - alphas_cumprod)
    
    # Start from pure noise
    x = torch.randn(shape, device=device)
    
    # Reverse diffusion: iterate from T to 0
    for t in reversed(range(timesteps)):
        x = p_sample(model, x, t, sqrt_recip_alphas, sqrt_one_minus_alphas_cumprod,
                     betas, posterior_variance)
    
    return x


# ==================== USAGE EXAMPLE ====================

if __name__ == "__main__":
    print("Diffusion Model Example")
    print("=" * 60)
    
    # Example: 2D data
    input_dim = 2
    timesteps = 1000
    
    # Create model
    model = SimpleDiffusionModel(input_dim=input_dim)
    
    # Example training data (2D points)
    # In practice, you'd use real data
    print(f"Model created with {sum(p.numel() for p in model.parameters())} parameters")
    
    # Example: Generate samples
    print("\nGenerating samples...")
    generated = sample(model, shape=(5, input_dim), timesteps=timesteps)
    print(f"Generated samples shape: {generated.shape}")
    print(f"Sample values:\n{generated}")