One DDPM Ancestral Sampling Step
Problem Statement
Take one step of the DDPM reverse chain: given xt​ and the network's predicted noise, produce xt−1​.
Background
Substituting x^0​=(xt​−1−αˉt​​ε^)/αˉt​​ into the posterior mean and simplifying gives the form Algorithm 2 of DDPM actually uses:
xt−1​=αt​​1​(xt​−1−αˉt​​βt​​ε^θ​(xt​,t))+σt​z,z∼N(0,I)
The mean subtracts a small fraction of the predicted noise -- βt​/1−αˉt​​, not all of it -- and then rescales. Removing all the noise at once would be the DDIM jump straight to x^0​, not an ancestral step.
For the variance DDPM reports two choices that work about equally well:
- "posterior": σt2​=β~​t​=1−αˉt​1−αˉt−1​​βt​ (optimal for x0​∼N(0,I))
- "beta": σt2​=βt​ (optimal for deterministic x0​)
At t=0 no noise is added at all -- the final step must output a clean sample, and β~​0​ is zero anyway.
Your Task
Implement:
def ddpm_step(x_t, eps, t, betas, z, variance_type="posterior"):
Derive alphas and alpha_bars from betas inside the function. Return xt−1​ as an array shaped like x_t. Raise ValueError for an unknown variance_type.
Input Format
- x_t, eps, z: NumPy arrays of matching shape (z is the pre-drawn standard normal sample).
- t (int): 0-based timestep index.
- betas: 1-D NumPy array.
- variance_type (str): "posterior" or "beta".
Output Format
A NumPy array shaped like x_t.
Sample
betas = np.array([0.1, 0.2, 0.3])
x_t = np.array([0.5, -0.5])
eps = np.array([0.2, 0.1])
z = np.array([1.0, -1.0])
print(np.round(ddpm_step(x_t, eps, 1, betas, z), 4).tolist())
alpha_1 = 0.8, alpha_bar_1 = 0.72, so the mean is (x_t - 0.2/sqrt(0.28) * eps) / sqrt(0.8), and *sigma = sqrt((0.1/0.28)0.2) is added times z.
Example:
betas = np.array([0.1, 0.2, 0.3]) x_t = np.array([0.5, -0.5]) eps = np.array([0.2, 0.1]) z = np.array([1.0, -1.0]) print(np.round(ddpm_step(x_t, eps, 1, betas, z), 4).tolist())
[0.7418, -0.8685]
At t = 1: beta = 0.2, alpha = 0.8, alpha_bar = 0.72, so 1 - alpha_bar = 0.28. The bracket is x_t - (0.2/sqrt(0.28))*eps, divided by sqrt(0.8). Since t > 0, noise is added with sigma = sqrt((1-0.9)/0.28 * 0.2) = 0.2673, scaled by z = [1, -1].
Constraints:
0 <= t < len(betas).- The noise coefficient inside the bracket is βt​/1−αˉt​​, and the whole bracket is divided by αt​​ (note: αt​, not αˉt​).
- When
t == 0, return the mean with no noise term. - Use αˉ−1​=1 in β~​t​.
- An unrecognised
variance_typemust raiseValueError. - Do not round inside the function.
1. Background Knowledge
Diffusion Models operate by gradually adding noise to data (forward process) and then learning to reverse this process (reverse process). The DDPM (Denoising Diffusion Probabilistic Model) reverse step is a probabilistic operation that estimates the previous state xt−1​ given the current noisy state xt​ and a neural network's prediction of the noise ε^. Unlike deterministic samplers like DDIM, DDPM is an ancestral sampler, meaning it explicitly adds stochastic noise at each step to approximate the true posterior distribution p(xt−1​∣xt​).
The core update rule for DDPM is derived from the Gaussian posterior of the forward process. It consists of two main components: a mean term and a variance term. The mean term effectively denoises the input by subtracting a scaled version of the predicted noise and rescaling by the square root of the current alpha coefficient. The variance term controls the amount of randomness injected, ensuring the sample remains diverse and follows the learned distribution.
Crucially, the coefficients αt​ and αˉt​ are derived from the beta schedule (βt​). αt​=1−βt​ represents the retention factor at step t, while αˉt​ is the cumulative product of alphas up to step t, representing the total signal retention from x0​ to xt​. Understanding how to compute these cumulative products efficiently is key to implementing the sampler correctly.
2. Algorithm Approach
The approach involves implementing the exact mathematical formula provided in the problem description using NumPy for vectorized operations. The algorithm follows a direct computational graph:
- Precompute Coefficients: Calculate αt​ and αˉt​ from the input betas. Since αˉt​ is a cumulative product, use np.cumprod.
- Select Variance: Determine σt2​ based on the variance_type argument. This involves a conditional check to select between the "posterior" formula (using β~​t​) or the "beta" formula (using βt​).
- Compute Mean: Apply the mean update formula: αt​​1​(xt​−1−αˉt​​βt​​ε^).
- Compute Noise Term: Calculate σt​z. Note that if t=0, this term must be zero.
- Combine: Sum the mean and noise terms to produce xt−1​.
This is a direct evaluation pattern. There are no loops over data dimensions; all operations should be element-wise or broadcasted across the array shape.
3. Step-by-Step Strategy
- Derive Alphas:
- Compute alphas as 1.0 - betas.
- Compute alpha_bars using np.cumprod(alphas). Ensure alpha_bars has the same length as betas. You may need to prepend a 1.0 if the indexing logic requires αˉ0​=1, but typically for step t, you access index t. Check the problem's indexing convention: usually αˉt​ corresponds to the cumulative product up to index t.
- Handle Variance Type:
- If variance_type is "posterior":
- Calculate β~​t​=1−αˉt​1−αˉt−1​​βt​.
- Be careful with indexing: αˉt−1​ is the previous cumulative alpha. For t=0, this term is undefined or zero, but the noise is zero anyway.
- If variance_type is "beta":
- Set σt2​=βt​.
- Raise ValueError if the type is unknown.
- Compute the Mean Term:
- Extract alpha_t and alpha_bar_t at index t.
- Calculate the noise coefficient: noise_coeff = betas[t] / np.sqrt(1 - alpha_bar_t).
- Calculate the mean: mean = (x_t - noise_coeff * eps) / np.sqrt(alpha_t).
- Compute the Noise Term:
- If t == 0: Set sigma = 0.0.
- Else: Calculate sigma = np.sqrt(variance).
- Compute noise_term = sigma * z.
- Final Output:
- Return mean + noise_term.
4. Common Pitfalls
- Indexing Errors: Confusing t with t−1 when accessing alpha_bars. Remember that αˉt​ is the cumulative product including βt​. If your alpha_bars array starts with αˉ0​, ensure you access the correct index.
- Division by Zero: At t=0, 1−αˉ0​ might be zero if αˉ0​=1. However, the noise term is zero at t=0, so the division in the mean term uses 1−αˉ0​​? No, the mean term uses 1−αˉt​​. At t=0, αˉ0​=1, so 1−αˉ0​=0. This causes a division by zero in the noise coefficient 1−αˉ0​​β0​​. Wait, look at the formula: the mean term is αt​​1​(xt​−1−αˉt​​βt​​ε^). At t=0, αˉ0​=1, so the denominator is 0. However, in practice, the reverse process usually starts from T down to 1, and the step from 1→0 is handled carefully. But the problem asks for a general step. If t=0, the formula might be ill-defined numerically. However, note that at t=0, we are predicting x−1​? No, xt−1​ where t=0 implies x−1​ which doesn't exist. Usually, the loop runs for t from T down to 1. If t=0 is passed, it might be an edge case. The problem states "At t=0 no noise is added". It does not explicitly say the mean term is skipped. But mathematically, if t=0, αˉ0​=1, so 1−αˉ0​​=0. This suggests t=0 might not be a valid input for the noise subtraction part, or the formula simplifies. Actually, standard DDPM implementations often handle t=0 by just returning the mean prediction of x0​ without noise. But the formula provided has 1−αˉt​​ in the denominator. If t=0, this is 0. This implies the problem likely assumes t≥1 for the noise subtraction, or you must handle the t=0 case specifically to avoid division by zero. Given the sample uses t=1, focus on t≥1. If t=0 is tested, check if eps is ignored or if the term becomes 0.
- Variance Calculation: For "posterior", ensure you use αˉt−1​ in the numerator. If t=0, αˉ−1​ is undefined. Again, t=0 is a special case.
- Data Types: Ensure all calculations are done in float64 or float32 consistently to avoid precision issues.
5. Time & Space Complexity
- Time Complexity: O(N), where N is the number of timesteps (length of betas), due to the cumulative product calculation. The actual step computation is O(D), where D is the number of elements in x_t, as it involves element-wise operations. Since N is typically small (e.g., 1000) and D can be large (image pixels), the dominant factor is the array size D.
- Space Complexity: O(N) to store the alphas and alpha_bars arrays. The output array takes O(D) space. Auxiliary space is minimal.