fast.ai · Parte 2 · Unidad 3 de 5
Diffusion models desde cero: DDPM, DDIM y Karras
El núcleo matemático de todo lo que hoy llamamos "difusión" — desde el paper original de 2020 hasta las mejoras que hicieron viable generar una imagen en segundos en vez de minutos.
1. DDPM: noising hacia adelante, denoising hacia atrás
Denoising Diffusion Probabilistic Models (Ho et al., 2020) simplificó una idea que técnicamente existía desde 2015 pero era difícil de entrenar. El proceso hacia adelante (forward) agrega ruido gaussiano a una imagen en pasos sucesivos, según un schedule de varianza creciente:
β = torch.linspace(β_min, β_max, n_steps) # variance schedule, lineal
α = 1. - β
ᾱ = torch.cumprod(α, dim=0) # producto acumulado
# noisificar la imagen x0 hasta el timestep t, en un solo paso:
xt = ᾱ[t].sqrt()*x0 + (1-ᾱ[t]).sqrt()*ruido
La genialidad matemática del paper es que ᾱ (el producto acumulado de todos los pasos hasta
t) permite saltar directo a cualquier nivel de ruido en un solo cálculo, sin
simular los t pasos intermedios uno por uno — esto es lo que hace viable entrenar en la
práctica: durante el entrenamiento, se elige un timestep al azar por ejemplo del batch, se noisifica
directo a ese nivel, y se le pide a una red neuronal que prediga qué ruido se agregó.
2. Entrenar un DDPM es sorprendentemente simple
Todo el entrenamiento se reduce a una MSELoss entre el ruido real agregado y el ruido que
predice un U-Net (arquitectura que se cubre en la próxima unidad):
class DDPMCB(TrainCB):
def before_batch(self, learn):
ε = torch.randn(x0.shape) # ruido real, x_T
t = torch.randint(0, self.n_steps, (n,)) # timestep al azar por ejemplo
ᾱ_t = self.ᾱ[t].reshape(-1,1,1,1)
xt = ᾱ_t.sqrt()*x0 + (1-ᾱ_t).sqrt()*ε # noisificar en un paso
learn.batch = ((xt, t), ε) # input: (imagen ruidosa, t) — target: el ruido
learn = Learner(model, dls, nn.MSELoss(), cbs=[DDPMCB(n_steps=1000, ...)])
learn.fit(epochs)
La generación (sampling) es el proceso inverso: arrancar de ruido puro
(x_T = torch.randn(...)) y, para cada timestep de 999 a 0, pedirle al modelo que prediga el
ruido presente, restarlo con la fórmula del paper, y repetir — mil pasos, cada uno una pasada por la red
completa. En el notebook, generar imágenes reconocibles de Fashion-MNIST toma exactamente esos 1000 pasos
secuenciales; no hay forma de paralelizarlos porque cada paso depende del resultado del anterior.
3. DDIM: el mismo modelo, 10× menos pasos
Mil pasos secuenciales por imagen es exactamente el motivo por el que la generación con difusión temprana era lenta. Denoising Diffusion Implicit Models (Song et al., 2020) resuelve esto sin reentrenar nada: reformula el proceso de sampling para que sea determinista (dado el mismo ruido inicial, siempre da la misma imagen) y permite saltar pasos:
def ddim_step(x_t, t, noise, abar_t, abar_t1, bbar_t, bbar_t1, eta):
x_0_hat = ((x_t - bbar_t.sqrt()*noise) / abar_t.sqrt())
x_t = abar_t1.sqrt()*x_0_hat + (bbar_t1-sig**2).sqrt()*noise
return x_t
El parámetro eta interpola entre DDPM puro (eta=1, estocástico) y DDIM puro
(eta=0, completamente determinista). El notebook mide el efecto real de reducir pasos usando
FID (Fréchet Inception Distance) y KID (Kernel Inception Distance) —
métricas estándar que comparan la distribución estadística de imágenes generadas contra imágenes reales,
más bajo es mejor:
| Pasos de sampling | Calidad |
|---|---|
| 1000 (DDPM completo) | Referencia |
| 333 → 200 → 100 | FID/KID prácticamente estables |
| 50 | Degradación notoria pero aceptable |
| 25 | Calidad claramente peor |
La conclusión práctica, medida directamente: se puede bajar de 1000 a ~100 pasos sin pérdida de calidad perceptible — una mejora de 10× en velocidad de generación sin reentrenar el modelo, solo cambiando cómo se samplea. Esto es, literalmente, la diferencia entre Stable Diffusion tardando minutos o segundos en generar una imagen.
4. Karras: preconditioning en vez de un schedule fijo
El paper de Karras et al. (2022), "Elucidating the Design Space of Diffusion-Based
Generative Models", replantea el problema desde otro ángulo: en vez de un schedule de ruido fijo e
indexado por timestep entero, muestrea directamente el nivel de ruido (sigma) de una
distribución log-normal continua, y reescala el input/output del modelo con tres coeficientes derivados
analíticamente (c_skip, c_out, c_in) para que la red siempre vea
activaciones de escala consistente sin importar cuánto ruido tenga la imagen:
def scalings(sig):
totvar = sig**2 + sig_data**2
return sig_data**2/totvar, sig*sig_data/totvar.sqrt(), 1/totvar.sqrt()
def noisify(x0):
sig = (torch.randn([len(x0)])*1.2-1.2).exp() # log-normal, no lineal
c_skip,c_out,c_in = scalings(sig)
noised_input = x0 + torch.randn_like(x0)*sig
target = (x0 - c_skip*noised_input)/c_out
return (noised_input*c_in, sig), target
La idea central que sobrevive el detalle matemático: en vez de tratar "cuánto ruido hay" como un índice discreto en una tabla fija (como hacía DDPM con sus 1000 timesteps), tratarlo como una cantidad continua que se puede escalar analíticamente da un espacio de diseño mucho más flexible — es la base conceptual de los samplers modernos (Euler, Heun, LMS) que reemplazaron por completo al sampler original de DDPM en herramientas como Stable Diffusion y ComfyUI.
5. Resumen
- DDPM noisifica una imagen hacia adelante con un schedule fijo, y entrena una red para predecir el ruido agregado en cualquier paso.
- El truco matemático de
ᾱ(producto acumulado) permite noisificar a cualquier nivel en un solo cálculo, sin simular pasos intermedios. - DDIM reformula el sampling para ser determinista y saltar pasos — de 1000 a ~100 sin pérdida perceptible de calidad, medido con FID/KID.
- Karras reemplaza el schedule discreto por un nivel de ruido continuo con preconditioning analítico — la base de los samplers modernos.
- Nada de esto requirió cambiar la arquitectura de la red — son todas mejoras al proceso matemático de noising/denoising alrededor de un modelo que sigue prediciendo ruido.