Al cerrar el módulo 6 dejamos la caja de herramientas completa: sabemos entrenar con Keras, controlar el entrenamiento con callbacks, montar pipelines con tf.data y empaquetar modelos para producción. Toca cumplir la primera promesa pendiente desde el módulo 3: construir, de principio a fin, el clasificador de imágenes de producto de TecnoMarket. En esta lección desarrollarás un proyecto completo — exploración de datos, pipeline, modelo, entrenamiento, evaluación, análisis de errores, mejora iterativa y entrega — siguiendo la metodología del curso: prototipo con un dataset público (CIFAR-10) y traslado al caso ficticio de TecnoMarket.
Contenido
- Enunciado del proyecto y plan de trabajo
- Fase 1: exploración del dataset
- Fase 2: pipeline de datos con
tf.datay aumento de datos - Fase 3: el modelo — una mini-VGG con BN y dropout
- Fase 4: entrenamiento con callbacks y TensorBoard
- Fase 5: evaluación — matriz de confusión por clases
- Fase 6: análisis visual de errores y mejora iterativa
- Fase 7: entrega — guardado y conexión con la cola de revisión
Enunciado del proyecto y plan de trabajo
Contexto de negocio. Los vendedores de TecnoMarket suben fotos al dar de alta un artículo y hoy eligen la categoría a mano; el 12 % de las altas acaba mal clasificada y perjudica al buscador. Queremos un modelo que proponga la categoría automáticamente a partir de la foto, con la salvaguarda que diseñamos en 03-04: si la confianza no supera un umbral, el alta va a la cola de revisión humana.
Stand-in. Como no tenemos (todavía) el dataset real de TecnoMarket, prototipamos con CIFAR-10: 60 000 imágenes 32×32 en 10 clases (avión, coche, pájaro, gato, ciervo, perro, rana, caballo, barco, camión). Diez clases hacen de stand-in de diez categorías de producto; todo el pipeline será transplantable cuando lleguen las fotos reales.
Plan por fases (el mismo esqueleto que usaremos en todos los proyectos del módulo):
| Fase | Entregable |
|---|---|
| Exploración | Conocer tamaños, clases, balance |
| Pipeline | tf.data con aumento de datos integrado |
| Modelo | CNN propia (mini-VGG de 03-03 + BN/dropout de 05-04) |
| Entrenamiento | Callbacks (06-01), curvas, TensorBoard |
| Evaluación | Exactitud en test + matriz de confusión interpretada |
| Mejora | Iteraciones guiadas por diagnóstico |
| Entrega | Modelo+preprocesado guardados (06-05) + umbral de confianza (03-04) |
Fase 1: exploración del dataset
Nunca entrenes sobre datos que no has mirado. Trabajamos en tecnomarket-dl/notebooks/07_01_clasificador.ipynb (estructura de proyecto de 06-04):
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
tf.random.set_seed(42) # reproducibilidad, como fijamos en 06-04
np.random.seed(42)
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
CLASES = ["avion", "coche", "pajaro", "gato", "ciervo",
"perro", "rana", "caballo", "barco", "camion"]
print(x_train.shape, x_test.shape) # (50000, 32, 32, 3) (10000, 32, 32, 3)
print(np.bincount(y_train.flatten())) # 5000 por clase: dataset balanceadoPuntos que verificamos y por qué importan:
- Forma: 32×32×3 — imágenes en color, muy pequeñas. Nuestra CNN debe estar dimensionada para ello (la fórmula de tamaños de salida de 03-02 nos dirá cuántos bloques caben antes de quedarnos sin píxeles).
- Balance: 5 000 imágenes por clase. Esto legitima usar exactitud como métrica principal; en 07-03 veremos qué pasa cuando no hay balance.
- Inspección visual: pinta una rejilla de 25 imágenes con su etiqueta. Verás que a 32×32 hasta a un humano le cuesta distinguir gato de perro — calibra expectativas.
plt.figure(figsize=(8, 8))
for i in range(25):
plt.subplot(5, 5, i + 1)
plt.imshow(x_train[i])
plt.title(CLASES[int(y_train[i])], fontsize=8)
plt.axis("off")
plt.tight_layout()Fase 2: pipeline de datos con tf.data y aumento de datos
Aplicamos el patrón map → shuffle → batch → prefetch de 06-01, y le añadimos el aumento de datos con capas de Keras que vimos en 05-04. Reservamos además un conjunto de validación separado del test:
# Separar validación (últimas 5000 de train) — el test queda congelado (06-05)
x_val, y_val = x_train[45000:], y_train[45000:]
x_train, y_train = x_train[:45000], y_train[:45000]
BATCH = 64
AUTOTUNE = tf.data.AUTOTUNE
# Aumento de datos como capas (05-04): solo actúa en entrenamiento
aumento = tf.keras.Sequential([
tf.keras.layers.RandomFlip("horizontal"),
tf.keras.layers.RandomTranslation(0.1, 0.1),
tf.keras.layers.RandomZoom(0.1),
], name="aumento")
def preparar(x, y):
return tf.cast(x, tf.float32) / 255.0, y # normalización a [0, 1]
train_ds = (tf.data.Dataset.from_tensor_slices((x_train, y_train))
.map(preparar, num_parallel_calls=AUTOTUNE)
.shuffle(10_000)
.batch(BATCH)
.map(lambda x, y: (aumento(x, training=True), y),
num_parallel_calls=AUTOTUNE)
.prefetch(AUTOTUNE))
val_ds = (tf.data.Dataset.from_tensor_slices((x_val, y_val))
.map(preparar).batch(BATCH).prefetch(AUTOTUNE))
test_ds = (tf.data.Dataset.from_tensor_slices((x_test, y_test))
.map(preparar).batch(BATCH).prefetch(AUTOTUNE))Detalles que marcan la diferencia:
- El aumento va después del
batchy contraining=Trueexplícito: se aplica a lotes completos y solo en el pipeline de entrenamiento. Validación y test ven las imágenes originales, como exige la validación honesta de 06-05. - Volteo horizontal sí, vertical no: un coche boca abajo no es una foto de catálogo realista. El aumento debe generar variaciones plausibles (criterio de 05-04).
- La normalización (
/255) vive enpreparar; en la fase de entrega la meteremos dentro del modelo para que viaje con él (06-05).
Fase 3: el modelo — una mini-VGG con BN y dropout
Retomamos la mini-VGG que construimos en 03-03 (bloques Conv-Conv-Pool con filtros que se duplican) y le incorporamos las mejoras de 05-04: BatchNormalization tras cada convolución y Dropout creciente:
from tensorflow.keras import layers, models
def bloque_conv(x, filtros, dropout):
for _ in range(2):
x = layers.Conv2D(filtros, 3, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.MaxPooling2D()(x)
return layers.Dropout(dropout)(x)
entradas = layers.Input(shape=(32, 32, 3))
x = bloque_conv(entradas, 32, 0.2) # 32x32 -> 16x16
x = bloque_conv(x, 64, 0.3) # 16x16 -> 8x8
x = bloque_conv(x, 128, 0.4) # 8x8 -> 4x4
x = layers.Flatten()(x)
x = layers.Dense(128, activation="relu")(x)
x = layers.Dropout(0.5)(x)
salidas = layers.Dense(10, activation="softmax")(x)
modelo = models.Model(entradas, salidas, name="tecnomarket_cnn_v1")
modelo.summary() # ~1.2 M de parámetrosDecisiones explicadas:
- API funcional (introducida en 03-03): nos deja factorizar el bloque en una función y facilita futuras cirugías (extraer embeddings como en 03-04, por ejemplo).
use_bias=Falseantes de BN: la BatchNormalization ya aporta su propio desplazamiento; el sesgo de la convolución sería redundante.- Dropout creciente (0.2 → 0.5): las capas profundas, con más parámetros, necesitan más regularización (05-04).
- Tres bloques de pooling llevan de 32×32 a 4×4 — con la fórmula de 03-02 se comprueba que un cuarto bloque dejaría mapas de 2×2, demasiado pobres.
Fase 4: entrenamiento con callbacks y TensorBoard
Compilamos con Adam y entropía cruzada (02-04) y entrenamos con los callbacks de 06-01:
modelo.compile(optimizer=tf.keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
callbacks = [
tf.keras.callbacks.EarlyStopping(monitor="val_accuracy", patience=8,
restore_best_weights=True),
tf.keras.callbacks.ModelCheckpoint("models/cnn_v1_mejor.keras",
monitor="val_accuracy",
save_best_only=True),
tf.keras.callbacks.ReduceLROnPlateau(monitor="val_loss", factor=0.5,
patience=3, min_lr=1e-5),
tf.keras.callbacks.TensorBoard(log_dir="logs/cnn_v1"),
]
historia = modelo.fit(train_ds, validation_data=val_ds,
epochs=60, callbacks=callbacks)Mientras entrena (en una GPU modesta, ~15-25 min; en CPU, cuenta con más de una hora), abre TensorBoard como en 06-04 (tensorboard --logdir logs) y vigila:
- Curvas train/val juntas y subiendo: gracias al aumento de datos y al dropout, la brecha entre entrenamiento y validación se mantiene pequeña (sin ellos, esta misma red se dispara a 99 % en train y se estanca en ~72 % en validación — sobreajuste de libro, 05-04).
- Escalones al bajar el learning rate:
ReduceLROnPlateausuele provocar una mejora visible de 1-2 puntos cuando actúa.
Resultado típico honesto: la validación se estanca entre el 82 % y el 85 % hacia la época 40-50. No esperes más de una CNN propia entrenada desde cero sobre 32×32; está bien.
Fase 5: evaluación — matriz de confusión por clases
La exactitud global no basta para una decisión de negocio: necesitamos saber qué confunde el modelo.
from sklearn.metrics import confusion_matrix, classification_report
test_loss, test_acc = modelo.evaluate(test_ds)
print(f"Exactitud en test: {test_acc:.3f}") # típico: 0.82 - 0.85
y_prob = modelo.predict(test_ds)
y_pred = np.argmax(y_prob, axis=1)
print(classification_report(y_test.flatten(), y_pred, target_names=CLASES))
cm = confusion_matrix(y_test.flatten(), y_pred)Interpretación típica de la matriz (los números exactos variarán):
| Confusión frecuente | Lectura | Análogo en TecnoMarket |
|---|---|---|
| gato ↔ perro | Animales pequeños, texturas parecidas a 32×32 | «auriculares» ↔ «cascos gaming» |
| avión ↔ barco | Fondos azules dominan la imagen | productos fotografiados sobre el mismo fondo |
| coche ↔ camión | Misma familia semántica | «móvil» ↔ «tablet» |
| rana, barco | Clases con >90 % de acierto | categorías visualmente distintivas |
La lección de negocio: los errores no se reparten uniformemente. Las parejas confundibles son exactamente donde el umbral de confianza de 03-04 tendrá que trabajar más.
Fase 6: análisis visual de errores y mejora iterativa
Antes de tocar nada, mira los fallos con tus ojos:
errores = np.where(y_pred != y_test.flatten())[0]
plt.figure(figsize=(10, 6))
for i, idx in enumerate(errores[:15]):
plt.subplot(3, 5, i + 1)
plt.imshow(x_test[idx])
plt.title(f"real: {CLASES[int(y_test[idx])]}\npred: {CLASES[y_pred[idx]]}"
f" ({y_prob[idx].max():.2f})", fontsize=7)
plt.axis("off")
plt.tight_layout()Verás tres familias de errores: imágenes genuinamente ambiguas (ni tú aciertas), errores con confianza baja (el umbral los atrapará) y errores con confianza alta (los peligrosos). Con ese diagnóstico, itera en este orden — de lo más barato a lo más caro:
- Más épocas / paciencia: si las curvas seguían subiendo al parar, es mejora gratis.
- Ajustar el aumento de datos: si hay brecha train-val, intensifícalo; si el train no llega a buen nivel, suavízalo (el aumento excesivo también daña, 05-04).
- Ajustar regularización: mover dropout ±0.1 según la brecha.
- Capacidad: un cuarto bloque convolucional o más filtros — solo si el train se ha quedado corto.
- Cambiar de arquitectura: la puerta que abriremos en 07-05.
Regla de oro del módulo: un cambio por iteración, con semilla fija (06-04), o no sabrás qué causó qué.
Fase 7: entrega — guardado y conexión con la cola de revisión
Siguiendo 06-05, empaquetamos modelo + preprocesado como una unidad, para que la API no pueda desincronizarse del entrenamiento:
# Modelo de inferencia: normalización dentro + probabilidades fuera
entrada = layers.Input(shape=(32, 32, 3), dtype=tf.uint8)
x = layers.Rescaling(1.0 / 255)(tf.cast(entrada, tf.float32))
prob = modelo(x, training=False)
modelo_servicio = models.Model(entrada, prob)
modelo_servicio.save("models/clasificador_categorias_v1.keras")Y aplicamos la política de 03-04 con el umbral de confianza:
UMBRAL = 0.80 # calibrado sobre validación, nunca sobre test
def clasificar_alta(imagen_uint8):
p = modelo_servicio.predict(imagen_uint8[np.newaxis, ...], verbose=0)[0]
if p.max() >= UMBRAL:
return {"categoria": CLASES[int(p.argmax())], "auto": True}
return {"categoria": None, "auto": False, "destino": "cola_revision",
"sugerencias": [CLASES[i] for i in p.argsort()[-3:][::-1]]}Con el umbral en 0.80, un resultado típico es que el modelo resuelve automáticamente en torno al 70 % de las altas con una exactitud sobre ese subconjunto cercana al 92-94 %, y deriva el resto a revisión humana con las tres categorías más probables como sugerencia — humanos y modelo colaborando, como diseñamos en 03-04. El servicio se expondría con la misma plantilla FastAPI de 06-05.
Errores Comunes y Consejos
- Aplicar el aumento de datos también a validación/test: infla artificialmente la dificultad y te hace tomar malas decisiones. El aumento solo vive en
train_ds. - Elegir el umbral de confianza mirando el test: el test es el conjunto congelado (06-05); si lo usas para calibrar, ya no mide nada. Umbral y ajustes, siempre con validación.
- Cambiar tres cosas a la vez entre iteraciones: no sabrás qué funcionó. Un cambio, una semilla fija, una comparación.
- Frustrarse por no pasar del 85 %: es el techo razonable de este enfoque con este dataset. La mejora grande no vendrá de insistir, sino de cambiar de estrategia (07-05).
- Olvidar
restore_best_weights=True: sin ello, EarlyStopping te deja los pesos de la última época, no los mejores.
Ejercicios
- Añade un cuarto bloque convolucional de 256 filtros (ajustando el dropout) y compara exactitud y tiempo de entrenamiento con la versión de tres bloques. ¿Compensa?
- Genera la lista de las 20 predicciones erróneas con mayor confianza. ¿Qué parejas de clases aparecen? Propón un cambio de aumento de datos dirigido a ellas.
- Calcula, para umbrales 0.6, 0.7, 0.8 y 0.9 sobre el conjunto de validación, qué porcentaje de imágenes se automatiza y qué exactitud tiene ese subconjunto. Presenta la tabla y elige umbral justificando el compromiso.
Soluciones
- Sustituye la última llamada por
x = bloque_conv(x, 256, 0.4)tras el bloque de 128 (los mapas pasan de 4×4 a 2×2, justo en el límite). Resultado típico: +0.5-1 punto de exactitud a cambio de ~1.5× tiempo por época. Compensa solo si ese punto importa; documenta ambas cifras. idx = errores[np.argsort(-y_prob[errores].max(axis=1))[:20]]y pinta como en la fase 6. Suelen dominar gato↔perro y coche↔camión. Cambio dirigido:RandomContrast(0.2)o recortes aleatorios (RandomCropcon padding) que fuercen a la red a fijarse en la forma y no en el fondo.- Para cada umbral
u:mask = p_val.max(axis=1) >= u; cobertura =mask.mean(); exactitud automatizada =(pred_val[mask] == y_val_flat[mask]).mean(). Tabla típica: 0.6 → 85 %/89 %; 0.7 → 78 %/91 %; 0.8 → 70 %/93 %; 0.9 → 55 %/96 %. La elección depende del coste de un error frente al coste de revisar: para altas de catálogo, 0.8 suele ser un buen compromiso.
Conclusión
Has entregado el primer proyecto completo de TecnoMarket: un clasificador de categorías con pipeline tf.data, aumento de datos, una mini-VGG regularizada, entrenamiento controlado por callbacks, evaluación con matriz de confusión, análisis de errores y un empaquetado listo para servir tras un umbral de confianza con cola de revisión. El resultado — 82-85 % en test — es honesto para una CNN entrenada desde cero, y en 07-05 lo batiremos con transfer learning sin escribir apenas más código. Antes, cambiamos de modalidad: en la próxima lección cumplimos la promesa de 04-04 y construimos un generador de texto carácter a carácter para los borradores de descripciones de producto 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
