{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Red Neuronal de Clasificación MNIST - Versión Optimizada\n",
    "## Aplicando principios SOLID y mejores prácticas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Importaciones y Configuración"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "import tensorflow_datasets as tfds\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import math\n",
    "\n",
    "# Configuración global\n",
    "VALIDATION_SPLIT = 0.10\n",
    "BATCH_SIZE = 32\n",
    "EPOCHS = 10\n",
    "IMAGE_SHAPE = (28, 28, 1)\n",
    "NUM_CLASSES = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Clase para Gestión de Datos (Single Responsibility Principle)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MNISTDataManager:\n",
    "    \"\"\"Gestiona la carga, preparación y normalización de datos MNIST\"\"\"\n",
    "    \n",
    "    def __init__(self, validation_split=0.10):\n",
    "        self.validation_split = validation_split\n",
    "        self.datos = None\n",
    "        self.metadatos = None\n",
    "        self.nombres_clases = None\n",
    "        \n",
    "    def cargar_datos(self):\n",
    "        \"\"\"Carga el dataset MNIST\"\"\"\n",
    "        self.datos, self.metadatos = tfds.load(\n",
    "            \"mnist\", \n",
    "            as_supervised=True, \n",
    "            with_info=True\n",
    "        )\n",
    "        self.nombres_clases = self.metadatos.features[\"label\"].names\n",
    "        return self\n",
    "    \n",
    "    @staticmethod\n",
    "    def normalizar(imagenes, etiquetas):\n",
    "        \"\"\"Normaliza imágenes de 0-255 a 0-1\"\"\"\n",
    "        imagenes = tf.cast(imagenes, tf.float32) / 255.0\n",
    "        return imagenes, etiquetas\n",
    "    \n",
    "    def preparar_datasets(self, batch_size=32):\n",
    "        \"\"\"Prepara y divide los datasets\"\"\"\n",
    "        num_train = self.metadatos.splits['train'].num_examples\n",
    "        num_val = int(num_train * self.validation_split)\n",
    "        num_train_final = num_train - num_val\n",
    "        \n",
    "        # División de datos\n",
    "        train_raw = self.datos[\"train\"].take(num_train_final)\n",
    "        val_raw = self.datos[\"train\"].skip(num_train_final).take(num_val)\n",
    "        test_raw = self.datos[\"test\"]\n",
    "        \n",
    "        # Normalización y optimización\n",
    "        train_ds = (train_raw\n",
    "                   .map(self.normalizar)\n",
    "                   .cache()\n",
    "                   .shuffle(num_train_final)\n",
    "                   .batch(batch_size)\n",
    "                   .repeat())\n",
    "        \n",
    "        val_ds = (val_raw\n",
    "                 .map(self.normalizar)\n",
    "                 .cache()\n",
    "                 .batch(batch_size))\n",
    "        \n",
    "        test_ds = (test_raw\n",
    "                  .map(self.normalizar)\n",
    "                  .cache()\n",
    "                  .batch(batch_size))\n",
    "        \n",
    "        print(f\"Ejemplos de entrenamiento: {num_train_final}\")\n",
    "        print(f\"Ejemplos de validación: {num_val}\")\n",
    "        print(f\"Ejemplos de prueba: {self.metadatos.splits['test'].num_examples}\")\n",
    "        \n",
    "        return train_ds, val_ds, test_ds, num_train_final, num_val"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Clase para Construcción del Modelo (Open/Closed Principle)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ModelBuilder:\n",
    "    \"\"\"Constructor de modelos de clasificación\"\"\"\n",
    "    \n",
    "    @staticmethod\n",
    "    def crear_modelo_clasificacion(input_shape, num_classes, hidden_units=[50, 50]):\n",
    "        \"\"\"Crea un modelo de clasificación con capas configurables\"\"\"\n",
    "        modelo = tf.keras.Sequential([\n",
    "            tf.keras.layers.Flatten(input_shape=input_shape)\n",
    "        ])\n",
    "        \n",
    "        # Capas ocultas configurables\n",
    "        for units in hidden_units:\n",
    "            modelo.add(tf.keras.layers.Dense(units, activation='relu'))\n",
    "        \n",
    "        # Capa de salida\n",
    "        modelo.add(tf.keras.layers.Dense(num_classes, activation='softmax'))\n",
    "        \n",
    "        return modelo\n",
    "    \n",
    "    @staticmethod\n",
    "    def compilar_modelo(modelo, optimizer='adam'):\n",
    "        \"\"\"Compila el modelo con configuración estándar\"\"\"\n",
    "        modelo.compile(\n",
    "            optimizer=optimizer,\n",
    "            loss=tf.keras.losses.SparseCategoricalCrossentropy(),\n",
    "            metrics=['accuracy']\n",
    "        )\n",
    "        return modelo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Clase para Visualización (Single Responsibility Principle)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Visualizador:\n",
    "    \"\"\"Gestiona todas las visualizaciones del modelo\"\"\"\n",
    "    \n",
    "    def __init__(self, nombres_clases):\n",
    "        self.nombres_clases = nombres_clases\n",
    "    \n",
    "    @staticmethod\n",
    "    def mostrar_imagen_simple(imagen):\n",
    "        \"\"\"Muestra una imagen individual\"\"\"\n",
    "        plt.figure()\n",
    "        plt.imshow(imagen, cmap=plt.cm.binary)\n",
    "        plt.colorbar()\n",
    "        plt.show()\n",
    "    \n",
    "    @staticmethod\n",
    "    def mostrar_grid_imagenes(dataset, num_imagenes=25):\n",
    "        \"\"\"Muestra un grid de imágenes\"\"\"\n",
    "        filas = columnas = int(math.sqrt(num_imagenes))\n",
    "        plt.figure(figsize=(10, 10))\n",
    "        \n",
    "        for i, (imagen, _) in enumerate(dataset.take(num_imagenes)):\n",
    "            plt.subplot(filas, columnas, i + 1)\n",
    "            plt.imshow(imagen, cmap=plt.cm.binary)\n",
    "            plt.axis('off')\n",
    "        \n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "    \n",
    "    def visualizar_predicciones(self, imagenes, etiquetas_reales, predicciones, num_imagenes=25):\n",
    "        \"\"\"Visualiza predicciones con etiquetas correctas/incorrectas\"\"\"\n",
    "        filas = 5\n",
    "        columnas = 5\n",
    "        plt.figure(figsize=(2 * 2 * columnas, 2 * filas))\n",
    "        \n",
    "        for i in range(min(num_imagenes, len(imagenes))):\n",
    "            # Imagen\n",
    "            plt.subplot(filas, 2 * columnas, 2 * i + 1)\n",
    "            self._graficar_imagen(i, predicciones, etiquetas_reales, imagenes)\n",
    "            \n",
    "            # Gráfico de barras\n",
    "            plt.subplot(filas, 2 * columnas, 2 * i + 2)\n",
    "            self._graficar_barras(i, predicciones, etiquetas_reales)\n",
    "        \n",
    "        plt.tight_layout()\n",
    "        plt.show()\n",
    "    \n",
    "    def _graficar_imagen(self, idx, predicciones, etiquetas_reales, imagenes):\n",
    "        \"\"\"Grafica una imagen con su predicción\"\"\"\n",
    "        pred = predicciones[idx]\n",
    "        etiqueta_real = etiquetas_reales[idx]\n",
    "        img = imagenes[idx]\n",
    "        \n",
    "        plt.grid(False)\n",
    "        plt.xticks([])\n",
    "        plt.yticks([])\n",
    "        plt.imshow(img[..., 0], cmap=plt.cm.binary)\n",
    "        \n",
    "        etiqueta_pred = np.argmax(pred)\n",
    "        color = 'blue' if etiqueta_pred == etiqueta_real else 'red'\n",
    "        \n",
    "        plt.xlabel(\n",
    "            f\"{self.nombres_clases[etiqueta_pred]} {100*np.max(pred):.0f}% ({self.nombres_clases[etiqueta_real]})\",\n",
    "            color=color\n",
    "        )\n",
    "    \n",
    "    def _graficar_barras(self, idx, predicciones, etiquetas_reales):\n",
    "        \"\"\"Grafica barras de probabilidad\"\"\"\n",
    "        pred = predicciones[idx]\n",
    "        etiqueta_real = etiquetas_reales[idx]\n",
    "        \n",
    "        plt.grid(False)\n",
    "        plt.xticks(range(10))\n",
    "        plt.yticks([])\n",
    "        \n",
    "        grafica = plt.bar(range(10), pred, color=\"#777777\")\n",
    "        plt.ylim([0, 1])\n",
    "        \n",
    "        etiqueta_pred = np.argmax(pred)\n",
    "        grafica[etiqueta_pred].set_color('red')\n",
    "        grafica[etiqueta_real].set_color('blue')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Ejecución Principal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Cargar y preparar datos\n",
    "data_manager = MNISTDataManager(validation_split=VALIDATION_SPLIT)\n",
    "data_manager.cargar_datos()\n",
    "\n",
    "train_ds, val_ds, test_ds, num_train, num_val = data_manager.preparar_datasets(BATCH_SIZE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Visualizar datos\n",
    "visualizador = Visualizador(data_manager.nombres_clases)\n",
    "\n",
    "# Mostrar una imagen\n",
    "for imagen, _ in train_ds.take(1):\n",
    "    visualizador.mostrar_imagen_simple(imagen[0])\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Mostrar grid de imágenes\n",
    "visualizador.mostrar_grid_imagenes(train_ds.unbatch(), num_imagenes=25)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Crear y compilar modelo\n",
    "modelo = ModelBuilder.crear_modelo_clasificacion(\n",
    "    input_shape=IMAGE_SHAPE,\n",
    "    num_classes=NUM_CLASSES,\n",
    "    hidden_units=[50, 50]\n",
    ")\n",
    "\n",
    "modelo = ModelBuilder.compilar_modelo(modelo)\n",
    "modelo.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Entrenar modelo\n",
    "history = modelo.fit(\n",
    "    train_ds,\n",
    "    epochs=EPOCHS,\n",
    "    steps_per_epoch=math.ceil(num_train / BATCH_SIZE),\n",
    "    validation_data=val_ds,\n",
    "    validation_steps=math.ceil(num_val / BATCH_SIZE)\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Evaluar y visualizar predicciones\n",
    "for imagenes_test, etiquetas_test in test_ds.take(1):\n",
    "    imagenes_test = imagenes_test.numpy()\n",
    "    etiquetas_test = etiquetas_test.numpy()\n",
    "    predicciones = modelo.predict(imagenes_test)\n",
    "    \n",
    "    visualizador.visualizar_predicciones(\n",
    "        imagenes_test,\n",
    "        etiquetas_test,\n",
    "        predicciones,\n",
    "        num_imagenes=25\n",
    "    )"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
