En 05-01 montaste el esqueleto de una DCGAN — generador con Conv2DTranspose, discriminador convolucional, la metáfora del falsificador y el policía — y dejamos pendiente lo más delicado: el bucle de entrenamiento completo. Con el GradientTape que dominaste en 06-01, ya tienes la herramienta exacta para escribirlo. En este cuarto proyecto entrenarás una GAN de principio a fin sobre Fashion-MNIST (prendas de ropa como stand-in de fotos de producto) como prototipo de un generador de imágenes para las creatividades promocionales de TecnoMarket, aprendiendo a leer la dinámica de un entrenamiento adversario: qué es normal, qué es mode collapse y qué tocar cuando algo va mal.
Contenido
- Enunciado del proyecto y expectativas realistas
- Fase 1: datos — Fashion-MNIST con
tf.data - Fase 2: generador y discriminador (retomando 05-01)
- Fase 3: las dos pérdidas adversarias
- Fase 4: un paso de entrenamiento con GradientTape, línea a línea
- Fase 5: el bucle por épocas con rejillas de muestras
- Fase 6: leer la dinámica — lo normal, el mode collapse y qué tocar
- Fase 7: evaluación, guardado del generador y uso posterior
Enunciado del proyecto y expectativas realistas
Contexto de negocio. El equipo de marketing de TecnoMarket quiere explorar la generación de imágenes para creatividades promocionales (fondos, variaciones de producto, ilustraciones de campaña). Antes de invertir, piden un prototipo educativo que demuestre que el equipo domina la mecánica generativa.
Stand-in. Fashion-MNIST: 60 000 imágenes 28×28 en escala de grises de prendas (camisetas, zapatillas, bolsos...), un stand-in razonable de fotos de producto simples. Pequeño, rápido de entrenar y suficientemente variado para ver los fenómenos que importan.
Expectativas, por escrito antes de empezar: una DCGAN de este tamaño produce prendas reconocibles pero borrosas a 28×28. No es StyleGAN ni un producto comercial; los sistemas de producción actuales usan modelos de difusión (los verás mencionados en 08-03) y toda generación de imágenes comerciales arrastra implicaciones éticas que trataremos en 08-01. El objetivo aquí es dominar el mecanismo adversario — que es también la base conceptual para entender los sistemas modernos.
Fase 1: datos — Fashion-MNIST con tf.data
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
tf.random.set_seed(42)
(x_train, _), _ = tf.keras.datasets.fashion_mnist.load_data()
# Normalizar a [-1, 1]: el generador terminara en tanh (05-01)
x_train = (x_train.astype("float32") - 127.5) / 127.5
x_train = x_train[..., np.newaxis] # (60000, 28, 28, 1)
BATCH = 128
train_ds = (tf.data.Dataset.from_tensor_slices(x_train)
.shuffle(60_000)
.batch(BATCH, drop_remainder=True)
.prefetch(tf.data.AUTOTUNE))Detalle importante que ya anticipamos en 05-01: el rango [-1, 1] (no [0, 1]) porque la salida del generador será tanh. Generador y datos reales deben hablar el mismo idioma numérico o el discriminador los distinguirá por el rango, no por el contenido. No hay etiquetas ni conjunto de test: en una GAN los «datos» son solo la referencia de realidad.
Fase 2: generador y discriminador (retomando 05-01)
Instanciamos el esqueleto DCGAN de 05-01 adaptado a 28×28. No reexplicamos las piezas (repásalas allí); anotamos las decisiones:
from tensorflow.keras import layers, models
DIM_RUIDO = 100
def crear_generador():
return models.Sequential([
layers.Input(shape=(DIM_RUIDO,)),
layers.Dense(7 * 7 * 256, use_bias=False),
layers.BatchNormalization(), layers.LeakyReLU(),
layers.Reshape((7, 7, 256)),
layers.Conv2DTranspose(128, 5, strides=1, padding="same", use_bias=False),
layers.BatchNormalization(), layers.LeakyReLU(), # 7x7
layers.Conv2DTranspose(64, 5, strides=2, padding="same", use_bias=False),
layers.BatchNormalization(), layers.LeakyReLU(), # 14x14
layers.Conv2DTranspose(1, 5, strides=2, padding="same",
activation="tanh"), # 28x28, [-1,1]
], name="generador")
def crear_discriminador():
return models.Sequential([
layers.Input(shape=(28, 28, 1)),
layers.Conv2D(64, 5, strides=2, padding="same"),
layers.LeakyReLU(), layers.Dropout(0.3), # 14x14
layers.Conv2D(128, 5, strides=2, padding="same"),
layers.LeakyReLU(), layers.Dropout(0.3), # 7x7
layers.Flatten(),
layers.Dense(1), # logit: real o falso
], name="discriminador")
generador = crear_generador()
discriminador = crear_discriminador()Recordatorios de 05-01 aplicados: Conv2DTranspose con strides=2 duplica la resolución (7→14→28), BN y LeakyReLU estabilizan el generador, el discriminador usa dropout (no BN) y devuelve un logit sin sigmoid — la pérdida lo gestionará.
Fase 3: las dos pérdidas adversarias
El juego de 05-01, ahora en código. Ambas pérdidas parten de la misma entropía cruzada binaria sobre logits:
bce = tf.keras.losses.BinaryCrossentropy(from_logits=True)
def perdida_discriminador(logits_reales, logits_falsos):
# El policia quiere: reales -> 1, falsas -> 0
perdida_real = bce(tf.ones_like(logits_reales), logits_reales)
perdida_falsa = bce(tf.zeros_like(logits_falsos), logits_falsos)
return perdida_real + perdida_falsa
def perdida_generador(logits_falsos):
# El falsificador quiere que SUS falsas parezcan reales (etiqueta 1)
return bce(tf.ones_like(logits_falsos), logits_falsos)
# Dos optimizadores separados: cada red aprende por su cuenta (05-01)
opt_gen = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)
opt_disc = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)La asimetría es la esencia: el discriminador puntúa las mismas imágenes falsas con objetivo 0 y el generador con objetivo 1. Los hiperparámetros lr=2e-4, beta_1=0.5 son los clásicos de DCGAN — funcionan; no los toques en la primera pasada.
Fase 4: un paso de entrenamiento con GradientTape, línea a línea
Aquí se junta todo: el GradientTape de 06-01 nos deja calcular dos conjuntos de gradientes de un mismo grafo y aplicárselos a redes distintas. Comentado línea a línea:
@tf.function # compila el paso a grafo: ~5-10x mas rapido (06-01)
def paso_entrenamiento(imagenes_reales):
ruido = tf.random.normal([BATCH, DIM_RUIDO]) # 1. lote de ruido
with tf.GradientTape() as tape_g, tf.GradientTape() as tape_d:
imagenes_falsas = generador(ruido, training=True) # 2. falsificar
logits_reales = discriminador(imagenes_reales, training=True) # 3. juzgar reales
logits_falsos = discriminador(imagenes_falsas, training=True) # 4. juzgar falsas
perd_g = perdida_generador(logits_falsos) # 5. cuanto engaña
perd_d = perdida_discriminador(logits_reales, logits_falsos) # 6. cuanto acierta
# 7. Gradientes de la perdida de CADA red respecto a SUS variables
grads_g = tape_g.gradient(perd_g, generador.trainable_variables)
grads_d = tape_d.gradient(perd_d, discriminador.trainable_variables)
# 8. Cada optimizador actualiza solo su red
opt_gen.apply_gradients(zip(grads_g, generador.trainable_variables))
opt_disc.apply_gradients(zip(grads_d, discriminador.trainable_variables))
return perd_g, perd_dPuntos finos que debes entender, no solo copiar:
- Dos tapes, una pasada: ambos graban las mismas operaciones, pero el paso 7 pide a cada tape gradientes solo de su pérdida respecto a sus variables. Al actualizar el generador no tocamos el discriminador, y viceversa — si mezclaras variables, cada red sabotearía a la otra.
training=Trueen ambas redes siempre: la BN del generador debe usar estadísticas del lote también cuando sus imágenes alimentan la pérdida del discriminador.@tf.function: el mismo decorador que vimos en 06-01; en un bucle personalizado como este, la diferencia de velocidad es enorme.- Este es el patrón que Keras
fit()no te da hecho: dos redes, dos pérdidas enfrentadas, un paso simultáneo. Por eso la promesa de 05-01 tuvo que esperar a 06-01.
Fase 5: el bucle por épocas con rejillas de muestras
En una GAN, la pérdida no cuenta toda la verdad (fase 6), así que el bucle genera periódicamente una rejilla de muestras con el mismo ruido fijo, para comparar épocas en igualdad de condiciones:
ruido_fijo = tf.random.normal([16, DIM_RUIDO], seed=42) # SIEMPRE el mismo
def guardar_rejilla(epoca):
muestras = generador(ruido_fijo, training=False)
muestras = (muestras + 1) / 2 # [-1,1] -> [0,1] para pintar
plt.figure(figsize=(4, 4))
for i in range(16):
plt.subplot(4, 4, i + 1)
plt.imshow(muestras[i, :, :, 0], cmap="gray")
plt.axis("off")
plt.savefig(f"logs/gan/rejilla_epoca_{epoca:03d}.png")
plt.close()
EPOCAS = 50
for epoca in range(1, EPOCAS + 1):
perdidas_g, perdidas_d = [], []
for lote in train_ds:
pg, pd = paso_entrenamiento(lote)
perdidas_g.append(float(pg)); perdidas_d.append(float(pd))
print(f"Epoca {epoca:3d} | G: {np.mean(perdidas_g):.3f} "
f"| D: {np.mean(perdidas_d):.3f}")
if epoca % 5 == 0 or epoca == 1:
guardar_rejilla(epoca)
generador.save(f"models/generador_epoca_{epoca:03d}.keras")En GPU, cada época tarda ~20-40 s (50 épocas ≈ media hora); en CPU es lento — reduce a 15-20 épocas o usa Colab (01-05). Guardamos checkpoints del generador cada 5 épocas: en las GAN, la «mejor época» se elige mirando rejillas, no pérdidas, así que conviene poder volver atrás.
Fase 6: leer la dinámica — lo normal, el mode collapse y qué tocar
Evolución visual típica y honesta (tus épocas exactas variarán):
| Épocas | Qué verás en la rejilla |
|---|---|
| 1-3 | Ruido gris con manchas: nada reconocible |
| 5-10 | Manchas claras sobre fondo oscuro: «proto-prendas» borrosas |
| 15-25 | Siluetas claras: se distinguen camisetas, pantalones, zapatos |
| 30-50 | Prendas reconocibles con textura básica; bordes aún blandos |
Pérdidas: qué es normal. A diferencia de un clasificador, aquí las pérdidas no deben converger a cero: es un equilibrio, no un descenso. Sano: G oscilando en ~0.7-1.5 y D en ~1.0-1.3, ambas moviéndose sin tendencia clara. Señales de alarma:
| Síntoma | Diagnóstico | Qué tocar (de 05-01) |
|---|---|---|
D → 0 y G crece sin parar |
Discriminador demasiado fuerte: el generador no recibe señal útil | Baja el lr del discriminador (p. ej. 1e-4), o añade ruido/etiquetas suavizadas (0.9 en vez de 1) a los reales |
| Las 16 muestras de la rejilla son casi idénticas | Mode collapse: el generador encontró una imagen que engaña y la repite | Más entropía: sube lr del generador ligeramente, revisa el suavizado de etiquetas, reinicia desde un checkpoint anterior |
| Todo oscila violentamente y las muestras empeoran | Learning rates demasiado altos | Baja ambos lr a la mitad |
| Rejillas estancadas 15+ épocas | Equilibrio muerto | Prueba más capacidad en el generador o más épocas; a veces solo necesita tiempo |
El diagnóstico se hace con las rejillas primero y las curvas después — exactamente al revés que en los proyectos anteriores.
Fase 7: evaluación, guardado del generador y uso posterior
Evaluación cualitativa. El protocolo mínimo: (1) rejillas del ruido fijo época a época — ¿mejoran?; (2) diversidad — genera 64 muestras nuevas y comprueba que aparecen varios tipos de prenda; (3) un vistazo a vecinos — para alguna muestra generada, busca la imagen real más parecida del dataset y verifica que no es una copia memorizada.
Métricas cuantitativas, solo el concepto. En investigación se usa el FID (Fréchet Inception Distance): compara estadísticas de imágenes reales y generadas en el espacio de características de una red preentrenada — menor es mejor. Para este prototipo basta saber que existe y que la inspección visual disciplinada es el estándar práctico a esta escala.
Entrega. Solo se despliega el generador — el discriminador fue el entrenador personal y se queda en casa:
generador.save("models/generador_creatividades_v1.keras")
# Uso posterior: nuevas imagenes desde ruido
gen = tf.keras.models.load_model("models/generador_creatividades_v1.keras")
nuevas = gen(tf.random.normal([8, DIM_RUIDO]), training=False)
nuevas = ((nuevas + 1) / 2).numpy() # a [0,1]: el "des-preprocesado" viaja documentado (06-05)Informe honesto para marketing: «dominamos la mecánica generativa y podemos producir imágenes sintéticas de baja resolución; para creatividades en calidad de producción se necesitan modelos de difusión (08-03) y un análisis previo de implicaciones éticas y de derechos (08-01) — toda imagen sintética publicada debe identificarse como tal».
Errores Comunes y Consejos
- Entrenar el discriminador hasta la perfección «primero»: un discriminador perfecto da gradiente casi nulo al generador. Deben crecer juntos: un paso cada uno, como en nuestro bucle.
- Cambiar el ruido de la rejilla en cada época: sin ruido fijo no puedes saber si mejora el generador o solo cambió la muestra. El ruido fijo es tu conjunto congelado particular (06-05).
- Interpretar las pérdidas como en un clasificador: que la pérdida del generador suba no significa que empeore — quizá el discriminador mejoró. Rejillas primero.
- Olvidar
drop_remainder=True: un último lote de tamaño distinto puede romper formas dentro de@tf.function. - Desesperar en la época 10: las GAN son lentas en arrancar y no lineales en mejorar. Juzga cada 5-10 épocas, con checkpoints para volver al mejor punto.
Ejercicios
- Añade suavizado de etiquetas al discriminador (reales = 0.9 en lugar de 1.0) y compara la estabilidad de las curvas y las rejillas con la versión original durante 20 épocas.
- Entrena con
DIM_RUIDO = 2y visualiza qué genera el modelo recorriendo una malla de valores del ruido (por ejemplo, de -2 a 2 en cada dimensión). ¿Qué observas sobre el espacio latente? ¿Aparece mode collapse? - Interpola en el espacio latente: toma dos vectores de ruido
z1, z2, genera imágenes paraz = (1-t)*z1 + t*z2contde 0 a 1 en 8 pasos, y muestra la transición. Relación con los embeddings de 03-04.
Soluciones
- Cambia en
perdida_discriminador:bce(tf.ones_like(logits_reales) * 0.9, logits_reales). Efecto típico: la pérdida deDdeja de hundirse hacia 0, la deGoscila menos y las rejillas progresan de forma más estable — es uno de los remedios de 05-01 en acción. - Con solo 2 dimensiones latentes, la malla
[(x, y) for x in np.linspace(-2,2,8) for y in np.linspace(-2,2,8)]genera una rejilla donde regiones vecinas producen prendas parecidas: visualizas el espacio latente entero. La diversidad total baja (2 dimensiones dan poco margen) y el riesgo de collapse aumenta: verás pocas clases de prenda representadas. Conclusión: la dimensión del ruido acota cuánta variedad puede codificar el generador. for t in np.linspace(0, 1, 8): imgs.append(gen((1-t)*z1 + t*z2, training=False)). La transición es suave: una camiseta se transforma gradualmente en un pantalón pasando por formas intermedias plausibles. Como en los embeddings de 03-04, la cercanía en el espacio latente codifica similitud semántica — el generador ha organizado el ruido en un mapa continuo de prendas.
Conclusión
Cuarto proyecto entregado y la promesa más antigua del curso saldada: el bucle adversario completo, escrito a mano con dos GradientTape, dos pérdidas y dos optimizadores, con rejillas de ruido fijo como instrumento de evaluación y un diagnóstico práctico de la dinámica GAN (equilibrio, no convergencia; mode collapse y sus remedios). El resultado es un prototipo honesto — prendas reconocibles, no fotografía de producto — y un informe claro de qué haría falta para producción. Queda un proyecto, y es el que cierra el círculo: en 07-05 volveremos al clasificador de imágenes de 07-01 y batiremos su 85 % con transfer learning, comparando cifras cara a cara para tomar la decisión final de TecnoMarket.
Curso de Deep Learning
Módulo 1: Introducción a Deep Learning
- ¿Qué es Deep Learning?
- Historia y evolución del Deep Learning
- Aplicaciones de Deep Learning
- Conceptos básicos de redes neuronales
- Preparación del entorno de trabajo
Módulo 2: Fundamentos de Redes Neuronales
- Perceptrón y Perceptrón Multicapa
- Función de activación
- Propagación hacia adelante y hacia atrás
- Optimización y función de pérdida
- Tu primera red neuronal completa
Módulo 3: Redes Neuronales Convolucionales (CNN)
- Introducción a las CNN
- Capas convolucionales y de pooling
- Arquitecturas populares de CNN
- Aplicaciones de CNN en reconocimiento de imágenes
Módulo 4: Redes Neuronales Recurrentes (RNN)
- Introducción a las RNN
- LSTM y GRU
- Aplicaciones de RNN en procesamiento del lenguaje natural
- Secuencias y series temporales
Módulo 5: Técnicas Avanzadas en Deep Learning
- Redes Generativas Adversariales (GAN)
- Autoencoders
- Transfer Learning
- Regularización y técnicas de mejora
- Mecanismos de atención y Transformers
Módulo 6: Herramientas y Frameworks
- Introducción a TensorFlow
- Introducción a PyTorch
- Comparación de frameworks
- Entornos de desarrollo y recursos adicionales
- Guardar, cargar y desplegar modelos
Módulo 7: Proyectos Prácticos
- Clasificación de imágenes con CNN
- Generación de texto con RNN
- Detección de anomalías con Autoencoders
- Creación de una GAN para generación de imágenes
- Fine-tuning de un modelo preentrenado
