{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1d058850",
   "metadata": {},
   "source": [
    "# 11 · Frameworks de IA: PyTorch y TensorFlow lado a lado\n",
    "\n",
    "Casi toda la investigación y producción de IA moderna se ejecuta sobre dos frameworks de Python: **PyTorch**\n",
    "(Meta, hoy la PyTorch Foundation) y **TensorFlow** con su API de alto nivel **Keras** (Google). Los dos hacen las\n",
    "mismas tres cosas: tensores en CPU o GPU, diferenciación automática y construir y entrenar redes neuronales.\n",
    "Este módulo resuelve un mismo problema con ambos, línea a línea, para que se vean las diferencias.\n",
    "\n",
    "> Nuestro curso gratuito *Redes Neuronales 1001* construye estas ideas desde cero; aquí el foco son los propios\n",
    "> frameworks.\n",
    "\n",
    "**Aprenderás**\n",
    "1. Tensores y diferenciación automática en cada framework\n",
    "2. El mismo modelo entrenado en ambos: el bucle explícito de PyTorch y el `fit` de Keras\n",
    "3. Comparar exactitud y velocidad de forma justa\n",
    "4. Guardar, cargar y exportar modelos\n",
    "5. Cómo elegir, y el resto del ecosistema (JAX, Hugging Face, ONNX)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f10db4d6",
   "metadata": {},
   "source": [
    "## 1. Tensores y gradientes\n",
    "Los dos frameworks registran las operaciones sobre tensores y pueden derivar a través de ellas. Comprobación: la\n",
    "derivada de $f(x) = x^3 + 2x$ en $x = 2$ es $3 \\cdot 2^2 + 2 = 14$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c52c3c02",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:12:51.371596Z",
     "iopub.status.busy": "2026-10-18T10:12:51.370987Z",
     "iopub.status.idle": "2026-10-18T10:13:02.142331Z",
     "shell.execute_reply": "2026-10-18T10:13:02.139750Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
      "I0000 00:00:1792318374.940065 1036723 cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
      "I0000 00:00:1792318378.580599 1036723 cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch 2.14.1+cpu | TensorFlow 2.21.0\n",
      "PyTorch    df/dx = 14.0\n",
      "TensorFlow df/dx = 14.0\n",
      "tensor([ 3., 12.]) [ 3. 12.]\n"
     ]
    }
   ],
   "source": [
    "import os, time\n",
    "os.environ.setdefault(\"TF_CPP_MIN_LOG_LEVEL\", \"3\")       # arranque de TensorFlow más silencioso\n",
    "import numpy as np\n",
    "import torch\n",
    "import tensorflow as tf\n",
    "\n",
    "print(\"PyTorch\", torch.__version__, \"| TensorFlow\", tf.__version__)\n",
    "\n",
    "x_pt = torch.tensor(2.0, requires_grad=True)\n",
    "f_pt = x_pt ** 3 + 2 * x_pt\n",
    "f_pt.backward()                                            # PyTorch: los gradientes se acumulan en .grad\n",
    "print(\"PyTorch    df/dx =\", x_pt.grad.item())\n",
    "\n",
    "x_tf = tf.Variable(2.0)\n",
    "with tf.GradientTape() as tape:                             # TensorFlow: se graba dentro de una \"cinta\"\n",
    "    f_tf = x_tf ** 3 + 2 * x_tf\n",
    "print(\"TensorFlow df/dx =\", tape.gradient(f_tf, x_tf).numpy())\n",
    "\n",
    "a = np.arange(6, dtype=np.float32).reshape(2, 3)\n",
    "print(torch.from_numpy(a) @ torch.ones(3), tf.linalg.matvec(tf.constant(a), tf.ones(3)).numpy())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1f526e68",
   "metadata": {},
   "source": [
    "## 2. La tarea\n",
    "Dígitos manuscritos (8×8 píxeles, 1.797 imágenes, incluidos en scikit-learn), divididos una sola vez para que\n",
    "ambos frameworks vean exactamente las mismas imágenes de entrenamiento y de prueba."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c1f9397c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:13:02.148575Z",
     "iopub.status.busy": "2026-10-18T10:13:02.147173Z",
     "iopub.status.idle": "2026-10-18T10:13:02.776557Z",
     "shell.execute_reply": "2026-10-18T10:13:02.774831Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(1347, 8, 8) (450, 8, 8)\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 900x140 with 8 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "from sklearn.model_selection import train_test_split\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "digits = load_digits()\n",
    "X = (digits.images / 16.0).astype(np.float32)               # escalar los píxeles a [0, 1]\n",
    "y = digits.target.astype(np.int64)\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, stratify=y, random_state=0)\n",
    "print(X_train.shape, X_test.shape)\n",
    "\n",
    "fig, axes = plt.subplots(1, 8, figsize=(9, 1.4))\n",
    "for ax, img, label in zip(axes, X_train, y_train):\n",
    "    ax.imshow(img, cmap=\"gray_r\"); ax.set_title(label); ax.axis(\"off\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "becce801",
   "metadata": {},
   "source": [
    "## 3. PyTorch: definir un módulo y escribir el bucle\n",
    "PyTorch hace explícito el bucle de entrenamiento. Más código, pero cada paso es visible y fácil de adaptar, una\n",
    "de las razones por las que domina la investigación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b46607ad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:13:02.780505Z",
     "iopub.status.busy": "2026-10-18T10:13:02.780147Z",
     "iopub.status.idle": "2026-10-18T10:13:11.153816Z",
     "shell.execute_reply": "2026-10-18T10:13:11.151844Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch: exactitud en prueba 0.971 tras 30 épocas, 5.1 s, 38,282 parámetros\n"
     ]
    }
   ],
   "source": [
    "torch.manual_seed(0)\n",
    "EPOCHS, BATCH, LR = 30, 64, 1e-3\n",
    "\n",
    "class SmallCNN(torch.nn.Module):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.features = torch.nn.Sequential(\n",
    "            torch.nn.Conv2d(1, 16, 3, padding=1), torch.nn.ReLU(), torch.nn.MaxPool2d(2),   # 8x8 -> 4x4\n",
    "            torch.nn.Conv2d(16, 32, 3, padding=1), torch.nn.ReLU(),\n",
    "        )\n",
    "        self.head = torch.nn.Sequential(torch.nn.Flatten(), torch.nn.Linear(32 * 4 * 4, 64), torch.nn.ReLU(),\n",
    "                                        torch.nn.Dropout(0.2), torch.nn.Linear(64, 10))\n",
    "    def forward(self, x):\n",
    "        return self.head(self.features(x))\n",
    "\n",
    "pt_model = SmallCNN()\n",
    "optimizer = torch.optim.Adam(pt_model.parameters(), lr=LR)\n",
    "loss_fn = torch.nn.CrossEntropyLoss()\n",
    "Xtr, ytr = torch.from_numpy(X_train[:, None]), torch.from_numpy(y_train)      # (N, 1, 8, 8): canales primero\n",
    "Xte, yte = torch.from_numpy(X_test[:, None]), torch.from_numpy(y_test)\n",
    "loader = torch.utils.data.DataLoader(torch.utils.data.TensorDataset(Xtr, ytr), batch_size=BATCH, shuffle=True)\n",
    "\n",
    "pt_history, t0 = [], time.perf_counter()\n",
    "for epoch in range(EPOCHS):\n",
    "    pt_model.train()\n",
    "    for xb, yb in loader:\n",
    "        optimizer.zero_grad()                 # 1. poner a cero los gradientes\n",
    "        loss = loss_fn(pt_model(xb), yb)      # 2. pasada hacia delante\n",
    "        loss.backward()                       # 3. retropropagar\n",
    "        optimizer.step()                      # 4. actualizar los pesos\n",
    "    pt_model.eval()\n",
    "    with torch.no_grad():\n",
    "        pt_history.append((pt_model(Xte).argmax(1) == yte).float().mean().item())\n",
    "pt_time = time.perf_counter() - t0\n",
    "print(f\"PyTorch: exactitud en prueba {pt_history[-1]:.3f} tras {EPOCHS} épocas, {pt_time:.1f} s, \"\n",
    "      f\"{sum(p.numel() for p in pt_model.parameters()):,} parámetros\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "477c2c7b",
   "metadata": {},
   "source": [
    "## 4. TensorFlow / Keras: declarar el modelo y llamar a `fit`\n",
    "La misma arquitectura. Keras oculta el bucle tras `compile` y `fit`, que es conciso y difícil de equivocar.\n",
    "(También se puede escribir un bucle propio con `GradientTape` cuando hace falta.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "673ffa8a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:13:11.158192Z",
     "iopub.status.busy": "2026-10-18T10:13:11.157265Z",
     "iopub.status.idle": "2026-10-18T10:13:23.316754Z",
     "shell.execute_reply": "2026-10-18T10:13:23.315260Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TensorFlow: exactitud en prueba 0.978 tras 30 épocas, 12.0 s, 38,282 parámetros\n"
     ]
    }
   ],
   "source": [
    "tf.keras.utils.set_random_seed(0)\n",
    "tf_model = tf.keras.Sequential([\n",
    "    tf.keras.layers.Input(shape=(8, 8, 1)),                                   # canales al final\n",
    "    tf.keras.layers.Conv2D(16, 3, padding=\"same\", activation=\"relu\"), tf.keras.layers.MaxPooling2D(2),\n",
    "    tf.keras.layers.Conv2D(32, 3, padding=\"same\", activation=\"relu\"),\n",
    "    tf.keras.layers.Flatten(), tf.keras.layers.Dense(64, activation=\"relu\"),\n",
    "    tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(10),\n",
    "])\n",
    "tf_model.compile(optimizer=tf.keras.optimizers.Adam(LR),\n",
    "                 loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=[\"accuracy\"])\n",
    "t0 = time.perf_counter()\n",
    "hist = tf_model.fit(X_train[..., None], y_train, validation_data=(X_test[..., None], y_test),\n",
    "                    epochs=EPOCHS, batch_size=BATCH, verbose=0)\n",
    "tf_time = time.perf_counter() - t0\n",
    "tf_history = hist.history[\"val_accuracy\"]\n",
    "print(f\"TensorFlow: exactitud en prueba {tf_history[-1]:.3f} tras {EPOCHS} épocas, {tf_time:.1f} s, \"\n",
    "      f\"{tf_model.count_params():,} parámetros\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bfb2bcdd",
   "metadata": {},
   "source": [
    "## 5. Compararlos\n",
    "Mismos datos, misma arquitectura, mismo optimizador y mismo número de parámetros. Las diferencias en la exactitud\n",
    "final vienen de la inicialización aleatoria y del barajado, no del framework."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6405581d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:13:23.321072Z",
     "iopub.status.busy": "2026-10-18T10:13:23.320666Z",
     "iopub.status.idle": "2026-10-18T10:13:24.027061Z",
     "shell.execute_reply": "2026-10-18T10:13:24.025240Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 700x340 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "los dos modelos coinciden en el 97.8% de las imágenes de prueba; ambos fallan en la misma imagen: 7\n"
     ]
    }
   ],
   "source": [
    "plt.figure(figsize=(7, 3.4))\n",
    "plt.plot(range(1, EPOCHS + 1), pt_history, label=f\"PyTorch ({pt_time:.1f} s)\")\n",
    "plt.plot(range(1, EPOCHS + 1), tf_history, label=f\"TensorFlow ({tf_time:.1f} s)\")\n",
    "plt.xlabel(\"época\"); plt.ylabel(\"exactitud en prueba\"); plt.ylim(0.8, 1.0); plt.legend(); plt.title(\"La misma CNN, dos frameworks\")\n",
    "plt.tight_layout(); plt.show()\n",
    "\n",
    "with torch.no_grad():\n",
    "    p_pt = pt_model(Xte).argmax(1).numpy()\n",
    "p_tf = tf_model.predict(X_test[..., None], verbose=0).argmax(1)\n",
    "print(f\"los dos modelos coinciden en el {np.mean(p_pt == p_tf):.1%} de las imágenes de prueba; \"\n",
    "      f\"ambos fallan en la misma imagen: {np.sum((p_pt != y_test) & (p_tf != y_test))}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3a2a981",
   "metadata": {},
   "source": [
    "## 6. Guardar, cargar, exportar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "578929a4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:13:24.031416Z",
     "iopub.status.busy": "2026-10-18T10:13:24.030981Z",
     "iopub.status.idle": "2026-10-18T10:13:24.740087Z",
     "shell.execute_reply": "2026-10-18T10:13:24.738305Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch, recarga idéntica: True\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Keras, recarga idéntica: True\n",
      "{'cnn.pt': '153 KB', 'cnn.keras': '485 KB'}\n"
     ]
    }
   ],
   "source": [
    "import tempfile\n",
    "d = tempfile.mkdtemp()\n",
    "\n",
    "torch.save(pt_model.state_dict(), os.path.join(d, \"cnn.pt\"))      # PyTorch: guardar los pesos\n",
    "restored = SmallCNN(); restored.load_state_dict(torch.load(os.path.join(d, \"cnn.pt\"))); restored.eval()\n",
    "with torch.no_grad():\n",
    "    print(\"PyTorch, recarga idéntica:\", torch.equal(restored(Xte).argmax(1), pt_model(Xte).argmax(1)))\n",
    "\n",
    "tf_model.save(os.path.join(d, \"cnn.keras\"))                         # Keras: arquitectura + pesos en un solo archivo\n",
    "reloaded = tf.keras.models.load_model(os.path.join(d, \"cnn.keras\"))\n",
    "print(\"Keras, recarga idéntica:\", np.array_equal(reloaded.predict(X_test[..., None], verbose=0).argmax(1), p_tf))\n",
    "\n",
    "print({f: f\"{os.path.getsize(os.path.join(d, f)) / 1024:.0f} KB\" for f in os.listdir(d)})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee4abefe",
   "metadata": {},
   "source": [
    "Para el despliegue, los modelos suelen exportarse a un formato neutral u optimizado: **ONNX** (ambos frameworks,\n",
    "se ejecuta en cualquier sitio con ONNX Runtime), **TorchScript / torch.export** (PyTorch), **SavedModel** y\n",
    "**LiteRT** (antes TensorFlow Lite, para móviles y microcontroladores).\n",
    "\n",
    "## 7. Elegir\n",
    "| | PyTorch | TensorFlow / Keras |\n",
    "|---|---|---|\n",
    "| estilo | explícito, pythónico, ejecución inmediata por defecto | `fit` de alto nivel, compilación de grafos con `tf.function` |\n",
    "| donde más destaca | investigación, arquitecturas nuevas, la mayoría de los artículos nuevos y el trabajo con LLM | flujos de producción (TFX), móviles y dispositivos embebidos (LiteRT), TPU |\n",
    "| ecosistema | Hugging Face Transformers, PyTorch Lightning, torchvision | Keras, TensorFlow Serving, TF.js en el navegador |\n",
    "| nota | | Keras 3 también puede usar PyTorch o JAX como motor |\n",
    "\n",
    "El panorama más amplio: **JAX** (Google; API de NumPy con transformaciones componibles `grad`, `jit`, `vmap`,\n",
    "popular en investigación a gran escala), **Hugging Face** (modelos preentrenados y datasets para todos los\n",
    "frameworks), **scikit-learn** (aprendizaje automático clásico, a menudo el primer modelo adecuado), **ONNX\n",
    "Runtime** (inferencia rápida en cualquier sitio).\n",
    "\n",
    "## Ejercicios\n",
    "1. Escribe el entrenamiento del modelo de Keras con un bucle propio usando `tf.GradientTape`, imitando el bucle de PyTorch.\n",
    "2. Quita las capas convolucionales y usa solo capas densas. ¿Cuánta exactitud se pierde?\n",
    "3. Exporta el modelo de PyTorch a ONNX (`torch.onnx.export`) y ejecútalo con `onnxruntime`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
