File size: 7,135 Bytes
41dd7e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | """
core.py — EDM core math for conditional diffusion.
Reimplements the design of NVlabs/edm
(https://github.com/NVlabs/edm; specifically ``training/training_loop.py``)
following Karras et al. 2022, "Elucidating the Design Space of
Diffusion-Based Generative Models", NeurIPS 2022.
Method-only. No architecture, no data loading, no training loop.
Six components:
- σ sampling at training time
- preconditioning (c_skip, c_out, c_in, c_noise)
- denoiser wrapper D_θ
- training loss with λ(σ) weighting
- σ schedule for sampling (ρ-parameterized)
- Heun's 2nd-order sampler with Euler fallback at σ=0
"""
import torch
import math
# --- 1. Training-time σ sampling ---
def sample_training_sigma(batch_size, P_mean, P_std, device):
""" see Table 1 in EDM paper. Formula for noise distribution under Training section.
Returns: tensor of shape (batch_size,).
"""
ln_sigma = torch.empty(batch_size, device=device).normal_(mean=P_mean, std=P_std)
sigma = torch.exp(ln_sigma)
return sigma
# --- 2. Preconditioning (EDM Table 1) ---
def c_skip(sigma, sigma_data):
"""Skip-connection scaling."""
c_skip = sigma_data**2 / (sigma**2 +sigma_data**2)
return c_skip
def c_out(sigma, sigma_data):
"""Output scaling."""
c_out = sigma * sigma_data / (torch.sqrt(sigma_data**2+sigma**2))
return c_out
def c_in(sigma, sigma_data):
"""Input scaling."""
c_in=1 / (torch.sqrt(sigma**2+sigma_data**2))
return c_in
def c_noise(sigma):
"""Time-like scalar fed into the network."""
c_noise=0.25 * torch.log(sigma)
return c_noise
# --- 3. Denoiser wrapper ---
def denoise(F_theta, x_noisy, sigma, x_0, sigma_data):
"""
Wraps the raw network F_theta into the preconditioned denoiser D_theta. This corresponds to equation 7 in the EDM paper.
D_θ(x, σ, x_0) = c_skip(σ)·x + c_out(σ) · F_θ(c_in(σ)·x, c_noise(σ), x_0)
Used identically in both training (target reconstruction) and sampling (Heuns method)
Returns predicted clean x_T (same shape as x_noisy).
"""
# Step 1: normalize the noisy input
x_normalized = c_in(sigma, sigma_data) * x_noisy
# Step 2: embed the noise level
sigma_embedded = c_noise(sigma)
# Step 3: feed normalized inputs + conditioning through the network
F_out = F_theta(x_normalized, sigma_embedded, x_0)
# Step 4: combine via skip connection + scaled output
D_theta = c_skip(sigma, sigma_data) * x_noisy + c_out(sigma, sigma_data) * F_out
return D_theta
# --- 4. Loss ---
def lambda_weight(sigma, sigma_data):
"""Per-σ loss weighting (EDM Table 1, section training)."""
lambda_sigma = (sigma**2 + sigma_data**2)/(sigma*sigma_data)**2
return lambda_sigma
def edm_loss(F_theta, x_T_batch, x_0_batch, sigma_data, P_mean, P_std):
"""
Compute the EDM training loss for one batch.
Args:
F_theta: the raw network
x_T_batch: clean data, shape [B, D]
x_0_batch: conditioning, shape [B, D]
sigma_data: fixed scalar
P_mean, P_std: log-normal sampling hyperparameters
Returns:
scalar loss
"""
batch_size = x_T_batch.shape[0]
device = x_T_batch.device
# Step 1: sample sigma per example
sigma = sample_training_sigma(batch_size, P_mean, P_std, device)
# reshape for broadcasting: [B] -> [B, 1] for vector data
sigma = sigma.view(-1, 1)
# Step 2: sample noise
epsilon = torch.randn_like(x_T_batch)
# Step 3: add noise (EDM's simple forward process)
x_noisy = x_T_batch + sigma * epsilon
# Step 4: run through denoiser
D_theta = denoise(F_theta, x_noisy, sigma, x_0_batch, sigma_data)
# Step 5: compute weighted squared error
squared_error = (D_theta - x_T_batch).pow(2).sum(dim=-1) # [B]
weighted_loss = lambda_weight(sigma.squeeze(-1), sigma_data) * squared_error # [B]
# Step 6: average over batch
return weighted_loss.mean()
# --- 5. Sampling-time σ schedule ---
def sampling_sigma_schedule(N, sigma_min, sigma_max, rho, device):
"""ρ-parameterized power-law schedule.
Returns tensor of length N+1 with σ_0 = σ_max, σ_N = 0,
and σ_i for 0 < i < N as in EDM Eq. 5.
"""
# i goes from 0 to N-1
i = torch.arange(N, device=device, dtype=torch.float32)
# Apply the power-law formula
inv_rho = 1.0 / rho
sigma_max_inv = sigma_max ** inv_rho
sigma_min_inv = sigma_min ** inv_rho
sigmas = (sigma_max_inv + (i / (N - 1)) * (sigma_min_inv - sigma_max_inv)) ** rho
# Append the final σ = 0 (we want to reach truly clean data)
sigmas = torch.cat([sigmas, torch.zeros(1, device=device)])
return sigmas
# --- 6. Heun sampler (EDM Algorithm 1) ---
def heun_sample(network, x_0, n_samples, N, sigma_min, sigma_max, rho, sigma_data, device):
"""Sample from p(x_T | x_0) via Heun's 2nd-order method.
Initializes at x ~ N(0, σ_max² · I), iterates the σ schedule with
Heun steps, falls back to Euler at the final step where σ_next = 0.
Returns: tensor of shape (n_samples, x_0.shape[-1]).
"""
# 1. Build the σ schedule (length N+1, ending at 0)
sigmas = sampling_sigma_schedule(N, sigma_min, sigma_max, rho, device)
# 2. Broadcast x_0 to (n_samples, D) so every generated sample uses
# the same conditioning but different starting noise.
if x_0.ndim == 1:
x_0_batch = x_0.unsqueeze(0).expand(n_samples, -1)
else:
x_0_batch = x_0
D_dim = x_0_batch.shape[-1]
# 3. Initialize at pure noise scaled to σ_max.
# y₀ ~ N(0, σ_max²·I) means y₀ = σ_max · ε for ε ~ N(0, I).
y = sigma_max * torch.randn(n_samples, D_dim, device=device)
# 4. Iterate Heun steps along the schedule.
for i in range(N):
sigma_cur = sigmas[i] # current noise level
sigma_next = sigmas[i + 1] # target noise level for this step
# Reshape σ to match batch shape for broadcasting
sigma_cur_b = sigma_cur.expand(n_samples, 1)
# --- Predictor: Euler step ---
# Evaluate the denoiser at the current point
D_cur = denoise(network, y, sigma_cur_b, x_0_batch, sigma_data)
# Compute the slope dy/dσ at the current point
d_cur = (y - D_cur) / sigma_cur
# Euler step toward σ_next (note: σ_next − σ_cur is negative since σ decreases)
y_pred = y + (sigma_next - sigma_cur) * d_cur
# --- Corrector: Heun's average slope ---
if sigma_next > 0:
# Evaluate the denoiser at the predicted next point
sigma_next_b = sigma_next.expand(n_samples, 1)
D_next = denoise(network, y_pred, sigma_next_b, x_0_batch, sigma_data)
# Slope at the predicted point
d_next = (y_pred - D_next) / sigma_next
# Take the average slope and redo the step
y = y + (sigma_next - sigma_cur) * 0.5 * (d_cur + d_next)
else:
# Final step: σ_next = 0 would divide by zero in d_next.
# Just use the Euler estimate. This is the only Euler-style step.
y = y_pred
return y
|