tutoriales.com

Generación de Imágenes Condicionales con GANs y Autoencoders Variacionales en TensorFlow y PyTorch

Este tutorial explora la generación de imágenes condicionales, una técnica poderosa en IA que permite crear imágenes basadas en atributos específicos. Cubriremos la teoría y la implementación práctica utilizando GANs y VAEs con las bibliotecas de aprendizaje profundo TensorFlow y PyTorch, proporcionando ejemplos detallados para ambos frameworks.

Intermedio35 min de lectura11 views
Reportar error

La generación de imágenes condicionales es una de las aplicaciones más fascinantes del aprendizaje profundo, permitiéndonos crear nuevas imágenes que adhieren a características específicas que definimos. Imagina poder generar una imagen de un "gato siamés sonriendo" o una "puesta de sol en la playa con palmeras". Esta capacidad tiene implicaciones enormes en campos como el diseño gráfico, la síntesis de datos, la edición de imágenes y la creación de contenido multimedia.

En este tutorial, profundizaremos en dos arquitecturas clave que han impulsado esta área: las Redes Generativas Antagónicas (GANs) y los Autoencoders Variacionales (VAEs). Veremos cómo adaptar estas arquitecturas fundamentales para la generación condicional, lo que significa que la salida generada no es aleatoria, sino que está influenciada por una entrada o condición específica.


🎯 ¿Qué es la Generación de Imágenes Condicionales?

La generación de imágenes condicional va más allá de simplemente crear imágenes nuevas y realistas. Su objetivo es generar imágenes que satisfagan una condición dada, que puede ser cualquier cosa, desde una etiqueta de clase (ej. "generar una imagen de perro"), un texto descriptivo (ej. "generar un paisaje nevado con un río"), una imagen de entrada (ej. "convertir un boceto en una foto"), o incluso datos numéricos que describen atributos.

En esencia, en lugar de aprender a mapear un ruido aleatorio a una imagen (como en las GANs incondicionales), el modelo aprende a mapear ruido + condición a una imagen.

📌 **Nota:** La condición puede ser tan simple como un vector *one-hot* que representa una clase, o tan compleja como un embedding de texto o una imagen completa.

🛠️ Herramientas y Requisitos

Para seguir este tutorial, necesitarás:

  • Python 3.x: Se recomienda Python 3.8 o superior.
  • TensorFlow: Versión 2.x o superior.
  • PyTorch: Versión 1.x o superior.
  • Bibliotecas comunes de manipulación de datos: NumPy, Matplotlib, scikit-learn.
  • GPU (recomendado): Para un entrenamiento más rápido, especialmente con datasets grandes.

✨ Generación Condicional con Redes Generativas Antagónicas (cGANs)

Las Redes Generativas Antagónicas Condicionales (cGANs) son una extensión de las GANs tradicionales que permiten la generación de datos condicionales. Introducen la condición tanto en el Generador como en el Discriminador, guiando al Generador para producir datos que coincidan con la condición y permitiendo al Discriminador juzgar la autenticidad y la coincidencia con la condición.

📖 Arquitectura de una cGAN

Una cGAN se compone de dos redes neuronales que compiten entre sí:

  1. Generador (G): Toma como entrada un vector de ruido aleatorio z y un vector de condición c, y produce una imagen sintética x'. Su objetivo es generar imágenes que el Discriminador no pueda distinguir de las reales y que cumplan la condición c.
  2. Discriminador (D): Toma como entrada una imagen (real o generada) y el vector de condición c. Su tarea es determinar si la imagen es real o falsa, y si la imagen generada corresponde a la condición c.
Arquitectura de una cGAN Ruido (z) Condición (c) Generador (G) Imagen Falsa Imagen Real Discriminador (D) ¿Es real? ¿Coincide con (c)? Retroalimentación (Entrenamiento)

💡 Implementación de una cGAN en TensorFlow

Vamos a construir una cGAN simple para generar dígitos condicionales del dataset MNIST.

import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, losses
import numpy as np
import matplotlib.pyplot as plt

# 1. Preparar el Dataset
(X_train, y_train), (_, _) = tf.keras.datasets.mnist.load_data()
X_train = (X_train.astype('float32') - 127.5) / 127.5 # Normalizar a [-1, 1]
X_train = X_train[..., np.newaxis] # Añadir dimensión de canal

BUFFER_SIZE = 60000
BATCH_SIZE = 256

train_dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train)).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

# 2. Definir el Generador
def make_generator_model(noise_dim, num_classes):
    model = models.Sequential()
    model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(noise_dim + num_classes,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Reshape((7, 7, 256)))
    assert model.output_shape == (None, 7, 7, 256) # Nota: None es el tamaño del batch

    model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())
    assert model.output_shape == (None, 7, 7, 128)

    model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())
    assert model.output_shape == (None, 14, 14, 64)

    model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
    assert model.output_shape == (None, 28, 28, 1)

    return model

# 3. Definir el Discriminador
def make_discriminator_model(num_classes):
    model = models.Sequential()
    model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1 + num_classes])) # Canal extra para la condición
    model.add(layers.LeakyReLU())
    model.add(layers.Dropout(0.3))

    model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
    model.add(layers.LeakyReLU())
    model.add(layers.Dropout(0.3))

    model.add(layers.Flatten())
    model.add(layers.Dense(1))

    return model

# 4. Funciones de Pérdida y Optimizadores
cross_entropy = losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    total_loss = real_loss + fake_loss
    return total_loss

def generator_loss(fake_output):
    return cross_entropy(tf.ones_like(fake_output), fake_output)

generator_optimizer = optimizers.Adam(1e-4)
discriminator_optimizer = optimizers.Adam(1e-4)

# 5. Training Step
@tf.function
def train_step(images, labels, noise_dim, num_classes):
    noise = tf.random.normal([BATCH_SIZE, noise_dim])
    
    # Convertir labels a one-hot y luego a un "mapa" de condiciones
    labels_one_hot = tf.one_hot(labels, num_classes)
    labels_one_hot_reshaped = tf.reshape(labels_one_hot, [-1, 1, 1, num_classes])
    labels_map = tf.cast(tf.ones([BATCH_SIZE, 28, 28, 1]) * labels_one_hot_reshaped, tf.float32)

    # Concatena la condición al ruido para el Generador
    gen_input = tf.concat([noise, labels_one_hot], axis=1)

    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(gen_input, training=True)

        # Concatena la condición a las imágenes para el Discriminador
        real_images_cond = tf.concat([images, labels_map], axis=-1)
        fake_images_cond = tf.concat([generated_images, labels_map], axis=-1)

        real_output = discriminator(real_images_cond, training=True)
        fake_output = discriminator(fake_images_cond, training=True)

        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)

    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)

    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))

# 6. Bucle de Entrenamiento
EPOCHS = 50
noise_dim = 100
num_classes = 10 # Para MNIST (0-9)

generator = make_generator_model(noise_dim, num_classes)
discriminator = make_discriminator_model(num_classes)

def generate_and_save_images(model, epoch, test_input):
    predictions = model(test_input, training=False)
    fig = plt.figure(figsize=(10, 10))

    for i in range(predictions.shape[0]):
        plt.subplot(10, 10, i+1)
        plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
        plt.axis('off')

    plt.savefig('image_at_epoch_{:04d}.png'.nformat(epoch))
    plt.close()

# Generar una semilla fija para la visualización
seed_noise = tf.random.normal([100, noise_dim])
seed_labels = tf.constant([i % 10 for i in range(100)], dtype=tf.int32) # 0-9, repetido 10 veces
seed_labels_one_hot = tf.one_hot(seed_labels, num_classes)
seed_input = tf.concat([seed_noise, seed_labels_one_hot], axis=1)

for epoch in range(EPOCHS):
    for image_batch, label_batch in train_dataset:
        train_step(image_batch, label_batch, noise_dim, num_classes)

    if (epoch + 1) % 5 == 0:
        print(f'Epoch {epoch + 1} completada.')
        generate_and_save_images(generator, epoch + 1, seed_input)

print("Entrenamiento completado.")
💡 Consejo: La concatenación de la condición a la entrada del Generador (ruido) y a la entrada del Discriminador (imágenes) es el secreto para la condicionalidad en las cGANs. Para las imágenes, a menudo se crea un "mapa" de la condición para que coincida con las dimensiones espaciales de la imagen.

🚀 Implementación de una cGAN en PyTorch

Ahora, veamos cómo implementar una cGAN para el mismo propósito (MNIST) usando PyTorch.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

# 1. Preparar el Dataset
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

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

# 2. Definir el Generador
class Generator(nn.Module):
    def __init__(self, noise_dim, num_classes, img_shape):
        super(Generator, self).__init__()
        self.img_shape = img_shape
        self.label_embedding = nn.Embedding(num_classes, num_classes)

        self.model = nn.Sequential(
            nn.Linear(noise_dim + num_classes, 128),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(128, 256),
            nn.BatchNorm1d(256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 512),
            nn.BatchNorm1d(512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, np.prod(img_shape)),
            nn.Tanh()
        )

    def forward(self, noise, labels):
        # Concatenar embedding de etiqueta al ruido
        gen_input = torch.cat((self.label_embedding(labels), noise), -1)
        img = self.model(gen_input)
        img = img.view(img.size(0), *self.img_shape)
        return img

# 3. Definir el Discriminador
class Discriminator(nn.Module):
    def __init__(self, num_classes, img_shape):
        super(Discriminator, self).__init__()
        self.label_embedding = nn.Embedding(num_classes, num_classes)

        self.model = nn.Sequential(
            nn.Linear(np.prod(img_shape) + num_classes, 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1),
            nn.Sigmoid() # Usamos Sigmoid para salida de probabilidad
        )

    def forward(self, img, labels):
        # Aplanar imagen
        img_flat = img.view(img.size(0), -1)
        # Concatenar embedding de etiqueta a la imagen
        d_input = torch.cat((img_flat, self.label_embedding(labels)), -1)
        validity = self.model(d_input)
        return validity

# 4. Hiperparámetros y Entrenamiento
noise_dim = 100
num_classes = 10
img_shape = (1, 28, 28)

generator = Generator(noise_dim, num_classes, img_shape).to(device)
discriminator = Discriminator(num_classes, img_shape).to(device)

# Optimizadores
optimizer_g = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_d = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))

# Función de pérdida
criterion = nn.BCELoss()

# Bucle de Entrenamiento
EPOCHS = 50

def sample_image(gen_model, noise_vec, labels_vec, epoch, num_samples=10):
    gen_model.eval()
    with torch.no_grad():
        gen_imgs = gen_model(noise_vec.to(device), labels_vec.to(device)).cpu().numpy()
    gen_imgs = 0.5 * gen_imgs + 0.5 # Desnormalizar a [0, 1]
    
    fig, axs = plt.subplots(1, num_samples, figsize=(10, 1))
    for i in range(num_samples):
        axs[i].imshow(gen_imgs[i, 0, :, :], cmap='gray')
        axs[i].axis('off')
    plt.savefig(f'pytorch_image_at_epoch_{epoch:04d}.png')
    plt.close()
    gen_model.train()

# Generar ruido y etiquetas fijas para muestreo
fixed_noise = torch.randn(10, noise_dim)
fixed_labels = torch.LongTensor([i for i in range(10)]) # Etiquetas 0-9

for epoch in range(EPOCHS):
    for i, (imgs, labels) in enumerate(train_loader):
        
        batch_size = imgs.shape[0]
        real_imgs = imgs.to(device)
        labels = labels.to(device)

        # Etiquetas para el entrenamiento del Discriminador
        valid = torch.ones(batch_size, 1).to(device)
        fake = torch.zeros(batch_size, 1).to(device)

        # --- Entrenamiento del Generador ---
        optimizer_g.zero_grad()

        z = torch.randn(batch_size, noise_dim).to(device)
        gen_labels = torch.randint(0, num_classes, (batch_size,), dtype=torch.long).to(device)

        gen_imgs = generator(z, gen_labels)
        g_loss = criterion(discriminator(gen_imgs, gen_labels), valid)

        g_loss.backward()
        optimizer_g.step()

        # --- Entrenamiento del Discriminador ---
        optimizer_d.zero_grad()

        # Pérdida con imágenes reales
        real_loss = criterion(discriminator(real_imgs, labels), valid)

        # Pérdida con imágenes falsas
        fake_loss = criterion(discriminator(gen_imgs.detach(), gen_labels), fake)

        d_loss = (real_loss + fake_loss) / 2

        d_loss.backward()
        optimizer_d.step()

        if i % 100 == 0:
            print(f"[Epoch {epoch}/{EPOCHS}] [Batch {i}/{len(train_loader)}] D_loss: {d_loss.item():.4f} G_loss: {g_loss.item():.4f}")
    
    if (epoch + 1) % 5 == 0:
        sample_image(generator, fixed_noise, fixed_labels, epoch + 1)

print("Entrenamiento completado en PyTorch.")

💡 Generación Condicional con Autoencoders Variacionales (cVAEs)

Los Autoencoders Variacionales (VAEs) son modelos generativos probabilísticos que aprenden una distribución latente de los datos. Para hacerlos condicionales (cVAEs), simplemente inyectamos la información de la condición en el Encoder y el Decoder.

📖 Arquitectura de un cVAE

Un cVAE, al igual que un VAE estándar, tiene dos componentes principales:

  1. Encoder (codificador): Toma una imagen de entrada x y una condición c, y las mapea a los parámetros (media μ y log-varianza log σ²) de una distribución gaussiana en el espacio latente z. Su objetivo es aprender a comprimir la imagen y su condición en una representación latente útil.
  2. Decoder (decodificador): Toma una muestra z del espacio latente y la condición c, y genera una reconstrucción de la imagen original x'. Su objetivo es aprender a reconstruir imágenes realistas a partir de la representación latente y la condición.

La función de pérdida de un VAE se compone de dos partes: una pérdida de reconstrucción (que asegura que la imagen generada sea similar a la original) y una pérdida KL-divergence (que fuerza a la distribución latente a parecerse a una distribución gaussiana estándar, lo que permite muestrear nuevas representaciones). Para el cVAE, la pérdida de reconstrucción también tiene en cuenta la condición.

Imagen (x) Cond. (c) ENCODER μ log_var z Cond. (c) DECODER x' Reconstrucción

🚀 Implementación de un cVAE en TensorFlow

Continuaremos con el dataset MNIST para demostrar la implementación de un cVAE.

import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, losses
import numpy as np
import matplotlib.pyplot as plt

# 1. Preparar el Dataset
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()
X_train = X_train.astype('float32') / 255.0 # Normalizar a [0, 1]
X_train = X_train[..., np.newaxis] # Añadir dimensión de canal
X_test = X_test.astype('float32') / 255.0
X_test = X_test[..., np.newaxis]

BUFFER_SIZE = 60000
BATCH_SIZE = 128
LATENT_DIM = 2 # Dimensión del espacio latente
NUM_CLASSES = 10
IMG_SHAPE = (28, 28, 1)

train_dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train)).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
test_dataset = tf.data.Dataset.from_tensor_slices((X_test, y_test)).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

# 2. Definir el CVAE (Encoder y Decoder)
class CVAE(tf.keras.Model):
    def __init__(self, latent_dim, num_classes):
        super(CVAE, self).__init__()
        self.latent_dim = latent_dim
        self.num_classes = num_classes

        # Encoder
        self.encoder = tf.keras.Sequential([
            layers.InputLayer(input_shape=(28, 28, 1 + self.num_classes)), # Imagen + Condición
            layers.Conv2D(32, 3, activation='relu', strides=2, padding='same'),
            layers.Conv2D(64, 3, activation='relu', strides=2, padding='same'),
            layers.Flatten(),
            layers.Dense(latent_dim + latent_dim) # Para mu y log_var
        ])

        # Decoder
        self.decoder = tf.keras.Sequential([
            layers.InputLayer(input_shape=(latent_dim + self.num_classes,)), # Latente + Condición
            layers.Dense(7 * 7 * 32, activation='relu'),
            layers.Reshape((7, 7, 32)),
            layers.Conv2DTranspose(64, 3, activation='relu', strides=2, padding='same'),
            layers.Conv2DTranspose(32, 3, activation='relu', strides=2, padding='same'),
            layers.Conv2DTranspose(1, 3, activation='sigmoid', padding='same') # Salida de imagen
        ])

    @tf.function
    def encode(self, x, c):
        # Crear mapa de condición para concatenar a la imagen
        c_reshaped = tf.reshape(tf.one_hot(c, self.num_classes), [-1, 1, 1, self.num_classes])
        c_map = tf.cast(tf.ones([tf.shape(x)[0], 28, 28, 1]) * c_reshaped, tf.float32)
        x_cond = tf.concat([x, c_map], axis=-1)

        mean, logvar = tf.split(self.encoder(x_cond), num_or_size_splits=2, axis=1)
        return mean, logvar

    @tf.function
    def decode(self, z, c):
        # Concatenar condición al vector latente
        z_cond = tf.concat([z, tf.one_hot(c, self.num_classes)], axis=-1)
        return self.decoder(z_cond)

    @tf.function
    def sample(self, eps=None, c=None):
        if eps is None:
            eps = tf.random.normal(shape=(100, self.latent_dim))
        if c is None:
            c = tf.constant([i % self.num_classes for i in range(100)], dtype=tf.int32)
        return self.decode(eps, c)

    def call(self, inputs):
        x, c = inputs
        mean, logvar = self.encode(x, c)
        eps = tf.random.normal(shape=tf.shape(mean))
        z = mean + tf.exp(logvar * .5) * eps
        reconstruction = self.decode(z, c)
        return reconstruction, mean, logvar

# 3. Función de Pérdida (Reconstrucción + KL-divergence)
def log_normal_pdf(sample, mean, logvar, raxis=1):
    log2pi = tf.math.log(2. * np.pi)
    return tf.reduce_sum(
        -.5 * ((sample - mean) ** 2. * tf.exp(-logvar) + logvar + log2pi), axis=raxis)

def compute_loss(model, x, c):
    reconstruction, mean, logvar = model((x, c))
    cross_ent = tf.nn.sigmoid_cross_entropy_with_logits(logits=reconstruction, labels=x)
    logpx_z = -tf.reduce_sum(cross_ent, axis=[1, 2, 3])
    logpz = log_normal_pdf(mean, 0., 0.)
    logqz_x = log_normal_pdf(mean, mean, logvar)
    return -tf.reduce_mean(logpx_z + logpz - logqz_x)

optimizer = optimizers.Adam(1e-4)

# 4. Training Step
@tf.function
def train_step(model, x, c, optimizer):
    with tf.GradientTape() as tape:
        loss = compute_loss(model, x, c)
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))

# 5. Bucle de Entrenamiento
cvae = CVAE(LATENT_DIM, NUM_CLASSES)

def generate_and_save_cvae_images(model, epoch):
    generated_images = model.sample()
    fig = plt.figure(figsize=(10, 10))
    for i in range(generated_images.shape[0]):
        plt.subplot(10, 10, i + 1)
        plt.imshow(generated_images[i, :, :, 0], cmap='gray')
        plt.axis('off')
    plt.savefig('cvae_image_at_epoch_{:04d}.png'.format(epoch))
    plt.close()

EPOCHS = 100

for epoch in range(1, EPOCHS + 1):
    start_time = tf.timestamp()
    for x, y in train_dataset:
        train_step(cvae, x, y, optimizer)
    end_time = tf.timestamp()
    if epoch % 10 == 0:
        loss = tf.keras.metrics.Mean()
        for x, y in test_dataset:
            loss(compute_loss(cvae, x, y))
        elbo = -loss.result()
        print(f'Epoch: {epoch}, Test set ELBO: {elbo:.4f}, time: {end_time - start_time:.2f}s')
        generate_and_save_cvae_images(cvae, epoch)

print("Entrenamiento cVAE completado.")

🚀 Implementación de un cVAE en PyTorch

Vamos a construir un cVAE en PyTorch para generar imágenes condicionales de MNIST.

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

# 1. Preparar el Dataset
transform = transforms.Compose([
    transforms.ToTensor(),
    # No normalizamos a [-1,1] como en GANs, sino a [0,1] para BCE_with_logits
])

train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)

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

# 2. Definir el CVAE
class CVAE(nn.Module):
    def __init__(self, latent_dim, num_classes):
        super(CVAE, self).__init__()
        self.latent_dim = latent_dim
        self.num_classes = num_classes

        # Embedding para las etiquetas (condición)
        self.label_emb_encoder = nn.Embedding(num_classes, num_classes)
        self.label_emb_decoder = nn.Embedding(num_classes, num_classes)

        # Encoder
        self.encoder_fc1 = nn.Linear(np.prod((1, 28, 28)) + num_classes, 512)
        self.encoder_fc2 = nn.Linear(512, 256)
        self.mu_fc = nn.Linear(256, latent_dim)
        self.logvar_fc = nn.Linear(256, latent_dim)

        # Decoder
        self.decoder_fc1 = nn.Linear(latent_dim + num_classes, 256)
        self.decoder_fc2 = nn.Linear(256, 512)
        self.decoder_fc3 = nn.Linear(512, np.prod((1, 28, 28)))

    def encode(self, x, labels):
        # Concatenar imagen aplanada y embedding de etiqueta
        x_flat = x.view(x.size(0), -1)
        c_emb = self.label_emb_encoder(labels)
        enc_input = torch.cat([x_flat, c_emb], dim=1)

        h1 = F.relu(self.encoder_fc1(enc_input))
        h2 = F.relu(self.encoder_fc2(h1))
        return self.mu_fc(h2), self.logvar_fc(h2)

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def decode(self, z, labels):
        # Concatenar vector latente y embedding de etiqueta
        c_emb = self.label_emb_decoder(labels)
        dec_input = torch.cat([z, c_emb], dim=1)

        h1 = F.relu(self.decoder_fc1(dec_input))
        h2 = F.relu(self.decoder_fc2(h1))
        return torch.sigmoid(self.decoder_fc3(h2)).view(-1, 1, 28, 28)

    def forward(self, x, labels):
        mu, logvar = self.encode(x, labels)
        z = self.reparameterize(mu, logvar)
        return self.decode(z, labels), mu, logvar

# 3. Función de Pérdida
def loss_function(recon_x, x, mu, logvar):
    # BCE_with_logits requiere que recon_x no tenga sigmoid. Si el decoder ya tiene sigmoid, usar BCELoss
    BCE = F.binary_cross_entropy(recon_x, x.view(-1, 784), reduction='sum') # Ocupa x.view(-1, 784)

    # KL Divergence para VAE
    KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())

    return BCE + KLD

# 4. Hiperparámetros y Entrenamiento
LATENT_DIM = 20
NUM_CLASSES = 10

cvae = CVAE(LATENT_DIM, NUM_CLASSES).to(device)
optimizer = optim.Adam(cvae.parameters(), lr=1e-3)

# Bucle de Entrenamiento
EPOCHS = 100

def generate_and_save_cvae_images_pytorch(model, epoch, fixed_latent_sample, fixed_labels_sample, num_samples=10):
    model.eval()
    with torch.no_grad():
        sample = model.decode(fixed_latent_sample.to(device), fixed_labels_sample.to(device)).cpu()
    
    fig, axs = plt.subplots(1, num_samples, figsize=(10, 1))
    for i in range(num_samples):
        axs[i].imshow(sample[i, 0, :, :], cmap='gray')
        axs[i].axis('off')
    plt.savefig(f'pytorch_cvae_image_at_epoch_{epoch:04d}.png')
    plt.close()
    model.train()

# Generar una muestra latente fija y etiquetas para visualización
fixed_latent_sample = torch.randn(10, LATENT_DIM)
fixed_labels_sample = torch.LongTensor([i for i in range(10)])

for epoch in range(1, EPOCHS + 1):
    cvae.train()
    train_loss = 0
    for batch_idx, (data, labels) in enumerate(train_loader):
        data, labels = data.to(device), labels.to(device)
        optimizer.zero_grad()
        recon_batch, mu, logvar = cvae(data, labels)
        loss = loss_function(recon_batch, data, mu, logvar)
        loss.backward()
        train_loss += loss.item()
        optimizer.step()

    print(f'====> Epoch: {epoch} Average loss: {train_loss / len(train_loader.dataset):.4f}')
    if epoch % 10 == 0:
        generate_and_save_cvae_images_pytorch(cvae, epoch, fixed_latent_sample, fixed_labels_sample, num_samples=10)

print("Entrenamiento cVAE completado en PyTorch.")

📈 Evaluación de Modelos Generativos Condicionales

Evaluar modelos generativos es inherentemente complicado. Para los modelos condicionales, la evaluación se centra en dos aspectos principales:

  1. Realismo de las imágenes generadas: ¿Se ven las imágenes generadas como imágenes reales?
  2. Fidelidad condicional: ¿Las imágenes generadas realmente cumplen con la condición dada?

Aquí hay algunas métricas comunes:

MétricaDescripción¿Qué evalúa?
---------
Inception Score (IS)Mide la calidad de las imágenes (claridad y diversidad). Se espera que las imágenes generadas tengan objetos identificables (alta confianza en una clase) y que haya una buena variedad entre las imágenes. (Más alto es mejor)Realismo y diversidad
Fréchet Inception Distance (FID)Mide la distancia entre la distribución de características de las imágenes reales y las generadas. Es más robusto que IS y se correlaciona mejor con la percepción humana de calidad. (Más bajo es mejor)Realismo
---------
Precisión CondicionalUna evaluación más cualitativa o basada en clasificadores, donde se entrena un clasificador auxiliar para predecir la condición a partir de la imagen generada. Si el clasificador predice correctamente la condición, indica que el modelo generador entiende la condición.Fidelidad Condicional
Estudios HumanosPreguntar a personas si las imágenes son realistas y si cumplen con la condición. A menudo el "estándar de oro", pero costoso y difícil de escalar.Realismo y fidelidad (subjetiva)
⚠️ Advertencia: Calcular IS y FID requiere un modelo Inception pre-entrenado y es computacionalmente intensivo, especialmente para grandes datasets. Para la mayoría de los experimentos iniciales, la inspección visual es suficiente.

Generación de Muestras Condicionales para Evaluación

Para evaluar visualmente o con métricas, necesitamos generar lotes de imágenes para condiciones específicas.

# Ejemplo de generación condicional con cGAN (TensorFlow)
# Generar 100 imágenes de cada dígito (0-9)

num_samples_per_class = 10
noise_dim = 100
num_classes = 10

all_generated_images = []
all_generated_labels = []

for i in range(num_classes):
    target_label = i
    target_labels = tf.fill([num_samples_per_class], target_label) # Repetir la etiqueta
    target_labels_one_hot = tf.one_hot(target_labels, num_classes)
    
    noise = tf.random.normal([num_samples_per_class, noise_dim])
    gen_input = tf.concat([noise, target_labels_one_hot], axis=1)
    
    images = generator(gen_input, training=False)
    all_generated_images.append(images)
    all_generated_labels.append(target_labels)

final_images = tf.concat(all_generated_images, axis=0)
final_labels = tf.concat(all_generated_labels, axis=0)

# Ahora 'final_images' contiene 100 imágenes de cada dígito (1000 en total),
# y 'final_labels' sus condiciones correspondientes. Estos se pueden usar para FID/IS o inspección.

# Ejemplo de visualización para una clase específica
plt.figure(figsize=(10, 2))
for i in range(num_samples_per_class):
    plt.subplot(1, num_samples_per_class, i+1)
    plt.imshow(final_images[i, :, :, 0] * 127.5 + 127.5, cmap='gray') # Las primeras 10 son del dígito 0
    plt.axis('off')
plt.title(f"Dígitos generados para la clase {final_labels[0].numpy()}")
plt.show()

📝 Consideraciones y Desafíos

La generación de imágenes condicionales es un campo activo de investigación y presenta varios desafíos:

  • Estabilidad del Entrenamiento: Las GANs son notoriamente difíciles de entrenar. Las cGANs heredan este problema y pueden ser aún más sensibles a los hiperparámetros y la arquitectura.
  • Modos de Colapso: Tanto GANs como VAEs pueden sufrir de colapso de modo, donde el modelo solo produce una pequeña variedad de salidas, ignorando la diversidad en los datos de entrenamiento o en las condiciones.
  • Calidad de la Condición: La calidad de las imágenes generadas depende en gran medida de la calidad y expresividad de la condición. Una condición pobre puede llevar a resultados subóptimos o no condicionales.
  • Métricas de Evaluación: Como se mencionó, evaluar modelos generativos condicionales es un desafío, y las métricas existentes no siempre capturan completamente la calidad y la fidelidad condicional.
  • Escalabilidad: Generar imágenes de alta resolución y de gran diversidad con condiciones complejas (ej. texto) sigue siendo un área de investigación intensa, requiriendo modelos y recursos computacionales significativos.

FAQs y Recursos Adicionales

¿Cuál es la diferencia principal entre cGANs y cVAEs?

Las cGANs son entrenadas adversariamente, lo que las hace excelentes para generar imágenes muy nítidas y realistas. Sin embargo, son más difíciles de entrenar y pueden sufrir de inestabilidad. Los cVAEs son más fáciles de entrenar y proporcionan un espacio latente estructurado que permite interpolaciones suaves, pero las imágenes generadas a menudo son más borrosas que las de las GANs. Ambos inyectan la condición en el proceso de generación.

¿Puedo usar condiciones de texto?

¡Sí! Esto se conoce como "Text-to-Image Synthesis". Requiere un módulo adicional (generalmente un codificador de texto como un Transformer) para convertir el texto en un embedding vectorial que luego se utiliza como condición en la GAN o VAE. Modelos como DALL-E, Imagen o Stable Diffusion son ejemplos avanzados de esto.

¿Qué otros tipos de condiciones puedo usar?

Además de etiquetas de clase y texto, puedes usar:

  • Mapas de segmentación: Para generar imágenes fotorrealistas a partir de máscaras semánticas.
  • Bordes o bocetos: Para "colorear" un boceto o convertirlo en una imagen real.
  • Imágenes de baja resolución: Para super-resolución condicional.
  • Atributos faciales: Para modificar expresiones o características en rostros.

¿Dónde puedo aprender más?


✅ Conclusión

La generación de imágenes condicionales es una herramienta extraordinariamente poderosa en el arsenal de la inteligencia artificial, abriendo puertas a la creatividad y la síntesis de datos de maneras antes inimaginables. Hemos explorado cómo tanto las cGANs como los cVAEs permiten a los modelos no solo generar imágenes, sino hacerlo bajo el control de condiciones específicas, lo que nos da un control sin precedentes sobre el proceso generativo.

Al dominar estas técnicas, estás un paso más cerca de crear sistemas de IA capaces de comprender y materializar visiones complejas. ¡El futuro de la creación de contenido es condicional!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!