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.
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.
🛠️ 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í:
- 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.
- 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.
💡 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.")
🚀 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:
- 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.
- 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.
🚀 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:
- Realismo de las imágenes generadas: ¿Se ven las imágenes generadas como imágenes reales?
- Fidelidad condicional: ¿Las imágenes generadas realmente cumplen con la condición dada?
Aquí hay algunas métricas comunes:
| Métrica | Descripció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 Condicional | Una 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 Humanos | Preguntar 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) |
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
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?
- Artículos originales:
- Documentación oficial:
- Cursos de Aprendizaje Profundo: Coursera, Udacity, fast.ai ofrecen excelentes módulos sobre modelos generativos.
✅ 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
- Cuantización de Modelos de IA con TensorFlow y PyTorch: Más Allá de la Precisión de 32 bitsintermediate25 min
- Transfer Learning con TensorFlow y PyTorch: Más Allá de la Congelación de Capasintermediate20 min
- Optimización de Modelos en TensorFlow y PyTorch: Una Guía Práctica para un Entrenamiento Eficienteintermediate20 min
- Atención y Transformers desde Cero: Implementando Redes Neuronales Auto-Atentivas en TensorFlow y PyTorchintermediate18 min
- Detección de Anomalías con Autoencoders Variacionales (VAE) en TensorFlow y PyTorchintermediate30 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!