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
- El proyecto y el dataset: por qué MNIST
- Cargar y explorar los datos
- Preprocesar: normalización y splits
- Definir el modelo (justificando cada elección)
- Entrenar y leer las curvas de loss/accuracy
- Evaluar en test y hacer predicciones individuales
- Guardar el modelo en
tecnomarket-dl/ - 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,386Haz 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.9772Cada é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.977Un 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 antesEl 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
- 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? - 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?
- 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
- ¿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
