En el proyecto 1 trabajaste con datos tabulares, el terreno natural de scikit-learn. Ahora cambiamos de mundo: imágenes. El objetivo de este proyecto es doble. Primero, comprobar hasta dónde llegan los métodos clásicos del módulo 4 con imágenes pequeñas (los dígitos manuscritos de load_digits); después, dar el salto a Fashion-MNIST —70.000 fotos de prendas de ropa en 10 categorías— y resolverlo con Keras, comparando una red densa (MLP) con una red convolucional (CNN), retomando en la práctica todo lo que viste en 07-04 "Redes neuronales profundas (Deep Learning)". Al final tendrás criterio propio para responder la pregunta importante: ¿cuándo basta lo clásico y cuándo hace falta deep learning?
Contenido
- Definición del problema y métrica objetivo
- Etapa A: dígitos con métodos clásicos
- Los límites del enfoque clásico
- Etapa B: Fashion-MNIST con Keras
- MLP densa: el baseline neuronal
- CNN pequeña: convoluciones en acción
- Overfitting en la práctica: dropout y early stopping
- Evaluación final: matriz de confusión de las 10 clases
- Conclusiones: ¿clásico o deep?
Definición del problema y métrica objetivo
- Problema: clasificación multiclase de imágenes — 10 dígitos primero, 10 prendas después.
- Métrica principal: accuracy. A diferencia del proyecto de fraude que te espera en 09-04, aquí las clases están perfectamente balanceadas (10 % cada una), así que la accuracy no engaña — es el caso benigno que describimos en 06-02 "Métricas de evaluación". La acompañaremos con la matriz de confusión para ver qué se confunde con qué.
- Criterio de éxito: en dígitos, superar el 95 % (es un dataset fácil); en Fashion-MNIST, superar el 90 %, que exige una CNN.
Una imagen en escala de grises es una matriz de píxeles (0 = negro, 255 = blanco). Para un modelo clásico, cada píxel es simplemente una columna más: una imagen de 8×8 son 64 features. Esa "traducción" — aplanar la imagen a un vector — es la clave de la etapa A y también su talón de Aquiles.
Etapa A: dígitos con métodos clásicos
load_digits trae 1.797 dígitos manuscritos de 8×8 píxeles, en el mismo formato que cualquier dataset tabular de sklearn:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split, cross_val_score
digits = load_digits()
X, y = digits.data, digits.target # X: (1797, 64), valores 0-16
print(X.shape, np.unique(y))
# Veamos algunos ejemplos
fig, ejes = plt.subplots(2, 8, figsize=(10, 3))
for eje, img, etiqueta in zip(ejes.ravel(), digits.images, y):
eje.imshow(img, cmap="gray_r")
eje.set_title(etiqueta); eje.axis("off")
plt.tight_layout(); plt.show()Dividimos (estratificando, como siempre desde 06-01 "División de datos: entrenamiento, validación y prueba") y probamos dos clásicos con escalado en Pipeline — la logística de 04-02 "Regresión logística" y la SVM de 04-04 "Máquinas de soporte vectorial (SVM)":
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
clasicos = {
"Logística": Pipeline([("esc", StandardScaler()),
("m", LogisticRegression(max_iter=2000))]),
"SVM (RBF)": Pipeline([("esc", StandardScaler()),
("m", SVC(kernel="rbf", gamma="scale"))]),
}
for nombre, modelo in clasicos.items():
puntuaciones = cross_val_score(modelo, X_train, y_train, cv=5)
print(f"{nombre}: {puntuaciones.mean():.3f} ± {puntuaciones.std():.3f}")Resultados típicos: logística ≈ 0,96 y SVM ≈ 0,98 de accuracy en validación cruzada. Con imágenes diminutas y bien centradas, lo clásico funciona de maravilla.
Para entender por qué, visualiza el dataset con el PCA de 05-03 "Análisis de componentes principales (PCA)":
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_2d = pca.fit_transform(StandardScaler().fit_transform(X_train))
plt.figure(figsize=(8, 6))
sc = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y_train, cmap="tab10", s=10, alpha=0.6)
plt.colorbar(sc, label="Dígito")
plt.title("Dígitos proyectados a 2D con PCA")
plt.show()
print("Varianza explicada:", pca.explained_variance_ratio_.sum().round(3))Aun con solo 2 componentes (≈ 22 % de la varianza), se intuyen nubes por dígito: las clases son bastante separables incluso tras aplanar la imagen. Ese es el secreto del éxito clásico aquí.
Los límites del enfoque clásico
¿Por qué no seguir así con cualquier imagen? Porque aplanar destruye la estructura espacial:
- Un píxel deja de "saber" quiénes eran sus vecinos: la columna 13 y la 14 son adyacentes en la imagen, pero para la logística son dos features tan independientes como
MedIncyLatitudeen el proyecto 1. - El modelo no es invariante a traslaciones: el mismo dígito desplazado dos píxeles activa columnas completamente distintas y parece "otro" patrón.
- La dimensionalidad explota: 8×8 son 64 features; una foto modesta de 224×224 en color son 150.528. Con features tan crudas, los métodos del módulo 4 se ahogan.
Con dígitos de 8×8, centrados y limpios, estos problemas casi no se notan. Con fotos de ropa de 28×28 empezarán a pesar; con fotos reales, serían fatales. Esta es exactamente la motivación de las convoluciones que viste en 07-04: procesar la imagen respetando su geometría.
Etapa B: Fashion-MNIST con Keras
Fashion-MNIST es el sustituto moderno del MNIST original: 60.000 imágenes de entrenamiento y 10.000 de test, de 28×28 en escala de grises, con 10 tipos de prenda. Viene incluido en Keras:
import tensorflow as tf
from tensorflow import keras
(X_train, y_train), (X_test, y_test) = keras.datasets.fashion_mnist.load_data()
clases = ["Camiseta", "Pantalón", "Jersey", "Vestido", "Abrigo",
"Sandalia", "Camisa", "Zapatilla", "Bolso", "Botín"]
print(X_train.shape, X_test.shape) # (60000, 28, 28) (10000, 28, 28)Normalización de píxeles: los valores van de 0 a 255; las redes entrenan mucho mejor con entradas pequeñas y homogéneas (es el mismo principio de 03-05 "Normalización y estandarización", aplicado a imágenes). Dividimos entre 255 para llevarlos a [0, 1], y apartamos un conjunto de validación para vigilar el entrenamiento:
X_train = X_train / 255.0
X_test = X_test / 255.0
# Validación: últimas 10.000 imágenes del train
X_val, y_val = X_train[50000:], y_train[50000:]
X_tr, y_tr = X_train[:50000], y_train[:50000]MLP densa: el baseline neuronal
Empezamos por la arquitectura que ya conoces de 04-07 "Redes neuronales" y 07-04: capas densas sobre la imagen aplanada. Es nuestro baseline "sin estructura espacial":
mlp = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)), # 784 features
keras.layers.Dense(256, activation="relu"),
keras.layers.Dense(128, activation="relu"),
keras.layers.Dense(10, activation="softmax"), # 10 probabilidades
])
mlp.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
hist_mlp = mlp.fit(X_tr, y_tr, epochs=20, batch_size=128,
validation_data=(X_val, y_val), verbose=2)Recordatorio de 07-04: softmax convierte las 10 salidas en probabilidades que suman 1, y sparse_categorical_crossentropy es la pérdida adecuada cuando las etiquetas son enteros (0–9) en lugar de one-hot. Resultado típico: ≈ 0,89 de accuracy en validación, con la curva de entrenamiento despegándose de la de validación en las últimas épocas — overfitting asomando, como en 06-05 "Overfitting y underfitting".
CNN pequeña: convoluciones en acción
Ahora la alternativa que respeta la geometría. Retomando 07-04: una capa Conv2D desliza filtros pequeños (3×3) por toda la imagen, aprendiendo detectores locales (bordes, texturas) que se aplican igual en cualquier posición — eso da invariancia a traslaciones y muchísimos menos parámetros. MaxPooling2D reduce la resolución a la mitad quedándose con la activación más fuerte de cada ventana 2×2, ganando robustez a pequeños desplazamientos:
cnn = keras.Sequential([
keras.layers.Reshape((28, 28, 1), input_shape=(28, 28)),
keras.layers.Conv2D(32, (3, 3), activation="relu"), # 32 detectores 3x3
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Conv2D(64, (3, 3), activation="relu"), # combina patrones
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Flatten(),
keras.layers.Dense(128, activation="relu"),
keras.layers.Dropout(0.3),
keras.layers.Dense(10, activation="softmax"),
])
cnn.summary()Lee el summary(): las capas convolucionales tienen pocos miles de parámetros (los filtros se comparten en toda la imagen), mientras que en la MLP la primera capa densa sola tenía 784 × 256 ≈ 200.000. Menos parámetros haciendo más trabajo útil: esa es la idea convolucional.
Overfitting en la práctica: dropout y early stopping
Dos defensas de 07-04 aplicadas de verdad:
- Dropout(0.3): en cada paso de entrenamiento apaga al azar el 30 % de las neuronas de esa capa, impidiendo que la red memorice co-adaptaciones frágiles.
- Early stopping: vigila la pérdida de validación y detiene el entrenamiento cuando deja de mejorar, restaurando los mejores pesos — el equivalente práctico de elegir el punto óptimo de la curva de 06-05.
cnn.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
parada = keras.callbacks.EarlyStopping(
monitor="val_loss", patience=3, restore_best_weights=True
)
hist_cnn = cnn.fit(X_tr, y_tr, epochs=30, batch_size=128,
validation_data=(X_val, y_val),
callbacks=[parada], verbose=2)
# Curvas de aprendizaje
plt.plot(hist_cnn.history["accuracy"], label="train")
plt.plot(hist_cnn.history["val_accuracy"], label="validación")
plt.xlabel("Época"); plt.ylabel("Accuracy"); plt.legend()
plt.title("CNN: curvas de aprendizaje"); plt.show()Resultado típico: ≈ 0,91–0,92 en validación, con las curvas mucho más pegadas que en la MLP. El early stopping suele parar entre las épocas 10 y 15.
Evaluación final: matriz de confusión de las 10 clases
Como siempre, el test se abre una sola vez, con el modelo ya decidido:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, accuracy_score
y_pred = cnn.predict(X_test).argmax(axis=1)
print("Accuracy test CNN:", round(accuracy_score(y_test, y_pred), 4))
cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(cm, display_labels=clases)
fig, eje = plt.subplots(figsize=(9, 9))
disp.plot(ax=eje, cmap="Blues", xticks_rotation=45, colorbar=False)
plt.title("Fashion-MNIST: matriz de confusión (test)")
plt.tight_layout(); plt.show()| Modelo | Accuracy (validación) | Accuracy (test) | Parámetros aprox. |
|---|---|---|---|
| MLP densa | ≈ 0,89 | ≈ 0,88 | ≈ 235.000 |
| CNN pequeña | ≈ 0,92 | ≈ 0,91 | ≈ 225.000 |
La matriz de confusión cuenta la historia interesante: los errores no se reparten al azar. Las confusiones dominantes son Camisa ↔ Camiseta ↔ Jersey ↔ Abrigo (todas son prendas de torso con silueta parecida a 28×28 píxeles) y, en menor medida, Zapatilla ↔ Botín ↔ Sandalia. En cambio, Pantalón y Bolso apenas se confunden con nada: sus siluetas son inconfundibles. La red se equivoca donde un humano, mirando una miniatura borrosa de 28×28, también dudaría — un error razonable es señal de un modelo que ha aprendido patrones reales y no ruido.
Errores Comunes y Consejos
- Olvidar normalizar los píxeles. Con valores 0–255 la red entrena mal o muy lento (gradientes desproporcionados). Dividir entre 255 es una línea que cambia todo.
- Normalizar train y test con criterios distintos. Aquí es una constante (255), pero si usaras media y desviación, se calcularían solo con el train — la misma disciplina de 03-05 y 06-01.
- Evaluar en test cada época. El test no es la validación: si lo usas para decidir cuándo parar o qué arquitectura elegir, deja de medir generalización. Para eso está
validation_data. - Comparar redes con una sola ejecución. La inicialización aleatoria hace variar el resultado unas décimas; para afirmar que la CNN gana a la MLP conviene repetir con varias semillas (con datos tabulares usarías la CV de 06-03; con redes grandes se repiten entrenamientos, porque la CV completa es cara).
- Redes enormes para problemas pequeños. La tentación de apilar capas llega antes que la necesidad. Empieza pequeño, mide, y crece solo si la validación lo pide.
Retos para ampliar
- Data augmentation. Genera variantes de las imágenes de entrenamiento (desplazamientos, giros leves, espejo horizontal) para enseñar a la red invariancias que los datos no muestran. Pista: investiga las capas
keras.layers.RandomTranslationyRandomFlipcolocadas al inicio del modelo; con ropa, cuidado con el espejo vertical (¿un botín boca abajo sigue siendo un botín?). - Transfer learning. En problemas reales casi nunca se entrena una CNN desde cero: se parte de una red preentrenada en millones de fotos (ResNet, MobileNet, disponibles en
keras.applications) y se reentrena solo la última capa. Pista: requiere redimensionar las imágenes al tamaño de entrada de la red elegida y convertirlas a 3 canales; con Fashion-MNIST la ganancia es modesta, pero el patrón es el que usarás en tu trabajo. - Tus propias fotos. Fotografía una zapatilla o una camiseta sobre fondo claro, conviértela a escala de grises de 28×28 con
PIL, invierte el contraste si hace falta (Fashion-MNIST tiene fondo negro) y pásasela a la CNN. Pista: probablemente falle más de lo que esperas — reflexiona sobre el drift entre tus fotos y las de entrenamiento, el mismo fenómeno de 08-03 "Mantenimiento y monitoreo de modelos".
Conclusión
Este proyecto te ha dado la respuesta práctica a "¿clásico o deep?": con imágenes diminutas y centradas como los dígitos, una SVM con escalado roza el 98 % y no necesitas nada más; con imágenes apenas un poco más ricas como Fashion-MNIST, aplanar píxeles empieza a costar puntos y las convoluciones — filtros locales compartidos, pooling, invariancia a traslaciones — los recuperan con menos parámetros. También has practicado el oficio de entrenar redes: normalizar píxeles, vigilar las curvas train/validación, frenar el overfitting con dropout y early stopping, y leer la matriz de confusión como un mapa de errores razonables. La regla general que te llevas: empieza por el método más simple que pueda funcionar, y que sean los datos quienes justifiquen cada capa adicional. En el próximo proyecto aplicarás esa misma regla a otro tipo de dato no tabular — el texto — donde los métodos clásicos, con la representación adecuada, todavía dan mucha guerra.
Curso de Machine Learning
Módulo 1: Introducción al Machine Learning
- ¿Qué es el Machine Learning?
- Historia y evolución del Machine Learning
- Tipos de Machine Learning
- Aplicaciones del Machine Learning
- El flujo de trabajo de un proyecto de Machine Learning
Módulo 2: Fundamentos de Estadística y Probabilidad
- Conceptos básicos de estadística
- Distribuciones de probabilidad
- Correlación y covarianza
- Inferencia estadística
- Teorema de Bayes
Módulo 3: Preprocesamiento de Datos
- Limpieza de datos
- Manejo de datos faltantes
- Transformación de datos
- Codificación de variables categóricas
- Normalización y estandarización
- Ingeniería de características
Módulo 4: Algoritmos de Machine Learning Supervisado
- Regresión lineal
- Regresión logística
- Árboles de decisión
- Máquinas de soporte vectorial (SVM)
- K-Vecinos más cercanos (K-NN)
- Naive Bayes
- Redes neuronales
Módulo 5: Algoritmos de Machine Learning No Supervisado
- Clustering: K-means
- Clustering jerárquico
- Análisis de componentes principales (PCA)
- Análisis de agrupamiento DBSCAN
- Visualización de datos con t-SNE y UMAP
Módulo 6: Evaluación y Validación de Modelos
- División de datos: entrenamiento, validación y prueba
- Métricas de evaluación
- Validación cruzada
- Curva ROC y AUC
- Overfitting y underfitting
Módulo 7: Técnicas Avanzadas y Optimización
- Regularización: Ridge, Lasso y Elastic Net
- Ensemble Learning
- Gradient Boosting
- Redes neuronales profundas (Deep Learning)
- Optimización de hiperparámetros
Módulo 8: Implementación y Despliegue de Modelos
- Frameworks y bibliotecas populares
- Implementación de modelos en producción
- Mantenimiento y monitoreo de modelos
- Consideraciones éticas y de privacidad
Módulo 9: Proyectos Prácticos
- Proyecto 1: Predicción de precios de viviendas
- Proyecto 2: Clasificación de imágenes
- Proyecto 3: Análisis de sentimientos en redes sociales
- Proyecto 4: Detección de fraudes
- Proyecto 5: Segmentación de clientes
