Este es el momento que llevamos preparando todo el módulo. Ya sabes qué es un perceptrón y por qué se apilan capas (02-01), qué activación poner en cada sitio (02-02), cómo la red aprende con forward y backpropagation (02-03) y qué pérdida y optimizador elegir (02-04). Hoy lo ensamblamos todo en un proyecto de principio a fin: construir, entrenar y evaluar en Keras una red densa que reconoce dígitos escritos a mano usando MNIST, el dataset clásico de la disciplina. Seguimos la metodología de TecnoMarket presentada en el módulo 1 —prototipo con datasets públicos → aplicación a datos ficticios de TecnoMarket—: antes de intentar clasificar fotos de productos, el equipo valida el flujo de trabajo completo con un banco de pruebas estándar. Ese flujo (cargar → explorar → preprocesar → definir → entrenar → evaluar → predecir → guardar) es idéntico en cualquier proyecto real, y a partir de hoy es tuyo.

Contenido

  1. El proyecto y el dataset: por qué MNIST
  2. Cargar y explorar los datos
  3. Preprocesar: normalización y splits
  4. Definir el modelo (justificando cada elección)
  5. Entrenar y leer las curvas de loss/accuracy
  6. Evaluar en test y hacer predicciones individuales
  7. Guardar el modelo en tecnomarket-dl/
  8. Los límites del prototipo: por qué las fotos reales pedirán otra arquitectura

El proyecto y el dataset: por qué MNIST

MNIST contiene 70 000 imágenes en escala de grises de 28×28 píxeles con dígitos manuscritos (0–9), ya divididas en 60 000 de entrenamiento y 10 000 de test. Es el "hola mundo" del deep learning por tres razones prácticas: se descarga con una línea, entrena en minutos sin GPU, y es lo bastante difícil como para que los conceptos importen y lo bastante fácil como para que todo funcione a la primera.

Encaje con TecnoMarket: el objetivo final del equipo de visión es clasificar fotos de productos en categorías (portátil, cafetera, aspiradora…). MNIST es su banco de pruebas: mismo tipo de problema (clasificación multiclase de imágenes), tamaño manejable, resultado conocido. Si el flujo funciona aquí, migrar a las fotos reales será cuestión de arquitectura (módulo 3) y datos, no de metodología.

Trabaja donde preparaste el entorno en el módulo 1: un notebook en tecnomarket-dl/notebooks/ (o Colab, si elegiste esa vía).

Cargar y explorar los datos

Nunca entrenes sobre datos que no has mirado. Es la primera regla de cualquier proyecto.

import numpy as np
import matplotlib.pyplot as plt
from tensorflow import keras

(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()

print("Train:", X_train.shape, y_train.shape)   # (60000, 28, 28) (60000,)
print("Test: ", X_test.shape, y_test.shape)     # (10000, 28, 28) (10000,)
print("Tipo:", X_train.dtype, "| Rango:", X_train.min(), "-", X_train.max())
print("Clases:", np.unique(y_train))             # [0 1 2 ... 9]
print("Ejemplos por clase:", np.bincount(y_train))
Train: (60000, 28, 28) (60000,)
Test:  (10000, 28, 28) (10000,)
Tipo: uint8 | Rango: 0 - 255
Clases: [0 1 2 3 4 5 6 7 8 9]
Ejemplos por clase: [5923 6742 5958 6131 5842 5421 5918 6265 5851 5949]

Lectura de la exploración: cada imagen es una matriz 28×28 de enteros 0–255 (0 = negro, 255 = blanco), las etiquetas son enteros 0–9, y las diez clases están razonablemente equilibradas (entre 5 400 y 6 700 ejemplos cada una) — importante, porque una clase con muy pocos ejemplos se aprendería mal. Veamos algunas imágenes con sus etiquetas:

fig, ejes = plt.subplots(2, 5, figsize=(10, 4))
for i, eje in enumerate(ejes.flat):
    eje.imshow(X_train[i], cmap="gray")
    eje.set_title(f"etiqueta: {y_train[i]}")
    eje.axis("off")
plt.tight_layout(); plt.show()

Dedica un minuto a mirarlas: verás sietes con y sin barra, unos inclinados, nueves que casi parecen cuatros. Esa variabilidad es exactamente lo que la red tendrá que absorber.

Preprocesar: normalización y splits

Tres transformaciones separan los datos crudos de los datos listos:

1. Aplanar. Nuestra red densa espera un vector por ejemplo, no una matriz: convertimos cada imagen 28×28 en un vector de 784 valores. (Sí, esto destruye la estructura espacial; volveremos sobre esa herida en el apartado 8.)

2. Normalizar. Escalamos los píxeles de 0–255 a 0–1. ¿Por qué? Por lo aprendido en 02-03: los gradientes dependen de la magnitud de las entradas, y con entradas de hasta 255 las sumas ponderadas serían enormes, saturarían activaciones y obligarían a learning rates minúsculos. Entradas en torno a [0, 1] mantienen el entrenamiento en la zona cómoda.

3. Separar validación. Del vocabulario de 01-04: train para ajustar pesos, validation para vigilar el progreso con datos que la red no usa para aprender, test para el examen final, una sola vez. MNIST ya trae el test separado; reservamos ahora el 10 % del train como validación.

# 1. Aplanar: (60000, 28, 28) -> (60000, 784)
X_train_prep = X_train.reshape(-1, 784).astype("float32")
X_test_prep  = X_test.reshape(-1, 784).astype("float32")

# 2. Normalizar a [0, 1]
X_train_prep /= 255.0
X_test_prep  /= 255.0

# 3. Split de validacion: ultimos 6000 ejemplos del train
X_val, y_val     = X_train_prep[54000:], y_train[54000:]
X_train_prep, y_train_prep = X_train_prep[:54000], y_train[:54000]

print(X_train_prep.shape, X_val.shape, X_test_prep.shape)
# (54000, 784) (6000, 784) (10000, 784)

Las etiquetas las dejamos como enteros 0–9: en 02-04 vimos que sparse_categorical_crossentropy las acepta directamente, sin conversión one-hot.

Definir el modelo (justificando cada elección)

Construimos una red densa 784 → 128 → 64 → 10. Cada decisión sale de una lección de este módulo:

Decisión Valor Justificación (lección)
Tipo de capas Dense (todas con todas) Es el MLP de 02-01, versión Keras
Activación oculta ReLU Por defecto en ocultas; evita el vanishing gradient de las sigmoides (02-02, 02-03)
Neuronas de salida 10, softmax Multiclase excluyente con 10 clases (02-02)
Pérdida sparse_categorical_crossentropy Pareja de softmax; etiquetas enteras (02-04)
Optimizador Adam Punto de partida robusto (02-04)
Tamaños ocultos (128, 64) Hiperparámetro Elección razonable de partida; embudo progresivo hacia las 10 clases
modelo = keras.Sequential([
    keras.layers.Input(shape=(784,)),                 # entrada: el vector aplanado
    keras.layers.Dense(128, activation="relu"),       # oculta 1
    keras.layers.Dense(64, activation="relu"),        # oculta 2
    keras.layers.Dense(10, activation="softmax"),     # salida: 10 probabilidades que suman 1
])

modelo.compile(
    optimizer="adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

modelo.summary()
Layer (type)          Output Shape    Param #
dense (Dense)         (None, 128)     100480
dense_1 (Dense)       (None, 64)      8256
dense_2 (Dense)       (None, 10)      650
Total params: 109,386

Haz la comprobación que ya hiciste con la red 3-4-2-1 de 29 parámetros en 01-04, ahora a lo grande: $784 \times 128 + 128 = 100480$; $128 \times 64 + 64 = 8256$; $64 \times 10 + 10 = 650$. Total 109 386 parámetros — pesos y sesgos que backpropagation ajustará. El mismo recuento de siempre, tres órdenes de magnitud más arriba.

Entrenar y leer las curvas de loss/accuracy

historial = modelo.fit(
    X_train_prep, y_train_prep,
    epochs=15,                          # 15 pasadas completas por el train
    batch_size=64,                      # mini-batch (02-04)
    validation_data=(X_val, y_val),     # vigila datos que no usa para aprender
    verbose=2,
)
Epoch 1/15  - loss: 0.2949 - accuracy: 0.9152 - val_loss: 0.1493 - val_accuracy: 0.9558
Epoch 5/15  - loss: 0.0522 - accuracy: 0.9836 - val_loss: 0.0857 - val_accuracy: 0.9743
Epoch 10/15 - loss: 0.0219 - accuracy: 0.9929 - val_loss: 0.0870 - val_accuracy: 0.9770
Epoch 15/15 - loss: 0.0117 - accuracy: 0.9962 - val_loss: 0.1010 - val_accuracy: 0.9772

Cada época son $54000 / 64 \approx 844$ actualizaciones mini-batch de los 109 386 parámetros, cada una con su forward, su backward y su paso de Adam: todo 02-03 y 02-04 girando por dentro. El objeto historial guarda las métricas por época; dibujarlas es obligatorio:

h = historial.history
fig, (eje1, eje2) = plt.subplots(1, 2, figsize=(12, 4))
eje1.plot(h["loss"], label="train"); eje1.plot(h["val_loss"], label="validacion")
eje1.set_title("Perdida"); eje1.set_xlabel("epoca"); eje1.legend(); eje1.grid(alpha=0.3)
eje2.plot(h["accuracy"], label="train"); eje2.plot(h["val_accuracy"], label="validacion")
eje2.set_title("Accuracy"); eje2.set_xlabel("epoca"); eje2.legend(); eje2.grid(alpha=0.3)
plt.show()

Cómo leer las curvas:

  • Las dos pérdidas bajan juntas al principio: la red aprende patrones generales. Bien.
  • Hacia la época 5–7 se separan: la de train sigue cayendo (hasta 0.01) pero la de validación se estanca e incluso sube ligeramente (0.086 → 0.101). La red empieza a memorizar peculiaridades del train que no generalizan: es el sobreajuste (overfitting). Con esta magnitud es leve (el val_accuracy se mantiene ≈ 97.7 %), pero la señal es inequívoca: entrenar más épocas ya no mejora nada útil.
  • Regla práctica: la curva que importa es la de validación. Las técnicas para combatir el sobreajuste (regularización, dropout, early stopping) tienen su propia lección en el módulo 5; por ahora, basta con saber detectarlo y no entrenar de más.

Evaluar en test y hacer predicciones individuales

El test se toca una sola vez, al final, para obtener la cifra honesta que reportarías al equipo de TecnoMarket:

perdida_test, acc_test = modelo.evaluate(X_test_prep, y_test, verbose=0)
print(f"Accuracy en test: {acc_test:.4f}")   # -> ~0.977

Un 97.7 % con una red densa sencilla: unas 230 imágenes falladas de 10 000. Ahora, predicciones individuales, que es como el modelo se usaría en producción:

probs = modelo.predict(X_test_prep[:5], verbose=0)   # forma (5, 10)
print(np.round(probs[0], 3))
# [0. 0. 0. 0. 0. 0. 0. 0.999 0. 0.001]  <- softmax: casi todo a la clase 7
print("Prediccion:", probs.argmax(axis=1))   # [7 2 1 0 4]
print("Realidad:  ", y_test[:5])             # [7 2 1 0 4]

predict devuelve las 10 probabilidades del softmax por imagen; argmax elige la clase más probable. Aún más instructivo es mirar los errores:

probs_all = modelo.predict(X_test_prep, verbose=0)
pred = probs_all.argmax(axis=1)
errores = np.where(pred != y_test)[0]
print(f"{len(errores)} errores de {len(y_test)}")

fig, ejes = plt.subplots(1, 5, figsize=(12, 3))
for eje, idx in zip(ejes, errores[:5]):
    eje.imshow(X_test[idx], cmap="gray")
    eje.set_title(f"real {y_test[idx]} / pred {pred[idx]}")
    eje.axis("off")
plt.show()

Verás que muchos fallos son dígitos genuinamente ambiguos (cuatros que parecen nueves, sietes retorcidos) donde incluso un humano dudaría. Analizar errores concretos —no solo la métrica global— es un hábito profesional que reaparecerá en todos los proyectos del módulo 7.

Guardar el modelo en tecnomarket-dl/

Un modelo que solo vive en la memoria del notebook se pierde al cerrar la sesión. Guardamos los 109 386 pesos, la arquitectura y la configuración de compile en un solo fichero, dentro de la estructura de carpetas que creamos en 01-05:

modelo.save("tecnomarket-dl/models/mnist_denso_v1.keras")

# Comprobacion: recargar y verificar que predice igual
modelo_cargado = keras.models.load_model("tecnomarket-dl/models/mnist_denso_v1.keras")
print(modelo_cargado.predict(X_test_prep[:1], verbose=0).argmax())   # -> 7, como antes

El formato .keras es el nativo moderno de Keras y basta un load_model para recuperar el modelo entero, listo para predecir o seguir entrenando. Esto es todo lo que necesitas por ahora: el versionado serio, los formatos alternativos y el despliegue real de modelos (servirlos en producción para la web de TecnoMarket) tienen su propia lección en el módulo 6.

Los límites del prototipo: por qué las fotos reales pedirán otra arquitectura

El prototipo está validado; ¿puede el equipo lanzarse ya a clasificar las fotos de productos de TecnoMarket con esta misma red? No, y entender por qué es la mejor transición al próximo módulo:

  • Aplanar destruye la estructura espacial. Al convertir 28×28 en 784 valores, la red no sabe que el píxel 30 está justo debajo del píxel 2. Aprende a base de fuerza bruta estadística, cosa viable con dígitos centrados de 28×28, pero no con fotos de 224×224 donde la cafetera puede aparecer en cualquier rincón.
  • Sin invarianza a la posición. Si desplazas un dígito tres píxeles a la derecha, para la red densa es un vector completamente distinto — cada píxel cae en otra entrada con otros pesos. Las fotos reales de productos varían en posición, escala, ángulo e iluminación.
  • Explosión de parámetros. Con fotos en color de 224×224 (150 528 entradas), solo la primera capa densa de 128 neuronas tendría 19 millones de parámetros. Inviable e ineficiente.

La solución son las redes convolucionales (CNN): capas que miran la imagen por parches, comparten pesos y respetan la estructura espacial. Son exactamente el tema del módulo 3.

Errores Comunes y Consejos

  • Olvidar la normalización. Con píxeles 0–255 la red puede seguir entrenando, pero peor y más despacio (accuracy típico varios puntos por debajo). Si tus resultados son sospechosamente malos, revisa antes que nada el rango de las entradas: X.min(), X.max().
  • Normalizar train y test con criterios distintos. Toda transformación se define con el train y se aplica idéntica al test. Aquí es trivial (dividir por 255), pero el principio evitará errores graves con datos tabulares.
  • Evaluar en test muchas veces mientras ajustas. Si pruebas diez arquitecturas mirando el test y te quedas con la mejor, el test ha dejado de ser un examen honesto: has ajustado a él. Para iterar está la validación; el test es la nota final.
  • Fijarse solo en el accuracy de train. Un 99.6 % en train con 97.7 % en validación es sobreajuste, no éxito. La cifra que cuenta es siempre la de validación/test.
  • Olvidar aplanar/preprocesar en el momento de predecir. El modelo espera exactamente el formato con el que se entrenó: vector de 784 floats en [0, 1], con forma (n, 784) (¡aunque sea una sola imagen: (1, 784), no (784,)!).
  • Épocas de más "por si acaso". Cuando la curva de validación lleva varias épocas plana o subiendo, seguir entrenando solo aumenta el sobreajuste. Detente y quédate con la zona buena.

Ejercicios

  1. Fashion-MNIST, mismo flujo. Repite el proyecto completo con keras.datasets.fashion_mnist (mismas formas: 28×28, 10 clases, pero prendas de ropa: camisetas, zapatillas, bolsos — un banco de pruebas aún más cercano a los productos de TecnoMarket). ¿Qué accuracy de test consigues? ¿Por qué crees que es más bajo que en MNIST?
  2. El tamaño importa (¿o no?). Sobre MNIST, entrena tres variantes: (a) sin capas ocultas (784 → 10 softmax, una regresión logística multiclase), (b) una oculta de 32, (c) la red del texto (128 + 64). Compara accuracy de validación y número de parámetros de cada una en una pequeña tabla. ¿Compensa cada salto de tamaño?
  3. Informe de confianza. Escribe una función predecir_con_confianza(modelo, imagen) que devuelva la clase predicha, su probabilidad, y la segunda clase más probable con la suya; pruébala sobre 3 imágenes acertadas y 3 falladas del test. ¿Qué observas en la confianza de los fallos?

Soluciones

Ejercicio 1. El código es idéntico cambiando la línea de carga por keras.datasets.fashion_mnist.load_data(). El accuracy de test típico ronda el 88–90 %, frente al ~98 % de MNIST. La razón: las clases de ropa se parecen mucho más entre sí (camiseta vs. camisa vs. abrigo comparten silueta) que los dígitos, y la textura/forma fina importa más — dificultad que anticipa la de las fotos reales de productos y que motiva aún más las CNN del módulo 3.

Ejercicio 2. Resultados típicos (los tuyos variarán algo por la aleatoriedad de la inicialización):

Variante Parámetros Val accuracy
(a) 784 → 10 7 850 ~92.5 %
(b) 784 → 32 → 10 25 450 ~96.5 %
(c) 784 → 128 → 64 → 10 109 386 ~97.7 %

El primer salto (añadir una capa oculta) es el que más aporta: pasa de un modelo lineal a uno no lineal, +4 puntos. El segundo salto multiplica por 4 los parámetros para ganar ~1 punto: rendimientos decrecientes. Lección: más grande ayuda, pero cada vez menos, y el salto cualitativo lo da la no linealidad (como sabías desde 02-02).

Ejercicio 3.

def predecir_con_confianza(modelo, imagen):
    probs = modelo.predict(imagen.reshape(1, 784), verbose=0)[0]
    orden = probs.argsort()[::-1]          # clases de mayor a menor probabilidad
    return {"prediccion": int(orden[0]), "confianza": float(probs[orden[0]]),
            "segunda": int(orden[1]), "conf_segunda": float(probs[orden[1]])}

En las imágenes acertadas la confianza suele superar 0.99 y la segunda opción queda por debajo de 0.01. En las falladas es habitual ver confianzas menores (0.5–0.9) con una segunda opción fuerte que a menudo es la clase correcta (real 4, pred 9 con 0.55, segunda 4 con 0.42). Moraleja para TecnoMarket: las probabilidades del softmax permiten políticas de "revisión humana si la confianza es baja", igual que el umbral graduable del detector de fraude.

Conclusión

Acabas de completar tu primer proyecto de deep learning de principio a fin: exploraste MNIST, lo normalizaste y particionaste, definiste una red densa 784-128-64-10 justificando cada pieza con las lecciones del módulo (ReLU en ocultas, softmax + entropía cruzada categórica en la salida, Adam como optimizador, mini-batches de 64), entrenaste vigilando las curvas y detectando el inicio del sobreajuste, obtuviste un ~97.7 % honesto en test, examinaste errores individuales y guardaste el modelo versionado en tecnomarket-dl/models/. El flujo que has seguido es el mismo que usarás en todos los proyectos que quedan del curso; solo cambiarán los datos y las arquitecturas.

Y precisamente de arquitecturas va el siguiente paso. Hemos visto que la red densa trata la imagen como una lista plana de píxeles: le funcionó a duras penas con dígitos de 28×28 y le costaría mucho más con las prendas de Fashion-MNIST — y sería inviable con las fotos reales de productos de TecnoMarket. En el módulo 3 conocerás las redes neuronales convolucionales (CNN), la arquitectura que entiende las imágenes como lo que son: estructuras espaciales donde la posición, los bordes y las formas importan.

Curso de Deep Learning

Módulo 1: Introducción a Deep Learning

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados