📖 Teoría

Generative Adversarial Networks

El juego entre generador y discriminador: desde la intuición del falsificador y el detective hasta StyleGAN, ESRGAN y Real-ESRGAN. Fundamentos matemáticos (minimax, divergencias, WGAN), entrenamiento práctico con código PyTorch y TensorFlow, aplicaciones, modelos avanzados, y la relación de las GANs con el ecosistema generativo moderno.

⚔️ ¿Qué son las GANs?

Las Generative Adversarial Networks (GANs) son una familia de modelos generativos donde dos redes neuronales compiten entre sí en un juego de suma cero: un generador G intenta crear datos indistinguibles de los reales, mientras que un discriminador D intenta detectar las falsificaciones.

💡

La idea clave: Dos redes enfrentadas que se mejoran mutuamente. El generador aprende a producir datos cada vez más realistas porque el discriminador le «enseña» qué falla. El discriminador se vuelve cada vez más exigente porque el generador cada vez es mejor. Este bucle de retroalimentación adversarial es lo que hace a las GANs tan poderosas.

A diferencia de los autoencoders que aprenden a reconstruir, las GANs aprenden a crear de la nada: generan muestras completamente nuevas a partir de ruido aleatorio, sin necesidad de una entrada de referencia.

📜 Historia de las GANs

Las GANs fueron propuestas por Ian Goodfellow y colaboradores en su paper seminal "Generative Adversarial Nets" (NeurIPS 2014). Según la leyenda, la idea surgió durante una discusión en un bar de Montreal: ¿podría una red neuronal aprender a generar datos realistas si otra red le dice cuándo falla?

2014
GAN original — Goodfellow et al. MLP simple, imágenes borrosas pero la idea es revolucionaria. 📄 Generative Adversarial Nets, NeurIPS 2014
2015
DCGAN — Radford, Metz & Chintala. Convoluciones profundas + técnicas de estabilización. Primera GAN que genera imágenes reconocibles (caras, dormitorios). 📄 Unsupervised Representation Learning with DCGANs, ICLR 2016
2017
WGAN — Arjovsky, Chintala & Bottou. Introduce la distancia Wasserstein y resuelve problemas de entrenamiento fundamentales. 📄 Wasserstein GAN, ICML 2017
2017
Pix2Pix — Isola et al. Traducción imagen-a-imagen con GANs condicionales. 📄 Image-to-Image Translation with Conditional Adversarial Networks, CVPR 2017
2017
CycleGAN — Zhu et al. Traducción sin datos pareados (caballo ↔ cebra). 📄 Unpaired Image-to-Image Translation using Cycle-Consistent ANs, ICCV 2017
2018
ProGAN — Karras et al. Entrenamiento progresivo: genera caras de 1024×1024. 📄 Progressive Growing of GANs, ICLR 2018
2019
StyleGAN / StyleGAN2 — Karras et al. (NVIDIA). Control estilístico sin precedentes. Genera caras fotorrealistas que no existen. 📄 A Style-Based Generator Architecture for GANs, CVPR 2019
2021
Real-ESRGAN — Wang et al. Super-resolución ciega para imágenes reales degradadas. 📄 Real-ESRGAN: Training Real-World Blind Super-Resolution, ICCVW 2021
2021+
Era de la difusión — Los modelos de difusión (ver sección) superan a las GANs en calidad FID, pero las GANs siguen siendo fundamentales en super-resolución, edición y aplicaciones en tiempo real.
📊

Dato: Yann LeCun calificó las GANs como "the most interesting idea in the last 10 years in machine learning" (2016). Entre 2014 y 2021, se publicaron más de 10.000 papers relacionados con GANs.

🎭 La intuición: el falsificador y el detective

La analogía más conocida compara las GANs con un falsificador de billetes (el generador) y un detective (el discriminador):

z ~ N(0,I) ruido 🎨 GENERADOR G(z; θ_g) "falsificador" FAKE G(z) REAL x ~ p_data 🔍 DISCRIMINADOR D(x; θ_d) "detective" Real? P(real) ∈ [0,1] gradientes adversariales (feedback)
🎨 Generador G

Recibe ruido aleatorio z y produce datos sintéticos G(z). Su objetivo: engañar al discriminador para que clasifique sus creaciones como reales. No ve nunca los datos reales directamente — aprende solo a través de los gradientes de D.

🔍 Discriminador D

Recibe datos reales x y datos falsos G(z), y debe clasificar cada uno como real o falso. Devuelve D(x) ∈ [0,1] — la probabilidad de que la entrada sea real.

📐 Definición formal: el juego minimax

Goodfellow formalizó las GANs como un juego minimax entre dos jugadores. La función objetivo (value function) es:

$$ \min_G \max_D \; V(D, G) = \mathbb{E}_{x \sim p_{\text{data}}} [\log D(x)] + \mathbb{E}_{z \sim p_z} [\log(1 - D(G(z)))] $$

Desglosemos cada término:

1
\( \mathbb{E}_{x \sim p_{\text{data}}} [\log D(x)] \) — El discriminador quiere maximizar este término: que D(x) sea cercano a 1 para datos reales, haciendo log D(x) ≈ 0 (lo más alto posible).
2
\( \mathbb{E}_{z \sim p_z} [\log(1 - D(G(z)))] \) — Para datos falsos G(z): el discriminador quiere D(G(z)) ≈ 0 (maximizando log(1 − 0) = 0), mientras que el generador quiere D(G(z)) ≈ 1 (minimizando log(1 − 1) → −∞).
3
Equilibrio de Nash: El juego converge cuando \( p_g = p_{\text{data}} \) y \( D(x) = \frac{1}{2} \) para todo x. En ese punto, el discriminador no puede distinguir real de falso — está al 50%, como lanzar una moneda.

Para un generador G fijo, la función objetivo respecto a D es:

$$ V(D, G) = \int_x \left[ p_{\text{data}}(x) \log D(x) + p_g(x) \log(1 - D(x)) \right] dx $$

Para cada x, la función \( f(y) = a \log y + b \log(1 - y) \) alcanza su máximo en:

$$ D^*(x) = \frac{p_{\text{data}}(x)}{p_{\text{data}}(x) + p_g(x)} $$

Derivando e igualando a cero: \( \frac{a}{y} - \frac{b}{1-y} = 0 \Rightarrow y = \frac{a}{a+b} \).

Sustituyendo D* en V(D*,G) se obtiene:

$$ V(D^*, G) = -\log 4 + 2 \cdot D_{JS}(p_{\text{data}} \| p_g) $$

donde \( D_{JS} \) es la divergencia de Jensen-Shannon. El mínimo global de V(D*,G) se alcanza si y solo si \( p_g = p_{\text{data}} \), y en ese punto \( V = -\log 4 \).

🔄 Entrenamiento supervisado vs adversarial

El entrenamiento de una GAN es fundamentalmente diferente al entrenamiento supervisado clásico. Entender estas diferencias es crucial:

Aspecto Supervisado clásico Adversarial (GAN)
Objetivo Minimizar una loss fija (ej: cross-entropy) Juego minimax: dos redes con objetivos opuestos
Labels Se necesitan etiquetas (yi) del dataset No supervisado: no requiere etiquetas
Función de loss Fija (MSE, CE, etc.) — landscape estático La loss cambia en cada paso porque D se actualiza
Convergencia Convergencia garantizada (convexo/suave) Sin garantías; puede oscilar, colapsar o divergir
Métrica de éxito Loss ↓, accuracy ↑, métricas claras FID, IS — métricas indirectas; la loss de G/D no es fiable
Nº de modelos 1 modelo, 1 optimizador 2 modelos, 2 optimizadores que deben mantenerse en equilibrio
Analogía Un estudiante con exámenes corregidos Un falsificador y un detective que evolucionan juntos
⚠️

La loss landscape cambiante: En entrenamiento supervisado, la loss function define un paisaje fijo que el optimizador recorre. En GANs, el paisaje se mueve bajo tus pies en cada paso — porque al actualizar D, la loss de G cambia, y viceversa. Esto hace que las GANs sean notoriamente difíciles de entrenar.

SUPERVISADO landscape fijo → converge ✓ ADVERSARIAL landscape cambiante → puede oscilar ⚠️

⚖️ GANs vs VAEs: dos filosofías generativas

Como vimos en la sección de autoencoders, los VAEs también son modelos generativos. Pero el enfoque es radicalmente distinto:

Criterio VAE GAN
Enfoque Modelar explícitamente p(x) vía ELBO Juego adversarial — p(x) implícita
Training Estable (maximizar ELBO) Inestable (equilibrio dinámico)
Calidad Borroso (penalización de reconstrucción pixel a pixel) Nítido, fotorrealista
Espacio latente Suave, interpolable, estructurado Menos estructurado, pero generable
Diversidad Alta (cubre bien la distribución) Riesgo de mode collapse
Evaluación ELBO (métrica directa) FID, IS (métricas externas)
🔗

Convergencia: Los VAEs y las GANs no son excluyentes. Modelos como VAE-GAN (Larsen et al., 2016) combinan un encoder VAE con un discriminador GAN para obtener lo mejor de ambos mundos: espacio latente estructurado + imágenes nítidas. Además, el VQ-VAE + GAN es la base de modelos como DALL-E. → Ver sección VAE

🎯 Objetivo del Discriminador

El discriminador D es un clasificador binario: recibe una muestra y devuelve la probabilidad de que sea real. Su objetivo es maximizar la log-verosimilitud de clasificar correctamente datos reales y falsos:

$$ \max_D \; \mathbb{E}_{x \sim p_{\text{data}}} [\log D(x)] + \mathbb{E}_{z \sim p_z} [\log(1 - D(G(z)))] $$

Esto es equivalente a binary cross-entropy con etiquetas: real = 1, fake = 0. Para cada paso de entrenamiento de D:

1
Samplear un mini-batch de datos reales \( \{x^{(1)}, \ldots, x^{(m)}\} \sim p_{\text{data}} \)
2
Samplear ruido \( \{z^{(1)}, \ldots, z^{(m)}\} \sim p_z(z) \) y generar datos falsos \( G(z^{(i)}) \)
3
Actualizar D mediante ascenso de gradiente:
$$ \nabla_{\theta_d} \frac{1}{m} \sum_{i=1}^{m} \left[ \log D(x^{(i)}) + \log(1 - D(G(z^{(i)}))) \right] $$

🎨 Objetivo del Generador

El generador G quiere que D clasifique sus muestras como reales. Formalmente, quiere minimizar la función de valor:

$$ \min_G \; \mathbb{E}_{z \sim p_z} [\log(1 - D(G(z)))] $$
⚠️

Problema de gradientes saturados: Al inicio del entrenamiento, G es malo y D lo rechaza fácilmente, así que \( D(G(z)) \approx 0 \) y \( \log(1 - D(G(z))) \approx \log(1) = 0 \). El gradiente es casi cero — G no aprende nada.

Solución práctica: En vez de minimizar \( \log(1 - D(G(z))) \), maximizamos \( \log D(G(z)) \). Esto da gradientes más fuertes al inicio:

$$ \max_G \; \mathbb{E}_{z \sim p_z} [\log D(G(z))] $$

Este truco se conoce como non-saturating loss y es el que se usa en la práctica. Ambas formulaciones tienen el mismo punto fijo (\( p_g = p_{\text{data}} \)), pero el paisaje de gradientes es muy diferente al inicio.

log(1 − D(G(z))) — Saturating D(G(z)) ⚠️ gradiente ≈ 0 −log D(G(z)) — Non-saturating D(G(z)) ✅ gradiente fuerte

📊 Divergencias: KL, JS y Wasserstein

Las GANs pueden entenderse como una forma de minimizar una divergencia entre la distribución real \( p_{\text{data}} \) y la distribución generada \( p_g \). Cada variante de GAN se diferencia, entre otras cosas, por la divergencia que optimiza:

KL
KL Divergence: \( D_{KL}(p \| q) = \mathbb{E}_{x \sim p}\left[\log \frac{p(x)}{q(x)}\right] \). No es simétrica (\( D_{KL}(p \| q) \neq D_{KL}(q \| p) \)). Se usa en VAEs (→ ver sección) pero no directamente en GANs.
JS
Jensen-Shannon Divergence: \( D_{JS}(p \| q) = \frac{1}{2} D_{KL}(p \| m) + \frac{1}{2} D_{KL}(q \| m) \) donde \( m = \frac{p+q}{2} \). Simétrica, acotada en [0, log 2]. Es lo que minimiza la GAN original (como vimos en la demostración de D*).
W
Wasserstein Distance (Earth Mover's): \( W(p, q) = \inf_{\gamma \in \Pi(p,q)} \mathbb{E}_{(x,y) \sim \gamma}[\|x - y\|] \). Mide el «coste mínimo de transporte» para transformar p en q. Más suave, no se satura — es la base de WGAN.

Cuando \( p_{\text{data}} \) y \( p_g \) tienen soportes disjuntos (no se solapan en el espacio de alta dimensión — algo muy común al inicio del entrenamiento), la JS divergence vale exactamente \( \log 2 \) — una constante. Esto significa que el gradiente es exactamente cero: G no recibe señal útil.

La distancia de Wasserstein, en cambio, sigue siendo informativa incluso cuando las distribuciones no se solapan: mide cuánto hay que «mover» una para convertirla en la otra. Por eso WGAN resolvió uno de los problemas más graves de estabilidad de las GANs.

$$ \text{JS: } D_{JS}(p_{\text{data}} \| p_g) = \log 2 \quad \text{(soportes disjuntos)} $$ $$ \text{Wasserstein: } W(p_{\text{data}}, p_g) = d \quad \text{(proporcional a la distancia)} $$

📐 WGAN: Wasserstein GAN

WGAN (Arjovsky et al., 2017) reemplaza la JS divergence por la distancia de Wasserstein, resolviendo los problemas de gradientes y estabilidad:

$$ \min_G \max_{D \in \mathcal{D}} \; \mathbb{E}_{x \sim p_{\text{data}}}[D(x)] - \mathbb{E}_{z \sim p_z}[D(G(z))] $$

donde \( \mathcal{D} \) es el conjunto de funciones 1-Lipschitz. Notas clave:

🔢 Crítico, no discriminador

D ya no produce una probabilidad ∈ [0,1] — devuelve un score real sin sigmoid. Por eso se le llama critic en vez de discriminator. La salida puede ser cualquier valor real.

📏 Restricción Lipschitz

La función D debe ser 1-Lipschitz: \( |D(x_1) - D(x_2)| \leq \|x_1 - x_2\| \). En WGAN original se usa weight clipping (pesos acotados a [−c, c]). WGAN-GP usa gradient penalty, mucho más estable.

WGAN-GP (Gulrajani et al., 2017) añade un término de penalización al gradiente:

$$ \mathcal{L}_{\text{GP}} = \lambda \, \mathbb{E}_{\hat{x} \sim p_{\hat{x}}} \left[ \left( \|\nabla_{\hat{x}} D(\hat{x})\|_2 - 1 \right)^2 \right] $$

donde \( \hat{x} = \epsilon x + (1-\epsilon)G(z) \) con \( \epsilon \sim U(0,1) \) — es decir, puntos interpolados entre datos reales y generados. Típicamente \( \lambda = 10 \). La loss total del critic es:

$$ \mathcal{L}_D = \underbrace{\mathbb{E}[D(G(z))] - \mathbb{E}[D(x)]}_{\text{Wasserstein}} + \underbrace{\lambda \, \mathbb{E}\left[(\|\nabla D(\hat{x})\|_2 - 1)^2\right]}_{\text{gradient penalty}} $$

🌊 Convergencia y estabilidad

La convergencia de las GANs es uno de los problemas más estudiados en deep learning. Los principales desafíos:

💥 Mode Collapse

G aprende a generar solo unas pocas muestras que engañan a D, ignorando el resto de la distribución. Si entrenamos con MNIST, G podría generar solo «7»s perfectos e ignorar los otros 9 dígitos.

🔄 Training oscillation

G y D oscilan sin converger: D se vuelve muy bueno → G colapsa; G mejora → D pierde → G empeora. Las loss curves oscilan indefinidamente sin mejorar.

📉 Vanishing gradients

Si D es demasiado bueno, clasifica todo perfectamente y los gradientes que llegan a G son prácticamente cero — G deja de aprender. Hay que mantener a D «suficientemente bueno pero no perfecto».

💀 D too strong / too weak

El equilibrio entre G y D es delicado. Si D es demasiado débil, G no recibe señal útil. Si es demasiado fuerte, los gradientes se saturan. Encontrar el balance es un arte.

p_data (distribución real) múltiples modos ✓ p_g (mode collapse) ¡solo 1 modo! ✗
🧠

Resumen convergencia: Teóricamente, el equilibrio de Nash existe. Prácticamente, SGD con dos jugadores no garantiza convergencia a un equilibrio — puede oscilar o divergir. Por eso el entrenamiento de GANs requiere tantos trucos heurísticos, como veremos en la sección de entrenamiento.

📏 Métricas de evaluación

A diferencia de los modelos supervisados, evaluar una GAN no es trivial. Las dos métricas más usadas:

📊 FID (Fréchet Inception Distance)
$$ \text{FID} = \|\mu_r - \mu_g\|^2 + \text{Tr}(\Sigma_r + \Sigma_g - 2(\Sigma_r \Sigma_g)^{1/2}) $$

Compara las estadísticas (media y covarianza) de features de InceptionV3 entre imágenes reales y generadas. Menor es mejor. Captura calidad y diversidad.

📈 IS (Inception Score)
$$ \text{IS} = \exp(\mathbb{E}_{x \sim p_g}[D_{KL}(p(y|x) \| p(y))]) $$

Mide dos cosas: (1) cada imagen genera predicciones seguras (\( p(y|x) \) concentrada) y (2) hay variedad (la marginal \( p(y) \) es uniforme). Mayor es mejor. No requiere datos reales como referencia.

💡

Limitaciones: Ambas métricas usan InceptionV3 (entrenada en ImageNet), por lo que son parciales. FID es más fiable que IS para detectar mode collapse. Para dominios fuera de imágenes naturales (médicas, artísticas, etc.), estas métricas pueden no reflejar bien la calidad percibida.

⚙️ Algoritmo de entrenamiento

El entrenamiento de una GAN alterna entre actualizar D y actualizar G. En cada iteración:

PASO 1: Entrenar D (k veces) for _ in range(k): 1. Samplear batch real x ~ p_data 2. Samplear ruido z ~ N(0,I) 3. Generar fake = G(z) [G congelado] 4. Loss_D = -[log D(x) + log(1-D(fake))] 5. Backprop → actualizar θ_d 💡 k=1 en la práctica (Goodfellow recomienda) k>1 → D más fuerte, gradientes más útiles para G PASO 2: Entrenar G (1 vez) una vez: 1. Samplear ruido z ~ N(0,I) 2. Generar fake = G(z) 3. Loss_G = -log D(fake) [D congelado] 4. Backprop → actualizar θ_g ⚠️ Non-saturating loss: maximizar log D(G(z)) en vez de minimizar log(1−D(G(z)))
🔑

Regla de oro: Nunca entrenes G y D juntos en el mismo paso. Siempre congela uno mientras entrenas el otro. En PyTorch esto se hace con requires_grad = False o .detach(); en TensorFlow con trainable = False o tf.stop_gradient().

🎯 Trucos y buenas prácticas

Décadas de investigación han producido un conjunto de técnicas que mejoran drásticamente la estabilidad del entrenamiento:

Truco Qué hace Por qué funciona
Batch Normalization Normalizar activaciones entre capas Estabiliza gradientes, permite learning rates más altos
Spectral Normalization Normalizar pesos por su mayor valor singular Garantiza restricción Lipschitz sin gradient penalty
Label smoothing Usar 0.9 en vez de 1.0 para «real» Evita que D sea demasiado confiado → gradientes más suaves
Learning rates diferentes lr_D < lr_G (ej: 0.0001 vs 0.0002) Evita que D se vuelva demasiado fuerte demasiado rápido
Adam β₁ = 0.5 Reducir el momentum de Adam Menos «inercia» → responde más rápido a los cambios del juego
LeakyReLU en D Usar LeakyReLU(0.2) en vez de ReLU Evita neuronas muertas → gradientes más informativos
Convolutions (no pooling) Strided convolutions en D, transposed en G El modelo aprende su propio downsampling/upsampling
Tanh en salida de G Salida en [-1, 1]; datos normalizados igual Matching de rango entre datos reales y generados

💥 Mode Collapse en detalle

El mode collapse es el problema más frustrante de las GANs. Se manifiesta cuando el generador produce poca variedad:

1
Collapse parcial: G genera muestras de solo algunos modos de la distribución. Ejemplo: genera dígitos 1, 7 y 9, pero nunca 0, 2, 3, 4, 5, 6, 8.
2
Collapse total: G mapea todo el espacio latente z al mismo punto (o casi). Todas las muestras generadas son prácticamente idénticas.
3
Cycling: G alterna entre modos: genera «7»s durante un rato, luego cambia a «3»s, luego a «1»s — nunca aprende a generar todo simultáneamente.

Soluciones:

  • Minibatch discrimination: D recibe información sobre todo el batch, no solo muestras individuales
  • Unrolled GANs: G optimiza considerando k pasos futuros de D, no solo el estado actual
  • WGAN / WGAN-GP: La distancia Wasserstein es más suave y reduce mode collapse
  • Feature matching: G minimiza la diferencia de features intermedias de D, no la salida final
  • Diversity loss: Penalizar explícitamente la baja varianza en las muestras generadas

🧪 Widget: Simulador de entrenamiento GAN

Experimenta con los hiperparámetros del entrenamiento y observa cómo afectan al equilibrio G vs D:

⚔️ Simulador GAN: equilibrio G vs D
0.0002
0.0001
1
1.00

💻 Implementación completa: DCGAN

Veamos una implementación completa de DCGAN (Deep Convolutional GAN) — la arquitectura estándar para empezar con GANs. Genera imágenes de 64×64 a partir de ruido.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# ─── Hiperparámetros ───
LATENT_DIM = 100
IMG_CHANNELS = 1    # 1 para MNIST, 3 para color
FEATURES_G = 64
FEATURES_D = 64
LR = 0.0002
BETA1 = 0.5
BATCH_SIZE = 128
EPOCHS = 50

# ─── Generador ───
class Generator(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            # z: (batch, 100, 1, 1) → (batch, 512, 4, 4)
            nn.ConvTranspose2d(LATENT_DIM, FEATURES_G * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(FEATURES_G * 8),
            nn.ReLU(True),

            # → (batch, 256, 8, 8)
            nn.ConvTranspose2d(FEATURES_G * 8, FEATURES_G * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(FEATURES_G * 4),
            nn.ReLU(True),

            # → (batch, 128, 16, 16)
            nn.ConvTranspose2d(FEATURES_G * 4, FEATURES_G * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(FEATURES_G * 2),
            nn.ReLU(True),

            # → (batch, 64, 32, 32)
            nn.ConvTranspose2d(FEATURES_G * 2, FEATURES_G, 4, 2, 1, bias=False),
            nn.BatchNorm2d(FEATURES_G),
            nn.ReLU(True),

            # → (batch, 1, 64, 64)
            nn.ConvTranspose2d(FEATURES_G, IMG_CHANNELS, 4, 2, 1, bias=False),
            nn.Tanh()   # salida en [-1, 1]
        )

    def forward(self, z):
        return self.net(z)

# ─── Discriminador ───
class Discriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            # (batch, 1, 64, 64) → (batch, 64, 32, 32)
            nn.Conv2d(IMG_CHANNELS, FEATURES_D, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),

            # → (batch, 128, 16, 16)
            nn.Conv2d(FEATURES_D, FEATURES_D * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(FEATURES_D * 2),
            nn.LeakyReLU(0.2, inplace=True),

            # → (batch, 256, 8, 8)
            nn.Conv2d(FEATURES_D * 2, FEATURES_D * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(FEATURES_D * 4),
            nn.LeakyReLU(0.2, inplace=True),

            # → (batch, 512, 4, 4)
            nn.Conv2d(FEATURES_D * 4, FEATURES_D * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(FEATURES_D * 8),
            nn.LeakyReLU(0.2, inplace=True),

            # → (batch, 1, 1, 1)
            nn.Conv2d(FEATURES_D * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x).view(-1)

# ─── Inicialización de pesos (DCGAN paper) ───
def weights_init(m):
    if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
        nn.init.normal_(m.weight, 0.0, 0.02)
    elif isinstance(m, nn.BatchNorm2d):
        nn.init.normal_(m.weight, 1.0, 0.02)
        nn.init.zeros_(m.bias)

# ─── Setup ───
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

G = Generator().to(device)
D = Discriminator().to(device)
G.apply(weights_init)
D.apply(weights_init)

opt_G = optim.Adam(G.parameters(), lr=LR, betas=(BETA1, 0.999))
opt_D = optim.Adam(D.parameters(), lr=LR, betas=(BETA1, 0.999))
criterion = nn.BCELoss()

# Dataset: MNIST redimensionado a 64x64
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.ToTensor(),
    transforms.Normalize([0.5], [0.5]),  # → [-1, 1]
])
dataset = datasets.MNIST(root="./data", train=True, transform=transform, download=True)
loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)

# Ruido fijo para visualizar progreso
fixed_noise = torch.randn(64, LATENT_DIM, 1, 1, device=device)

# ─── Training loop ───
for epoch in range(EPOCHS):
    for i, (real, _) in enumerate(loader):
        real = real.to(device)
        batch_size = real.size(0)

        # ── Entrenar D ──
        D.zero_grad()
        # Real
        label_real = torch.ones(batch_size, device=device)    # o 0.9 con label smoothing
        output_real = D(real)
        loss_D_real = criterion(output_real, label_real)

        # Fake
        noise = torch.randn(batch_size, LATENT_DIM, 1, 1, device=device)
        fake = G(noise).detach()    # ¡detach! No queremos gradientes en G
        label_fake = torch.zeros(batch_size, device=device)
        output_fake = D(fake)
        loss_D_fake = criterion(output_fake, label_fake)

        loss_D = loss_D_real + loss_D_fake
        loss_D.backward()
        opt_D.step()

        # ── Entrenar G ──
        G.zero_grad()
        noise = torch.randn(batch_size, LATENT_DIM, 1, 1, device=device)
        fake = G(noise)
        output = D(fake)
        # Non-saturating: maximizar log D(G(z)) = minimizar -log D(G(z))
        loss_G = criterion(output, label_real)  # queremos que D diga "real"
        loss_G.backward()
        opt_G.step()

    print(f"Epoch [{epoch+1}/{EPOCHS}] Loss_D: {loss_D:.4f} Loss_G: {loss_G:.4f}")

    # Generar muestras de evaluación
    with torch.no_grad():
        samples = G(fixed_noise).cpu()
        # Guardar/visualizar samples...
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np

# ─── Hiperparámetros ───
LATENT_DIM = 100
IMG_SHAPE = (64, 64, 1)
LR = 0.0002
BETA_1 = 0.5
BATCH_SIZE = 128
EPOCHS = 50

# ─── Generador ───
def build_generator():
    model = keras.Sequential([
        # z → (4, 4, 512)
        layers.Dense(4 * 4 * 512, use_bias=False, input_shape=(LATENT_DIM,)),
        layers.Reshape((4, 4, 512)),
        layers.BatchNormalization(),
        layers.ReLU(),

        # → (8, 8, 256)
        layers.Conv2DTranspose(256, 4, strides=2, padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.ReLU(),

        # → (16, 16, 128)
        layers.Conv2DTranspose(128, 4, strides=2, padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.ReLU(),

        # → (32, 32, 64)
        layers.Conv2DTranspose(64, 4, strides=2, padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.ReLU(),

        # → (64, 64, 1)
        layers.Conv2DTranspose(1, 4, strides=2, padding='same', use_bias=False,
                               activation='tanh'),
    ], name='generator')
    return model

# ─── Discriminador ───
def build_discriminator():
    model = keras.Sequential([
        # (64, 64, 1) → (32, 32, 64)
        layers.Conv2D(64, 4, strides=2, padding='same', input_shape=IMG_SHAPE),
        layers.LeakyReLU(0.2),

        # → (16, 16, 128)
        layers.Conv2D(128, 4, strides=2, padding='same'),
        layers.BatchNormalization(),
        layers.LeakyReLU(0.2),

        # → (8, 8, 256)
        layers.Conv2D(256, 4, strides=2, padding='same'),
        layers.BatchNormalization(),
        layers.LeakyReLU(0.2),

        # → (4, 4, 512)
        layers.Conv2D(512, 4, strides=2, padding='same'),
        layers.BatchNormalization(),
        layers.LeakyReLU(0.2),

        # → 1
        layers.Flatten(),
        layers.Dense(1, activation='sigmoid'),
    ], name='discriminator')
    return model

# ─── Setup ───
generator = build_generator()
discriminator = build_discriminator()
cross_entropy = keras.losses.BinaryCrossentropy()
opt_G = keras.optimizers.Adam(LR, beta_1=BETA_1)
opt_D = keras.optimizers.Adam(LR, beta_1=BETA_1)

# ─── Train step ───
@tf.function
def train_step(real_images):
    batch_size = tf.shape(real_images)[0]
    noise = tf.random.normal([batch_size, LATENT_DIM])

    with tf.GradientTape() as tape_d, tf.GradientTape() as tape_g:
        # Generar imágenes falsas
        fake_images = generator(noise, training=True)

        # Discriminador evalúa ambas
        real_output = discriminator(real_images, training=True)
        fake_output = discriminator(fake_images, training=True)

        # Loss D: real → 1, fake → 0
        loss_d_real = cross_entropy(tf.ones_like(real_output), real_output)
        loss_d_fake = cross_entropy(tf.zeros_like(fake_output), fake_output)
        loss_d = loss_d_real + loss_d_fake

        # Loss G: queremos que D diga "real" a nuestras fake
        loss_g = cross_entropy(tf.ones_like(fake_output), fake_output)

    # Actualizar D
    grads_d = tape_d.gradient(loss_d, discriminator.trainable_variables)
    opt_D.apply_gradients(zip(grads_d, discriminator.trainable_variables))

    # Actualizar G
    grads_g = tape_g.gradient(loss_g, generator.trainable_variables)
    opt_G.apply_gradients(zip(grads_g, generator.trainable_variables))

    return loss_d, loss_g

# ─── Dataset: MNIST ───
(train_images, _), (_, _) = keras.datasets.mnist.load_data()
train_images = train_images.reshape(-1, 28, 28, 1).astype("float32")
train_images = tf.image.resize(train_images, (64, 64))
train_images = (train_images - 127.5) / 127.5  # → [-1, 1]
dataset = tf.data.Dataset.from_tensor_slices(train_images)
dataset = dataset.shuffle(60000).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)

# Ruido fijo para evaluación
fixed_noise = tf.random.normal([16, LATENT_DIM])

# ─── Training loop ───
for epoch in range(EPOCHS):
    for batch in dataset:
        loss_d, loss_g = train_step(batch)

    print(f"Epoch {epoch+1}/{EPOCHS} - D: {loss_d:.4f}, G: {loss_g:.4f}")

    # Generar muestras
    samples = generator(fixed_noise, training=False)
    # Guardar/visualizar...

📈 Monitorización del entrenamiento

A diferencia del entrenamiento supervisado, la loss de una GAN no baja monótonamente. Aquí las claves para saber si el entrenamiento va bien:

Señal Significado Qué hacer
Loss D ≈ 0.5−1.0, Loss G ≈ 1−3 Equilibrio saludable — D y G están parejos ✅ Seguir entrenando, inspeccionar muestras visualmente
Loss D → 0 rápidamente D es demasiado fuerte — clasifica todo perfectamente Reducir lr_D, añadir label smoothing, más ruido al input de D
Loss G no baja / sube G no recibe gradientes útiles (vanishing) Verificar non-saturating loss, reducir lr_D, WGAN
Ambas losses oscilan mucho Inestabilidad — lr demasiado alto o arquitectura desequilibrada Reducir lr, añadir spectral normalization
Muestras repetitivas Mode collapse WGAN-GP, minibatch discrimination, cambiar arquitectura
FID baja progresivamente La calidad mejora de forma real ✅ Buen indicador — mejor que mirar las losses
📸

La mejor métrica es visual: Guarda muestras periódicamente con un fixed_noise (el mismo vector z en cada época). Así puedes ver cómo las muestras evolucionan — la calidad visual dice más que las loss curves. También puedes crear un GIF con la evolución de las muestras a lo largo del entrenamiento.

🖼️ Aplicaciones en imágenes

Las GANs han tenido un impacto extraordinario en el procesamiento de imágenes. Estas son las aplicaciones más destacadas:

🧑
Generación de caras
Caras fotorrealistas que no existen. StyleGAN genera caras indistinguibles de fotos reales.
z ~ N(0,I) cara 1024²
🔍
Super-resolución
SRGAN, ESRGAN: aumentar resolución de imágenes sin perder detalle.
img 64×64 img 256×256
🎨
Transferencia de estilo
Transformar fotos al estilo de un pintor, época o dominio visual.
foto + estilo foto estilizada
🖌️
Inpainting
Rellenar regiones faltantes de una imagen de forma coherente.
img + máscara img completa
🔄
Traducción imagen
Pix2Pix, CycleGAN: transformar entre dominios (día→noche, sketch→foto).
dominio A dominio B
👴→👦
Edición facial
Cambiar edad, expresión, gafas, pelo... manipulando el espacio latente.
cara + attr cara editada

🌐 Más allá de las imágenes

Aunque las GANs se asocian principalmente con imágenes, se han aplicado con éxito en muchos otros dominios:

Dominio Modelo / Técnica Descripción Paper / Referencia
🔤 Texto SeqGAN, TextGAN Generación de texto — más difícil que imágenes por la naturaleza discreta Yu et al., 2017 (AAAI)
🎵 Audio WaveGAN, GANSynth Generación de audio realista, síntesis musical, text-to-speech Donahue et al., 2019 (ICLR)
🎬 Vídeo DVD-GAN, MoCoGAN Generación de vídeo, predicción de frames, deepfakes Clark et al., 2019 (arXiv)
🏥 Medicina MedGAN, Pix2Pix Aumento de datos médicos (CT, MRI), síntesis de historiales Choi et al., 2017 (ML4H)
💊 Fármacos MolGAN, ORGAN Generación de moléculas con propiedades deseadas De Cao & Kipf, 2018 (ICML-W)
🗺️ Datos tabulares CTGAN, TVAE Datos sintéticos para tablas con columnas mixtas (categóricas + numéricas) Xu et al., 2019 (NeurIPS)
🎮 3D 3D-GAN, GET3D Generación de objetos 3D, texturas, escenas Gao et al., 2022 (NeurIPS)
📊 Aumento de datos DAGAN, BalancingGAN Generar muestras sintéticas para clases minoritarias Antoniou et al., 2017 (BMVC)

🔬 Super-resolución con GANs

La super-resolución es una de las aplicaciones más exitosas y prácticas de las GANs. El objetivo: a partir de una imagen de baja resolución, reconstruir una versión de alta resolución con detalles realistas.

1
SRGAN (Ledig et al., CVPR 2017): Primera GAN para super-resolución. Introduce la perceptual loss — medir diferencias en el espacio de features de VGG en vez de pixel a pixel. Resultado: ×4 upscaling con texturas realistas. 📄 Photo-Realistic Single Image Super-Resolution Using a GAN
2
ESRGAN (Wang et al., ECCVW 2018): Mejora SRGAN con Residual-in-Residual Dense Blocks (RRDB), eliminando BatchNorm del generador y usando relativistic discriminator. Gana el PIRM Challenge 2018. 📄 ESRGAN: Enhanced SRGAN
3
Real-ESRGAN (Wang et al., ICCVW 2021): Diseñada para imágenes reales con degradaciones complejas (compresión JPEG, ruido, blur). Usa un pipeline de degradación de segundo orden para simular artefactos del mundo real. 📄 Real-ESRGAN: Training Real-World Blind Super-Resolution
64×64 (LR) Generador (RRDB × 23) RRDB RRDB RRDB ↑×4 skip connections + pixel shuffle upsampling HR 256×256 (HR) D (VGG perceptual)
💡

Perceptual loss vs Pixel loss: MSE pixel a pixel produce imágenes borrosas porque promedia entre posibles soluciones. La perceptual loss compara features de alto nivel de VGG, y el discriminador añade un término adversarial que fuerza texturas realistas. El resultado: imágenes nítidas con detalles que «parecen» reales aunque no sean exactos.

📊 GANs para aumento de datos

En muchos dominios (medicina, industria, ciencias), los datasets son pequeños o desbalanceados. Las GANs pueden generar datos sintéticos para aumentar el conjunto de entrenamiento:

✅ Ventajas
  • Genera muestras realistas de clases minoritarias
  • Preserva privacidad (datos sintéticos, no reales)
  • Puede generar condiciones raras (ej: enfermedades infrecuentes)
  • Complementa data augmentation clásico (rotaciones, flips...)
⚠️ Riesgos
  • Si la GAN sufre mode collapse, los datos sintéticos no aportan diversidad
  • Puede amplificar sesgos del dataset original
  • La calidad debe validarse antes de usar para entrenamiento
  • No sustituye a datos reales — es un complemento
🏥

Caso de uso real: En dermatología, donde ciertas lesiones son raras, GANs entrenadas con dermatoscopia han generado imágenes sintéticas de melanoma que, combinadas con datos reales, mejoraron la accuracy de clasificadores hasta en un 7% (Baur et al., 2018). En radiografías de tórax, GANs han generado imágenes de COVID-19 cuando los datos reales eran escasos (Waheed et al., 2020).

📝→🖼️ Text-to-Image con GANs

Antes de que los modelos de difusión dominaran, las GANs fueron pioneras en la generación de imágenes a partir de texto:

1
Reed et al. (2016) — Primera GAN condicionada a texto. Genera imágenes 64×64 de flores y pájaros a partir de descripciones. 📄 Generative Adversarial Text to Image Synthesis, ICML 2016
2
StackGAN (Zhang et al., 2017) — Generación en dos etapas: sketch 64×64 → refinamiento 256×256. Mejora dramática en calidad. 📄 StackGAN: Text to Photo-Realistic Image Synthesis, ICCV 2017
3
AttnGAN (Xu et al., 2018) — Atención por palabras: cada palabra del texto guía la generación de diferentes regiones de la imagen. 📄 AttnGAN: Fine-Grained Text to Image Generation with Attentional GANs, CVPR 2018
🔗

La era post-GAN: Modelos como DALL-E, Stable Diffusion e Imagen han superado a las GANs en text-to-image, usando modelos de difusión + transformers. Sin embargo, las GANs sentaron las bases conceptuales y demostraron que la generación condicionada por texto era posible.

🏗️ DCGAN: la base moderna

Antes de hablar de modelos avanzados, hay que entender DCGAN (Radford, Metz & Chintala, 2016), que estableció las convenciones arquitectónicas que todas las GANs posteriores heredaron:

1
Convoluciones en vez de capas fully connected: El generador usa ConvTranspose2d para hacer upsampling; el discriminador usa Conv2d con stride para downsampling. Ni pooling ni dense en capas internas.
2
BatchNorm en todas las capas (excepto la salida de G y la entrada de D). ReLU en G, LeakyReLU(0.2) en D. Tanh en la salida de G.
3
No bias cuando se usa BatchNorm (el bias es absorbido por BN). Pesos inicializados con \( \mathcal{N}(0, 0.02) \).

DCGAN fue la primera GAN que generó imágenes reconocibles de 64×64 (caras, dormitorios) y demostró que el espacio latente aprendido tiene estructura semántica (ej: \( z_{\text{hombre con gafas}} - z_{\text{hombre}} + z_{\text{mujer}} \approx z_{\text{mujer con gafas}} \)).

💅 StyleGAN: control estilístico total

StyleGAN (Karras, Laine & Aila, CVPR 2019, NVIDIA) revolucionó la generación de imágenes introduciendo un generador basado en estilos que permite un control sin precedentes sobre los atributos generados.

Innovaciones clave de StyleGAN

Mapping Network z ~ N(0,I) FC + LeakyReLU FC + LeakyReLU × 8 capas w ∈ W estilo aprendido ↓ Affine transform w⁺ (por capa) style mixing z → w desacopla los factores de variación Synthesis Network (Generador) const 4×4×512 AdaIN + Conv 4×4 + noise AdaIN + Conv 8×8 AdaIN + Conv 16×16 AdaIN + Conv 512×512 AdaIN + Conv 1024×1024 Imagen 1024×1024 Coarse styles pose, forma, cara Middle styles expresión, pelo Fine styles color, textura, piel
🧠 Mapping Network

8 capas FC que transforman \( z \in \mathcal{Z} \) a \( w \in \mathcal{W} \). ¿Por qué? El espacio Z es uniforme/gaussiano, pero los factores de variación (edad, género, pose...) están entrelazados en Z. La mapping network los desacopla: en W cada dirección controla un atributo independiente.

🎨 AdaIN (Adaptive Instance Norm)

El estilo w se inyecta en cada capa mediante AdaIN:

$$ \text{AdaIN}(x, y) = y_s \cdot \frac{x - \mu(x)}{\sigma(x)} + y_b $$

donde \( y_s, y_b \) son escalas y biases derivados de w. Cada capa recibe su propio w (style mixing).

📐 Constant Input

A diferencia de DCGAN (que empieza desde z), StyleGAN empieza de una constante aprendida 4×4×512. Todo el contenido visual viene inyectado vía estilos w. Esto separa completamente el «qué generar» del «cómo generarlo».

🎲 Noise Injection

Se inyecta ruido gaussiano por pixel en cada capa para generar detalles estocásticos (pecas, pelo individual, textura de piel). Los estilos controlan la estructura global; el ruido controla los detalles finos.

StyleGAN2 (Karras et al., CVPR 2020) eliminó los artefactos de «droplets» causados por AdaIN, reemplazándolo por weight demodulation. También introdujo path length regularization y eliminó el progressive growing, usando skip connections y residual connections en su lugar.

StyleGAN3 (Karras et al., NeurIPS 2021) resolvió el problema de texture sticking — donde las texturas se «pegaban» a coordenadas fijas de píxeles en vez de moverse con las features. Lo logró haciendo la red equivariante a traslaciones y rotaciones continuas, usando filtros anti-aliasing entre capas.

🔍 ESRGAN: Enhanced Super-Resolution

ESRGAN (Wang et al., ECCVW 2018) es la GAN de referencia para super-resolución. Introduce tres mejoras clave sobre SRGAN:

1
RRDB (Residual-in-Residual Dense Block): Bloques densos anidados dentro de bloques residuales. Más capacidad de aprendizaje que los ResBlocks de SRGAN. Sin BatchNorm — porque BN introduce artefactos cuando se entrenan con batches pequeños.
2
Relativistic Discriminator: En vez de clasificar «real/fake», el discriminador predice si una imagen real es más realista que una generada:
$$ D(x_r, x_f) = \sigma(C(x_r) - \mathbb{E}[C(x_f)]) $$
Esto da gradientes tanto de las reales como de las falsas, acelerando la convergencia.
3
Perceptual loss mejorada: Usa features antes de la activación de VGG (en vez de después), produciendo texturas más detalladas y bordes más nítidos.
$$ \mathcal{L}_G = \underbrace{\lambda_{\text{percep}} \mathcal{L}_{\text{percep}}}_{\text{VGG features}} + \underbrace{\lambda_{\text{adv}} \mathcal{L}_{\text{adv}}}_{\text{adversarial}} + \underbrace{\lambda_{\text{pixel}} \mathcal{L}_{\text{pixel}}}_{\text{L1 pixel}} $$

Típicamente: \( \lambda_{\text{percep}} = 1 \), \( \lambda_{\text{adv}} = 0.005 \), \( \lambda_{\text{pixel}} = 0.01 \). La perceptual loss domina, forzando coherencia de alto nivel. La loss adversarial añade detalles de textura realistas. La L1 pixel estabiliza.

🌍 Real-ESRGAN: para el mundo real

Real-ESRGAN (Wang et al., ICCVW 2021) extiende ESRGAN para manejar imágenes reales con degradaciones complejas, no solo downscaling bicúbico:

Pipeline de degradación de segundo orden (entrenamiento) HR limpia blur (iso/aniso) + noise (gauss) + resize ↓ blur (2ª vez) + noise (2ª vez) + JPEG compress LR degradada Real-ESRGAN RRDB + U-Net D SR restaurada degradación doble → el modelo aprende a manejar artefactos complejos del mundo real
🔁 Degradación de 2º orden

En vez de entrenar solo con downscaling bicúbico (poco realista), aplica dos rondas de degradación (blur + ruido + resize + JPEG). Esto simula degradaciones del mundo real: fotos de internet, compresión social media, escaneos, etc.

🏗️ U-Net Discriminator

Reemplaza el discriminador PatchGAN por un U-Net discriminator que da feedback por pixel (no solo por patch), con skip connections entre capas del encoder y decoder. Permite al generador recibir señales más detalladas.

🚀

Uso práctico: Real-ESRGAN es uno de los modelos de super-resolución más usados en producción. Está integrado en herramientas como Topaz Gigapixel, waifu2x, y muchas apps de restauración de fotos. El modelo pre-entrenado (RealESRGAN_x4plus) está disponible en GitHub con un solo pip install.

🧪 Widget: Style Mixing interactivo

Simula cómo StyleGAN inyecta estilos a diferentes resoluciones. Mezcla el estilo «coarse» de una fuente con el estilo «fine» de otra:

💅 Style Mixing Simulator
64×64

🔄 Pix2Pix: traducción imagen-a-imagen

Pix2Pix (Isola et al., CVPR 2017) es una GAN condicional que traduce imágenes de un dominio a otro usando pares de entrenamiento: sketch → foto, mapa → satélite, día → noche, bordes → imagen, etc.

Dominio A sketch mapa Generator (U-Net) skip connections Dominio B foto satélite PatchGAN (D) Recibe: (A, B_real) o (A, B_fake) → mapa de parches N×N real / fake por región Loss = cGAN adversarial + λ · L1 (pixel-level reconstruction)
🏗️ U-Net Generator

Arquitectura encoder-decoder con skip connections entre capas simétricas. Los skips transmiten detalles de baja resolución del input directamente al output, mientras que el bottleneck captura la semántica global. Es perfecto para tareas donde la estructura del input se debe preservar en el output.

🔲 PatchGAN Discriminator

En vez de clasificar la imagen completa, D clasifica cada parche N×N como real o fake. Esto captura estadísticas locales (texturas) mejor que un discriminador global. El tamaño del parche (70×70 es típico) controla el balance entre detalle local y coherencia global.

⚠️

Requisito: pares de datos. Pix2Pix necesita pares (A, B) perfectamente alineados — la misma escena en ambos dominios. Esto limita su aplicabilidad: ¿cómo consigues un par (caballo, cebra) o (Monet, foto) de la misma escena? Aquí es donde entra CycleGAN.

♻️ CycleGAN: traducción sin pares

CycleGAN (Zhu et al., ICCV 2017) resuelve la limitación de Pix2Pix: traduce entre dominios sin necesitar pares de datos. Solo necesita un conjunto de imágenes del dominio A y otro del dominio B (sin correspondencia).

La idea clave es la cycle consistency loss — si traduzco A→B→A, debería recuperar la imagen original:

$$ \mathcal{L}_{\text{cyc}} = \mathbb{E}_{x \sim A}[\|F(G(x)) - x\|_1] + \mathbb{E}_{y \sim B}[\|G(F(y)) - y\|_1] $$
🐴 Dominio A (caballos) G: A → B 🦓 Dominio B (cebras) F: B → A Cycle consistency: 🐴 → G → 🦓 → F → 🐴 ≈ original ✓ 🦓 → F → 🐴 → G → 🦓 ≈ original ✓ 4 redes: G, F, D_A, D_B
🔑

Aplicaciones famosas de CycleGAN: Caballo ↔ Cebra, Manzana ↔ Naranja, Foto ↔ Monet/Van Gogh, Verano ↔ Invierno, Joven ↔ Viejo. También se ha usado en medicina (CT → MRI sin pares) y en simulación (sim-to-real para robótica).

📈 ProGAN: crecimiento progresivo

ProGAN (Progressive Growing of GANs, Karras et al., ICLR 2018) introdujo la idea de entrenar de baja a alta resolución progresivamente:

1
Empezar entrenando G y D para generar/discriminar imágenes de 4×4. Rápido de converger — aprende estructura global (silueta, color dominante).
2
Añadir capas para 8×8. Introducirlas gradualmente (blending con alpha) para no destruir lo aprendido. Entrenar hasta estabilizar.
3
Repetir: 16×16 → 32×32 → 64×64 → 128×128 → 256×256 → 512×512 → 1024×1024. Cada resolución añade detalles finos a la estructura global ya aprendida.

ProGAN fue la primera GAN en generar caras de 1024×1024 con calidad fotorrealista. Su idea de crecimiento progresivo fue heredada por StyleGAN (que luego lo abandonó en StyleGAN2 en favor de skip connections).

🗂️ Otros modelos notables

Modelo Año Innovación principal Aplicación
BigGAN 2019 Scale is all you need — GANs masivas con class-conditional generation (ImageNet 128/256/512) Generación de alta calidad condicionada a clase
GauGAN / SPADE 2019 Normalización espacialmente adaptativa (SPADE) — mapa semántico → imagen fotorrealista Paisajes, escenas interactivas (NVIDIA Canvas)
StarGAN 2018 Multi-domain translation con un solo generador (ej: cambiar expresión + edad + género) Edición facial multi-atributo
SAGAN 2019 Self-Attention en GANs — capturar dependencias de largo alcance con attention maps Generación de imágenes con estructuras complejas
ProjectedGAN 2021 Discriminador sobre features proyectadas de redes pre-entrenadas — convergencia 40× más rápida Entrenamiento eficiente con pocos datos
GigaGAN 2023 Escalar GANs a la era text-to-image con 1B parámetros — más rápido que modelos de difusión Text-to-image en tiempo real, super-resolución

GANs vs Modelos de difusión

Desde 2021, los modelos de difusión (DDPM, Stable Diffusion, DALL-E 2/3, Imagen) han superado a las GANs en muchos benchmarks de generación de imágenes. ¿Significa esto el fin de las GANs?

Criterio GANs Difusión
Velocidad de generación 1 forward pass (ms) ~50–1000 pasos (segundos)
Calidad (FID) Buena, pero con artefactos Mejor FID en ImageNet
Diversidad Mode collapse posible Alta diversidad natural
Estabilidad training Difícil, requiere trucos Estable (loss simple)
Controlabilidad Buena (StyleGAN, cGAN) Excelente (text guidance)
Super-resolución ESRGAN domina en velocidad Buena, pero lenta
Tiempo real (ideal para apps) No sin destilación
🔮

Conclusión: Las GANs no están muertas. Son insustituibles donde la velocidad importa (super-resolución en tiempo real, apps móviles, streaming), y modelos como GigaGAN (2023) demuestran que las GANs pueden escalar al nivel de los modelos de difusión. Además, la difusión a menudo usa componentes GAN (ej: el decoder de Stable Diffusion es un VAE-GAN). → Ver sección de difusión

🌳 El ecosistema generativo: conexiones

Modelos Generativos VAE / Autoencoders ⚔️ GANs Difusión β-VAE VQ-VAE CVAE StyleGAN ESRGAN Pix2Pix CycleGAN DDPM Stable Diff. DALL-E 2 VAE-GAN VQ-VAE encoder en Stable Diffusion Modelos Híbridos Modernos DALL-E (VQ-VAE+Transformer) · Stable Diffusion (VAE+U-Net) · GigaGAN
🧠

Todo está conectado: Los modelos generativos modernos combinan ideas de VAEs, GANs y difusión. Stable Diffusion usa un VAE como encoder/decoder + un U-Net para el proceso de difusión. DALL-E original usaba VQ-VAE + Transformer. GigaGAN demuestra que las GANs pueden escalar al nivel de los modelos de difusión. Entender cada pieza por separado es fundamental para comprender los sistemas modernos.

📋 Resumen: el legado de las GANs

Concepto Contribución Impacto actual
Entrenamiento adversarial Dos redes que se mejoran mutuamente Usado en robustez, domain adaptation, RL
Generación implícita No modelar p(x) explícitamente — samplear directamente Generación en un solo paso (vs. iterativa en difusión)
Perceptual / adversarial loss Juzgar calidad con una red aprendida, no pixel a pixel Estándar en super-resolución, style transfer, inpainting
Espacios latentes controlables StyleGAN demostró control semántico preciso Base de edición de imágenes moderna
Traducción entre dominios Pix2Pix / CycleGAN inauguraron image-to-image Aplicaciones en medicina, simulación, arte
Super-resolución realista SRGAN → ESRGAN → Real-ESRGAN Estándar de la industria para upscaling
Estabilización del training WGAN, spectral norm, progressive growing Técnicas adoptadas en todo el deep learning
💡

En resumen: Las GANs introdujeron la idea revolucionaria del entrenamiento adversarial y dominaron la generación de imágenes durante 7 años (2014–2021). Aunque los modelos de difusión les han superado en calidad general, las GANs siguen siendo insustituibles donde se necesita velocidad (tiempo real), super-resolución, o edición precisa. Más importante: los conceptos que las GANs introdujeron (adversarial loss, perceptual loss, espacios latentes controlables) son fundamentales en todo el ecosistema generativo actual.