{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# PyTorch hace en cuatro líneas lo que escribiste a mano\n",
    "\n",
    "La misma red de los capítulos anteriores, ahora con la librería que se usa en el trabajo. Y la comprobación de que sale lo mismo.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 9 de **Deep learning desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/deep-learning-desde-cero/pytorch/\n",
    "\n",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Llevas siete capítulos escribiendo redes con numpy. Ahora te voy a enseñar la\n",
    "librería con la que se hacen en el trabajo, y quiero que veas que no te van a\n",
    "enseñar nada nuevo: te van a ahorrar teclas 🧵\n",
    "\n",
    "Ese orden importa y por eso el capítulo está aquí y no al principio. Quien\n",
    "empieza por la librería aprende a llamar funciones. Quien empieza por la cuenta\n",
    "aprende qué hacen esas funciones, y cuando algo se rompe sabe dónde mirar.\n",
    "\n",
    "Antes de correr nada, quédate con esta pregunta: **¿qué parte de lo que\n",
    "escribiste a mano crees que PyTorch hace distinto?** Al final del\n",
    "capítulo la contestamos con números 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los mismos datos y el mismo listón"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto es el arranque del capítulo 7, sin tocar una\n",
    "coma, más las tres líneas que convierten las matrices en tensores."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import torch\n",
    "from torch import nn\n",
    "from torch.utils.data import DataLoader, TensorDataset\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "# Con datos de este tamano los cuatro hilos de torch se estorban entre ellos y\n",
    "# tarda mas que con uno. Con datos de verdad esta linea se quita.\n",
    "torch.set_num_threads(1)\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v['ciudad'] = (v['ciudad'].str.strip().str.lower()\n",
    "                   .str.normalize('NFKD')\n",
    "                   .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "    for col in ['fecha', 'fecha_ultima_compra']:\n",
    "        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "def prepara(v):\n",
    "    v = v.sort_values(['cliente_id', 'fecha']).copy()\n",
    "    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)\n",
    "    v['sin_descuento'] = v['descuento'].isna().astype(int)\n",
    "    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)\n",
    "    v['precio_unitario'] = v['monto'] / v['unidades']\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos['compro']\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,\n",
    "                                          random_state=42, stratify=y)\n",
    "logistica = Pipeline([\n",
    "    ('pre', ColumnTransformer([\n",
    "        ('num', Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                          ('e', StandardScaler())]), NUMERICAS),\n",
    "        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),\n",
    "    ])),\n",
    "    ('mod', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "]).fit(X_tr, y_tr)\n",
    "\n",
    "T_tr = logistica.named_steps['pre'].transform(X_tr)\n",
    "T_te = logistica.named_steps['pre'].transform(X_te)\n",
    "objetivo = y_tr.values.astype(float).reshape(-1, 1)\n",
    "\n",
    "# Las tres lineas nuevas: una matriz de numpy se vuelve tensor y ya esta.\n",
    "E_tr = torch.tensor(T_tr)\n",
    "E_te = torch.tensor(T_te)\n",
    "O_tr = torch.tensor(objetivo)\n",
    "\n",
    "print('entreno con', tuple(E_tr.shape), 'y pruebo con', tuple(E_te.shape))\n",
    "print('el liston a batir:', round(roc_auc_score(\n",
    "    y_te, logistica.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un tensor es una matriz de numpy que además se acuerda de las operaciones que\n",
    "le hicieron. Eso es todo lo que es, y es lo que hace posible lo que viene\n",
    "ahora 📦"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los gradientes que escribiste son los que saca autograd"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 6 repartiste la culpa hacia atrás a\n",
    "mano, con la regla de la cadena, y te salieron cuatro matrices de derivadas.\n",
    "Aquí voy a calcular esas mismas cuatro de las dos maneras, con los mismos pesos\n",
    "de partida, y a compararlas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\n",
    "\n",
    "r = np.random.default_rng(0)\n",
    "W1 = r.normal(0, 0.1, (T_tr.shape[1], 8)); b1 = np.zeros(8)\n",
    "W2 = r.normal(0, 0.1, (8, 1)); b2 = np.zeros(1)\n",
    "\n",
    "# 1) A mano, como en el capitulo de retropropagacion.\n",
    "h = np.tanh(T_tr @ W1 + b1)\n",
    "p = sigmoide(h @ W2 + b2)\n",
    "d2 = (p - objetivo) / len(objetivo)\n",
    "d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "mios = {'W1': T_tr.T @ d1, 'b1': d1.sum(axis=0),\n",
    "        'W2': h.T @ d2, 'b2': d2.sum(axis=0)}\n",
    "\n",
    "# 2) Con autograd: los mismos numeros, marcados con requires_grad.\n",
    "sueltos = {n: torch.tensor(v, requires_grad=True)\n",
    "           for n, v in [('W1', W1), ('b1', b1), ('W2', W2), ('b2', b2)]}\n",
    "z = (torch.tanh(E_tr @ sueltos['W1'] + sueltos['b1'])\n",
    "     @ sueltos['W2'] + sueltos['b2'])\n",
    "nn.functional.binary_cross_entropy_with_logits(z, O_tr).backward()\n",
    "\n",
    "for nombre in ['W1', 'b1', 'W2', 'b2']:\n",
    "    suyo = sueltos[nombre].grad.numpy()\n",
    "    print('%-3s de %-8s  el mio y el de torch son iguales: %s'\n",
    "          % (nombre, str(np.shape(mios[nombre])), np.allclose(mios[nombre], suyo)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro veces True 🎉\n",
    "\n",
    "Esa línea de `backward()` es el capítulo entero de\n",
    "retropropagación. PyTorch fue apuntando cada operación que hiciste con los\n",
    "tensores, y al llamar a `backward()` recorrió esa lista al revés\n",
    "aplicando la regla de la cadena, que es exactamente lo que hiciste tú con lápiz.\n",
    "No es otra cuenta: es la misma cuenta, hecha por un programa.\n",
    "\n",
    "Y fíjate en el detalle que hace que salga igual:\n",
    "`binary_cross_entropy_with_logits`. Tu `d2` era\n",
    "`(p - objetivo) / n`, y esa resta tan limpia es la derivada de la\n",
    "entropía cruzada con sigmoide. Si le hubiera puesto otra pérdida, las cuatro\n",
    "habrían dado False, y con razón."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La red entera, en cuatro líneas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora la red completa. Le copio los mismos pesos de partida para que la\n",
    "comparación sea justa, y le pido las mismas cinco fotos del capítulo\n",
    "7."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "red = nn.Sequential(nn.Linear(28, 8), nn.Tanh(), nn.Linear(8, 1)).double()\n",
    "with torch.no_grad():\n",
    "    red[0].weight.copy_(torch.tensor(W1.T)); red[0].bias.copy_(torch.tensor(b1))\n",
    "    red[2].weight.copy_(torch.tensor(W2.T)); red[2].bias.copy_(torch.tensor(b2))\n",
    "\n",
    "optimizador = torch.optim.SGD(red.parameters(), lr=0.5)\n",
    "perdida = nn.BCEWithLogitsLoss()\n",
    "\n",
    "def mide(modelo):\n",
    "    modelo.eval()\n",
    "    with torch.no_grad():\n",
    "        return (roc_auc_score(y_tr, modelo(E_tr).numpy().ravel()),\n",
    "                roc_auc_score(y_te, modelo(E_te).numpy().ravel()))\n",
    "\n",
    "marcas = (0, 500, 2000, 5000, 10000)\n",
    "for vuelta in range(max(marcas) + 1):\n",
    "    if vuelta in marcas:\n",
    "        entrena, prueba = mide(red)\n",
    "        print('  vuelta %6d  entrena %.4f  prueba %.4f  brecha %+.4f'\n",
    "              % (vuelta, entrena, prueba, entrena - prueba))\n",
    "    red.train()\n",
    "    optimizador.zero_grad()\n",
    "    perdida(red(E_tr), O_tr).backward()\n",
    "    optimizador.step()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ve a buscar la tabla del capítulo 7 y ponla al\n",
    "lado 😄\n",
    "\n",
    "Es la misma, cifra por cifra, en las quince casillas. Y no es que se parezca:\n",
    "es que es la misma cuenta con los mismos pesos, así que tenía que salir igual.\n",
    "Cuando alguien te venda una librería como si fuera inteligencia, acuérdate de\n",
    "esta tabla.\n",
    "\n",
    "Las tres líneas del medio son el bucle entero:\n",
    "\n",
    "- `optimizador.zero_grad()` borra los gradientes de la vuelta\n",
    "anterior, porque PyTorch los suma en vez de reemplazarlos.\n",
    "\n",
    "- `perdida(...).backward()` es tu retropropagación.\n",
    "\n",
    "- `optimizador.step()` es tu `W -= paso * gradiente`,\n",
    "que en el capítulo 5 escribiste a mano.\n",
    "\n",
    "Y hay dos que no hacen nada aquí y sí harán falta enseguida:\n",
    "`red.train()` y `red.eval()`. Le dicen a la red si está\n",
    "entrenando o contestando, y las capas que se comportan distinto en cada caso las\n",
    "miran. Ahora mismo esta red no tiene ninguna de esas capas, así que las dos dan\n",
    "lo mismo. En cuanto le metamos dropout, dejan de darlo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora lo que a mano no ibas a escribir"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hasta aquí PyTorch solo ha empatado. Lo que gana es lo que viene: los tres\n",
    "frenos del capítulo 8 ya están escritos, los lotes\n",
    "también, y hay optimizadores que a mano no te apetece programar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.manual_seed(0)\n",
    "red2 = nn.Sequential(nn.Linear(28, 16), nn.ReLU(), nn.Dropout(0.2),\n",
    "                     nn.Linear(16, 1)).double()\n",
    "carga = DataLoader(TensorDataset(E_tr, O_tr), batch_size=64, shuffle=True,\n",
    "                   generator=torch.Generator().manual_seed(0))\n",
    "opt = torch.optim.Adam(red2.parameters(), lr=0.01, weight_decay=1e-3)\n",
    "\n",
    "for epoca in range(1, 31):\n",
    "    red2.train()\n",
    "    for filas, etiquetas in carga:\n",
    "        opt.zero_grad()\n",
    "        perdida(red2(filas), etiquetas).backward()\n",
    "        opt.step()\n",
    "    if epoca in (1, 2, 3, 5, 10, 20, 30):\n",
    "        entrena, prueba = mide(red2)\n",
    "        print('  epoca %3d  entrena %.4f  prueba %.4f  brecha %+.4f'\n",
    "              % (epoca, entrena, prueba, entrena - prueba))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El mejor resultado del capítulo está en la primera línea 😅\n",
    "\n",
    "La época 1 da 0,7233 en prueba, que es el mejor número que ha sacado ninguna\n",
    "red en este libro sobre estos datos, y le pasa por delante al listón de la\n",
    "regresión logística, que era 0,7214. A partir de ahí solo empeora. Treinta\n",
    "épocas de entrenamiento para que la buena fuera la primera.\n",
    "\n",
    "Y antes de que lo cuentes por ahí: esa ventaja es de 19 diezmilésimas, con\n",
    "una semilla y una partición. El capítulo 5 ya dejó\n",
    "escrito que una corrida no es un resultado, y esto es exactamente eso.\n",
    "**La conclusión del libro no cambia: en tabla, la red empata con la\n",
    "logística en el mejor de los casos y cuesta cien veces más.** Lo que sí\n",
    "es nuevo aquí es que empatar le costó una época en vez de quinientas\n",
    "vueltas 🐢\n",
    "\n",
    "Eso no es un fallo de PyTorch ni de Adam. Es la misma lección del capítulo\n",
    "7 con otra ropa: **estos datos dan para lo que\n",
    "dan**, y una red con 481 números que ajustar se los aprende de memoria en\n",
    "cuanto la dejas. Lo que cambió es la velocidad a la que llega: donde el descenso\n",
    "a mano necesitaba 500 vueltas por todas las filas, Adam con lotes de 64 llegó\n",
    "más alto en una sola pasada.\n",
    "\n",
    "Los nombres nuevos, uno por uno, y todos son cosas que ya conoces:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Lo que pone | Qué es, en lo que ya sabes |\n",
    "|---|---|\n",
    "| `nn.Dropout(0.2)` | El freno 2 del capítulo 8, apagar neuronas al azar |\n",
    "| `weight_decay=1e-3` | El freno 1, encoger los pesos grandes |\n",
    "| `DataLoader` | Partir las filas en lotes y barajarlas en cada época |\n",
    "| `Adam` | Descenso de gradiente con un paso distinto para cada peso, ajustado sobre la marcha |\n",
    "| `época` | Una pasada por todas las filas, o sea 36 lotes de 64 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora sí, `red2.train()` y `red2.eval()` hacen algo:\n",
    "el dropout apaga neuronas mientras entrena y no las apaga al medir. Si se te\n",
    "olvida el `eval()`, tus números de prueba salen peores de lo que son\n",
    "y encima cambian en cada corrida."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los dos errores que salen la primera vez"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Estos dos me los sé de memoria porque los he visto en todos los cursos que he\n",
    "dado. El primero sale al mezclar precisiones."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    capa = nn.Linear(28, 8)     # nace en float32\n",
    "    capa(E_tr)                  # y mis datos son float64\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "RuntimeError: mat1 and mat2 must have the same dtype, but got Double and Float\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Double es float64 y Float es float32, que es un vocabulario de C asomando por\n",
    "debajo. PyTorch crea las capas en float32 porque es la mitad de memoria y en GPU\n",
    "va al doble de rápido, y numpy trabaja en float64 por costumbre. Se arregla\n",
    "eligiendo un lado: `.double()` a la red, que es lo que hice arriba\n",
    "para poder comparar con numpy, o `dtype=torch.float32` a los tensores,\n",
    "que es lo que harías con datos de verdad.\n",
    "\n",
    "El segundo sale al querer sacar un número de un tensor que todavía está\n",
    "enganchado al grafo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    suelto = torch.tensor([2.0], requires_grad=True)\n",
    "    (suelto * 3).numpy()\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este error es un favor. Ese tensor lleva colgando el historial entero de\n",
    "operaciones que hacen falta para el `backward()`, y si lo pasaras a\n",
    "numpy sin avisar, ese historial se quedaría vivo en memoria pegado a algo que ya\n",
    "no lo usa. Por eso te obliga a decirlo en voz alta con `detach()`, o\n",
    "a envolverlo en `with torch.no_grad():` como hice en\n",
    "`mide`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo de la GPU, que es una línea y no es magia"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo lo de este capítulo corrió en el procesador de tu computadora y tardó\n",
    "segundos. Cuando los datos crecen, se mueve a la tarjeta gráfica así:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "aparato = 'cuda' if torch.cuda.is_available() else 'cpu'\n",
    "red2.to(aparato)\n",
    "E_tr = E_tr.to(aparato)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ya. Lo importante es lo que **no** hace: una GPU no mejora tu\n",
    "modelo ni un decimal. Hace la misma cuenta más rápido, porque puede multiplicar\n",
    "matrices grandes en paralelo. Con 2.250 filas y 28 columnas no vas a notar nada,\n",
    "y muy probablemente vaya más lento, porque mover los datos a la tarjeta cuesta\n",
    "más que la cuenta que ahorras.\n",
    "\n",
    "La regla que uso: **GPU cuando el modelo no entra o el entrenamiento no\n",
    "cabe en una tarde**. Antes de eso es un gasto y una fuente de errores\n",
    "nuevos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Una analista entrena su primera red en PyTorch, copia el bucle de un tutorial y lo adapta. La pérdida empieza en 0,69, baja un poco y después se dispara hasta dar nan. Prueba con menos paso, prueba con menos neuronas y sigue igual.\n",
    "\n",
    "```\n",
    "for epoca in range(30):\n",
    "    for filas, etiquetas in carga:\n",
    "        salida = red(filas)\n",
    "        error = perdida(salida, etiquetas)\n",
    "        error.backward()\n",
    "        opt.step()\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Falta `opt.zero_grad()` al principio del lote. PyTorch **suma** los gradientes en vez de reemplazarlos, así que en el lote 10 el gradiente que se aplica es la suma de los diez anteriores, en el lote 100 la de los cien, y el paso efectivo crece sin parar hasta que los pesos se van a nan. La pista está en que empeora con el tiempo en vez de empeorar desde el principio. Y que sume no es un descuido de PyTorch: es lo que permite juntar varios lotes chicos en uno grande cuando no te cabe en memoria. Lo raro es útil, pero hay que saberlo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis. El 4 es el que hago yo antes de creerme cualquier resultado 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cuatro optimizadores con el mismo paso\n",
    "\n",
    "Entrena la misma red diez épocas con SGD, SGD con momento,\n",
    "RMSprop y Adam, todos con paso 0,01, y compara."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "def arma(ancho=16, semilla=0):\n",
    "    torch.manual_seed(semilla)\n",
    "    return nn.Sequential(nn.Linear(28, ancho), nn.ReLU(), nn.Dropout(0.2),\n",
    "                         nn.Linear(ancho, 1)).double()\n",
    "\n",
    "def entrena_lotes(modelo, opt, epocas=10, lote=64):\n",
    "    datos = DataLoader(TensorDataset(E_tr, O_tr), batch_size=lote, shuffle=True,\n",
    "                       generator=torch.Generator().manual_seed(0))\n",
    "    for _ in range(epocas):\n",
    "        modelo.train()\n",
    "        for filas, etiquetas in datos:\n",
    "            opt.zero_grad()\n",
    "            perdida(modelo(filas), etiquetas).backward()\n",
    "            opt.step()\n",
    "    return mide(modelo)\n",
    "\n",
    "for nombre, hace in [\n",
    "        ('SGD', lambda p: torch.optim.SGD(p, lr=0.01)),\n",
    "        ('SGD momento', lambda p: torch.optim.SGD(p, lr=0.01, momentum=0.9)),\n",
    "        ('RMSprop', lambda p: torch.optim.RMSprop(p, lr=0.01)),\n",
    "        ('Adam', lambda p: torch.optim.Adam(p, lr=0.01))]:\n",
    "    m = arma()\n",
    "    a, b = entrena_lotes(m, hace(m.parameters()))\n",
    "    print('%-13s entrena %.4f  prueba %.4f' % (nombre, a, b))\n",
    "SGD           entrena 0.6042  prueba 0.6136\n",
    "SGD momento   entrena 0.7159  prueba 0.7167\n",
    "RMSprop       entrena 0.7571  prueba 0.6941\n",
    "Adam          entrena 0.7587  prueba 0.7011\n",
    "Guarda esta tabla, que desmonta dos cosas a la vez 🧨\n",
    "\n",
    "La primera: el SGD pelado con paso 0,01 casi no aprende, 0,6136. No es que\n",
    "sea mal optimizador, es que ese paso le queda corto. Arriba, con paso 0,5, el\n",
    "mismo SGD llegó a 0,7150.\n",
    "\n",
    "**La segunda es la buena: el que gana en prueba es el SGD con momento,\n",
    "no Adam.** Adam memoriza más (0,7587 contra 0,7159 en entrenamiento) y\n",
    "generaliza peor. Adam es el que casi todo el mundo pone por defecto, y en\n",
    "problemas grandes suele merecerlo, pero aquí no. Medir cuesta cuatro líneas y te\n",
    "ahorra creerte una costumbre."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El ancho de la capa oculta\n",
    "\n",
    "Prueba 4, 16, 64 y 256 neuronas ocultas y saca también\n",
    "cuántos números tiene que ajustar cada red."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "for ancho in [4, 16, 64, 256]:\n",
    "    m = arma(ancho)\n",
    "    a, b = entrena_lotes(m, torch.optim.Adam(m.parameters(), lr=0.01))\n",
    "    cuantos = sum(t.numel() for t in m.parameters())\n",
    "    print('%4d neuronas  %7d parametros  entrena %.4f  prueba %.4f'\n",
    "          % (ancho, cuantos, a, b))\n",
    "   4 neuronas      121 parametros  entrena 0.7288  prueba 0.7147\n",
    "  16 neuronas      481 parametros  entrena 0.7587  prueba 0.7011\n",
    "  64 neuronas     1921 parametros  entrena 0.8018  prueba 0.6724\n",
    " 256 neuronas     7681 parametros  entrena 0.8502  prueba 0.6357\n",
    "Las cuatro filas van en la misma dirección: cuanto más grande, mejor\n",
    "entrenamiento y peor prueba, sin una sola excepción 📉\n",
    "\n",
    "La red de 121 parámetros le gana en prueba a la de 7.681 por casi ocho\n",
    "centésimas. Con 2.250 filas, 7.681 números que ajustar son demasiados, y el\n",
    "capítulo 8 explica por qué. La costumbre que te\n",
    "recomiendo: **empieza por la red más chica que se te ocurra y crece solo\n",
    "si la prueba mejora**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El tamaño del lote\n",
    "\n",
    "Entrena con lotes de 16, 64, 256 y con las 2.250 filas de\n",
    "golpe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "for lote in [16, 64, 256, 2250]:\n",
    "    m = arma()\n",
    "    a, b = entrena_lotes(m, torch.optim.Adam(m.parameters(), lr=0.01), lote=lote)\n",
    "    print('lote %5d  entrena %.4f  prueba %.4f' % (lote, a, b))\n",
    "lote    16  entrena 0.7708  prueba 0.6939\n",
    "lote    64  entrena 0.7587  prueba 0.7011\n",
    "lote   256  entrena 0.7399  prueba 0.7165\n",
    "lote  2250  entrena 0.6906  prueba 0.7099\n",
    "Al revés de lo que suele contarse 🙃\n",
    "\n",
    "Se dice que los lotes chicos regularizan porque meten ruido, y aquí el lote\n",
    "más chico es el que peor generaliza. Lo que pasa es que con lotes de 16 hay 141\n",
    "pasos por época y con lotes de 256 hay 9, así que en las mismas diez épocas la\n",
    "red del lote chico da quince veces más pasos y le da tiempo a memorizar mucho\n",
    "más. Se ve en la columna de entrenamiento: 0,7708 contra 0,7399.\n",
    "\n",
    "**El tamaño del lote no es solo el ruido: es también cuántos pasos\n",
    "das.** Cuando compares tamaños de lote, mira si estás comparando épocas o\n",
    "pasos, porque no es lo mismo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Guarda la red y vuelve a cargarla\n",
    "\n",
    "Entrena tres épocas, guarda los pesos, cárgalos en una red\n",
    "recién hecha y comprueba que contesta exactamente igual."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "import io\n",
    "\n",
    "m = arma(16)\n",
    "entrena_lotes(m, torch.optim.Adam(m.parameters(), lr=0.01), epocas=3)\n",
    "m.eval()\n",
    "with torch.no_grad():\n",
    "    antes = m(E_te).numpy().ravel()\n",
    "\n",
    "archivo = io.BytesIO()          # en tu maquina, la ruta de un .pt\n",
    "torch.save(m.state_dict(), archivo)\n",
    "archivo.seek(0)\n",
    "\n",
    "otra = nn.Sequential(nn.Linear(28, 16), nn.ReLU(), nn.Dropout(0.2),\n",
    "                     nn.Linear(16, 1)).double()\n",
    "otra.load_state_dict(torch.load(archivo, weights_only=True))\n",
    "otra.eval()\n",
    "with torch.no_grad():\n",
    "    despues = otra(E_te).numpy().ravel()\n",
    "\n",
    "print('salen identicas:', np.array_equal(antes, despues))\n",
    "print('primeras tres antes  :', np.round(antes[:3], 6))\n",
    "print('primeras tres despues:', np.round(despues[:3], 6))\n",
    "salen identicas: True\n",
    "primeras tres antes  : [0.823251 0.833399 0.897478]\n",
    "primeras tres despues: [0.823251 0.833399 0.897478]\n",
    "Idénticas, no parecidas 🔐\n",
    "\n",
    "Esto es lo que hago antes de creerme que un modelo está listo, y no por\n",
    "desconfianza: es que un modelo entrenado que no sabes volver a cargar es un\n",
    "modelo que se muere cuando se apague el cuaderno. Fíjate en el\n",
    "`otra.eval()`, que aquí no es opcional: sin él, el dropout seguiría\n",
    "apagando neuronas al azar y las dos filas no coincidirían nunca."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La matriz que PyTorch guarda al revés\n",
    "\n",
    "Mira la forma de `weight` en una capa\n",
    "`Linear` y reproduce su salida multiplicando tú."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "lineal = m[0]\n",
    "print('lineal.weight.shape:', tuple(lineal.weight.shape))\n",
    "print('columnas de entrada:', T_tr.shape[1], ' neuronas:', 16)\n",
    "\n",
    "a_mano = T_tr @ lineal.weight.detach().numpy().T + lineal.bias.detach().numpy()\n",
    "with torch.no_grad():\n",
    "    suyo = lineal(E_tr).numpy()\n",
    "print('mi multiplicacion y la suya coinciden:', np.allclose(a_mano, suyo))\n",
    "lineal.weight.shape: (16, 28)\n",
    "columnas de entrada: 28  neuronas: 16\n",
    "mi multiplicacion y la suya coinciden: True\n",
    "Tu `W1` era de (28, 8) y el de PyTorch es de (16, 28), o sea al\n",
    "revés 🔄\n",
    "\n",
    "Por eso arriba, al copiar los pesos, escribí `W1.T` y no\n",
    "`W1`. Es la fuente de confusión más tonta y más frecuente del\n",
    "principio, y el remedio es el de este ejercicio: no te fíes de la documentación,\n",
    "imprime la forma y reproduce el resultado tú."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuenta los parámetros y comprueba la cuenta\n",
    "\n",
    "Lista los pesos con nombre y compara el total contra la\n",
    "fórmula escrita a mano."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "m = arma(16)\n",
    "for nombre, t in m.named_parameters():\n",
    "    print('%-10s %-12s %6d' % (nombre, str(tuple(t.shape)), t.numel()))\n",
    "print('%-10s %-12s %6d' % ('total', '', sum(t.numel() for t in m.parameters())))\n",
    "print('a mano: 28*16 + 16 + 16*1 + 1 =', 28 * 16 + 16 + 16 * 1 + 1)\n",
    "0.weight   (16, 28)        448\n",
    "0.bias     (16,)            16\n",
    "3.weight   (1, 16)          16\n",
    "3.bias     (1,)              1\n",
    "total                      481\n",
    "a mano: 28*16 + 16 + 16*1 + 1 = 481\n",
    "481 y 481 🎯\n",
    "\n",
    "Los índices 0 y 3 son las posiciones dentro del `Sequential`: la 1\n",
    "es la ReLU y la 2 el Dropout, y ninguna de las dos tiene nada que aprender.\n",
    "Saber contar parámetros a ojo es de las cosas que más rápido te hacen entender\n",
    "un modelo ajeno, y es una multiplicación y una suma por capa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Entrenas una red en PyTorch, la pérdida baja bien pero al medir en prueba los números salen distintos en cada corrida. ¿Qué miras primero?\n",
    "\n",
    "a) Si al medir llamaste a red.eval() antes\n",
    "\n",
    "b) Si el DataLoader está barajando las filas\n",
    "\n",
    "c) Si te falta fijar la semilla del optimizador\n",
    "\n",
    "d) Si el paso es demasiado grande\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Barajar al entrenar es lo correcto y no toca la medición, que se hace sobre el conjunto de prueba entero y de una vez.\n",
    "\n",
    "*c)* La semilla afecta a cómo empieza la red, así que cambiaría el resultado entre entrenamientos distintos. Pero aquí el modelo ya está entrenado y los números cambian al medir, que es otro momento.\n",
    "\n",
    "*d)* Un paso grande hace que la pérdida no baje o se dispare, y aquí la pérdida baja bien. El problema aparece después de entrenar.\n",
    "\n",
    "Sin eval() el dropout sigue apagando neuronas al azar mientras contestas, así que cada corrida apaga otras y sale otro número."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🔦 `backward()` es el capítulo de retropropagación entero, y con\n",
    "los mismos pesos saca los mismos gradientes que sacaste tú.\n",
    "\n",
    "- 📋 La red en PyTorch reprodujo la tabla del capítulo\n",
    "7 casilla por casilla, las quince.\n",
    "\n",
    "- 🔁 El bucle son tres líneas: `zero_grad()`,\n",
    "`backward()` y `step()`. Si te falta la primera, los\n",
    "gradientes se suman y acabas en nan.\n",
    "\n",
    "- 🏁 Con Adam y lotes de 64, el mejor resultado fue la época 1 con 0,7233. Más\n",
    "entrenamiento solo empeoró.\n",
    "\n",
    "- ⚖️ En los ejercicios, el SGD con momento le gana a Adam en prueba, y la red\n",
    "de 121 parámetros le gana a la de 7.681.\n",
    "\n",
    "- 🖥️ La GPU hace lo mismo más rápido. No mejora el modelo ni un decimal.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una librería no sabe más que tú. Escribe más rápido lo que tú ya entiendes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si quieres ver el mismo salto en el otro sentido, en el\n",
    "[libro de machine learning desde\n",
    "cero](https://missyera.com/guias/machine-learning-desde-cero/) está scikit-learn desde el primer capítulo, y ahí el pipeline hace el\n",
    "mismo papel que aquí hace `nn.Sequential` 🧰. El vocabulario suelto\n",
    "está definido en el [glosario de IA](https://missyera.com/glosario-ia/), y si el código\n",
    "de este capítulo te costó más que la idea, eso es de Python y se arregla en el\n",
    "[libro de Python desde cero](https://missyera.com/guias/python-desde-cero/) 🐍\n",
    "\n",
    "En el capítulo 10 viene la pieza que hace posible\n",
    "entrenar redes profundas, y ahora ya sabes qué es ese `eval()` del\n",
    "que va a hablar.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 9 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/pytorch/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
