Red Neuronal de Clasificación MNIST - Versión Optimizada¶

Aplicando principios SOLID y mejores prácticas¶

1. Importaciones y Configuración¶

In [ ]:
import tensorflow as tf
import tensorflow_datasets as tfds
import matplotlib.pyplot as plt
import numpy as np
import math

# Configuración global
VALIDATION_SPLIT = 0.10
BATCH_SIZE = 32
EPOCHS = 10
IMAGE_SHAPE = (28, 28, 1)
NUM_CLASSES = 10

2. Clase para Gestión de Datos (Single Responsibility Principle)¶

In [ ]:
class MNISTDataManager:
    """Gestiona la carga, preparación y normalización de datos MNIST"""
    
    def __init__(self, validation_split=0.10):
        self.validation_split = validation_split
        self.datos = None
        self.metadatos = None
        self.nombres_clases = None
        
    def cargar_datos(self):
        """Carga el dataset MNIST"""
        self.datos, self.metadatos = tfds.load(
            "mnist", 
            as_supervised=True, 
            with_info=True
        )
        self.nombres_clases = self.metadatos.features["label"].names
        return self
    
    @staticmethod
    def normalizar(imagenes, etiquetas):
        """Normaliza imágenes de 0-255 a 0-1"""
        imagenes = tf.cast(imagenes, tf.float32) / 255.0
        return imagenes, etiquetas
    
    def preparar_datasets(self, batch_size=32):
        """Prepara y divide los datasets"""
        num_train = self.metadatos.splits['train'].num_examples
        num_val = int(num_train * self.validation_split)
        num_train_final = num_train - num_val
        
        # División de datos
        train_raw = self.datos["train"].take(num_train_final)
        val_raw = self.datos["train"].skip(num_train_final).take(num_val)
        test_raw = self.datos["test"]
        
        # Normalización y optimización
        train_ds = (train_raw
                   .map(self.normalizar)
                   .cache()
                   .shuffle(num_train_final)
                   .batch(batch_size)
                   .repeat())
        
        val_ds = (val_raw
                 .map(self.normalizar)
                 .cache()
                 .batch(batch_size))
        
        test_ds = (test_raw
                  .map(self.normalizar)
                  .cache()
                  .batch(batch_size))
        
        print(f"Ejemplos de entrenamiento: {num_train_final}")
        print(f"Ejemplos de validación: {num_val}")
        print(f"Ejemplos de prueba: {self.metadatos.splits['test'].num_examples}")
        
        return train_ds, val_ds, test_ds, num_train_final, num_val

3. Clase para Construcción del Modelo (Open/Closed Principle)¶

In [ ]:
class ModelBuilder:
    """Constructor de modelos de clasificación"""
    
    @staticmethod
    def crear_modelo_clasificacion(input_shape, num_classes, hidden_units=[50, 50]):
        """Crea un modelo de clasificación con capas configurables"""
        modelo = tf.keras.Sequential([
            tf.keras.layers.Flatten(input_shape=input_shape)
        ])
        
        # Capas ocultas configurables
        for units in hidden_units:
            modelo.add(tf.keras.layers.Dense(units, activation='relu'))
        
        # Capa de salida
        modelo.add(tf.keras.layers.Dense(num_classes, activation='softmax'))
        
        return modelo
    
    @staticmethod
    def compilar_modelo(modelo, optimizer='adam'):
        """Compila el modelo con configuración estándar"""
        modelo.compile(
            optimizer=optimizer,
            loss=tf.keras.losses.SparseCategoricalCrossentropy(),
            metrics=['accuracy']
        )
        return modelo

4. Clase para Visualización (Single Responsibility Principle)¶

In [ ]:
class Visualizador:
    """Gestiona todas las visualizaciones del modelo"""
    
    def __init__(self, nombres_clases):
        self.nombres_clases = nombres_clases
    
    @staticmethod
    def mostrar_imagen_simple(imagen):
        """Muestra una imagen individual"""
        plt.figure()
        plt.imshow(imagen, cmap=plt.cm.binary)
        plt.colorbar()
        plt.show()
    
    @staticmethod
    def mostrar_grid_imagenes(dataset, num_imagenes=25):
        """Muestra un grid de imágenes"""
        filas = columnas = int(math.sqrt(num_imagenes))
        plt.figure(figsize=(10, 10))
        
        for i, (imagen, _) in enumerate(dataset.take(num_imagenes)):
            plt.subplot(filas, columnas, i + 1)
            plt.imshow(imagen, cmap=plt.cm.binary)
            plt.axis('off')
        
        plt.tight_layout()
        plt.show()
    
    def visualizar_predicciones(self, imagenes, etiquetas_reales, predicciones, num_imagenes=25):
        """Visualiza predicciones con etiquetas correctas/incorrectas"""
        filas = 5
        columnas = 5
        plt.figure(figsize=(2 * 2 * columnas, 2 * filas))
        
        for i in range(min(num_imagenes, len(imagenes))):
            # Imagen
            plt.subplot(filas, 2 * columnas, 2 * i + 1)
            self._graficar_imagen(i, predicciones, etiquetas_reales, imagenes)
            
            # Gráfico de barras
            plt.subplot(filas, 2 * columnas, 2 * i + 2)
            self._graficar_barras(i, predicciones, etiquetas_reales)
        
        plt.tight_layout()
        plt.show()
    
    def _graficar_imagen(self, idx, predicciones, etiquetas_reales, imagenes):
        """Grafica una imagen con su predicción"""
        pred = predicciones[idx]
        etiqueta_real = etiquetas_reales[idx]
        img = imagenes[idx]
        
        plt.grid(False)
        plt.xticks([])
        plt.yticks([])
        plt.imshow(img[..., 0], cmap=plt.cm.binary)
        
        etiqueta_pred = np.argmax(pred)
        color = 'blue' if etiqueta_pred == etiqueta_real else 'red'
        
        plt.xlabel(
            f"{self.nombres_clases[etiqueta_pred]} {100*np.max(pred):.0f}% ({self.nombres_clases[etiqueta_real]})",
            color=color
        )
    
    def _graficar_barras(self, idx, predicciones, etiquetas_reales):
        """Grafica barras de probabilidad"""
        pred = predicciones[idx]
        etiqueta_real = etiquetas_reales[idx]
        
        plt.grid(False)
        plt.xticks(range(10))
        plt.yticks([])
        
        grafica = plt.bar(range(10), pred, color="#777777")
        plt.ylim([0, 1])
        
        etiqueta_pred = np.argmax(pred)
        grafica[etiqueta_pred].set_color('red')
        grafica[etiqueta_real].set_color('blue')

5. Ejecución Principal¶

In [ ]:
# Cargar y preparar datos
data_manager = MNISTDataManager(validation_split=VALIDATION_SPLIT)
data_manager.cargar_datos()

train_ds, val_ds, test_ds, num_train, num_val = data_manager.preparar_datasets(BATCH_SIZE)
In [ ]:
# Visualizar datos
visualizador = Visualizador(data_manager.nombres_clases)

# Mostrar una imagen
for imagen, _ in train_ds.take(1):
    visualizador.mostrar_imagen_simple(imagen[0])
    break
In [ ]:
# Mostrar grid de imágenes
visualizador.mostrar_grid_imagenes(train_ds.unbatch(), num_imagenes=25)
In [ ]:
# Crear y compilar modelo
modelo = ModelBuilder.crear_modelo_clasificacion(
    input_shape=IMAGE_SHAPE,
    num_classes=NUM_CLASSES,
    hidden_units=[50, 50]
)

modelo = ModelBuilder.compilar_modelo(modelo)
modelo.summary()
In [ ]:
# Entrenar modelo
history = modelo.fit(
    train_ds,
    epochs=EPOCHS,
    steps_per_epoch=math.ceil(num_train / BATCH_SIZE),
    validation_data=val_ds,
    validation_steps=math.ceil(num_val / BATCH_SIZE)
)
In [ ]:
# Evaluar y visualizar predicciones
for imagenes_test, etiquetas_test in test_ds.take(1):
    imagenes_test = imagenes_test.numpy()
    etiquetas_test = etiquetas_test.numpy()
    predicciones = modelo.predict(imagenes_test)
    
    visualizador.visualizar_predicciones(
        imagenes_test,
        etiquetas_test,
        predicciones,
        num_imagenes=25
    )