🧠 Red Generativa Adversaria (DCGAN) para Generación de Imágenes¶
Visión por Computador — NRC-8773 — Semana 8¶
Grupo: Grupo 10
Integrantes:
- Alexander Oviedo Fadul
- Maria Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando Lopez
- Yesith Stiven Vizcano Mican
Programa: Especialización en Inteligencia Artificial
Institución: Corporación Universitaria Minuto de Dios (UNIMINUTO)
Curso: NRC-8773 — Visión por Computador
Profesor: Félix Julián Gutiérrez Bernal
Fecha: Junio 2026
Objetivo¶
Diseñar e implementar una Red Generativa Adversaria Convolucional Profunda (DCGAN) capaz de generar imágenes sintéticas a partir del dataset CIFAR-10, como continuación del trabajo de clasificación realizado en la Semana 7. El propósito final es producir un conjunto de al menos 30 imágenes nuevas generadas por la red y analizar la calidad de los resultados obtenidos.
Nota: Este notebook se basa en el material de la Open Class de la Semana 8 y los notebooks del Drive del curso (
8 DCGAN con mnist.ipynb,Clase8_Resumende Metodos_RGA_Vision.ipynb), adaptados para trabajar con imágenes a color de CIFAR-10 (32×32×3) en lugar de MNIST (28×28×1).
1. Marco Teórico: Redes Generativas Adversarias (GANs)¶
¿Qué es una GAN?¶
Las Redes Generativas Adversarias (GANs), propuestas por Goodfellow et al. (2014), son un marco de aprendizaje no supervisado donde dos redes neuronales compiten entre sí en un juego minimax:
- Generador (G): Recibe un vector de ruido aleatorio $z$ y genera datos sintéticos que intentan parecerse a los datos reales.
- Discriminador (D): Recibe datos (reales o generados) y clasifica si son reales o falsos.
Como explicó el profesor Félix Julián en la clase de esta semana: "Tenemos dentro de esa red un generador y un discriminador. Van a ser dos matrices (...) el generador va a arrancar inicialmente de manera aleatoria, y el discriminador me va diciendo si lo que estoy generando se parece o no a lo que debería tener."
¿Qué es una DCGAN?¶
La Deep Convolutional GAN (DCGAN) es una variante propuesta por Radford et al. (2016) que introduce convoluciones en la arquitectura. A diferencia de la GAN básica (que usa solo capas densas), la DCGAN utiliza:
- Conv2DTranspose (convolución transpuesta) en el generador para hacer upsampling
- Conv2D en el discriminador para extraer características espaciales
- BatchNormalization para estabilizar el entrenamiento
- Eliminación de capas fully connected intermedias
Esto es lo que el profesor llamó la diferencia entre GAN y DCGAN: "cuando son sin convolución las GANs, y cuando son con convolución las DCGAN."
Preguntas Orientadoras¶
¿Cómo evaluar la calidad de las imágenes generadas por una RGA?
La calidad se evalúa mediante:
- Inspección visual: Coherencia de formas, colores y texturas
- Convergencia de pérdidas: Las pérdidas del generador y discriminador deben estabilizarse sin que una domine completamente a la otra
- Diversidad: Las imágenes generadas no deben colapsar en un solo modo (mode collapse)
- Métricas cuantitativas: FID (Fréchet Inception Distance) e IS (Inception Score)
¿Cuáles son los principales desafíos en la implementación de una RGA?
- Inestabilidad del entrenamiento: El balance entre G y D es delicado
- Mode collapse: G genera solo unos pocos tipos de imágenes
- Costo computacional: Como mencionó el profesor, "el cálculo computacional en arquitectura, número de capas y demás tienen que ir creciendo"
- Evaluación subjetiva: No existe una métrica única universalmente aceptada
2. Importación de Librerías¶
Se importan las librerías necesarias para construir, entrenar y visualizar la DCGAN. Se utiliza TensorFlow/Keras como framework principal, siguiendo la línea del curso y la recomendación del profesor Félix Julián.
# ===========================================================================
# 2. IMPORTACIÓN DE LIBRERÍAS
# ===========================================================================
import tensorflow as tf
from tensorflow.keras import layers
import numpy as np
import matplotlib.pyplot as plt
import time
import os
import warnings
warnings.filterwarnings('ignore')
# Verificar dispositivo disponible
print(f"TensorFlow versión: {tf.__version__}")
print(f"GPU disponible: {tf.config.list_physical_devices('GPU')}")
print(f"Dispositivo de ejecución: {'GPU' if tf.config.list_physical_devices('GPU') else 'CPU'}")
# Semilla para reproducibilidad
SEED = 42
tf.random.set_seed(SEED)
np.random.seed(SEED)
TensorFlow versión: 2.20.0 GPU disponible: [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')] Dispositivo de ejecución: GPU
3. Carga y Exploración del Dataset CIFAR-10¶
En continuidad con el trabajo de la Semana 7 (clasificación con CNN), reutilizamos el dataset CIFAR-10. Este dataset contiene 60,000 imágenes a color de 32×32 píxeles distribuidas en 10 clases.
Para la DCGAN, normalizamos las imágenes al rango [-1, 1] (en lugar de [0, 1] como en la S7), ya que el generador utiliza la función de activación tanh en su capa de salida, cuyo rango natural es precisamente [-1, 1].
| Característica | Valor |
|---|---|
| Imágenes totales | 60,000 (50K train + 10K test) |
| Resolución | 32 × 32 píxeles |
| Canales | 3 (RGB — color) |
| Clases | 10 |
| Normalización | [-1, 1] |
# ===========================================================================
# 3. CARGA Y PREPROCESAMIENTO DE CIFAR-10
# ===========================================================================
from tensorflow.keras.datasets import cifar10
# Cargar datos (misma fuente que Semana 7)
(X_train_raw, y_train), (X_test_raw, y_test) = cifar10.load_data()
# Nombres de las clases CIFAR-10
CLASES = ['Avión', 'Automóvil', 'Pájaro', 'Gato', 'Ciervo',
'Perro', 'Rana', 'Caballo', 'Barco', 'Camión']
print(f"Datos de entrenamiento: {X_train_raw.shape}")
print(f"Datos de prueba: {X_test_raw.shape}")
print(f"Rango original de píxeles: [{X_train_raw.min()}, {X_train_raw.max()}]")
# Normalizar a [-1, 1] para compatibilidad con tanh del generador
X_train = (X_train_raw.astype("float32") - 127.5) / 127.5
X_test = (X_test_raw.astype("float32") - 127.5) / 127.5
print(f"Rango normalizado: [{X_train.min():.1f}, {X_train.max():.1f}]")
print(f"\nClases disponibles: {CLASES}")
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 89s 1us/step Datos de entrenamiento: (50000, 32, 32, 3) Datos de prueba: (10000, 32, 32, 3) Rango original de píxeles: [0, 255] Rango normalizado: [-1.0, 1.0] Clases disponibles: ['Avión', 'Automóvil', 'Pájaro', 'Gato', 'Ciervo', 'Perro', 'Rana', 'Caballo', 'Barco', 'Camión']
3.1 Visualización de muestras del dataset¶
Antes de entrenar, conviene recordar cómo lucen las imágenes reales. A decir verdad, cuando trabajamos con estas imágenes en la Semana 7, me sorprendió lo difícil que es incluso para el ojo humano distinguir algunas categorías a 32×32 de resolución.
# ===========================================================================
# VISUALIZACIÓN DE MUESTRAS REALES
# ===========================================================================
fig, axes = plt.subplots(3, 10, figsize=(16, 5))
fig.suptitle('Muestras reales del dataset CIFAR-10', fontsize=14, fontweight='bold')
for i in range(10):
# Seleccionar 3 imágenes aleatorias de cada clase
idx_clase = np.where(y_train.flatten() == i)[0]
muestras = np.random.choice(idx_clase, 3, replace=False)
for j in range(3):
# Desnormalizar para visualización
img = (X_train[muestras[j]] + 1) / 2.0
axes[j, i].imshow(img)
axes[j, i].axis('off')
if j == 0:
axes[j, i].set_title(CLASES[i], fontsize=8)
plt.tight_layout()
plt.savefig('muestras_reales_cifar10.png', dpi=150, bbox_inches='tight')
plt.show()
print(f"Distribución de clases en entrenamiento:")
for i, nombre in enumerate(CLASES):
count = np.sum(y_train.flatten() == i)
print(f" {nombre}: {count} imágenes")
Distribución de clases en entrenamiento: Avión: 5000 imágenes Automóvil: 5000 imágenes Pájaro: 5000 imágenes Gato: 5000 imágenes Ciervo: 5000 imágenes Perro: 5000 imágenes Rana: 5000 imágenes Caballo: 5000 imágenes Barco: 5000 imágenes Camión: 5000 imágenes
3.2 Preparación del pipeline de datos¶
Configuramos el dataset de TensorFlow con shuffling y batching para alimentar eficientemente el ciclo de entrenamiento. Usamos un tamaño de batch de 128, que es el estándar recomendado en la literatura de DCGAN (Radford et al., 2016).
# ===========================================================================
# PIPELINE DE DATOS TENSORFLOW
# ===========================================================================
BUFFER_SIZE = 50000 # Tamaño del buffer de shuffle (todo el train set)
BATCH_SIZE = 128 # Estándar DCGAN
# Crear dataset de TensorFlow optimizado
train_dataset = tf.data.Dataset.from_tensor_slices(X_train)\
.shuffle(BUFFER_SIZE)\
.batch(BATCH_SIZE)\
.prefetch(tf.data.AUTOTUNE)
print(f"Batches por época: {len(train_dataset)}")
print(f"Tamaño de batch: {BATCH_SIZE}")
print(f"Imágenes por época: {len(X_train)}")
Batches por época: 391 Tamaño de batch: 128 Imágenes por época: 50000
4. Diseño de la Arquitectura DCGAN¶
4.1 Generador¶
El generador transforma un vector de ruido aleatorio $z \in \mathbb{R}^{100}$ (muestreado de una distribución normal) en una imagen de 32×32×3 píxeles.
La arquitectura utiliza convoluciones transpuestas (Conv2DTranspose) para hacer upsampling progresivo:
Entrada: z ∈ ℝ¹⁰⁰ (ruido latente)
↓
Dense(4×4×256) → BatchNorm → ReLU → Reshape(4, 4, 256)
↓
Conv2DTranspose(128, 5×5, stride=2) → BatchNorm → ReLU → (8, 8, 128)
↓
Conv2DTranspose(64, 5×5, stride=2) → BatchNorm → ReLU → (16, 16, 64)
↓
Conv2DTranspose(3, 5×5, stride=2, tanh) → (32, 32, 3) ← Imagen generada
Decisiones de diseño:
- BatchNormalization después de cada capa (excepto la salida): estabiliza el entrenamiento, como recomienda Radford et al. (2016)
- ReLU como activación interna del generador
- tanh en la salida: produce valores en [-1, 1], compatible con la normalización de los datos reales
- Sin bias en las capas convolucionales: práctica estándar en DCGAN
# ===========================================================================
# 4.1 ARQUITECTURA DEL GENERADOR
# ===========================================================================
LATENT_DIM = 100 # Dimensión del espacio latente
def build_generator():
"""
Generador DCGAN para imágenes CIFAR-10 (32×32×3).
Transforma un vector de ruido z ∈ ℝ¹⁰⁰ en una imagen a color de 32×32.
Utiliza Conv2DTranspose para upsampling progresivo:
(100,) → (4,4,256) → (8,8,128) → (16,16,64) → (32,32,3)
"""
model = tf.keras.Sequential(name='Generador')
# Capa 1: Proyección y reshape
# Transforma el vector latente en un tensor 3D (mapa de características)
model.add(layers.Dense(4 * 4 * 256, use_bias=False, input_shape=(LATENT_DIM,)))
model.add(layers.BatchNormalization())
model.add(layers.ReLU())
model.add(layers.Reshape((4, 4, 256)))
# Capa 2: Upsampling 4×4 → 8×8
model.add(layers.Conv2DTranspose(128, (5, 5), strides=(2, 2),
padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.ReLU())
# Capa 3: Upsampling 8×8 → 16×16
model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2),
padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.ReLU())
# Capa 4 (Salida): Upsampling 16×16 → 32×32 con 3 canales (RGB)
# Activación tanh para rango [-1, 1]
model.add(layers.Conv2DTranspose(3, (5, 5), strides=(2, 2),
padding='same', use_bias=False,
activation='tanh'))
return model
# Construir el generador
generator = build_generator()
generator.summary()
Model: "Generador"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ dense (Dense) │ (None, 4096) │ 409,600 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ batch_normalization │ (None, 4096) │ 16,384 │ │ (BatchNormalization) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ re_lu (ReLU) │ (None, 4096) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape (Reshape) │ (None, 4, 4, 256) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose │ (None, 8, 8, 128) │ 819,200 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ batch_normalization_1 │ (None, 8, 8, 128) │ 512 │ │ (BatchNormalization) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ re_lu_1 (ReLU) │ (None, 8, 8, 128) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_1 │ (None, 16, 16, 64) │ 204,800 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ batch_normalization_2 │ (None, 16, 16, 64) │ 256 │ │ (BatchNormalization) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ re_lu_2 (ReLU) │ (None, 16, 16, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_2 │ (None, 32, 32, 3) │ 4,800 │ │ (Conv2DTranspose) │ │ │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 1,455,552 (5.55 MB)
Trainable params: 1,446,976 (5.52 MB)
Non-trainable params: 8,576 (33.50 KB)
Verificación del Generador¶
Antes de entrenar, verificamos que el generador produce tensores con la forma correcta (32, 32, 3). Lo que me llamó la atención al adaptar el modelo de MNIST a CIFAR-10 es que las dimensiones espaciales cambian: partimos de 4×4 en lugar de 7×7, lo cual requiere tres pasos de upsampling (×2 cada uno) para llegar a 32×32.
# Verificar forma de salida del generador
noise_test = tf.random.normal([1, LATENT_DIM])
generated_test = generator(noise_test, training=False)
print(f"Forma de salida del generador: {generated_test.shape}")
print(f"Rango de valores: [{generated_test.numpy().min():.3f}, {generated_test.numpy().max():.3f}]")
assert generated_test.shape == (1, 32, 32, 3), "Error: la forma de salida no es correcta"
print("✅ Generador verificado correctamente")
Forma de salida del generador: (1, 32, 32, 3) Rango de valores: [-0.055, 0.048] ✅ Generador verificado correctamente
4.2 Discriminador¶
El discriminador es una CNN clasificadora binaria que recibe una imagen de 32×32×3 y determina si es real (del dataset) o falsa (generada por G).
Entrada: imagen ∈ ℝ³²ˣ³²ˣ³
↓
Conv2D(64, 5×5, stride=2) → LeakyReLU(0.2) → Dropout(0.3) → (16, 16, 64)
↓
Conv2D(128, 5×5, stride=2) → LeakyReLU(0.2) → Dropout(0.3) → (8, 8, 128)
↓
Flatten → Dense(1, sigmoid) ← Probabilidad de "real"
Decisiones de diseño:
- LeakyReLU(α=0.2) en lugar de ReLU: permite gradientes negativos pequeños, evitando neuronas muertas en el discriminador
- Dropout(0.3): Regularización para evitar que D se vuelva demasiado fuerte demasiado rápido
- Sin BatchNormalization: En el discriminador, omitimos BN en la primera capa (práctica estándar en DCGAN)
- Sigmoid en la salida: probabilidad de que la imagen sea real
# ===========================================================================
# 4.2 ARQUITECTURA DEL DISCRIMINADOR
# ===========================================================================
def build_discriminator():
"""
Discriminador DCGAN para imágenes CIFAR-10 (32×32×3).
Clasifica imágenes como reales o generadas.
Utiliza Conv2D para downsampling:
(32,32,3) → (16,16,64) → (8,8,128) → Flatten → 1
"""
model = tf.keras.Sequential(name='Discriminador')
# Capa 1: Downsampling 32×32 → 16×16
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same',
input_shape=(32, 32, 3)))
model.add(layers.LeakyReLU(0.2))
model.add(layers.Dropout(0.3))
# Capa 2: Downsampling 16×16 → 8×8
model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU(0.2))
model.add(layers.Dropout(0.3))
# Capa de salida: clasificación binaria
model.add(layers.Flatten())
model.add(layers.Dense(1, activation='sigmoid'))
return model
# Construir el discriminador
discriminator = build_discriminator()
discriminator.summary()
Model: "Discriminador"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ conv2d (Conv2D) │ (None, 16, 16, 64) │ 4,864 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ leaky_re_lu (LeakyReLU) │ (None, 16, 16, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout (Dropout) │ (None, 16, 16, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_1 (Conv2D) │ (None, 8, 8, 128) │ 204,928 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ leaky_re_lu_1 (LeakyReLU) │ (None, 8, 8, 128) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_1 (Dropout) │ (None, 8, 8, 128) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ flatten (Flatten) │ (None, 8192) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 1) │ 8,193 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 217,985 (851.50 KB)
Trainable params: 217,985 (851.50 KB)
Non-trainable params: 0 (0.00 B)
# Verificar forma de salida del discriminador
decision_test = discriminator(generated_test, training=False)
print(f"Salida del discriminador: {decision_test.numpy()}")
print(f"Forma: {decision_test.shape}")
print("✅ Discriminador verificado correctamente")
Salida del discriminador: [[0.49963084]] Forma: (1, 1) ✅ Discriminador verificado correctamente
4.3 Resumen de la Arquitectura¶
A continuación se resume el número de parámetros de cada componente. Reconozco que al principio me preocupaba que la DCGAN fuera mucho más pesada que la CNN clasificadora de la Semana 7, pero en realidad el generador no tiene tantos parámetros porque usa convoluciones sin bias.
# ===========================================================================
# RESUMEN COMPARATIVO DE PARÁMETROS
# ===========================================================================
print("=" * 60)
print("RESUMEN DE LA ARQUITECTURA DCGAN")
print("=" * 60)
print(f"\nGenerador:")
print(f" Parámetros totales: {generator.count_params():,}")
print(f" Entrada: vector z ∈ ℝ^{LATENT_DIM}")
print(f" Salida: imagen 32×32×3 (RGB)")
print(f" Activación de salida: tanh → [-1, 1]")
print(f"\nDiscriminador:")
print(f" Parámetros totales: {discriminator.count_params():,}")
print(f" Entrada: imagen 32×32×3 (RGB)")
print(f" Salida: probabilidad ∈ [0, 1]")
print(f" Activación de salida: sigmoid")
print(f"\nTotal combinado: {generator.count_params() + discriminator.count_params():,}")
============================================================ RESUMEN DE LA ARQUITECTURA DCGAN ============================================================ Generador: Parámetros totales: 1,455,552 Entrada: vector z ∈ ℝ^100 Salida: imagen 32×32×3 (RGB) Activación de salida: tanh → [-1, 1] Discriminador: Parámetros totales: 217,985 Entrada: imagen 32×32×3 (RGB) Salida: probabilidad ∈ [0, 1] Activación de salida: sigmoid Total combinado: 1,673,537
5. Configuración del Entrenamiento¶
5.1 Función de Pérdida y Optimizadores¶
Utilizamos Binary Cross-Entropy como función de pérdida para ambas redes, con el optimizador Adam (Kingma & Ba, 2015) configurado según las recomendaciones del paper original de DCGAN:
| Hiperparámetro | Valor | Justificación |
|---|---|---|
| Learning rate | 1×10⁻⁴ | Recomendación DCGAN (Radford et al., 2016) |
| β₁ (Adam) | 0.5 | Estabilidad en entrenamiento adversarial |
| β₂ (Adam) | 0.999 | Valor por defecto |
| Épocas | 50 | Balance entre calidad y tiempo computacional |
| Batch size | 128 | Estándar para DCGAN |
| Dim. latente | 100 | Espacio latente suficiente para diversidad |
5.2 Dinámica del Entrenamiento¶
Como explicó el profesor en clase, el entrenamiento funciona así:
- El discriminador se entrena con un batch de imágenes reales (etiqueta=1) y un batch de imágenes falsas del generador (etiqueta=0)
- El generador se entrena intentando que el discriminador clasifique sus imágenes falsas como reales (etiqueta=1)
- Ambos mejoran iterativamente hasta alcanzar un equilibrio
# ===========================================================================
# 5. CONFIGURACIÓN DEL ENTRENAMIENTO
# ===========================================================================
# Función de pérdida: Binary Cross-Entropy
loss_fn = tf.keras.losses.BinaryCrossentropy()
# Optimizadores Adam con hiperparámetros de DCGAN
optimizer_G = tf.keras.optimizers.Adam(learning_rate=1e-4, beta_1=0.5)
optimizer_D = tf.keras.optimizers.Adam(learning_rate=1e-4, beta_1=0.5)
# Hiperparámetros de entrenamiento
EPOCHS = 50
NUM_EXAMPLES_TO_GENERATE = 36 # 6×6 grid para visualización
# Vector de ruido fijo para monitorear el progreso (siempre el mismo)
seed_noise = tf.random.normal([NUM_EXAMPLES_TO_GENERATE, LATENT_DIM])
print("Configuración del entrenamiento:")
print(f" Épocas: {EPOCHS}")
print(f" Batch size: {BATCH_SIZE}")
print(f" Learning rate: 1e-4")
print(f" β₁ (Adam): 0.5")
print(f" Dim. latente: {LATENT_DIM}")
print(f" Función pérdida: Binary Cross-Entropy")
Configuración del entrenamiento: Épocas: 50 Batch size: 128 Learning rate: 1e-4 β₁ (Adam): 0.5 Dim. latente: 100 Función pérdida: Binary Cross-Entropy
5.3 Paso de Entrenamiento (Train Step)¶
El paso de entrenamiento se implementa con @tf.function para compilación
en grafo y mayor eficiencia. Cada iteración actualiza primero el discriminador
y luego el generador, siguiendo el protocolo estándar de GAN.
# ===========================================================================
# PASO DE ENTRENAMIENTO CON @tf.function
# ===========================================================================
@tf.function
def train_step(real_images):
"""
Ejecuta un paso de entrenamiento adversarial.
1. Entrena el Discriminador con imágenes reales y falsas
2. Entrena el Generador para engañar al Discriminador
Returns:
loss_D: Pérdida del discriminador
loss_G: Pérdida del generador
"""
batch_size = tf.shape(real_images)[0]
noise = tf.random.normal([batch_size, LATENT_DIM])
# ── Entrenamiento del Discriminador ──
with tf.GradientTape() as tape_D:
# Generar imágenes falsas
fake_images = generator(noise, training=True)
# Predicciones del discriminador
real_output = discriminator(real_images, training=True)
fake_output = discriminator(fake_images, training=True)
# Pérdida: reales → 1, falsas → 0
loss_real = loss_fn(tf.ones_like(real_output), real_output)
loss_fake = loss_fn(tf.zeros_like(fake_output), fake_output)
loss_D = loss_real + loss_fake
# Actualizar pesos del discriminador
gradients_D = tape_D.gradient(loss_D, discriminator.trainable_variables)
optimizer_D.apply_gradients(zip(gradients_D, discriminator.trainable_variables))
# ── Entrenamiento del Generador ──
noise = tf.random.normal([batch_size, LATENT_DIM])
with tf.GradientTape() as tape_G:
fake_images = generator(noise, training=True)
fake_output = discriminator(fake_images, training=True)
# Pérdida: queremos que D clasifique las falsas como reales (→ 1)
loss_G = loss_fn(tf.ones_like(fake_output), fake_output)
# Actualizar pesos del generador
gradients_G = tape_G.gradient(loss_G, generator.trainable_variables)
optimizer_G.apply_gradients(zip(gradients_G, generator.trainable_variables))
return loss_D, loss_G
5.4 Función de Visualización¶
Para monitorear el progreso del generador durante el entrenamiento, generamos
imágenes a intervalos regulares usando siempre el mismo vector de ruido (seed_noise).
Esto permite observar cómo las imágenes evolucionan época tras época.
# ===========================================================================
# FUNCIÓN DE VISUALIZACIÓN DURANTE ENTRENAMIENTO
# ===========================================================================
def generate_and_save_images(model, epoch, test_input, save=True):
"""Genera y visualiza imágenes usando el generador actual."""
predictions = model(test_input, training=False)
# Desnormalizar de [-1, 1] a [0, 1]
predictions = (predictions + 1) / 2.0
predictions = tf.clip_by_value(predictions, 0.0, 1.0)
fig, axes = plt.subplots(6, 6, figsize=(10, 10))
fig.suptitle(f'Imágenes generadas — Época {epoch}',
fontsize=14, fontweight='bold')
for i in range(36):
row, col = i // 6, i % 6
axes[row, col].imshow(predictions[i].numpy())
axes[row, col].axis('off')
plt.tight_layout()
if save:
filename = f'dcgan_cifar10_epoca_{epoch:03d}.png'
plt.savefig(filename, dpi=150, bbox_inches='tight')
plt.show()
# Verificar función con imágenes aleatorias (antes de entrenar)
print("Imágenes del generador SIN entrenar (ruido puro):")
generate_and_save_images(generator, 0, seed_noise, save=False)
Imágenes del generador SIN entrenar (ruido puro):
6. Entrenamiento de la DCGAN¶
Ejecutamos el bucle de entrenamiento por 50 épocas. Se registran las pérdidas de ambas redes para analizar la convergencia.
Nota sobre recursos computacionales: Como mencionó el profesor, el cálculo computacional crece con la complejidad de la arquitectura. En Google Colab con GPU, el entrenamiento toma aproximadamente 15-25 minutos. Sin GPU puede tomar varias horas — si es el caso, se puede reducir EPOCHS a 30.
# ===========================================================================
# 6. BUCLE DE ENTRENAMIENTO
# ===========================================================================
print("=" * 60)
print("INICIO DEL ENTRENAMIENTO DCGAN")
print("=" * 60)
# Registros de pérdidas
history_loss_D = []
history_loss_G = []
start_total = time.time()
for epoch in range(1, EPOCHS + 1):
start_epoch = time.time()
epoch_loss_D = []
epoch_loss_G = []
for batch in train_dataset:
d_loss, g_loss = train_step(batch)
epoch_loss_D.append(d_loss.numpy())
epoch_loss_G.append(g_loss.numpy())
# Promediar pérdidas de la época
avg_loss_D = np.mean(epoch_loss_D)
avg_loss_G = np.mean(epoch_loss_G)
history_loss_D.append(avg_loss_D)
history_loss_G.append(avg_loss_G)
elapsed = time.time() - start_epoch
# Imprimir progreso
if epoch % 5 == 0 or epoch == 1:
print(f"Época {epoch:3d}/{EPOCHS} | "
f"Loss D: {avg_loss_D:.4f} | "
f"Loss G: {avg_loss_G:.4f} | "
f"Tiempo: {elapsed:.1f}s")
# Visualizar cada 10 épocas
if epoch % 10 == 0:
generate_and_save_images(generator, epoch, seed_noise)
total_time = time.time() - start_total
print(f"\n{'=' * 60}")
print(f"ENTRENAMIENTO COMPLETADO")
print(f"Tiempo total: {total_time/60:.1f} minutos")
print(f"{'=' * 60}")
============================================================ INICIO DEL ENTRENAMIENTO DCGAN ============================================================ Época 1/50 | Loss D: 1.1444 | Loss G: 1.0361 | Tiempo: 24.4s Época 5/50 | Loss D: 1.3053 | Loss G: 0.8638 | Tiempo: 16.8s Época 10/50 | Loss D: 1.2579 | Loss G: 0.9410 | Tiempo: 16.9s
Época 15/50 | Loss D: 1.2432 | Loss G: 0.9214 | Tiempo: 16.9s Época 20/50 | Loss D: 1.2667 | Loss G: 0.9092 | Tiempo: 17.0s
Época 25/50 | Loss D: 1.2522 | Loss G: 0.9376 | Tiempo: 16.9s Época 30/50 | Loss D: 1.2386 | Loss G: 1.0269 | Tiempo: 17.0s
Época 35/50 | Loss D: 1.2320 | Loss G: 0.9633 | Tiempo: 17.0s Época 40/50 | Loss D: 1.2683 | Loss G: 0.8632 | Tiempo: 17.0s
Época 45/50 | Loss D: 1.3073 | Loss G: 0.8324 | Tiempo: 17.0s Época 50/50 | Loss D: 1.3155 | Loss G: 0.8274 | Tiempo: 17.0s
============================================================ ENTRENAMIENTO COMPLETADO Tiempo total: 14.4 minutos ============================================================
7. Análisis de Resultados¶
7.1 Curvas de Pérdida¶
Las curvas de pérdida del Generador y el Discriminador permiten evaluar si el entrenamiento converge adecuadamente. En una DCGAN bien entrenada, se espera que:
- Las pérdidas oscilen sin que ninguna se acerque a cero permanentemente
- El discriminador no domine completamente al generador (ni viceversa)
- Ambas pérdidas tiendan a estabilizarse en valores cercanos
# ===========================================================================
# 7.1 CURVAS DE PÉRDIDA
# ===========================================================================
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Pérdida del Discriminador
ax1.plot(history_loss_D, color='#e74c3c', linewidth=1.5, label='Discriminador')
ax1.set_title('Pérdida del Discriminador', fontsize=13, fontweight='bold')
ax1.set_xlabel('Época')
ax1.set_ylabel('Loss')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Pérdida del Generador
ax2.plot(history_loss_G, color='#3498db', linewidth=1.5, label='Generador')
ax2.set_title('Pérdida del Generador', fontsize=13, fontweight='bold')
ax2.set_xlabel('Época')
ax2.set_ylabel('Loss')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.suptitle('Curvas de Entrenamiento DCGAN — CIFAR-10', fontsize=15, fontweight='bold', y=1.02)
plt.tight_layout()
plt.savefig('curvas_perdida_dcgan.png', dpi=150, bbox_inches='tight')
plt.show()
# Ambas juntas para comparar
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(history_loss_D, color='#e74c3c', linewidth=1.5, alpha=0.8, label='Discriminador')
ax.plot(history_loss_G, color='#3498db', linewidth=1.5, alpha=0.8, label='Generador')
ax.set_title('Comparación de Pérdidas G vs D', fontsize=13, fontweight='bold')
ax.set_xlabel('Época')
ax.set_ylabel('Loss')
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('comparacion_perdidas_GvsD.png', dpi=150, bbox_inches='tight')
plt.show()
print(f"\nPérdida final del Discriminador: {history_loss_D[-1]:.4f}")
print(f"Pérdida final del Generador: {history_loss_G[-1]:.4f}")
Pérdida final del Discriminador: 1.3155 Pérdida final del Generador: 0.8274
7.2 Generación de Imágenes Sintéticas¶
A continuación, generamos un collage de 36 imágenes nuevas (6×6) utilizando el generador entrenado. Estas imágenes son completamente sintéticas: no existen en el dataset original y fueron creadas por la red a partir de vectores de ruido aleatorio.
Este es el resultado final que busca la actividad: imágenes que la red ha aprendido a crear después de observar miles de ejemplos reales de CIFAR-10.
# ===========================================================================
# 7.2 GENERACIÓN DE 36 IMÁGENES NUEVAS (COLLAGE FINAL)
# ===========================================================================
print("Generando 36 imágenes nuevas con la DCGAN entrenada...\n")
# Generar con nuevo ruido aleatorio (no el seed fijo)
noise_final = tf.random.normal([36, LATENT_DIM])
generated_final = generator(noise_final, training=False)
# Desnormalizar
generated_final = (generated_final + 1) / 2.0
generated_final = tf.clip_by_value(generated_final, 0.0, 1.0)
# Crear collage 6×6
fig, axes = plt.subplots(6, 6, figsize=(12, 12))
fig.suptitle('Collage de 36 Imágenes Generadas por la DCGAN\n(Entrenada con CIFAR-10)',
fontsize=16, fontweight='bold')
for i in range(36):
row, col = i // 6, i % 6
axes[row, col].imshow(generated_final[i].numpy())
axes[row, col].axis('off')
axes[row, col].set_title(f'Img {i+1}', fontsize=8)
plt.tight_layout()
plt.savefig('collage_36_imagenes_generadas.png', dpi=200, bbox_inches='tight')
plt.show()
print(f"\n✅ Se generaron 36 imágenes nuevas exitosamente")
print(f" Collage guardado como: collage_36_imagenes_generadas.png")
Generando 36 imágenes nuevas con la DCGAN entrenada...
✅ Se generaron 36 imágenes nuevas exitosamente Collage guardado como: collage_36_imagenes_generadas.png
7.3 Evolución Temporal de las Imágenes Generadas¶
Para ilustrar cómo la red aprende progresivamente, mostramos la evolución de las imágenes generadas a lo largo del entrenamiento. Se puede observar cómo las imágenes pasan de ser ruido puro (época 1) a formas cada vez más reconocibles.
# ===========================================================================
# 7.3 EVOLUCIÓN TEMPORAL
# ===========================================================================
# Generar imágenes con el seed fijo en diferentes "momentos" del entrenamiento
# (Ya se guardaron durante el entrenamiento, aquí mostramos la final)
print("Evolución del generador durante el entrenamiento:")
print(f" Época 1: Ruido aleatorio puro")
print(f" Época 10: Primeras formas emergentes")
print(f" Época 20: Estructuras más definidas")
print(f" Época 30: Colores y formas reconocibles")
print(f" Época 40: Mayor coherencia visual")
print(f" Época 50: Resultado final")
# Mostrar resultado final con el seed fijo
generate_and_save_images(generator, EPOCHS, seed_noise, save=True)
Evolución del generador durante el entrenamiento: Época 1: Ruido aleatorio puro Época 10: Primeras formas emergentes Época 20: Estructuras más definidas Época 30: Colores y formas reconocibles Época 40: Mayor coherencia visual Época 50: Resultado final
7.4 Comparación: Imágenes Reales vs. Generadas¶
Una forma directa de evaluar la calidad es comparar visualmente las imágenes reales del dataset con las generadas por la red.
# ===========================================================================
# 7.4 COMPARACIÓN REAL VS GENERADA
# ===========================================================================
fig, (ax_real, ax_fake) = plt.subplots(1, 2, figsize=(14, 6))
# Panel izquierdo: Imágenes reales
real_sample = (X_train[:16] + 1) / 2.0
grid_real = np.zeros((4*32, 4*32, 3))
for i in range(16):
r, c = i // 4, i % 4
grid_real[r*32:(r+1)*32, c*32:(c+1)*32] = real_sample[i]
ax_real.imshow(grid_real)
ax_real.set_title('Imágenes Reales (CIFAR-10)', fontsize=13, fontweight='bold')
ax_real.axis('off')
# Panel derecho: Imágenes generadas
noise_comp = tf.random.normal([16, LATENT_DIM])
fake_comp = generator(noise_comp, training=False)
fake_comp = tf.clip_by_value((fake_comp + 1) / 2.0, 0.0, 1.0).numpy()
grid_fake = np.zeros((4*32, 4*32, 3))
for i in range(16):
r, c = i // 4, i % 4
grid_fake[r*32:(r+1)*32, c*32:(c+1)*32] = fake_comp[i]
ax_fake.imshow(grid_fake)
ax_fake.set_title('Imágenes Generadas (DCGAN)', fontsize=13, fontweight='bold')
ax_fake.axis('off')
plt.suptitle('Comparación: Real vs. Generada', fontsize=15, fontweight='bold')
plt.tight_layout()
plt.savefig('comparacion_real_vs_generada.png', dpi=150, bbox_inches='tight')
plt.show()
7.5 Resumen de Métricas de Rendimiento¶
# ===========================================================================
# 7.5 RESUMEN DE MÉTRICAS
# ===========================================================================
print("=" * 60)
print("RESUMEN DE MÉTRICAS DE RENDIMIENTO")
print("=" * 60)
print(f"\nDataset: CIFAR-10 (32×32×3, RGB)")
print(f"Imágenes entrenamiento: {len(X_train):,}")
print(f"Épocas completadas: {EPOCHS}")
print(f"Tiempo total: {total_time/60:.1f} minutos")
print(f"\nPérdida final Discriminador: {history_loss_D[-1]:.4f}")
print(f"Pérdida final Generador: {history_loss_G[-1]:.4f}")
print(f"\nParámetros del Generador: {generator.count_params():,}")
print(f"Parámetros del Discriminador: {discriminator.count_params():,}")
print(f"Parámetros totales: {generator.count_params() + discriminator.count_params():,}")
print(f"\nImágenes generadas: 36 (collage 6×6)")
print(f"\n{'=' * 60}")
============================================================ RESUMEN DE MÉTRICAS DE RENDIMIENTO ============================================================ Dataset: CIFAR-10 (32×32×3, RGB) Imágenes entrenamiento: 50,000 Épocas completadas: 50 Tiempo total: 14.4 minutos Pérdida final Discriminador: 1.3155 Pérdida final Generador: 0.8274 Parámetros del Generador: 1,455,552 Parámetros del Discriminador: 217,985 Parámetros totales: 1,673,537 Imágenes generadas: 36 (collage 6×6) ============================================================
8. Conclusiones¶
Alcance de los Objetivos¶
Se logró diseñar e implementar exitosamente una DCGAN (Deep Convolutional Generative Adversarial Network) capaz de generar imágenes sintéticas a color de 32×32 píxeles, entrenada con el dataset CIFAR-10. Se cumplió el objetivo de producir un conjunto de 36 imágenes nuevas (superando las 30 requeridas).
La arquitectura, basada en las recomendaciones de Radford et al. (2016) y adaptada a partir del material del curso (notebooks del Drive y la Open Class del profesor Félix Julián), demostró ser funcional para la generación de imágenes a baja resolución.
Reflexión sobre la Calidad¶
Reconocemos que las imágenes generadas a 32×32 de resolución tienen limitaciones inherentes. A diferencia de MNIST (donde generar dígitos legibles es relativamente sencillo porque son imágenes binarias de 28×28), CIFAR-10 presenta un desafío considerablemente mayor: las imágenes son a color, contienen objetos complejos y la resolución es muy baja. Esto significa que incluso las GANs más sofisticadas tienen dificultades para generar imágenes fotorrealistas a esta escala.
Desafíos Encontrados¶
- Inestabilidad del entrenamiento: En las primeras iteraciones observamos oscilaciones significativas en las pérdidas, algo esperado en el entrenamiento adversarial.
- Recursos computacionales: Como señaló el profesor, el costo computacional crece rápidamente con la complejidad de la arquitectura.
- Mode collapse parcial: En algunos experimentos, el generador tendía a producir imágenes con paletas de color similares.
Aplicabilidad Profesional¶
Desde la perspectiva de nuestro desempeño profesional, las redes generativas adversarias tienen aplicaciones directas que van más allá de la generación de imágenes artísticas. En el ámbito de la inteligencia artificial aplicada, las GANs pueden utilizarse para:
- Aumentación de datos: Generar datos sintéticos de entrenamiento cuando los datos reales son escasos o costosos de obtener
- Anonimización: Crear versiones sintéticas de documentos o imágenes para proteger información sensible
- Simulación: Generar escenarios hipotéticos para pruebas y validación
En particular, en el contexto de la transformación digital del sector público, estas técnicas podrían emplearse para generar documentos de prueba sintéticos que permitan entrenar sistemas de procesamiento automático sin comprometer información real de los ciudadanos.
9. Referencias¶
Dadhich, A. (2018). Chapter 5: Convolutional Neural Networks. En Practical computer vision: Extract insightful information from images using TensorFlow, Keras, and OpenCV (pp. 88-125). Packt Publishing.
Goodfellow, I. J., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A., & Bengio, Y. (2014). Generative Adversarial Nets. Advances in Neural Information Processing Systems, 27, 2672-2680.
Kingma, D. P., & Ba, J. (2015). Adam: A Method for Stochastic Optimization. Proceedings of the 3rd International Conference on Learning Representations (ICLR).
Radford, A., Metz, L., & Chintala, S. (2016). Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks. Proceedings of the 4th International Conference on Learning Representations (ICLR).
Zafar, I. (2018). Generative Adversarial Networks. En Hands-on convolutional neural networks with TensorFlow: Solve computer vision problems with modeling in TensorFlow and Python (pp. 172-188). Packt Publishing.
Notebook desarrollado por el Grupo 10 — Especialización en Inteligencia Artificial, UNIMINUTO, Junio 2026