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?
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):
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.
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:
Desglosemos cada término:
Para un generador G fijo, la función objetivo respecto a D es:
Para cada x, la función \( f(y) = a \log y + b \log(1 - y) \) alcanza su máximo en:
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:
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.
⚖️ 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:
Esto es equivalente a binary cross-entropy con etiquetas: real = 1, fake = 0. Para cada paso de entrenamiento de D:
🎨 Objetivo del Generador
El generador G quiere que D clasifique sus muestras como reales. Formalmente, quiere minimizar la función de valor:
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:
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.
📊 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:
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.
📐 WGAN: Wasserstein GAN
WGAN (Arjovsky et al., 2017) reemplaza la JS divergence por la distancia de Wasserstein, resolviendo los problemas de gradientes y estabilidad:
donde \( \mathcal{D} \) es el conjunto de funciones 1-Lipschitz. Notas clave:
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.
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:
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:
🌊 Convergencia y estabilidad
La convergencia de las GANs es uno de los problemas más estudiados en deep learning. Los principales desafíos:
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.
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.
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».
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.
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:
Compara las estadísticas (media y covarianza) de features de InceptionV3 entre imágenes reales y generadas. Menor es mejor. Captura calidad y diversidad.
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:
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:
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:
💻 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:
🌐 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.
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:
- 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...)
- 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:
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:
ConvTranspose2d para hacer upsampling;
el discriminador usa Conv2d con stride para downsampling.
Ni pooling ni dense en capas internas.
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
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.
El estilo w se inyecta en cada capa mediante AdaIN:
donde \( y_s, y_b \) son escalas y biases derivados de w. Cada capa recibe su propio w (style mixing).
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».
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:
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:
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.
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:
🔄 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.
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.
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:
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:
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 | Sí (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
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.