¿Tu primera red neuronal? Entrénala paso a paso con MNIST, TensorFlow y Google Colab 🐍⚡️

Por Administrador

En este post te comparto el código que usamos en el video del canal, para entrenar una red neuronal con MNIST. Si quieres ver todo el proceso explicado con mayor detalle y ejemplos visuales, te recomiendo ver el video completo.

¿Qué contiene el dataset MNIST?

MNIST fue creado por el National Institute of Standards and Technology en los años 80s, y es considerado el clásico «Hola Mundo» en el aprendizaje profundo. Está compuesto por imágenes en escala de grises de números escritos a mano, desde el 0 hasta el 9. Cada imagen tiene una resolución de 28×28 pixeles, está dividido en:

  • 60.000 imágenes para entrenamiento
  • 10.000 imágenes para prueba

¡A entrenar el modelo!

Acelera tu entorno en Google Colab

Si estás usando Google Colab, te recomiendo activar la aceleración por hardware. Puedes seleccionar una GPU (por ejemplo, T4) para que el entrenamiento sea mucho más rápido. Eso sí, no siempre está disponible, así que tenlo en cuenta.

🔢 ¡A cargar el dataset!

Vamos a importar los paquetes que necesitaremos: numpy, matplotlib y tensorflow.

import numpy as np
import matplotlib.pyplot as plt
plt.style.use("ggplot")
import tensorflow as tf

Luego, cargamos el dataset con:

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

# Dimensiones de los datos
print("Datos de entrenamiento", X_train.shape, y_train.shape)
print("Datos de prueba", X_test.shape, y_test.shape)

Al ejecutarlo, se descarga el dataset. Podemos imprimir sus dimensiones y comprobar que tenemos:

  • 60.000 ejemplos de entrenamiento de 28×28
  • 10.000 ejemplos de prueba de 28×28

Esta dataset está compuesto por imágenes de números escritos a mano del 0 al 9, entonces tendríamos 10 clases en total. Sería interesante explorar la cantidad de ejemplos por cada clase, para identificar si hay cierto grado de desbalanceo. Esto es importante conocer, ya que si tuviéramos muchísimos ejemplos en ciertas clases, y muy pocos de otras, el entrenamiento podría aprender más de aquellas clases con mayor número. 

Conjunto de entrenamiento:

values_train, count_train = np.unique(y_train, return_counts=True)

# Gráfico de barras
plt.figure(figsize=(7, 4))
plt.bar(values_train, count_train)
plt.title("Training label distribution")
plt.xlabel("Labels")
plt.ylabel("Frequency")
plt.xticks(values_train)
plt.show()

Como puedes ver, el dígito 1 es el que más aparece, con casi 7000 ejemplos, mientras que el dígito 5 es el menos frecuente, con alrededor de 5500 imágenes. El resto de los dígitos se encuentran en un rango intermedio, lo cual nos indica que hay una buena distribución general entre las clases. 

Aunque el dataset no está perfectamente balanceado, sí contamos con un nivel aceptable de balanceo. Es decir, hay una cantidad considerable de ejemplos para cada una de las clases. Ahora, si quisieras llevar esto un paso más allá y lograr un balance perfecto, podrías añadir más ejemplos para las clases menos representadas, o bien aplicar técnicas de balanceo de datos.

Conjunto de prueba:

values_test, count_test = np.unique(y_test, return_counts=True)

# Gráfico de barras
plt.figure(figsize=(7, 4))
plt.bar(values_test, count_test, color="royalblue")
plt.title("Testing label distribution")
plt.xlabel("Labels")
plt.ylabel("Frequency")
plt.xticks(values_test)
plt.show()

🧑‍🍳 ¡A preparar los datos!

Primero, aplanamos las imágenes de 28×28 a vectores de 784 elementos (28*28) para usar una red neuronal densa. Luego, normalizamos los pixeles dividiendo entre 255, lo que transforma los valores a un rango entre 0 y 1. Esto mejora el entrenamiento del modelo.

train_images = X_train.reshape((60_000, 28 * 28))
train_images = train_images.astype("float32") / 255

test_images = X_test.reshape((10_000, 28 * 28))
test_images = test_images.astype("float32") / 255

Y finalmente, aplicamos One Hot Encoding a las etiquetas:

from keras.utils import to_categorical

train_labels = to_categorical(y_train)
test_labels = to_categorical(y_test)

🛠️ ¡A construir la arquitectura del modelo!

Creamos una red secuencial con la clase Sequential. Utilizamos Input, Dense y funciones de activación ReLU y Softmax:

from tensorflow.keras import Sequential
from tensorflow.keras.layers import Input, Dense

model = Sequential([
    Input(shape=(28 * 28,)),
    Dense(64, activation="relu"),
    Dense(64, activation="relu"),
    Dense(10, activation="softmax")
])

model.summary()

Imprimimos la arquitectura con model.summary().

⚙️¡A compilar el modelo!

Usamos el optimizador adam, la pérdida categorical_crossentropy y como métrica, la exactitud (accuracy):

from tensorflow.keras.optimizers import Adam
model.compile(optimizer=Adam(learning_rate=0.001),
              loss="categorical_crossentropy",
              metrics=["accuracy"])

🏋️‍♂️ ¡A entrenar el modelo!

Entrenamos el modelo con:

history = model.fit(train_images,
          train_labels,
          validation_split=0.2,
          epochs=15,
          batch_size=64)

La división validation_split=0.2, quiere decir que el 20% de los datos usados en el entrenamiento irán al conjunto de validación, lo que nos permite monitorear si el modelo está aprendiendo de forma adecuada sin sobreajustarse.

📐 ¡A evaluar el modelo!

history.history.keys()
plt.figure(figsize=(6, 4))
plt.plot(np.arange(0, len(history.history["loss"])), history.history["loss"], label="train_loss")
plt.plot(np.arange(0, len(history.history["loss"])), history.history["val_loss"], label="val_loss")
plt.title("Train and Test Loss")
plt.xlabel("Epoch #")
plt.ylabel("Loss")
plt.legend()
plt.show()
plt.style.use("ggplot")
plt.figure(figsize=(6, 4))
plt.plot(np.arange(0, len(history.history["accuracy"])), history.history["accuracy"], label="train_accuracy")
plt.plot(np.arange(0, len(history.history["val_accuracy"])), history.history["val_accuracy"], label="val_accuracy")
plt.title("Train and Test Accuracy")
plt.xlabel("Epoch #")
plt.ylabel("Accuracy")
plt.legend()
plt.show()
model.evaluate(test_images, test_labels)

Graficamos la pérdida y la exactitud para entrenamiento y validación.

Prueba 1 – Observamos que la pérdida en entrenamiento disminuye mucho, mientras que la de validación se estanca: esto indica overfitting. Aun así, el modelo logra 0.97 de exactitud en el conjunto de prueba.

Prueba 2 – Reducción de overfitting: Entrenamos nuevamente, pero esta vez reducimos la tasa de aprendizaje (learning_rate=0.0001). El resultado: menor overfitting y comportamiento más equilibrado entre entrenamiento y validación. La exactitud en prueba fue del 95%, ligeramente menor, pero con mejor generalización.

💾 ¡ A guardar el modelo!

Guardamos el modelo con:

model.save("mnist_model.keras")

Recuerda guardar el modelo generado, ya que los vamos a usar en el siguiente tutorial, en donde lo desplegaremos con ayuda de OpenCV.

🤖 ¡A hacer predicciones con el modelo!

Tomamos la primera imagen de prueba, la reestructuramos y realizamos las predicciones. Para determinar el número que ha predicho, extraemos el índice con mayor probabilidad:

pred = model.predict(test_images[0].reshape(1, 784))
pred_label = np.argmax(pred)
pred_label

De la predicción obtenemos el número 7. Podemos comprobarlo visualizando la imagen correspondiente a este elemento de prueba:

plt.imshow(test_images[0].reshape(28, 28), cmap="gray")
plt.title(f"Predicción: {pred_label}")
plt.axis("off")
plt.show()

📅 Próximo paso: En el siguiente artículo aplicaremos este modelo a una práctica de visión por computador, en la que predeciremos dígitos escritos a mano en tiempo real desde un videostreaming. ¡No te lo pierdas!. Cuídate mucho, chao chao.