¿Qué tiene de malo DDPM?
DDPM funciona. Genera imágenes impresionantes, tiene una base matemática rigurosa, e impulsó la primera ola de generación de imágenes basada en difusión de 2020 a 2023. Pero después de años de uso práctico, tres debilidades se volvieron cada vez más difíciles de ignorar.
Primero, el proceso directo es fijo, no aprendido . DDPM define un schedule de ruido predeterminado $\{\beta_1, \ldots, \beta_T\}$ que controla cómo se corrompen los datos en ruido. Este schedule se diseña a mano (típicamente lineal o coseno), y el proceso inverso debe deshacer esta corrupción específica. El modelo no tiene voz en cómo se forman las trayectorias directas — solo puede aprender a revertir cualquier camino que el schedule fijo le imponga.
Segundo, el muestreo requiere muchos pasos . El proceso directo sigue un camino curvo a través del espacio de datos — el schedule de ruido crea una espiral de datos a ruido gaussiano, y el proceso inverso debe retrazar cuidadosamente esta trayectoria sinuosa. Cada paso hace una pequeña corrección, y si saltas demasiados pasos, el error acumulado descarrila la imagen. En la práctica, DDPM necesita 50-1000 pasos de eliminación de ruido para producir salidas limpias. Eso es lento, especialmente para aplicaciones en tiempo real.
Tercero, las matemáticas involucran cotas variacionales complejas . El objetivo de entrenamiento de DDPM se deriva de una cota inferior variacional sobre la log-verosimilitud, involucrando términos como $\bar{\alpha}_t = \prod_{s=1}^{t}(1-\beta_s)$, ponderación por relación señal-ruido, y una distribución posterior $q(x_{t-1} \mid x_t, x_0)$ que requiere manipulación algebraica cuidadosa. La pérdida simplificada de predicción de ruido $\|\epsilon_\theta(x_t, t) - \epsilon\|^2$ oculta mucha maquinaria por debajo.
Estos tres problemas comparten una causa común: el proceso directo lo dicta todo, y el modelo está forzado a trabajar dentro de sus restricciones. ¿Qué pasaría si pudiéramos evadir completamente el proceso de difusión fijo? ¿Qué pasaría si, en lugar de seguir caminos curvos de datos a ruido y de vuelta, pudiéramos aprender caminos rectos ? Caminos rectos significarían menos pasos (menos distancia a recorrer), matemáticas más simples (sin gimnasia de schedule de ruido), y un marco más flexible. Eso es exactamente lo que proporciona flow matching.
Flow Matching: aprendiendo caminos rectos
Flow matching (Lipman et al., 2023) replantea el modelado generativo como aprender un flujo : un campo vectorial dependiente del tiempo que transporta muestras de una distribución de ruido a una distribución de datos. En lugar de la configuración de difusión de dos fases (corrupción directa fija, eliminación de ruido inversa aprendida), flow matching define un camino directo de ruido a datos y entrena una red neuronal para seguirlo.
El camino de interpolación. La elección más simple es una línea recta. Dada una muestra de datos $x_0$ y una muestra de ruido $\epsilon \sim \mathcal{N}(0, I)$, definimos el punto intermedio en el tiempo $t \in [0, 1]$ como:
Verifiquemos esto en los límites. En $t = 0$: $x_0^{\text{path}} = (1 - 0)\,\epsilon + 0 \cdot x_0 = \epsilon$ — ruido puro. En $t = 1$: $x_1^{\text{path}} = (1 - 1)\,\epsilon + 1 \cdot x_0 = x_0$ — datos limpios. En $t = 0.5$: $x_{0.5} = 0.5\,\epsilon + 0.5\,x_0$ — una mezcla 50/50 de ruido y datos. El camino traza una línea recta en el espacio de datos desde la muestra de ruido hasta la muestra de datos.
Compara esto con el proceso directo de DDPM: $x_t = \sqrt{\bar{\alpha}_t}\,x_0 + \sqrt{1 - \bar{\alpha}_t}\,\epsilon$. Los coeficientes $\sqrt{\bar{\alpha}_t}$ y $\sqrt{1 - \bar{\alpha}_t}$ no suman 1 — están en un cuarto de círculo (ya que $(\sqrt{\bar{\alpha}_t})^2 + (\sqrt{1-\bar{\alpha}_t})^2 = 1$), lo que significa que el camino de DDPM se curva a través del espacio de datos. Los coeficientes lineales $(1-t)$ y $t$ de flow matching suman exactamente 1, produciendo una línea recta.
La velocidad. Si el camino es $x_t = (1 - t)\,\epsilon + t\,x_0$, ¿cuál es su velocidad? Solo tomamos la derivada temporal:
La velocidad es constante — no depende de $t$ en absoluto. Apunta desde la muestra de ruido $\epsilon$ hacia la muestra de datos $x_0$, y su magnitud es la distancia euclidiana entre ellos. Esta constancia es exactamente lo que hace el camino recto: la dirección y la velocidad nunca cambian a lo largo de la trayectoria.
La pérdida de entrenamiento. Entrenamos una red neuronal $v_\theta(x_t, t)$ para predecir esta velocidad. La pérdida es un simple error cuadrático medio:
El procedimiento de entrenamiento es notablemente directo:
- Muestrear $t \sim U(0, 1)$ — un paso de tiempo aleatorio uniforme
- Muestrear $x_0$ de los datos de entrenamiento
- Muestrear $\epsilon \sim \mathcal{N}(0, I)$ — un vector de ruido aleatorio de la distribución normal
- Calcular $x_t = (1 - t)\,\epsilon + t\,x_0$
- Calcular la velocidad objetivo: $x_0 - \epsilon$
- Minimizar $\|v_\theta(x_t, t) - (x_0 - \epsilon)\|^2$
Nota lo que está ausente : no hay schedule de ruido $\{\beta_t\}$, no hay producto acumulativo $\bar{\alpha}_t$, no hay ponderación por relación señal-ruido. El objetivo de entrenamiento es simplemente $x_0 - \epsilon$, la diferencia entre datos y ruido. Esto es discutiblemente aún más simple que la pérdida de predicción de ruido de DDPM $\|\epsilon_\theta(x_t, t) - \epsilon\|^2$, porque la fórmula de interpolación misma es más simple (coeficientes lineales en lugar de productos de raíces cuadradas).
Muestreo. Para generar una nueva imagen, partimos de ruido puro $x_0 = \epsilon \sim \mathcal{N}(0, I)$ y resolvemos la ecuación diferencial ordinaria (ODE):
desde $t = 0$ hasta $t = 1$. El solucionador más simple es el método de Euler: dividir $[0, 1]$ en $N$ pasos de tamaño $\Delta t = 1/N$, e iterar:
Aquí está la ventaja clave de los caminos rectos: si el campo vectorial aprendido fuera perfectamente recto, un solo paso de Euler sería suficiente (una línea recta solo necesita su punto de inicio y dirección). En la práctica, el campo aprendido no es perfectamente recto — hay muchos puntos de datos tirando en diferentes direcciones — pero es mucho más recto que las trayectorias inversas curvas de DDPM. Esto significa que se necesitan menos pasos de Euler para la misma calidad. Donde DDPM podría necesitar 50-1000 pasos, flow matching típicamente funciona bien con 20-50 pasos.
import math
# Compare path coefficients: DDPM vs flow matching
print("t | DDPM coeff(x0) DDPM coeff(eps) | FM coeff(x0) FM coeff(eps)")
print("-" * 78)
T_ddpm = 1000
beta_start, beta_end = 0.0001, 0.02
for t_frac in [0.0, 0.25, 0.5, 0.75, 1.0]:
# Flow matching: linear
fm_x0 = t_frac
fm_eps = 1 - t_frac
# DDPM: compute alpha_bar at equivalent timestep
t_step = int(t_frac * (T_ddpm - 1))
alpha_bar = 1.0
for s in range(t_step + 1):
beta_s = beta_start + (beta_end - beta_start) * s / (T_ddpm - 1)
alpha_bar *= (1 - beta_s)
ddpm_x0 = math.sqrt(alpha_bar)
ddpm_eps = math.sqrt(1 - alpha_bar)
print(f"{t_frac:.2f} | {ddpm_x0:.4f} {ddpm_eps:.4f} | {fm_x0:.4f} {fm_eps:.4f}")
print()
print("DDPM coefficients follow a quarter-circle (squares sum to 1)")
print("Flow matching coefficients follow a straight line (values sum to 1)")
Flujos rectificados: haciendo los caminos aún más rectos
La interpolación lineal $x_t = (1 - t)\,\epsilon + t\,x_0$ define caminos rectos entre pares individuales de ruido-datos. Pero el campo vectorial aprendido $v_\theta(x_t, t)$ debe manejar todos los pares posibles simultáneamente. En cualquier punto $x_t$ del espacio, la red ve contribuciones de muchas muestras de datos diferentes tirando en diferentes direcciones. El resultado: aunque cada par de entrenamiento define un camino recto, el campo vectorial promediado que la red aprende puede producir trayectorias curvas cuando realmente integras la ODE. Los caminos se cruzan e interfieren entre sí.
Rectified Flow (Liu et al., 2023) resuelve esto con un procedimiento iterativo elegante llamado rectificación . La idea es:
- Paso 1: Entrenar un modelo de flow matching $v_\theta$ en pares aleatorios (ruido, datos) usando la pérdida estándar.
- Paso 2: Usar el modelo entrenado para generar nuevas muestras de datos: partir de ruido $\epsilon_i$ e integrar la ODE para obtener $\hat{x}_i$. Ahora tienes trayectorias emparejadas $(\epsilon_i, \hat{x}_i)$ que el modelo realmente recorre.
- Paso 3: Entrenar un nuevo modelo en estos pares $(\epsilon_i, \hat{x}_i)$. Ya que estos pares fueron generados siguiendo el campo vectorial del propio modelo, la interpolación en línea recta entre ellos está mucho más cerca de la trayectoria real de la ODE.
- Repetir: Cada ronda de rectificación hace los caminos más rectos. Después de 2-3 rondas, las trayectorias son casi lineales.
¿Por qué funciona esto? El primer modelo mapea muestras de ruido aleatorio a muestras de datos, pero el emparejamiento es arbitrario — el vector de ruido $\epsilon_i$ no tiene relación especial con el punto de datos $x_j$, así que su interpolación en línea recta puede cruzar otras trayectorias. Después de una ronda de generación, $\epsilon_i$ y $\hat{x}_i$ sí están relacionados — son los extremos de la misma trayectoria ODE. Entrenar en estos extremos emparejados significa que la interpolación en línea recta ahora aproxima el camino real que toma el modelo, reduciendo la interferencia entre trayectorias que se cruzan.
La consecuencia práctica es dramática: después de la rectificación, los caminos son lo suficientemente rectos para que 1-4 pasos de Euler puedan producir muestras de alta calidad. Compara eso con 20-50 pasos para flow matching estándar o 50-1000 para DDPM. Esto no es solo una aceleración — permite la generación en tiempo real y hace que los modelos tipo difusión sean viables para aplicaciones sensibles a la latencia. Esto también es lo que hace atractivo a flow matching para aplicaciones de robótica , donde las acciones deben generarse a 10+ Hz.
Los flujos rectificados son la base de la última generación de generadores de imágenes. Stable Diffusion 3 y Flux ambos usan rectified flow matching en lugar de DDPM, precisamente porque caminos más rectos significan menos pasos de muestreo en tiempo de inferencia.
# Demonstrate how rectification straightens paths
# We simulate a 1D example: noise -> data mapping
import math
# Suppose we have 4 noise-data pairs
pairs_round0 = [
(-2.0, 3.0), # noise=-2, data=3
(-1.0, 1.0), # noise=-1, data=1
( 0.5, -0.5), # noise=0.5, data=-0.5
( 1.5, 2.5), # noise=1.5, data=2.5
]
def path_curvature(pairs):
"""Measure how much paths cross by counting intersections at t=0.5"""
midpoints = [(1 - 0.5) * n + 0.5 * d for n, d in pairs]
crossings = 0
for i in range(len(pairs)):
for j in range(i + 1, len(pairs)):
# Paths cross if ordering of endpoints flips
noise_order = pairs[i][0] < pairs[j][0]
mid_order = midpoints[i] < midpoints[j]
if noise_order != mid_order:
crossings += 1
return crossings
def straightness(pairs, steps=20):
"""Measure deviation from straight line (lower = straighter)"""
total_dev = 0
for noise, data in pairs:
velocity = data - noise
# Ideal straight path
for s in range(1, steps):
t = s / steps
ideal = (1 - t) * noise + t * data
# Simulate "curved" path from interference
actual = ideal # In our simple model, paths are already linear per-pair
total_dev += abs(velocity)
return total_dev / len(pairs)
crossings_r0 = path_curvature(pairs_round0)
# After "rectification": re-pair by sorting (simulating ODE endpoint matching)
noises = sorted([n for n, d in pairs_round0])
datas = sorted([d for n, d in pairs_round0])
pairs_round1 = list(zip(noises, datas)) # Monotone pairing = no crossings
crossings_r1 = path_curvature(pairs_round1)
print("Rectification reduces path crossings")
print("=" * 55)
print(f"\nRound 0 (random pairing):")
for n, d in pairs_round0:
print(f" noise={n:+.1f} -> data={d:+.1f} (velocity={d-n:+.1f})")
print(f" Path crossings at t=0.5: {crossings_r0}")
print(f"\nRound 1 (rectified pairing):")
for n, d in pairs_round1:
print(f" noise={n:+.1f} -> data={d:+.1f} (velocity={d-n:+.1f})")
print(f" Path crossings at t=0.5: {crossings_r1}")
print(f"\nCrossings reduced from {crossings_r0} to {crossings_r1}")
print("Fewer crossings = straighter learned vector field = fewer steps needed")
Flow Matching vs DDPM: una comparación lado a lado
Tanto DDPM como flow matching aprenden a mapear ruido a datos, pero difieren en casi cada detalle de cómo se define, entrena y muestrea ese mapeo. Pongamos los dos enfoques lado a lado.
Lo que predice el modelo. DDPM entrena una red $\epsilon_\theta(x_t, t)$ para predecir el ruido que se añadió en el paso de tiempo $t$. Flow matching entrena una red $v_\theta(x_t, t)$ para predecir la velocidad — la dirección y magnitud del paso de ruido hacia datos. Están relacionados: si conoces el ruido $\epsilon$ y los datos $x_0$, la velocidad es $v = x_0 - \epsilon$. Pero el planteamiento importa, porque determina cómo se usa la salida de la red durante el muestreo.
Cómo funciona el muestreo. DDPM genera imágenes mediante eliminación de ruido iterativa : partir de ruido $x_T \sim \mathcal{N}(0, I)$, y en cada paso predecir el ruido, restar una versión escalada del mismo, y opcionalmente añadir una pequeña cantidad de ruido fresco (la parte estocástica de DDPM). Flow matching genera imágenes mediante integración de ODE : partir de ruido $x_0 \sim \mathcal{N}(0, I)$ e integrar el campo de velocidad hacia adelante usando pasos de Euler (o un solucionador de orden superior). El muestreador de flow matching es determinista — no se añade ruido durante el muestreo.
Geometría del camino. Los caminos de DDPM son curvos porque los coeficientes del proceso directo $\sqrt{\bar{\alpha}_t}$ y $\sqrt{1 - \bar{\alpha}_t}$ trazan un cuarto de círculo. El proceso inverso debe seguir estas curvas, haciendo de cada paso una pequeña corrección angular. Los caminos de flow matching son rectos (o casi, después de la rectificación). Caminos más rectos significan que pasos más grandes son seguros, porque la dirección cambia menos entre pasos.
Consecuencias prácticas:
- Número de pasos: Flow matching típicamente necesita 20-50 pasos para alta calidad. Con rectificación, 1-4 pasos. DDPM necesita 50-1000 pasos, o 20-50 con schedulers avanzados como DDIM.
- Simplicidad de entrenamiento: Flow matching no tiene schedule de ruido $\{\beta_t\}$, ni productos $\bar{\alpha}_t$, ni ponderación por SNR. La interpolación es lineal y el objetivo es $x_0 - \epsilon$.
- Flexibilidad: Flow matching funciona con cualquier distribución fuente, no solo ruido gaussiano. En principio podrías transportar de una distribución de imágenes a otra, aunque el ruido gaussiano sigue siendo el punto de partida estándar.
- Arquitectura de red: Ambos pueden usar el mismo backbone de eliminación de ruido (U-Net o DiT). La arquitectura es agnóstica — solo cambian el objetivo de entrenamiento y el algoritmo de muestreo.
import json, js
rows = [
["Prediction target", "Noise (epsilon)", "Velocity (x0 - epsilon)"],
["Interpolation", "sqrt(alpha_bar)*x0 + sqrt(1-alpha_bar)*eps", "(1-t)*eps + t*x0"],
["Path shape", "Curved (quarter-circle)", "Straight (linear)"],
["Sampling method", "Iterative denoising (reverse SDE)", "ODE integration (Euler)"],
["Typical steps", "50-1000 (20-50 with DDIM)", "20-50 (1-4 with rectification)"],
["Noise schedule", "Required (beta_t, alpha_bar_t)", "Not needed"],
["Training loss", "||eps_theta - eps||^2 (+ SNR weighting)", "||v_theta - (x0 - eps)||^2"],
["Stochasticity", "Optional noise at each step", "Deterministic ODE"],
["Source distribution", "Gaussian only", "Any distribution"],
]
js.window.py_table_data = json.dumps({
"headers": ["Property", "DDPM", "Flow Matching"],
"rows": rows
})
print("DDPM vs Flow Matching: key differences across 9 dimensions")
import math, json, js
# Visualise the path geometry difference
# DDPM: coefficients trace a quarter-circle
# Flow matching: coefficients trace a straight line
T = 1000
beta_start, beta_end = 0.0001, 0.02
rows = []
for t_frac in [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]:
# Flow matching
fm_sig = t_frac
fm_noise = 1.0 - t_frac
# DDPM
t_step = int(t_frac * (T - 1))
alpha_bar = 1.0
for s in range(t_step + 1):
beta_s = beta_start + (beta_end - beta_start) * s / (T - 1)
alpha_bar *= (1 - beta_s)
ddpm_sig = math.sqrt(alpha_bar)
ddpm_noise = math.sqrt(1 - alpha_bar)
rows.append([f"{t_frac:.1f}", f"{ddpm_sig:.4f}", f"{ddpm_noise:.4f}", f"{fm_sig:.4f}", f"{fm_noise:.4f}"])
js.window.py_table_data = json.dumps({
"headers": ["t", "DDPM signal", "DDPM noise", "FM signal", "FM noise"],
"rows": rows
})
print("DDPM: signal^2 + noise^2 = 1 (quarter-circle in coefficient space)")
print("FM: signal + noise = 1 (straight line in coefficient space)")
print("\nThe straight line means uniform progress from noise to data.")
print("The quarter-circle means most change is compressed into the middle timesteps.")
Por qué SD3 y Flux eligieron flow matching
Stable Diffusion 3 (Esser et al., 2024) marcó una ruptura decisiva con DDPM. Adoptó rectified flow matching como su marco de entrenamiento, combinado con la arquitectura de transformer MMDiT que cubrimos en el artículo 5. Flux (Black Forest Labs, 2024) siguió el mismo enfoque. El cambio de DDPM a flow matching en 2023-2024 refleja el cambio paralelo de U-Net a eliminadores de ruido transformer: en ambos casos, el campo se movió hacia fundamentos más simples y escalables .
Pero SD3 no adoptó flow matching estándar sin cambios. Introdujo un refinamiento clave de entrenamiento: muestreo logit-normal de pasos de tiempo . En flow matching estándar, muestreamos $t \sim U(0,1)$ — cada paso de tiempo recibe igual peso de entrenamiento. Pero no todos los pasos de tiempo son igualmente importantes. En $t \approx 0$ la entrada es casi ruido puro y el trabajo del modelo es trivial (predecir aproximadamente $x_0 - \epsilon$, que está cerca de simplemente predecir la media de los datos). En $t \approx 1$ la entrada es casi datos limpios y de nuevo el trabajo del modelo es fácil (correcciones pequeñas). Las decisiones difíciles ocurren en el rango medio ($t \approx 0.3$ a $0.7$), donde el modelo debe resolver estructura ambigua — ¿ese borroso es un rostro o un edificio?
El muestreo logit-normal concentra el entrenamiento en estos pasos de tiempo medios críticos. En lugar de $t \sim U(0, 1)$, SD3 muestrea:
donde $\sigma$ es la función sigmoide $\sigma(z) = \frac{1}{1 + e^{-z}}$. Verifiquemos qué hace esto en los límites. Cuando $z \to -\infty$: $\sigma(z) \to 0$, así que $t \to 0$ (ruido extremo). Cuando $z \to +\infty$: $\sigma(z) \to 1$, así que $t \to 1$ (datos limpios). Cuando $z = 0$: $\sigma(0) = 0.5$, así que $t = 0.5$ (el punto medio). Como $z \sim \mathcal{N}(0, 1)$, la mayoría de las muestras de $z$ están cerca de 0, lo que significa que la mayoría de las muestras de $t$ están cerca de 0.5 — exactamente los pasos de tiempo medios donde más importa la señal de entrenamiento. Las colas ($t$ cerca de 0 o 1) se muestrean raramente, lo cual es apropiado ya que la tarea del modelo allí es más fácil.
import math, json, js
# Show how logit-normal sampling concentrates on middle timesteps
# Compare: uniform vs logit-normal distribution of t
def sigmoid(z):
return 1.0 / (1.0 + math.exp(-z))
# Sample from logit-normal: z ~ N(0,1), t = sigmoid(z)
# We'll compute the density at various t values
# For logit-normal: p(t) = (1 / (t*(1-t))) * phi(logit(t))
# where phi is the standard normal pdf and logit(t) = log(t/(1-t))
def normal_pdf(z):
return math.exp(-0.5 * z * z) / math.sqrt(2 * math.pi)
def logit_normal_pdf(t):
if t <= 0.001 or t >= 0.999:
return 0.0
logit_t = math.log(t / (1 - t))
return normal_pdf(logit_t) / (t * (1 - t))
rows = []
for t in [0.05, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 0.95]:
u_density = 1.0 # uniform density is 1 everywhere
ln_density = logit_normal_pdf(t)
ratio = ln_density / u_density
rows.append([f"{t:.2f}", f"{u_density:.3f}", f"{ln_density:.3f}", f"{ratio:.2f}x"])
js.window.py_table_data = json.dumps({
"headers": ["t", "Uniform", "Logit-Normal", "Ratio"],
"rows": rows
})
print("Logit-normal samples t=0.5 about 1.6x more often than uniform,")
print("while t=0.05 and t=0.95 are sampled ~3x less often.")
print("This focuses training on the 'interesting' middle timesteps.")
La receta combinada detrás de SD3 y Flux es: (1) rectified flow matching para el marco de entrenamiento (caminos rectos, predicción de velocidad, muestreo ODE), (2) un eliminador de ruido basado en transformer (DiT/MMDiT) en lugar de una U-Net, y (3) muestreo logit-normal de pasos de tiempo para enfocar el entrenamiento donde importa. Cada componente es más simple que lo que reemplazó, y juntos producen calidad de imagen de vanguardia con menos pasos de muestreo.
Quiz
Pon a prueba tu comprensión de flow matching y flujos rectificados.
En flow matching, el camino de interpolación es $x_t = (1 - t)\epsilon + t\,x_0$. ¿Cuál es la velocidad $dx_t/dt$?
¿Qué problema resuelve la rectificación en flow matching?
¿Por qué SD3 usa muestreo logit-normal de pasos de tiempo en lugar de muestreo uniforme?
¿Cómo difiere geométricamente el camino de interpolación de flow matching del proceso directo de DDPM?