{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Entrenar una red\n",
    "\n",
    "Más neuronas, más vueltas y más paso: los tres botones que todo el mundo sube, medidos uno por uno. Los tres la empeoran.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 7 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/entrenar-una-red/\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": [
    "Ya tenemos todas las piezas: la neurona, la activación, las capas, el\n",
    "gradiente y la retropropagación. Toca montarlas bien y soltarlas sobre las\n",
    "ventas de la distribuidora 🚀\n",
    "\n",
    "Y el capítulo va a consistir, básicamente, en subir botones y ver cómo todo\n",
    "empeora. Que suena raro y es lo más útil que te puedo enseñar aquí.\n",
    "\n",
    "Y te pregunto de entrada: **¿qué harías tú si tu modelo acertara el 100% en entrenamiento?** Guárdate la respuesta, que este capítulo va de eso 🎛️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\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",
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\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",
    "\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",
    "print('entreno con', T_tr.shape[0], 'filas y', T_tr.shape[1], 'columnas')\n",
    "print('el listón a batir:', round(roc_auc_score(\n",
    "    y_te, logistica.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Uso el preprocesamiento de scikit-learn a propósito, porque ya lo tenemos\n",
    "resuelto del libro anterior y lo que interesa aquí es la red. De ahí para\n",
    "adelante, todo es nuestro 🔧"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La red, con los dos números que hay que mirar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#parámetros=∑l(nl−1nl+nl)\n",
    "\n",
    "cada capa aporta un peso por cada par de neuronas conectadas más un sesgo por neurona, que es de dónde salen los millones de los que todo el mundo habla"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def entrena(ocultas=8, paso=0.5, semilla=0, marcas=(0, 500, 2000, 5000, 10000)):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    columnas = T_tr.shape[1]\n",
    "    W1 = r.normal(0, 0.1, (columnas, ocultas)); b1 = np.zeros(ocultas)\n",
    "    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)\n",
    "\n",
    "    for i in range(max(marcas) + 1):\n",
    "        if i in marcas:\n",
    "            en_train = roc_auc_score(y_tr, sigmoide(np.tanh(T_tr @ W1 + b1) @ W2 + b2).ravel())\n",
    "            en_prueba = roc_auc_score(y_te, sigmoide(np.tanh(T_te @ W1 + b1) @ W2 + b2).ravel())\n",
    "            print(f'  vuelta {i:6d}  entrena {en_train:.4f}  prueba {en_prueba:.4f}  '\n",
    "                  f'brecha {en_train - en_prueba:+.4f}')\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",
    "        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (T_tr.T @ d1); b1 -= paso * d1.sum(axis=0)\n",
    "\n",
    "print('8 neuronas ocultas, paso 0,5')\n",
    "entrena()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo de arriba abajo despacio, porque esta tabla es el capítulo 😨\n",
    "\n",
    "En la vuelta 500 la red va bien: 0,7343 en entrenamiento y **0,7150 en\n",
    "prueba**, con una brecha de dos centésimas.\n",
    "\n",
    "En la vuelta 2.000 el entrenamiento ha subido a 0,7948 y la prueba\n",
    "**ha bajado** a 0,6651. En la 10.000, entrenamiento 0,8328 y prueba\n",
    "0,6174.\n",
    "\n",
    "O sea que a partir de la vuelta 500, cada vuelta que le das **la\n",
    "empeora**. No es que deje de mejorar: es que empeora activamente mientras\n",
    "el número que estabas mirando sube.\n",
    "\n",
    "Eso es sobreajuste, y en el capítulo 6 ya lo habíamos visto en su forma más\n",
    "pura, con etiquetas de moneda. Aquí, con datos de verdad, tiene la misma\n",
    "pinta 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El botón de más neuronas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('32 neuronas ocultas, paso 0,5')\n",
    "entrena(ocultas=32)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 32 neuronas la red llega a **1,0000 de AUC en entrenamiento**\n",
    "en la vuelta 10.000. Perfecto, sin un solo error. Y su prueba en ese momento es\n",
    "0,5870, o sea casi tirar una moneda.\n",
    "\n",
    "Ahí tienes la memorización en su forma más literal: se aprendió las 2.250\n",
    "filas una por una. La brecha llega a 0,4130 😱\n",
    "\n",
    "Y fíjate en algo importante de las dos tablas: en la vuelta 500 las dos redes\n",
    "van casi igual (0,7150 y 0,7165). **La red grande no aprendió más\n",
    "rápido**, solo se estropeó más rápido."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los tres botones, medidos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a subirlos uno por uno y quedarnos solo con la prueba:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def solo_prueba(ocultas=8, paso=0.5, vueltas=2000, semilla=0):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    columnas = T_tr.shape[1]\n",
    "    W1 = r.normal(0, 0.1, (columnas, ocultas)); b1 = np.zeros(ocultas)\n",
    "    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)\n",
    "    for _ in range(vueltas):\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",
    "        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (T_tr.T @ d1); b1 -= paso * d1.sum(axis=0)\n",
    "    return roc_auc_score(y_te, sigmoide(np.tanh(T_te @ W1 + b1) @ W2 + b2).ravel())\n",
    "\n",
    "print('subiendo neuronas:')\n",
    "for ocultas in [4, 8, 16, 32]:\n",
    "    print(f'  {ocultas:3d} neuronas: {solo_prueba(ocultas=ocultas):.4f}')\n",
    "print('subiendo el paso:')\n",
    "for paso in [0.1, 0.5, 2.0]:\n",
    "    print(f'  paso {paso:<4}: {solo_prueba(paso=paso):.4f}')\n",
    "print('subiendo vueltas:')\n",
    "for vueltas in [2000, 5000, 10000]:\n",
    "    print(f'  {vueltas:6d} vueltas: {solo_prueba(vueltas=vueltas):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los tres botones, y los tres van en la misma dirección: **subirlos\n",
    "empeora** 🎚️\n",
    "\n",
    "Más neuronas: de 0,6934 con cuatro a 0,6210 con treinta y dos. Más paso: de\n",
    "0,7183 con 0,1 a 0,6422 con 2,0. Más vueltas: de 0,6651 con dos mil a 0,6174 con\n",
    "diez mil.\n",
    "\n",
    "Y mira dónde está el mejor de toda la tabla: **paso 0,1, que es el\n",
    "ajuste más conservador de todos**, con 0,7183. Casi el 0,7214 de la\n",
    "logística.\n",
    "\n",
    "Esto va justo en contra de la intuición que vende el marketing de la IA, donde\n",
    "más grande siempre es mejor. Con 2.250 filas de datos tabulares, cada aumento de\n",
    "capacidad es capacidad para memorizar 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Parar a tiempo, y hacerlo bien"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si en la vuelta 500 la red iba bien y luego se estropea, la solución es\n",
    "evidente: **parar en la 500**.\n",
    "\n",
    "Y aquí viene la trampa que arruina esto en la mitad de los proyectos que veo:\n",
    "si eliges dónde parar mirando la prueba, la prueba deja de ser prueba. Es la fuga\n",
    "del capítulo 6 del libro de machine learning, con otro disfraz.\n",
    "\n",
    "Se hace con un tercer corte, la **validación**, que sale de los\n",
    "datos de entrenamiento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_ent, X_val, y_ent, y_val = train_test_split(X_tr, y_tr, test_size=0.25,\n",
    "                                              random_state=42, stratify=y_tr)\n",
    "pre = logistica.named_steps['pre']\n",
    "E = pre.transform(X_ent)\n",
    "V = pre.transform(X_val)\n",
    "P = pre.transform(X_te)\n",
    "obj_ent = y_ent.values.astype(float).reshape(-1, 1)\n",
    "\n",
    "def con_parada(ocultas=8, paso=0.5, paciencia=20, maximo=20000, semilla=0):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    W1 = r.normal(0, 0.1, (E.shape[1], ocultas)); b1 = np.zeros(ocultas)\n",
    "    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)\n",
    "    mejor, guardados, espera, cuando = -1, None, 0, 0\n",
    "\n",
    "    for i in range(maximo + 1):\n",
    "        if i % 100 == 0:\n",
    "            auc = roc_auc_score(y_val, sigmoide(np.tanh(V @ W1 + b1) @ W2 + b2).ravel())\n",
    "            if auc > mejor:\n",
    "                mejor, espera, cuando = auc, 0, i\n",
    "                guardados = (W1.copy(), b1.copy(), W2.copy(), b2.copy())\n",
    "            else:\n",
    "                espera += 1\n",
    "                if espera >= paciencia:\n",
    "                    break\n",
    "        h = np.tanh(E @ W1 + b1)\n",
    "        p = sigmoide(h @ W2 + b2)\n",
    "        d2 = (p - obj_ent) / len(obj_ent)\n",
    "        d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (E.T @ d1); b1 -= paso * d1.sum(axis=0)\n",
    "\n",
    "    W1, b1, W2, b2 = guardados\n",
    "    return cuando, mejor, roc_auc_score(y_te, sigmoide(np.tanh(P @ W1 + b1) @ W2 + b2).ravel())\n",
    "\n",
    "for ocultas in [4, 8, 16, 32]:\n",
    "    cuando, val, prueba = con_parada(ocultas=ocultas)\n",
    "    print(f'{ocultas:3d} neuronas: paró en la vuelta {cuando:5d}  '\n",
    "          f'validación {val:.4f}  prueba {prueba:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí está el final del capítulo, que a mí me parece precioso 💛\n",
    "\n",
    "Los cuatro tamaños paran entre la vuelta 200 y la 300, de 20.000 posibles. Y\n",
    "los cuatro sacan entre 0,7172 y 0,7205, o sea **prácticamente lo mismo**.\n",
    "\n",
    "Con parada temprana, el tamaño de la red dejó de importar. Cuatro neuronas o\n",
    "treinta y dos, da igual: todas encuentran lo mismo, porque lo mismo es lo único\n",
    "que hay en estos datos.\n",
    "\n",
    "Y ese \"lo mismo\" es la solución lineal. Comparado con el 0,7217 de la\n",
    "logística sobre el mismo corte, la red no aporta nada y tampoco pierde nada,\n",
    "siempre que la pares a tiempo 🛑\n",
    "\n",
    "La `paciencia` de esa función es lo que hace `MLPClassifier`\n",
    "con `early_stopping=True`, que en el capítulo 1 subió una red de\n",
    "0,5834 a 0,7018. Ahora ya sabes exactamente qué hace por dentro."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Guardar el mejor y no el último\n",
    "\n",
    "Compara parar y quedarte con los pesos del momento, contra\n",
    "volver a los mejores que viste."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sin_guardar(ocultas=8, paso=0.5, paciencia=20, maximo=20000, semilla=0):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    W1 = r.normal(0, 0.1, (E.shape[1], ocultas)); b1 = np.zeros(ocultas)\n",
    "    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)\n",
    "    mejor, espera = -1, 0\n",
    "    for i in range(maximo + 1):\n",
    "        if i % 100 == 0:\n",
    "            auc = roc_auc_score(y_val, sigmoide(np.tanh(V @ W1 + b1) @ W2 + b2).ravel())\n",
    "            if auc > mejor:\n",
    "                mejor, espera = auc, 0\n",
    "            else:\n",
    "                espera += 1\n",
    "                if espera >= paciencia:\n",
    "                    break\n",
    "        h = np.tanh(E @ W1 + b1)\n",
    "        p = sigmoide(h @ W2 + b2)\n",
    "        d2 = (p - obj_ent) / len(obj_ent)\n",
    "        d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (E.T @ d1); b1 -= paso * d1.sum(axis=0)\n",
    "    return roc_auc_score(y_te, sigmoide(np.tanh(P @ W1 + b1) @ W2 + b2).ravel())\n",
    "\n",
    "print('guardando el mejor :', round(con_parada()[2], 4))\n",
    "print('quedándote el último:', round(sin_guardar(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "guardando el mejor : 0.7176\n",
    "quedándote el último: 0.6597\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,7176 guardando el mejor contra 0,6597 quedándote el último. **Casi\n",
    "seis centésimas** por una línea de código.\n",
    "\n",
    "Porque con paciencia 20 sigues entrenando 2.000 vueltas *después* del\n",
    "mejor momento, y en esas 2.000 vueltas la red se sigue estropeando. La parada\n",
    "temprana sin guardar los pesos está a medio hacer 💾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuánta paciencia conviene\n",
    "\n",
    "Prueba varias y mira dónde para y qué saca."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for paciencia in [3, 10, 20, 50]:\n",
    "    cuando, val, prueba = con_parada(paciencia=paciencia)\n",
    "    print(f'paciencia {paciencia:3d}: paró en {cuando:5d}  '\n",
    "          f'validación {val:.4f}  prueba {prueba:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "paciencia   3: paró en   300  validación 0.7061  prueba 0.7176\n",
    "paciencia  10: paró en   300  validación 0.7061  prueba 0.7176\n",
    "paciencia  20: paró en   300  validación 0.7061  prueba 0.7176\n",
    "paciencia  50: paró en   300  validación 0.7061  prueba 0.7176\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las cuatro paran exactamente en la vuelta 300 y sacan exactamente 0,7176. Da\n",
    "igual cuál pongas, y eso es buena señal.\n",
    "\n",
    "La paciencia importa cuando la curva de validación es irregular y sube y baja:\n",
    "con poca paciencia paras en el primer bache y te pierdes un pico posterior. Aquí\n",
    "la curva baja limpio desde el máximo, así que cualquier valor la caza 🧘‍♀️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El mismo corte para la logística\n",
    "\n",
    "Compara la red con parada contra la logística entrenada\n",
    "solo con la parte de entrenamiento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "log_corta = 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_ent, y_ent)\n",
    "\n",
    "print('red con parada  :', round(con_parada(ocultas=32)[2], 4))\n",
    "print('logística       :', round(roc_auc_score(\n",
    "    y_te, log_corta.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "red con parada  : 0.7205\n",
    "logística       : 0.7217\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,7205 la red y 0,7217 la logística, con las dos entrenadas sobre las mismas\n",
    "1.687 filas. Doce diezmilésimas de diferencia.\n",
    "\n",
    "Y la logística tarda dos décimas de segundo, se explica leyendo sus 28\n",
    "coeficientes y no tiene ni un botón que ajustar. **Empatar no es\n",
    "ganar** 🏁"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Diez semillas, para saber si el empate es real\n",
    "\n",
    "Repite con arranques distintos y mira cuánto baila."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "resultados = [con_parada(ocultas=8, semilla=s)[2] for s in range(10)]\n",
    "print('mejor :', round(max(resultados), 4))\n",
    "print('peor  :', round(min(resultados), 4))\n",
    "print('media :', round(float(np.mean(resultados)), 4))\n",
    "print('logística:', round(roc_auc_score(y_te, log_corta.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "mejor : 0.7203\n",
    "peor  : 0.7064\n",
    "media : 0.7159\n",
    "logística: 0.7217\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Diez arranques distintos y la media queda en 0,7159, con el mejor en 0,7203 y\n",
    "el peor en 0,7064.\n",
    "\n",
    "O sea que **ni con suerte pasa a la logística**: su mejor\n",
    "arranque de diez se queda catorce diezmilésimas por debajo del 0,7217.\n",
    "\n",
    "Es la lección del capítulo 10 del libro de machine learning aplicada aquí. Si\n",
    "me hubiera quedado con el mejor de los diez y lo hubiera reportado como \"la red\n",
    "saca 0,7203\", habría contado una verdad engañosa: **un número suelto no\n",
    "dice nada, hay que ver el rango** 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La curva de validación, vuelta a vuelta\n",
    "\n",
    "Imprime la validación cada 100 vueltas para ver la forma\n",
    "que tiene."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = np.random.default_rng(0)\n",
    "W1 = r.normal(0, 0.1, (E.shape[1], 8)); b1 = np.zeros(8)\n",
    "W2 = r.normal(0, 0.1, (8, 1)); b2 = np.zeros(1)\n",
    "for i in range(1501):\n",
    "    if i % 300 == 0:\n",
    "        v = roc_auc_score(y_val, sigmoide(np.tanh(V @ W1 + b1) @ W2 + b2).ravel())\n",
    "        e = roc_auc_score(y_ent, sigmoide(np.tanh(E @ W1 + b1) @ W2 + b2).ravel())\n",
    "        print(f'vuelta {i:5d}  entrena {e:.4f}  validación {v:.4f}')\n",
    "    h = np.tanh(E @ W1 + b1)\n",
    "    p = sigmoide(h @ W2 + b2)\n",
    "    d2 = (p - obj_ent) / len(obj_ent)\n",
    "    d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "    W2 -= 0.5 * (h.T @ d2); b2 -= 0.5 * d2.sum(axis=0)\n",
    "    W1 -= 0.5 * (E.T @ d1); b1 -= 0.5 * d1.sum(axis=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "vuelta     0  entrena 0.4041  validación 0.3888\n",
    "vuelta   300  entrena 0.7280  validación 0.7061\n",
    "vuelta   600  entrena 0.7497  validación 0.6997\n",
    "vuelta   900  entrena 0.7680  validación 0.6930\n",
    "vuelta  1200  entrena 0.7845  validación 0.6801\n",
    "vuelta  1500  entrena 0.7991  validación 0.6685\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La validación sube hasta la vuelta 300 y a partir de ahí baja, mientras el\n",
    "entrenamiento sigue subiendo tan contento.\n",
    "\n",
    "Esa forma de U al revés es la más importante de todo el deep learning\n",
    "práctico. **El punto más alto de la curva de validación es dónde hay que\n",
    "parar**, y todo lo que hay a la derecha es tiempo de cómputo gastado en\n",
    "empeorar el modelo 📈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuántos pesos contra cuántas filas\n",
    "\n",
    "Pon los dos números al lado para cada tamaño."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for ocultas in [4, 8, 16, 32]:\n",
    "    pesos = E.shape[1] * ocultas + ocultas + ocultas + 1\n",
    "    print(f'{ocultas:3d} neuronas: {pesos:4d} pesos para {len(y_ent)} filas  '\n",
    "          f'({pesos / len(y_ent):.3f} por fila)')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "  4 neuronas:  121 pesos para 1687 filas  (0.072 por fila)\n",
    "  8 neuronas:  241 pesos para 1687 filas  (0.143 por fila)\n",
    " 16 neuronas:  481 pesos para 1687 filas  (0.285 por fila)\n",
    " 32 neuronas:  961 pesos para 1687 filas  (0.570 por fila)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 32 neuronas hay 961 pesos para 1.687 filas, o sea 0,570 pesos por fila.\n",
    "Todavía no llega a uno por fila, y aun así memoriza hasta el 1,0000.\n",
    "\n",
    "Eso desmonta la regla de oro que se repite mucho, la de \"necesitas diez veces\n",
    "más filas que parámetros\". No es falsa del todo, pero es muchísimo más optimista\n",
    "de lo que la realidad aguanta 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Entrenar sin mirar nada\n",
    "\n",
    "Corre la red 30.000 vueltas sin validación y mira el\n",
    "desastre."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('10.000 vueltas a ciegas, 32 neuronas:', round(\n",
    "    solo_prueba(ocultas=32, vueltas=10000), 4))\n",
    "print('con parada temprana                 :', round(con_parada(ocultas=32)[2], 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "10.000 vueltas a ciegas, 32 neuronas: 0.587\n",
    "con parada temprana                 : 0.7205\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,5870 a ciegas contra 0,7205 con parada. Trece puntos de AUC entre correr\n",
    "el bucle y vigilarlo.\n",
    "\n",
    "Y el que corrió a ciegas hizo diez mil vueltas donde el otro hizo doscientas,\n",
    "o sea que *tardó cincuenta veces más*. Es de los pocos casos donde hacerlo\n",
    "bien es además más rápido 🏃‍♀️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. El error de validar con lo que entrenaste\n",
    "\n",
    "Pásale a `con_parada` el conjunto de\n",
    "entrenamiento como validación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "V_mal = E\n",
    "roc_auc_score(y_val, sigmoide(np.tanh(V_mal @ W1 + b1) @ W2 + b2).ravel())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: Found input variables with inconsistent numbers of samples: [563, 1687]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Revienta porque `y_val` tiene 563 filas y `E` tiene\n",
    "1.687. Los tamaños no cuadran y scikit-learn lo dice claro.\n",
    "\n",
    "Menos mal, porque si hubieran cuadrado (que pasa si te confundes de variable\n",
    "entre dos cortes del mismo tamaño) no habría dado error: la validación sobre\n",
    "datos de entrenamiento **nunca baja**, así que la parada temprana no\n",
    "pararía jamás y volverías a las 20.000 vueltas 😬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "La curva de entrenamiento sigue subiendo y la de validación empezó a bajar. ¿Qué haces?\n",
    "\n",
    "a) Paro ahí y me quedo con los pesos de antes de que empezara a bajar\n",
    "\n",
    "b) Sigo, porque el entrenamiento todavía mejora\n",
    "\n",
    "c) Añado más neuronas\n",
    "\n",
    "d) Bajo la tasa de aprendizaje\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Lo que mejora es la memoria del modelo sobre los datos que ya vio.\n",
    "\n",
    "*c)* Más neuronas van a memorizar más rápido, que es justo lo que ya está pasando.\n",
    "\n",
    "*d)* Ralentiza el problema, no lo quita.\n",
    "\n",
    "Ese punto donde las dos curvas se separan es el momento de parar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La parada temprana que mira donde no debe"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Entrenas vigilando las dos notas como manda el capítulo, guardas la mejor que viste en prueba y esa es la que reportas. Suena a parada temprana.\n",
    "\n",
    "```\n",
    "mejor = 0\n",
    "for vuelta in range(10000):\n",
    "    paso()\n",
    "    auc = evalua(X_prueba, y_prueba)\n",
    "    mejor = max(mejor, auc)\n",
    "\n",
    "print('el mejor AUC fue', mejor)\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Miraste la prueba diez mil veces y te quedaste con el máximo, así que ese número ya no estima nada: es **el mejor de diez mil intentos sobre esas filas concretas** 🎣 Aunque la red no aprendiera nada, el máximo de diez mil mediciones ruidosas sale alto solo por asomarse tantas veces.\n",
    "\n",
    "La parada temprana bien hecha necesita **tres trozos**: uno para entrenar, otro para decidir cuándo parar y un tercero que no se abre hasta el final. Si solo tienes dos, el que usas para parar deja de servir para reportar.\n",
    "\n",
    "Y hay una señal que lo delata sin mirar el código: si tu mejor AUC aparece en una vuelta suelta y las de al lado están mucho más abajo, no encontraste un buen momento. Encontraste un pico de ruido."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 👀 Se entrena vigilando dos números: cómo va en lo que ve y cómo va en lo que\n",
    "no.\n",
    "\n",
    "- 📉 A partir de la vuelta 500 esta red empeora activamente mientras el número\n",
    "que estabas mirando sube.\n",
    "\n",
    "- 😱 Con 32 neuronas llega a 1,0000 de AUC en entrenamiento y 0,5870 en\n",
    "prueba.\n",
    "\n",
    "- 🎚️ Los tres botones (neuronas, paso, vueltas) empeoran al subirlos. El mejor\n",
    "resultado sale del ajuste más conservador.\n",
    "\n",
    "- 🛑 Con parada temprana los cuatro tamaños paran entre la vuelta 200 y la 300\n",
    "y sacan lo mismo: el tamaño deja de importar.\n",
    "\n",
    "- 💾 Hay que guardar los mejores pesos, no quedarse los últimos: 0,7176 contra\n",
    "0,6597.\n",
    "\n",
    "- 🏁 Y aun bien parada, la red empata con la logística (0,7205 contra 0,7217),\n",
    "que tarda dos décimas de segundo y se lee entera.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El número que miras mientras entrenas y el número que puedes prometer nunca son el mismo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo de los tres trozos y la parada temprana bien hecha es la misma disciplina del [libro de machine learning desde cero](https://missyera.com/guias/machine-learning-desde-cero/) 🎯\n",
    "\n",
    "En el capítulo 8 nos dedicamos entero a lo que aquí salió por todas partes: el\n",
    "sobreajuste, y las tres formas de frenarlo.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 7 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/entrenar-una-red/\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
}
