¿Cómo se genera una imagen a partir de ruido?
Supongamos que tienes miles de fotografías de rostros y quieres un modelo que pueda generar rostros nuevos y realistas que nunca fueron fotografiados. Más formalmente, tienes muestras de alguna distribución de datos desconocida $p(x)$, y quieres aprender un modelo que pueda generar nuevas muestras de esa misma distribución. Este es el problema de modelado generativo , y durante años el enfoque dominante fueron las Redes Generativas Adversarias (GANs) : entrenar un generador para producir imágenes falsas y un discriminador para distinguir las falsas de las reales, dejando que las dos redes jueguen un juego minimax hasta que el generador gane. Las GANs producían imágenes impresionantes, pero eran notoriamente difíciles de entrenar. El colapso de modos (el generador aprende a producir solo unos pocos tipos de salidas, ignorando la diversidad de los datos reales) y la inestabilidad del entrenamiento (el generador y el discriminador oscilan en lugar de converger) afectaban a los practicantes y requerían un ajuste extensivo de hiperparámetros.
Los modelos de difusión toman un enfoque completamente diferente. En lugar de aprender a generar una imagen directamente a partir de ruido aleatorio en un solo paso, aprenden a eliminar ruido . La idea central es sorprendentemente simple: tomar una imagen limpia, corromperla gradualmente añadiendo ruido durante muchos pasos hasta que se convierta en estática pura, y luego entrenar una red neuronal para revertir cada paso. Si la red aprende a deshacer un pequeño paso de adición de ruido, podemos encadenar esas reversiones: partir de ruido puro e iterativamente eliminar ruido hasta que emerge una imagen limpia. El modelo nunca tiene que resolver el problema imposible de mapear ruido aleatorio a una imagen coherente en un solo paso. En cambio, resuelve un problema mucho más fácil (predecir el ruido que se añadió) miles de veces en secuencia.
Esta estructura de dos fases define cada modelo de difusión. El proceso directo (también llamado proceso de difusión) destruye gradualmente los datos añadiendo ruido gaussiano durante $T$ pasos. No requiere aprendizaje — es un procedimiento fijo y conocido. El proceso inverso aprende a deshacer la destrucción paso a paso, recuperando estructura a partir del ruido. El artículo seminal que hizo esto práctico fue (Ho et al., 2020) , que demostró que un simple objetivo de predicción de ruido es todo lo que se necesita. El resto de este artículo explica exactamente cómo funciona.
El proceso directo: destruyendo datos gradualmente
Comienza con una imagen limpia $x_0$ tomada de tu conjunto de entrenamiento. El proceso directo define una cadena de versiones cada vez más ruidosas $x_1, x_2, \ldots, x_T$, donde cada paso añade una pequeña cantidad de ruido gaussiano controlada por un schedule de ruido $\{\beta_1, \beta_2, \ldots, \beta_T\}$. La transición del paso $t-1$ al paso $t$ es:
Esto dice: para obtener $x_t$, toma la imagen anterior $x_{t-1}$, escálala por $\sqrt{1 - \beta_t}$, y añade ruido gaussiano con varianza $\beta_t$. El parámetro $\beta_t$ es el nivel de ruido en el paso $t$, que típicamente comienza muy pequeño ($\beta_1 = 0.0001$) y aumenta hasta $\beta_T = 0.02$ durante $T = 1000$ pasos. El factor de escala $\sqrt{1 - \beta_t}$ reduce ligeramente la señal en cada paso, mientras que $\beta_t$ inyecta ruido. Juntos, estos dos términos aseguran que la varianza total se mantenga acotada y no explote al infinito a lo largo de la cadena.
Verifiquemos los casos límite para construir intuición. Cuando $\beta_t = 0$, el factor de escala se convierte en $\sqrt{1 - 0} = 1$ y la varianza del ruido es $0$, así que $x_t = x_{t-1}$ exactamente — no pasa nada. Cuando $\beta_t = 1$, el factor de escala se convierte en $\sqrt{1 - 1} = 0$ y la varianza del ruido es $1$, así que la señal original se borra completamente y se reemplaza por ruido gaussiano puro en un solo paso. En la práctica, cada $\beta_t$ es muy pequeño, por lo que cada paso individual apenas cambia la imagen. Pero después de $T = 1000$ pasos así, el efecto acumulativo es destrucción total: $x_T \approx \mathcal{N}(0, I)$, ruido gaussiano puro sin rastro de la imagen original.
Una idea crucial del artículo de DDPM es que no necesitamos iterar a través de los $t$ pasos para obtener $x_t$. Definimos $\alpha_t = 1 - \beta_t$ y el producto acumulativo $\bar{\alpha}_t = \prod_{s=1}^{t} \alpha_s$. Entonces podemos saltar directamente de la imagen limpia $x_0$ a cualquier versión ruidosa $x_t$:
En la práctica, esto significa que podemos muestrear $x_t$ directamente usando el truco de reparametrización :
Esta es una suma ponderada de la imagen original y ruido aleatorio. El coeficiente $\sqrt{\bar{\alpha}_t}$ controla cuánto de la señal original sobrevive, mientras que $\sqrt{1 - \bar{\alpha}_t}$ controla cuánto ruido se añade. Como $\bar{\alpha}_t$ es un producto acumulativo de valores ligeramente menores que 1, decae hacia cero a medida que $t$ aumenta. En $t = 0$, $\bar{\alpha}_0 = 1$, así que $x_0 = 1 \cdot x_0 + 0 \cdot \epsilon$ — la imagen limpia, intacta. En $t = T$, $\bar{\alpha}_T \approx 0$, así que $x_T \approx 0 \cdot x_0 + 1 \cdot \epsilon = \epsilon$ — ruido puro. La señal se desvanece suavemente de máxima intensidad a nada.
El gráfico a continuación muestra cómo $\bar{\alpha}_t$ decae durante 1000 pasos de tiempo usando un schedule de ruido lineal ($\beta_t$ aumentando linealmente de $0.0001$ a $0.02$). Observa que la intensidad de la señal cae lentamente al principio (la imagen apenas se ve afectada durante los primeros cientos de pasos), luego se acelera en el medio, y para el paso 700-800 la señal original casi ha desaparecido por completo.
import math, json, js
T = 1000
beta_start = 0.0001
beta_end = 0.02
# Linear noise schedule
betas = [beta_start + (beta_end - beta_start) * t / (T - 1) for t in range(T)]
# Compute cumulative alpha_bar
alpha_bar = []
cumulative = 1.0
for beta in betas:
cumulative *= (1.0 - beta)
alpha_bar.append(cumulative)
# Also compute signal and noise coefficients
signal_coeff = [math.sqrt(ab) for ab in alpha_bar]
noise_coeff = [math.sqrt(1.0 - ab) for ab in alpha_bar]
timesteps = list(range(T))
plot_data = [
{
"title": "Forward process: signal decay over 1000 steps (linear schedule)",
"x_label": "Timestep t",
"y_label": "Coefficient value",
"x_data": timesteps,
"lines": [
{"label": "alpha_bar_t (signal preserved)", "data": alpha_bar, "color": "#3b82f6"},
{"label": "sqrt(alpha_bar_t) (signal coeff)", "data": signal_coeff, "color": "#10b981"},
{"label": "sqrt(1 - alpha_bar_t) (noise coeff)", "data": noise_coeff, "color": "#ef4444"}
]
}
]
js.window.py_plot_data = json.dumps(plot_data)
print(f"At t=0: alpha_bar = {alpha_bar[0]:.6f} (image is ~100% signal)")
print(f"At t=250: alpha_bar = {alpha_bar[249]:.6f} (still mostly signal)")
print(f"At t=500: alpha_bar = {alpha_bar[499]:.6f} (signal fading fast)")
print(f"At t=750: alpha_bar = {alpha_bar[749]:.6f} (almost all noise)")
print(f"At t=999: alpha_bar = {alpha_bar[999]:.6f} (essentially pure noise)")
El proceso inverso: aprendiendo a eliminar ruido
El proceso directo nos da una receta para destruir imágenes. Ahora necesitamos revertirlo: dada una imagen ruidosa $x_t$, recuperar la imagen ligeramente menos ruidosa $x_{t-1}$. Si pudiéramos hacer esto perfectamente para cada paso, podríamos partir de ruido puro $x_T \sim \mathcal{N}(0, I)$ e iterativamente eliminar ruido hasta obtener una imagen limpia $x_0$. El hecho notable (demostrado por Feller y otros en la teoría de procesos estocásticos) es que si cada paso directo añade solo una pequeña cantidad de ruido, entonces el paso inverso también es aproximadamente gaussiano. Así que parametrizamos el proceso inverso como:
Aquí $\mu_\theta(x_t, t)$ es la media predicha — la mejor estimación del modelo del centro de la distribución sobre $x_{t-1}$ dada la entrada ruidosa $x_t$ y el paso de tiempo $t$. La varianza $\sigma_t^2$ típicamente no se aprende sino que se fija en $\beta_t$ o $\tilde{\beta}_t = \frac{1 - \bar{\alpha}_{t-1}}{1 - \bar{\alpha}_t} \beta_t$ (ambos funcionan bien en la práctica). La red neuronal solo necesita aprender la media, que es donde ocurre todo el trabajo interesante.
La idea clave de (Ho et al., 2020) es que en lugar de predecir la media $\mu_\theta$ directamente, es mucho más efectivo predecir el ruido $\epsilon_\theta(x_t, t)$ que se añadió para crear $x_t$. ¿Por qué? Porque una vez que conoces el ruido, puedes recuperar la media analíticamente. Recordemos que $x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \epsilon$. Si el modelo predice $\epsilon$, podemos resolver para $x_0$ y luego calcular la media posterior. La fórmula resultante para la media predicha es:
Desglosemos cada pieza. El factor externo $\frac{1}{\sqrt{\alpha_t}}$ reescala el resultado para deshacer la reducción de señal del paso directo. Dentro del paréntesis, $x_t$ es la entrada ruidosa que tenemos actualmente, y $\frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\, \epsilon_\theta(x_t, t)$ es la estimación del modelo de cuánto ruido está incrustado en $x_t$, apropiadamente escalada. La razón $\frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}$ aparece porque $\beta_t$ es el ruido añadido en este paso y $\sqrt{1 - \bar{\alpha}_t}$ es el ruido total acumulado hasta el paso $t$. Restar el ruido estimado de $x_t$ y reescalar nos da la media predicha de $x_{t-1}$.
Con la reparametrización de predicción de ruido, la pérdida de entrenamiento se vuelve notablemente simple:
Eso es todo. La pérdida es el error cuadrático medio entre el ruido real $\epsilon$ que se muestreó y la predicción del modelo $\epsilon_\theta(x_t, t)$. La esperanza es sobre tres variables aleatorias: un paso de tiempo muestreado uniformemente $t \sim \text{Uniform}(1, T)$, una imagen limpia de entrenamiento $x_0$ del dataset, y ruido gaussiano $\epsilon \sim \mathcal{N}(0, I)$. Esta pérdida se deriva de la cota inferior variacional (el ELBO) sobre la log-verosimilitud, pero Ho et al. demostraron que esta versión simplificada en realidad funciona mejor en la práctica que la cota teóricamente más ajustada.
El algoritmo de entrenamiento es directo. En cada iteración: (1) muestrea una imagen limpia $x_0$ de tu dataset, (2) muestrea un paso de tiempo aleatorio $t$, (3) muestrea ruido $\epsilon \sim \mathcal{N}(0, I)$, (4) crea la imagen ruidosa $x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \epsilon$, (5) alimenta $x_t$ y $t$ a la red neuronal para obtener $\epsilon_\theta(x_t, t)$, y (6) calcula la pérdida MSE entre $\epsilon$ y $\epsilon_\theta$. Retropropaga y repite. La red (típicamente una arquitectura U-Net) toma una imagen ruidosa y un embedding de paso de tiempo como entrada y produce una predicción de ruido con las mismas dimensiones que la imagen.
# model: U-Net that predicts noise given (x_t, t)
# alpha_bar: precomputed cumulative products
for epoch in range(num_epochs):
for x_0 in dataloader: # (1) clean images
t = randint(1, T) # (2) random timestep
eps = torch.randn_like(x_0) # (3) sample noise
# (4) create noisy image using closed-form shortcut
x_t = sqrt(alpha_bar[t]) * x_0 + sqrt(1 - alpha_bar[t]) * eps
# (5) predict the noise
eps_pred = model(x_t, t)
# (6) simple MSE loss
loss = mse_loss(eps, eps_pred)
loss.backward()
optimizer.step()
optimizer.zero_grad()
Para hacer esto tangible, el código a continuación demuestra la lógica directa e inversa en datos 1D. Tomamos una "señal" simple (un conjunto de números), le añadimos ruido según el proceso directo, y luego mostramos qué sucede cuando el ruido se predice perfectamente versus cuando se predice con algún error.
import math, json, js
# --- 1D diffusion demo: forward + reverse on scalar data ---
# Our "image" is a single value
x_0 = 3.0
# Noise schedule (linear, 20 steps for clarity)
T = 20
beta_start, beta_end = 0.01, 0.3
betas = [beta_start + (beta_end - beta_start) * t / (T - 1) for t in range(T)]
# Precompute alpha_bar
alpha_bar = []
cum = 1.0
for b in betas:
cum *= (1.0 - b)
alpha_bar.append(cum)
# Forward process: noise x_0 at each timestep
# Using a FIXED seed so results are reproducible
import random
random.seed(42)
# Sample one noise value (in real diffusion, epsilon ~ N(0,I))
# We'll use a fixed epsilon for the demo
eps_true = 0.7 # pretend this was sampled from N(0,1)
forward_signal = [x_0]
forward_noise_level = [0.0]
for t in range(T):
ab = alpha_bar[t]
x_t = math.sqrt(ab) * x_0 + math.sqrt(1 - ab) * eps_true
forward_signal.append(x_t)
forward_noise_level.append(1 - ab)
# Reverse process: if we know eps_true perfectly, recover x_0
# mu_theta = (1/sqrt(alpha_t)) * (x_t - beta_t/sqrt(1-alpha_bar_t) * eps_pred)
print("=== Forward Process (destroying signal) ===")
print(f"x_0 = {x_0:.4f} (clean)")
for t in [4, 9, 14, 19]:
ab = alpha_bar[t]
x_t = math.sqrt(ab) * x_0 + math.sqrt(1 - ab) * eps_true
print(f"x_{t+1:2d} = {x_t:.4f} (alpha_bar = {ab:.4f}, signal: {math.sqrt(ab)*100:.1f}%, noise: {math.sqrt(1-ab)*100:.1f}%)")
print()
print("=== Reverse Process (one step, t=10 -> t=9) ===")
t = 9 # 0-indexed
ab = alpha_bar[t]
alpha_t = 1 - betas[t]
x_t = math.sqrt(ab) * x_0 + math.sqrt(1 - ab) * eps_true
# Perfect noise prediction
eps_pred_perfect = eps_true
mu_perfect = (1/math.sqrt(alpha_t)) * (x_t - betas[t]/math.sqrt(1 - ab) * eps_pred_perfect)
# Imperfect noise prediction (20% error)
eps_pred_bad = eps_true * 1.2
mu_bad = (1/math.sqrt(alpha_t)) * (x_t - betas[t]/math.sqrt(1 - ab) * eps_pred_bad)
# What x_{t-1} should be
ab_prev = alpha_bar[t-1]
x_t_minus_1_true = math.sqrt(ab_prev) * x_0 + math.sqrt(1 - ab_prev) * eps_true
print(f"x_10 = {x_t:.4f}")
print(f"True x_9 = {x_t_minus_1_true:.4f}")
print(f"Predicted mean (perfect eps): mu = {mu_perfect:.4f}")
print(f"Predicted mean (20% eps error): mu = {mu_bad:.4f}")
print()
print("Key insight: the model only needs to predict the noise accurately.")
Muestreo: del ruido a la imagen
Una vez que el modelo está entrenado, generar una nueva imagen es cuestión de ejecutar el proceso inverso de principio a fin. Comenzamos muestreando ruido puro $x_T \sim \mathcal{N}(0, I)$ y luego iteramos hacia atrás desde $t = T$ hasta $t = 1$. En cada paso, el modelo predice el ruido $\epsilon_\theta(x_t, t)$, calculamos la media $\mu_\theta(x_t, t)$ usando la fórmula anterior, y luego muestreamos $x_{t-1}$ añadiendo una pequeña cantidad de ruido gaussiano fresco (para estocasticidad). El paso de muestreo es:
El primer término es la media predicha (restando el ruido predicho y reescalando), y el segundo término $\sigma_t z$ añade aleatoriedad fresca. Esta estocasticidad es importante: significa que diferentes semillas aleatorias producen diferentes imágenes, dándonos diversidad en la generación. En el último paso ($t = 1$), típicamente establecemos $z = 0$ (sin ruido añadido) para que la salida final sea determinista dada la trayectoria hasta ese punto.
El problema obvio es la velocidad . El DDPM original usa $T = 1000$ pasos, y cada paso requiere un pase completo hacia adelante a través de la red neuronal (una U-Net grande con cientos de millones de parámetros). Generar una sola imagen de 256x256 toma alrededor de 20 segundos en una GPU moderna. Compara esto con una GAN, que genera una imagen en un solo pase hacia adelante (milisegundos). Esta brecha de velocidad motivó una ola de investigación en muestreadores más rápidos.
El primer avance fue DDIM (Denoising Diffusion Implicit Models) (Song et al., 2020) , que reinterpreta el proceso inverso como una ODE determinista (sin ruido añadido en cada paso), permitiendo saltar pasos . En lugar de eliminar ruido a través de los 1000 pasos de tiempo, DDIM selecciona una subsecuencia (digamos, pasos 1000, 950, 900, ..., 50, 0) y salta entre ellos. Esto reduce 1000 evaluaciones de red neuronal a 50 o incluso 20, con solo una modesta caída en calidad. La idea clave es que la trayectoria determinista es más suave y predecible que la estocástica, por lo que son posibles saltos más grandes sin perder coherencia.
Los muestreadores modernos han llevado esto aún más lejos. DPM-Solver trata el proceso inverso como una ODE y usa métodos numéricos de orden superior (análogos a Runge-Kutta) para dar menos pasos pero más precisos. Los muestreadores Euler y Heun del marco de difusión basado en scores logran aceleraciones similares. Hoy en día, los generadores de imágenes de vanguardia como Stable Diffusion típicamente usan 20-50 pasos de muestreo, haciendo la generación lo suficientemente rápida para uso interactivo (1-3 segundos por imagen). El compromiso calidad-velocidad sigue siendo un área de investigación activa, pero la brecha se ha reducido dramáticamente desde el DDPM original de 1000 pasos.
# model: trained noise-prediction network
# Start from pure noise
x = torch.randn(1, 3, 256, 256) # random noise "image"
for t in reversed(range(1, T + 1)):
# Predict the noise in x_t
eps_pred = model(x, t)
# Compute predicted mean
mu = (1 / sqrt(alpha[t])) * (x - beta[t] / sqrt(1 - alpha_bar[t]) * eps_pred)
# Add stochastic noise (except at t=1)
if t > 1:
z = torch.randn_like(x)
x = mu + sigma[t] * z
else:
x = mu # final step: no noise
# x is now a generated image
El schedule de ruido
El schedule de ruido $\{\beta_t\}_{t=1}^{T}$ determina qué tan rápido se destruye la información durante el proceso directo, y tiene un efecto significativo en la calidad de generación. Un schedule que añade ruido demasiado agresivamente dedicará la mayoría de sus pasos operando sobre ruido casi puro, dando al modelo muy poca señal de la cual aprender. Un schedule que añade ruido demasiado lentamente desperdiciará muchos pasos donde casi nada cambia, requiriendo una cadena más larga (y más pasos de muestreo en tiempo de generación) sin beneficio.
El schedule lineal del artículo original de DDPM aumenta $\beta_t$ linealmente de $\beta_1 = 10^{-4}$ a $\beta_T = 0.02$. Es simple y efectivo, pero tiene un problema: la curva de $\bar{\alpha}_t$ cae demasiado abruptamente en el medio de la cadena. Como vimos en el gráfico anterior, para el paso 600-700 la señal casi ha desaparecido, lo que significa que los últimos 300-400 pasos del proceso directo (y los primeros 300-400 pasos del proceso inverso) se gastan en una región donde $x_t$ es casi indistinguible del ruido puro. El modelo no puede aprender mucho de estos pasos porque esencialmente no queda señal que predecir.
El schedule coseno , introducido por (Nichol & Dhariwal, 2021) , aborda esto diseñando el schedule de manera que $\bar{\alpha}_t$ siga una curva coseno, distribuyendo la destrucción de información más uniformemente a lo largo de los pasos de tiempo. Específicamente, definen $\bar{\alpha}_t = \frac{f(t)}{f(0)}$ donde $f(t) = \cos\left(\frac{t/T + s}{1 + s} \cdot \frac{\pi}{2}\right)^2$ y $s = 0.008$ es un pequeño desplazamiento que evita que $\beta_t$ sea demasiado pequeño cerca de $t = 0$. La curva resultante decae más gradualmente — la señal persiste más tiempo en la cadena, y la transición de "principalmente señal" a "principalmente ruido" es más suave. Esto significa que el modelo obtiene señal de entrenamiento útil a lo largo de un rango más amplio de pasos de tiempo.
El gráfico a continuación compara los dos schedules. Observa cómo el schedule coseno preserva la señal mucho más tiempo (la línea azul se mantiene más alta), dando al modelo más pasos donde se puede aprender eliminación de ruido significativa, mientras aún llega a casi cero al final de la cadena.
import math, json, js
T = 1000
# --- Linear schedule ---
beta_start, beta_end = 0.0001, 0.02
betas_linear = [beta_start + (beta_end - beta_start) * t / (T - 1) for t in range(T)]
alpha_bar_linear = []
cum = 1.0
for b in betas_linear:
cum *= (1.0 - b)
alpha_bar_linear.append(cum)
# --- Cosine schedule (Nichol & Dhariwal, 2021) ---
s = 0.008
def f_cos(t):
return math.cos(((t / T) + s) / (1 + s) * math.pi / 2) ** 2
alpha_bar_cosine = []
for t in range(T):
alpha_bar_cosine.append(f_cos(t + 1) / f_cos(0))
# Clip to avoid numerical issues
alpha_bar_cosine = [max(ab, 1e-5) for ab in alpha_bar_cosine]
timesteps = list(range(T))
plot_data = [
{
"title": "Linear vs Cosine noise schedule: alpha_bar_t over 1000 steps",
"x_label": "Timestep t",
"y_label": "alpha_bar_t (signal preserved)",
"x_data": timesteps,
"lines": [
{"label": "Linear schedule", "data": alpha_bar_linear, "color": "#ef4444"},
{"label": "Cosine schedule", "data": alpha_bar_cosine, "color": "#3b82f6"}
]
}
]
js.window.py_plot_data = json.dumps(plot_data)
# Find the timestep where alpha_bar drops below 0.1
for name, ab_list in [("Linear", alpha_bar_linear), ("Cosine", alpha_bar_cosine)]:
for t, ab in enumerate(ab_list):
if ab < 0.1:
print(f"{name}: alpha_bar drops below 0.1 at step {t}")
break
# Compare signal at t=500
print(f"\nAt t=500:")
print(f" Linear: alpha_bar = {alpha_bar_linear[499]:.4f} ({math.sqrt(alpha_bar_linear[499])*100:.1f}% signal)")
print(f" Cosine: alpha_bar = {alpha_bar_cosine[499]:.4f} ({math.sqrt(alpha_bar_cosine[499])*100:.1f}% signal)")
La elección del schedule también se conecta con la relación señal-ruido (SNR) en cada paso de tiempo, definida como $\text{SNR}(t) = \bar{\alpha}_t / (1 - \bar{\alpha}_t)$. El schedule lineal crea una región en el medio donde la SNR cae muy rápidamente, lo que significa que el modelo debe aprender una transición abrupta. El schedule coseno distribuye esta transición más uniformemente en el espacio log-SNR, lo que empíricamente conduce a mejor calidad de muestras, especialmente para imágenes a resoluciones más bajas donde los detalles finos importan a lo largo de todo el proceso.
Quiz
Pon a prueba tu comprensión del marco de modelos de difusión.
En el proceso directo, ¿qué representa el producto acumulativo $\bar{\alpha}_t = \prod_{s=1}^{t} \alpha_s$?
¿Qué aprende a predecir la red neuronal de DDPM?
¿Por qué es esencial la fórmula en forma cerrada $x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \epsilon$ para el entrenamiento?
¿Cuál es la principal ventaja del schedule de ruido coseno sobre el schedule lineal?