Loading...
Implement the general DDIM update, with the interpolation parameter η that sweeps continuously from a deterministic ODE solver to full DDPM ancestral sampling.
DDIM (Song et al., 2021) observes that the DDPM objective constrains only the marginals q(xt∣x0), so you are free to pick a non-Markovian reverse process with the same marginals. That yields a family of samplers indexed by η≥0:
σt=η1−αˉt1−αˉt′1−αˉt′αˉt
x_{t'} = \underbrace{\sqrt{\bar{\alpha}_{t'}}\,\hat{x}_0}_{\text{predicted } x_0} + \underbrace{\sqrt{1-\bar{\alpha}_{t'}-\sigma_t^2}\;\hat{\varepsilon}}_{\text{direction pointing to } x_t} + \sigma_t z$$ where $t'$ is the *next* (smaller) timestep, not necessarily $t-1$. Read the middle term carefully: the amount of noise pointed back along $\hat{\varepsilon}$ is reduced by exactly the amount of fresh noise $\sigma_t$ that is about to be injected, so the total variance stays $1-\bar{\alpha}_{t'}$ whatever $\eta$ is. At $\eta = 0$ nothing random enters and the sampler becomes a deterministic map from the initial latent to the image -- which is what makes DDIM invertible and makes latent interpolation meaningful. At $\eta = 1$ with $t' = t-1$ it reduces exactly to DDPM ancestral sampling. ## Your Task Implement: ```python def ddim_step(x_t, eps, alpha_bar_t, alpha_bar_prev, eta=0.0, z=None): ``` Return $x_{t'}$. When **eta == 0** the **z** argument is unused and may be **None**. ## Input Format - **x_t**, **eps**: NumPy arrays of matching shape. - **alpha_bar_t** (float): $\bar{\alpha}$ at the current step, in **(0, 1)**. - **alpha_bar_prev** (float): $\bar{\alpha}$ at the target step, in **(0, 1]**. Use **1.0** for the final step to a clean sample. - **eta** (float): **0.0** is deterministic DDIM, **1.0** is DDPM. - **z**: array shaped like **x_t**, or **None**. ## Output Format A NumPy array shaped like **x_t**. ## Sample ```python x_t = np.array([0.4, -0.6]) eps = np.array([0.1, 0.2]) print(np.round(ddim_step(x_t, eps, 0.5, 0.8, eta=0.0), 4).tolist()) ``` **x0_hat = (x_t - sqrt(0.5)*eps)/sqrt(0.5)**, **sigma = 0** so the direction term is **sqrt(1-0.8)*eps**, and the output is **sqrt(0.8)*x0_hat + sqrt(0.2)*eps**.x_t = np.array([0.4, -0.6]) eps = np.array([0.1, 0.2]) print(np.round(ddim_step(x_t, eps, 0.5, 0.8, eta=0.0), 4).tolist())
[0.4612, -0.8484]
With eta = 0 the sigma term vanishes entirely. x0_hat = (x_t - sqrt(0.5)*eps)/sqrt(0.5) = [0.4657, -1.0485]. The direction term is sqrt(1 - 0.8 - 0) * eps = sqrt(0.2)*eps. Adding sqrt(0.8)*x0_hat to it gives the result. Note that the schedule moves up here (0.5 to 0.8), which is fine: DDIM only needs the two alpha-bar values.
max(1 - alpha_bar_prev - sigma**2, 0.0) to absorb floating-point negatives.sigma * z term only when eta > 0.alpha_bar_prev = 1.0 is legal and must yield exactly x^0 (sigma and the direction term both vanish).z is supplied.