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