PyTorch Red Generativa Antagónica (GAN)

Red Generativa Antagónica (Generative Adversarial Network, GAN)Es una de las arquitecturas de modelos más creativas en el aprendizaje profundo. Al hacer que dos redes neuronales compitan y aprendan entre sí, finalmente puede generar datos muy realistas. GAN se utiliza ampliamente en escenarios como generación de imágenes, transferencia de estilo y aumento de datos.


1. Principios fundamentales de GAN

La idea central de GAN proviene del "juego de suma cero" en la teoría de juegos. Contiene dos redes que compiten entre sí:

  • Generador: aprende a generar datos falsos, con el objetivo de que el discriminador no pueda distinguir entre datos generados y datos reales
  • Discriminador: aprende a distinguir entre datos reales y datos generados, con el objetivo de ser lo más preciso posible

Ambos compiten entre sí durante el entrenamiento y mejoran continuamente, hasta alcanzar un estado de equilibrio de Nash.

1.1 Función objetivo de GAN

El objetivo de entrenamiento de GAN se puede expresar como el siguiente juego minimax:

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

donde:

  • \(G\) representa la red del generador
  • \(D\) representa la red del discriminador
  • \(x\) representa los datos reales
  • \(z\) representa el vector de ruido aleatorio (generalmente sigue una distribución normal estándar)
  • \(G(z)\) representa los datos falsos generados por el generador a partir del ruido

1.2 Interpretación del proceso de entrenamiento

El entrenamiento de GAN se divide en dos etapas:

Primera etapa: entrenar el discriminador

Fijar el generador y mejorar la capacidad de discriminación del discriminador:

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

Segunda etapa: entrenar el generador

Fijar el discriminador y mejorar la capacidad de engaño del generador:

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

En la práctica, normalmente se entrena al discriminador durante k pasos y luego al generador durante 1 paso, para mantener el equilibrio.


2. Implementación básica de GAN

A continuación se implementa el GAN más simple: para generar puntos de datos bidimensionales.

2.1 Definición del generador y el discriminador

Ejemplo

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

# Establecer la semilla aleatoria
torch.manual_seed(42)

# ── Red del generador ──────────────────────────────────────
class Generator(nn.Module):
    """
Generador: genera datos a partir de ruido aleatorio
Entrada: vector de ruido (batch_size, noise_dim)
Salida: datos generados (batch_size, data_dim)
    """

    def __init__(self, noise_dim, data_dim, hidden_dim=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(noise_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, data_dim),
            # La salida no se activa, GAN aprenderá la distribución adecuada
        )

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


# ── Red del discriminador ──────────────────────────────────────
class Discriminator(nn.Module):
    """
Discriminador: distingue entre datos reales y datos generados
Entrada: puntos de datos (batch_size, data_dim)
Salida: probabilidad de datos reales (batch_size, 1)
    """

    def __init__(self, data_dim, hidden_dim=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(data_dim, hidden_dim),
            nn.LeakyReLU(0.2),  # LeakyReLU evita la desaparición del gradiente
            nn.Linear(hidden_dim, hidden_dim),
            nn.LeakyReLU(0.2),
            nn.Linear(hidden_dim, 1),
            nn.Sigmoid()  # Probabilidad de salida
        )

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


# Hiperparámetros
NOISE_DIM = 16
DATA_DIM = 2
HIDDEN_DIM = 64
BATCH_SIZE = 128

# Crear redes
generator = Generator(NOISE_DIM, DATA_DIM, HIDDEN_DIM)
discriminator = Discriminator(DATA_DIM, HIDDEN_DIM)

print(f"Cantidad de parámetros del generador: {sum(p.numel() for p in generator.parameters()):,}")
print(f"Cantidad de parámetros del discriminador: {sum(p.numel() for p in discriminator.parameters()):,}")

2.2 Bucle de entrenamiento

Ejemplo

# ── Optimizador ──────────────────────────────────────
lr = 0.001
g_optimizer = optim.Adam(generator.parameters(), lr=lr)
d_optimizer = optim.Adam(discriminator.parameters(), lr=lr)

# Función de pérdida: entropía cruzada binaria
criterion = nn.BCELoss()

# ── Datos de entrenamiento: distribución anular ──────────────────────────
def generate_real_data(batch_size):
    """Genera datos reales con distribución anular"""
    angles = torch.rand(batch_size) * 2 * torch.pi
    radius = 1.0 + torch.randn(batch_size) * 0.1  # Radio aproximadamente 1
    x = radius * torch.cos(angles)
    y = radius * torch.sin(angles)
    return torch.stack([x, y], dim=1)


# ── Bucle de entrenamiento ──────────────────────────────────────
NUM_EPOCHS = 1000
d_losses = []
g_losses = []

for epoch in range(NUM_EPOCHS):
    # 1. Entrenar el discriminador
    # Generar datos falsos
    noise = torch.randn(BATCH_SIZE, NOISE_DIM)
    fake_data = generator(noise).detach()  # detach evita calcular el gradiente del generador

    # Generar datos reales
    real_data = generate_real_data(BATCH_SIZE)

    # Pérdida del discriminador
    real_pred = discriminator(real_data)
    fake_pred = discriminator(fake_data)
    d_loss = criterion(real_pred, torch.ones_like(real_pred)) + \
             criterion(fake_pred, torch.zeros_like(fake_pred))

    # Actualizar el discriminador
    d_optimizer.zero_grad()
    d_loss.backward()
    d_optimizer.step()

    # 2. Entrenar el generador
    # Generar un nuevo lote de datos falsos
    noise = torch.randn(BATCH_SIZE, NOISE_DIM)
    fake_data = generator(noise)

    # Pérdida del generador: hacer que el discriminador crea que los datos generados son reales
    fake_pred = discriminator(fake_data)
    g_loss = criterion(fake_pred, torch.ones_like(fake_pred))

    # Actualizar el generador
    g_optimizer.zero_grad()
    g_loss.backward()
    g_optimizer.step()

    # Registrar pérdidas
    d_losses.append(d_loss.item())
    g_losses.append(g_loss.item())

    if (epoch + 1) % 100 == 0:
        print(f"Epoch {epoch+1:4d} | D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")

print("¡Entrenamiento completado!")

2.3 Visualización de los resultados generados

Ejemplo

# Generar datos y visualizar
def visualize_results(generator, num_samples=1000):
    noise = torch.randn(num_samples, NOISE_DIM)
    generated_data = generator(noise).detach().numpy()

    plt.figure(figsize=(6, 6))
    plt.scatter(generated_data[:, 0], generated_data[:, 1],
                alpha=0.5, s=10, c='blue', label='Generated')
    plt.xlim(-2, 2)
    plt.ylim(-2, 2)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title('GAN Generated Data')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()


# Ver el resultado de la generación
visualize_results(generator)

3. DCGAN - GAN convolucional profunda

DCGAN es una arquitectura clásica que introduce redes neuronales convolucionales en GAN, mejorando significativamente la calidad de generación de imágenes.

3.1 Puntos clave de la arquitectura DCGAN

  • Usa convolución transpuesta (Transposed Convolution) para realizar el upsampling y generar imágenes
  • Usa convoluciones con paso (stride) para realizar el downsampling y discriminar imágenes
  • Usa BatchNorm en el generador y el discriminador (pero no en las capas de salida y de entrada)
  • El generador usa ReLU, el discriminador usa LeakyReLU

3.2 Implementación de DCGAN

Ejemplo

import torch
import torch.nn as nn

# ── Generador DCGAN ─────────────────────────────────
class DCGenerator(nn.Module):
    """
Generador DCGAN: upsampling con convoluciones transpuestas
    """

    def __init__(self, noise_dim=100, channels=3, features_g=64):
        super().__init__()
        self.noise_dim = noise_dim

        # Entrada: noise_dim x 1 x 1
        self.net = nn.Sequential(
            # Convolución transpuesta: (batch, features_g*8, 4, 4)
            nn.ConvTranspose2d(noise_dim, features_g * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(features_g * 8),
            nn.ReLU(True),

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

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

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

            # Salida: (batch, channels, 64, 64)
            nn.ConvTranspose2d(features_g, channels, 4, 2, 1, bias=False),
            nn.Tanh()  # Rango de salida [-1, 1]
        )

    def forward(self, x):
        # x: (batch, noise_dim) -> (batch, noise_dim, 1, 1)
        x = x.view(x.size(0), x.size(1), 1, 1)
        return self.net(x)


# ── Discriminador DCGAN ─────────────────────────────────
class DCDiscriminator(nn.Module):
    """
Discriminador DCGAN: downsampling con convoluciones
    """

    def __init__(self, channels=3, features_d=64):
        super().__init__()

        # Entrada: (batch, channels, 64, 64)
        self.net = nn.Sequential(
            # (batch, features_d, 32, 32)
            nn.Conv2d(channels, features_d, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),

            # (batch, features_d*2, 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, features_d*4, 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, features_d*8, 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),

            # Salida: (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(x.size(0), -1)


# Probar la red
noise_dim = 100
generator = DCGenerator(noise_dim=noise_dim, channels=3, features_g=64)
discriminator = DCDiscriminator(channels=3, features_d=64)

# Probar la propagación hacia adelante
noise = torch.randn(2, noise_dim)
fake_images = generator(noise)
print(f"Forma de la imagen generada: {fake_images.shape}")  # torch.Size([2, 3, 64, 64])

decision = discriminator(fake_images)
print(f"Forma del resultado de discriminación: {decision.shape}")     # torch.Size([2, 1])

3.3 Código completo de entrenamiento de DCGAN

Ejemplo

# ── Configuración de entrenamiento ─────────────────────────────────────
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Dispositivo utilizado: {device}")

NOISE_DIM = 100
LEARNING_RATE = 0.0002
BETA1 = 0.5  # Parámetros de Adam

# Crear redes
generator = DCGenerator(noise_dim=NOISE_DIM).to(device)
discriminator = DCDiscriminator().to(device)

# Optimizador
g_optimizer = optim.Adam(generator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))
d_optimizer = optim.Adam(discriminator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))

criterion = nn.BCELoss()

# ── Bucle de entrenamiento ─────────────────────────────────────
fixed_noise = torch.randn(64, NOISE_DIM, device=device)  # Para visualización

def train_dcgan(generator, discriminator, g_optimizer, d_optimizer, criterion,
                num_epochs, device, fixed_noise):
    G_losses = []
    D_losses = []

    for epoch in range(num_epochs):
        for batch_idx in range(100):  # Asumiendo que cada época tiene 100 lotes (batches)
            # Entrenar el discriminador
            discriminator.zero_grad()

            # Imágenes reales (se asume que ya se tienen)
            # real_images = ...
            # Aquí se simulan con ruido aleatorio
            real_images = torch.randn(32, 3, 64, 64).to(device)

            batch_size = real_images.size(0)
            labels_real = torch.ones(batch_size, 1).to(device)
            labels_fake = torch.zeros(batch_size, 1).to(device)

            # Pérdida de imágenes reales
            output = discriminator(real_images)
            d_loss_real = criterion(output, labels_real)

            # Pérdida de imágenes generadas
            noise = torch.randn(batch_size, NOISE_DIM).to(device)
            fake_images = generator(noise)
            output = discriminator(fake_images.detach())
            d_loss_fake = criterion(output, labels_fake)

            # Pérdida total
            d_loss = d_loss_real + d_loss_fake
            d_loss.backward()
            d_optimizer.step()

            # Entrenar el generador
            generator.zero_grad()

            noise = torch.randn(batch_size, NOISE_DIM).to(device)
            fake_images = generator(noise)
            output = discriminator(fake_images)
            g_loss = criterion(output, labels_real)  # Se espera que las imágenes generadas sean clasificadas como reales

            g_loss.backward()
            g_optimizer.step()

            # Registrar la pérdida
            if batch_idx % 50 == 0:
                G_losses.append(g_loss.item())
                D_losses.append(d_loss.item())
                print(f"[{epoch}/{num_epochs}][{batch_idx}/100] "
                      f"D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")

    return G_losses, D_losses


# Comenzar el entrenamiento
# G_losses, D_losses = train_dcgan(generator, discriminator, g_optimizer,
#                                   d_optimizer, criterion, 5, device, fixed_noise)
print(¡La arquitectura DCGAN se ha definido por completo, ya se puede comenzar el entrenamiento!)

4. Técnicas de entrenamiento de GAN

4.1 Problemas comunes y soluciones

Problema Causa Solución
Colapso de modo (Mode Collapse) El generador solo produce unos pocos tipos limitados de muestras Usar WGAN, añadir discriminación por mini-lotes, usar suavizado de etiquetas
Discriminador demasiado fuerte El gradiente del generador desaparece y no puede aprender Entrenar el generador varias veces, reducir la tasa de aprendizaje del discriminador, usar LeakyReLU
Entrenamiento inestable La función objetivo de las GAN es no convexa y difícil de converger Usar normalización espectral, penalización por gradiente, calentamiento de la tasa de aprendizaje
Mala calidad de generación Capacidad insuficiente de la red o entrenamiento insuficiente Aumentar la profundidad de la red, usar más datos de entrenamiento, entrenar durante más tiempo

4.2 Mejoras de la función de pérdida

La GAN original usa la divergencia JS, que tiene problemas de desaparición de gradientes. WGAN usa la distancia de Wasserstein, que es más estable:

Ejemplo

# Función de pérdida de WGAN (reemplaza a BCE)
def wgan_d_loss(real_pred, fake_pred):
    """Pérdida del discriminador: las muestras reales obtienen puntuaciones altas, las generadas obtienen puntuaciones bajas"""
    return -(torch.mean(real_pred) - torch.mean(fake_pred))

def wgan_g_loss(fake_pred):
    """Pérdida del generador: lograr que las muestras generadas obtengan puntuaciones altas"""
    return -torch.mean(fake_pred)

# Penalización por gradiente (Gradient Penalty) - WGAN-GP
def gradient_penalty(discriminator, real_images, fake_images, device):
    """Término de penalización por gradiente de WGAN-GP"""
    batch_size = real_images.size(0)
    alpha = torch.rand(batch_size, 1, 1, 1).to(device)

    # Interpolar entre imágenes reales y generadas
    interpolated = alpha * real_images + (1 - alpha) * fake_images
    interpolated.requires_grad = True

    # Calcular la salida del discriminador para las imágenes interpoladas
    pred = discriminator(interpolated)

    # Calcular el gradiente
    gradients = torch.autograd.grad(
        outputs=pred,
        inputs=interpolated,
        grad_outputs=torch.ones_like(pred),
        create_graph=True,
        retain_graph=True,
        only_inputs=True
    )[0]

    # Calcular la norma del gradiente
    gradients = gradients.view(batch_size, -1)
    gradient_norm = gradients.norm(2, dim=1)
    penalty = ((gradient_norm - 1) ** 2).mean()

    return penalty

4.3 Normalización espectral (Spectral Normalization)

La normalización espectral puede estabilizar el entrenamiento de GANs y controlar la constante de Lipschitz del discriminador:

Ejemplo

import torch.nn.utils.spectral_norm as spectral_norm

# Discriminador con normalización espectral
class SNDiscriminator(nn.Module):
    def __init__(self, channels=3, features_d=64):
        super().__init__()
        self.net = nn.Sequential(
            spectral_norm(nn.Conv2d(channels, features_d, 4, 2, 1)),
            nn.LeakyReLU(0.2, inplace=True),

            spectral_norm(nn.Conv2d(features_d, features_d * 2, 4, 2, 1)),
            nn.LeakyReLU(0.2, inplace=True),

            spectral_norm(nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1)),
            nn.LeakyReLU(0.2, inplace=True),

            spectral_norm(nn.Conv2d(features_d * 4, 1, 4, 1, 0)),
            nn.Sigmoid()
        )

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

5. GAN condicional (cGAN)

La GAN condicional permite especificar la etiqueta de clase de los datos generados, logrando una generación condicional.

5.1 Arquitectura de cGAN

Ejemplo

import torch
import torch.nn as nn

class ConditionalGenerator(nn.Module):
    """Generador condicional: recibe simultáneamente ruido y etiquetas de clase"""
    def __init__(self, noise_dim, num_classes, embed_dim, img_channels, features_g=64):
        super().__init__()
        self.label_emb = nn.Embedding(num_classes, embed_dim)

        # Concatenar las incrustaciones de ruido y etiqueta
        self.net = nn.Sequential(
            nn.Linear(noise_dim + embed_dim, features_g * 8 * 4 * 4),
            nn.BatchNorm1d(features_g * 8 * 4 * 4),
            nn.ReLU(),
            # Luego conectar una convolución transpuesta (similar a DCGAN)
            # ...
        )

    def forward(self, noise, labels):
        # Incrustar las etiquetas de clase en la misma dimensión que el ruido
        label_embedding = self.label_emb(labels)
        # Concatenar las incrustaciones de ruido y etiqueta
        x = torch.cat([noise, label_embedding], dim=1)
        return self.net(x)


class ConditionalDiscriminator(nn.Module):
    """Discriminador condicional: recibe simultáneamente imágenes y etiquetas de clase"""
    def __init__(self, img_channels, num_classes, embed_dim, features_d=64):
        super().__init__()
        self.label_emb = nn.Embedding(num_classes, embed_dim)

        # Concatenar las incrustaciones de imagen y etiqueta
        self.net = nn.Sequential(
            nn.Conv2d(img_channels + embed_dim, features_d, 4, 2, 1),
            nn.LeakyReLU(0.2),
            # ...
        )

    def forward(self, img, labels):
        # Ajustar la incrustación de etiquetas a las mismas dimensiones espaciales que la imagen
        label_embedding = self.label_emb(labels)
        # Ajustar la forma para poder concatenar
        label_embedding = label_embedding.unsqueeze(2).unsqueeze(3)
        label_embedding = label_embedding.expand(-1, -1, img.size(2), img.size(3))
        # Concatenar la imagen y la etiqueta
        x = torch.cat([img, label_embedding], dim=1)
        return self.net(x)

6. Métricas de evaluación de GAN

6.1 Métricas de evaluación comunes

Métrica Descripción Ventajas Desventajas
Inception Score (IS) Usa Inception v3 para evaluar la calidad y la diversidad de las imágenes generadas Cálculo simple, tiene cierta correlación con el juicio humano No evalúa el sobreajuste ni puede detectar el colapso de modo
Fréchet Inception Distance (FID) Calcula la distancia entre las imágenes reales y las generadas en el espacio de características Más sensible al ruido y al colapso de modo Requiere una gran cantidad de muestras, cálculo lento
Evaluación humana Los humanos juzgan la calidad de las imágenes generadas Refleja con mayor precisión la percepción humana Subjetiva y requiere tiempo

6.2 Implementación del cálculo de FID

Ejemplo

import numpy as np
from scipy import linalg

def calculate_fid(real_activations, fake_activations):
    """
Calcular la Distancia de Fréchet Inception
real_activations: vectores de características de las imágenes reales (N, dim)
fake_activations: vectores de características de las imágenes generadas (N, dim)
    """

    # Calcular la media y la covarianza
    mu1, sigma1 = real_activations.mean(axis=0), np.cov(real_activations, rowvar=False)
    mu2, sigma2 = fake_activations.mean(axis=0), np.cov(fake_activations, rowvar=False)

    # Calcular el FID
    diff = mu1 - mu2
    # Calcular la suma de las matrices de covarianza
    covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)

    # Evitar números complejos
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    fid = diff.dot(diff) + np.trace(sigma1 + sigma2 - 2 * covmean)
    return fid


# Ejemplo simplificado: usar datos aleatorios
np.random.seed(42)
real_acts = np.random.randn(1000, 2048)  # Dimensión de salida de Inception v3
fake_acts = np.random.randn(1000, 2048)

fid_score = calculate_fid(real_acts, fake_acts)
print(f"FID Score: {fid_score:.2f}")
# Cuanto menor sea el FID, mejor; normalmente un valor inferior a 50 indica una buena calidad de generación

7. Variantes comunes de GAN

Desde su desarrollo, las GAN han dado lugar a numerosas variantes, adecuadas para diferentes escenarios de aplicación:

Modelo Nombre completo Características Escenarios de aplicación
DCGAN Deep Convolutional GAN Usa redes convolucionales para generar imágenes de alta calidad Generación de imágenes
WGAN Wasserstein GAN Usa la distancia de Wasserstein, entrenamiento más estable Entrenamiento estable
WGAN-GP WGAN with Gradient Penalty Penalización por gradiente en lugar de recorte de pesos Entrenamiento estable
CGAN Conditional GAN Añade información condicional, generación controlable Generación condicional
CycleGAN Cycle-Consistent GAN Conversión de imágenes no supervisada Transferencia de estilo
StyleGAN Style-Based GAN Control de estilo, generación de rostros de alta calidad Generación de rostros
BigGAN Big GAN Generación de imágenes a gran escala y alta calidad Imágenes de alta resolución
ProGAN Progressive Growing GAN Aumento progresivo de la resolución Generación de alta resolución
Otras extensiones