{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Frenar el sobreajuste\n",
    "\n",
    "Penalizar los pesos, apagar neuronas al azar y ensuciar las entradas. Uno funciona, uno ayuda a medias y uno casi no.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 8 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/frenar-el-sobreajuste/\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": [
    "En el capítulo 7 la red memorizó las 2.250 filas y su prueba se hundió. Aquí\n",
    "vamos a impedírselo, con las tres herramientas que se usan de verdad 🛑\n",
    "\n",
    "Y te pregunto antes de empezar: **¿cuál de los tres frenos crees que funciona mejor?** Apuesta ahora, porque la respuesta medida no es la que yo esperaba 🎯\n",
    "\n",
    "Y las vamos a escribir nosotras, que son tres o cuatro líneas cada una."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida, con el problema puesto a propósito"
   ]
  },
  {
   "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_tr, X_te, y_tr, y_te = train_test_split(\n",
    "    datos[NUMERICAS + CATEGORICAS], datos['compro'],\n",
    "    test_size=0.25, random_state=42, stratify=datos['compro'])\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",
    "print('el listón:', round(roc_auc_score(y_te, logistica.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los tres frenos, en la misma función"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en lo poco que ocupa cada uno. Están señalados con comentarios:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def corre(ocultas=32, vueltas=2000, paso=0.5, semilla=0,\n",
    "          decay=0.0, dropout=0.0, ruido=0.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",
    "    n = len(objetivo)\n",
    "\n",
    "    for _ in range(vueltas):\n",
    "        # FRENO 3: ensuciar las entradas\n",
    "        A = T_tr + r.normal(0, ruido, T_tr.shape) if ruido else T_tr\n",
    "\n",
    "        h = np.tanh(A @ W1 + b1)\n",
    "\n",
    "        # FRENO 2: apagar neuronas al azar\n",
    "        if dropout:\n",
    "            mascara = (r.random(h.shape) > dropout) / (1 - dropout)\n",
    "            h = h * mascara\n",
    "\n",
    "        p = sigmoide(h @ W2 + b2)\n",
    "        d2 = (p - objetivo) / n\n",
    "        d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "        if dropout:\n",
    "            d1 = d1 * mascara          # la culpa no pasa por las apagadas\n",
    "\n",
    "        # FRENO 1: encoger los pesos un poquito en cada vuelta\n",
    "        W2 -= paso * (h.T @ d2 + decay * W2); b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (A.T @ d1 + decay * W1); b1 -= paso * d1.sum(axis=0)\n",
    "\n",
    "    entrena = roc_auc_score(y_tr, sigmoide(np.tanh(T_tr @ W1 + b1) @ W2 + b2).ravel())\n",
    "    prueba = roc_auc_score(y_te, sigmoide(np.tanh(T_te @ W1 + b1) @ W2 + b2).ravel())\n",
    "    return entrena, prueba\n",
    "\n",
    "print(f\"{'freno':26} {'entrena':>8} {'prueba':>8} {'brecha':>8}\")\n",
    "for nombre, ajustes in [\n",
    "    ('nada', {}),\n",
    "    ('decay 0,001', dict(decay=0.001)),\n",
    "    ('decay 0,01', dict(decay=0.01)),\n",
    "    ('decay 0,1', dict(decay=0.1)),\n",
    "    ('dropout 0,2', dict(dropout=0.2)),\n",
    "    ('dropout 0,5', dict(dropout=0.5)),\n",
    "    ('ruido 0,5', dict(ruido=0.5)),\n",
    "]:\n",
    "    e, p = corre(**ajustes)\n",
    "    print(f'{nombre:26} {e:8.4f} {p:8.4f} {e - p:+8.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa tabla es el capítulo entero, así que vamos freno por freno 👇"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Freno 1: encoger los pesos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ltotal=L+λ2∑jwj2\n",
    "\n",
    "le sumas al error un castigo por tener pesos grandes, que es la forma más barata de que la red deje de memorizar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es una sola línea: `+ decay * W` dentro de la corrección. En cada\n",
    "vuelta, además de corregir el error, se le resta a cada peso una fracción de sí\n",
    "mismo.\n",
    "\n",
    "Lo que hace es **castigar los pesos grandes**, y los pesos\n",
    "grandes son justamente los que memorizan: para clavar una fila concreta hace\n",
    "falta un peso enorme en la columna que la distingue.\n",
    "\n",
    "Y funciona clarísimo. Sin nada, entrena 0,8847 y prueba 0,6210, con una brecha\n",
    "de +0,2636. Con `decay 0,01`, entrena 0,7194 y prueba\n",
    "**0,7207**, con una brecha de -0,0013 🎯\n",
    "\n",
    "Brecha negativa significa que le va *mejor* en la prueba que en el\n",
    "entrenamiento, o sea que ya no memoriza nada y lo poco que aprendió es real.\n",
    "\n",
    "Fíjate también en la dosis. Con 0,001 apenas hace nada (brecha +0,1881) y con\n",
    "0,1 empieza a apretar de más: entrena 0,7072, que ya es menos de lo que la\n",
    "logística consigue. Hay una ventana y hay que buscarla.\n",
    "\n",
    "Este freno es exactamente el Ridge del libro de machine learning, el mismo\n",
    "castigo a los pesos al cuadrado. En redes se le llama weight decay y en\n",
    "scikit-learn es el `alpha` de `MLPClassifier`, que en el\n",
    "capítulo 1 subió una red de 0,6584 a 0,7072 ⚖️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Freno 2: apagar neuronas al azar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "a~j=mj1−paj,mj~Bernoulli(1−p)\n",
    "\n",
    "apagas neuronas al azar durante el entrenamiento y escalas las que quedan, para que la suma siga valiendo lo mismo cuando en producción no apagas ninguna"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El dropout es más raro y más bonito. En cada vuelta, **apagas una parte\n",
    "de las neuronas al azar** y entrenas sin ellas.\n",
    "\n",
    "La idea es que ninguna neurona pueda confiarse: como en cualquier momento sus\n",
    "compañeras pueden desaparecer, todas tienen que aprender algo útil por su cuenta\n",
    "en vez de repartirse una regla frágil entre varias.\n",
    "\n",
    "La división por `(1 - dropout)` de la máscara es para que la suma\n",
    "total siga valiendo lo mismo aunque falte gente, y así al predecir con todas\n",
    "encendidas no haga falta ajustar nada.\n",
    "\n",
    "Con 0,2 la brecha baja de +0,2636 a +0,0889 y la prueba sube a 0,7032. Ayuda,\n",
    "y se queda por debajo del weight decay.\n",
    "\n",
    "Y con 0,5 pasa algo que hay que ver: entrena 0,4964 y prueba 0,5185.\n",
    "**El modelo se destruyó**, está por debajo del azar en\n",
    "entrenamiento 💀\n",
    "\n",
    "La razón es el tamaño. Apagar la mitad de 32 neuronas deja 16, y con tan\n",
    "pocas, la red no llega a formar nada estable entre tanto apagón. El dropout de\n",
    "0,5 es la receta estándar y está pensada para capas de cientos o miles de\n",
    "neuronas, no para esta.\n",
    "\n",
    "Es un buen ejemplo de por qué las recetas copiadas fallan: **0,5 no es\n",
    "un número mágico, es un número que funciona en redes grandes** 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Freno 3: ensuciar las entradas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El tercero es el más simple de entender: en cada vuelta le sumas ruido\n",
    "aleatorio a los datos, para que la red nunca vea dos veces exactamente la misma\n",
    "fila.\n",
    "\n",
    "Con ruido 0,5, que sobre columnas escaladas es muchísimo (media desviación\n",
    "típica), la brecha baja de +0,2636 a +0,1100 y la prueba sube a 0,6774.\n",
    "\n",
    "O sea que ayuda menos que los otros dos y hace falta bastante cantidad. En\n",
    "datos tabulares no suele valer la pena.\n",
    "\n",
    "Donde sí es la herramienta principal es en imágenes, y ahí tiene otro nombre:\n",
    "**aumento de datos**. Rotar la foto un poco, recortarla, cambiarle\n",
    "el brillo. Cada versión es una fila nueva que el modelo no puede memorizar, y en\n",
    "el capítulo 11 se entiende mejor por qué funciona tanto ahí y tan poco aquí 🖼️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que dice la tabla en conjunto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El mejor resultado de todo el capítulo es 0,7207, con weight decay 0,01. Y el\n",
    "listón de la logística es 0,7214.\n",
    "\n",
    "Entonces, otra vez lo mismo: **el freno que funciona es el que devuelve\n",
    "la red a la solución lineal**. No la mejora, la rescata 🙃\n",
    "\n",
    "Y eso no es un fracaso del capítulo. Es lo que necesitas saber hacer el día\n",
    "que trabajes con datos donde la red sí tenga algo que aportar, porque ahí la\n",
    "diferencia entre una red con freno y una sin freno son treinta puntos de AUC."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Buscar la dosis de weight decay\n",
    "\n",
    "Barre varios valores y encuentra la ventana."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for decay in [0.0, 0.003, 0.01, 0.03, 0.1, 0.3]:\n",
    "    e, p = corre(decay=decay)\n",
    "    print(f'decay {decay:<6} entrena {e:.4f}  prueba {p:.4f}  brecha {e - p:+.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "decay 0.0    entrena 0.8847  prueba 0.6210  brecha +0.2636\n",
    "decay 0.003  entrena 0.7502  prueba 0.7122  brecha +0.0380\n",
    "decay 0.01   entrena 0.7194  prueba 0.7207  brecha -0.0013\n",
    "decay 0.03   entrena 0.7146  prueba 0.7179  brecha -0.0032\n",
    "decay 0.1    entrena 0.7072  prueba 0.7147  brecha -0.0075\n",
    "decay 0.3    entrena 0.5000  prueba 0.5000  brecha +0.0000\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La ventana buena está entre 0,01 y 0,03. Con 0,1 ya se queda corto, y con\n",
    "0,3 el modelo **muere**: entrena 0,5000 y prueba 0,5000, o sea que\n",
    "el freno apretó tanto que la red contesta lo mismo a todo.\n",
    "\n",
    "Y fíjate en la brecha de esa última fila: +0,0000, perfecta. **Una\n",
    "brecha impecable con un modelo inservible**, que es el aviso de que la\n",
    "brecha nunca se mira sola 🚦\n",
    "\n",
    "Esta curva tiene forma de U al revés igual que la del capítulo 7, pero en otro\n",
    "eje: allá era el tiempo y aquí es la fuerza del freno. **Casi todo en deep\n",
    "learning tiene esta forma**, y el trabajo consiste en encontrar el\n",
    "pico 🎢"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Dropout en una red más grande\n",
    "\n",
    "Si 0,5 mató a la red de 32, prueba con 128 neuronas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for ocultas in [32, 128]:\n",
    "    for dr in [0.0, 0.5]:\n",
    "        e, p = corre(ocultas=ocultas, dropout=dr)\n",
    "        print(f'{ocultas:4d} neuronas, dropout {dr}: entrena {e:.4f}  prueba {p:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "  32 neuronas, dropout 0.0: entrena 0.8847  prueba 0.6210\n",
    "  32 neuronas, dropout 0.5: entrena 0.4964  prueba 0.5185\n",
    " 128 neuronas, dropout 0.0: entrena 0.9364  prueba 0.6365\n",
    " 128 neuronas, dropout 0.5: entrena 0.7973  prueba 0.6997\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 128 neuronas el dropout 0,5 ya no destruye nada: la prueba pasa de 0,6365\n",
    "sin dropout a **0,6997 con él**. Ahí sí ayuda, y bastante.\n",
    "\n",
    "Compáralo con la red de 32, donde ese mismo 0,5 la dejaba en 0,5185. La misma\n",
    "receta, resultados opuestos.\n",
    "\n",
    "Confirmado entonces lo de arriba: **el 0,5 no era malo, era demasiado\n",
    "para una capa de 32**. La regla práctica que uso es que la capa tenga al\n",
    "menos cien neuronas antes de plantearme un dropout alto 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Los dos frenos juntos\n",
    "\n",
    "Combina weight decay con dropout y mira si suman."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "e, p = corre(decay=0.01, dropout=0.2)\n",
    "print(f'los dos juntos: entrena {e:.4f}  prueba {p:.4f}')\n",
    "print('solo decay era: entrena 0.7194  prueba 0.7207   (de la tabla de arriba)')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "los dos juntos: entrena 0.7179  prueba 0.7207\n",
    "solo decay era: entrena 0.7194  prueba 0.7207   (de la tabla de arriba)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los dos juntos dan 0,7207 de prueba, o sea **exactamente lo mismo**\n",
    "que el decay solo. No suman.\n",
    "\n",
    "Y tiene sentido: el problema era el sobreajuste, el decay ya lo resolvió del\n",
    "todo, y a partir de ahí lo que añadas solo puede quitar. **Los frenos no se\n",
    "apilan por si acaso** 🧱"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Qué le pasa a los pesos con el freno\n",
    "\n",
    "Mide cuánto valen los pesos con y sin weight decay."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def tamano_pesos(decay):\n",
    "    r = np.random.default_rng(0)\n",
    "    W1 = r.normal(0, 0.1, (T_tr.shape[1], 32)); b1 = np.zeros(32)\n",
    "    W2 = r.normal(0, 0.1, (32, 1)); b2 = np.zeros(1)\n",
    "    for _ in range(2000):\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 -= 0.5 * (h.T @ d2 + decay * W2); b2 -= 0.5 * d2.sum(axis=0)\n",
    "        W1 -= 0.5 * (T_tr.T @ d1 + decay * W1); b1 -= 0.5 * d1.sum(axis=0)\n",
    "    return float(np.abs(W1).mean()), float(np.abs(W1).max())\n",
    "\n",
    "for decay in [0.0, 0.01]:\n",
    "    medio, maximo = tamano_pesos(decay)\n",
    "    print(f'decay {decay}: peso medio {medio:.4f}  el más grande {maximo:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "decay 0.0: peso medio 0.2415  el más grande 1.1419\n",
    "decay 0.01: peso medio 0.0208  el más grande 0.3018\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin freno el peso medio es 0,2415 y el mayor llega a 1,1419. Con freno, 0,0208\n",
    "y 0,3018: once veces más chicos de media.\n",
    "\n",
    "El freno hace literalmente lo que su nombre dice: mantiene los pesos chicos. Y\n",
    "pesos chicos significa una función más suave, que no puede dar los saltos que\n",
    "hacen falta para clavar filas sueltas 📉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Más datos también es un freno\n",
    "\n",
    "Entrena la red sin ningún freno pero con la mitad de las\n",
    "filas, y con todas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mitad = np.arange(len(objetivo)) % 2 == 0\n",
    "T_mitad, obj_mitad = T_tr[mitad], objetivo[mitad]\n",
    "\n",
    "def con_estos(T, obj, ocultas=32, vueltas=2000):\n",
    "    r = np.random.default_rng(0)\n",
    "    W1 = r.normal(0, 0.1, (T.shape[1], 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 @ W1 + b1)\n",
    "        p = sigmoide(h @ W2 + b2)\n",
    "        d2 = (p - obj) / len(obj)\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 * (T.T @ d1); b1 -= 0.5 * d1.sum(axis=0)\n",
    "    return roc_auc_score(y_te, sigmoide(np.tanh(T_te @ W1 + b1) @ W2 + b2).ravel())\n",
    "\n",
    "print(f'con {mitad.sum()} filas: {con_estos(T_mitad, obj_mitad):.4f}')\n",
    "print(f'con {len(objetivo)} filas: {con_estos(T_tr, objetivo):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "con 1125 filas: 0.5701\n",
    "con 2250 filas: 0.6210\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con la mitad de las filas saca 0,5701 y con todas 0,6210.\n",
    "\n",
    "Los datos son el freno más eficaz que existe, y el único que no tiene\n",
    "contraindicaciones. El problema es que casi nunca puedes conseguir más, y por eso\n",
    "existen los otros tres 🗄️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El error de olvidar la máscara en la vuelta atrás\n",
    "\n",
    "Quita el `d1 = d1 * mascara` y compara el\n",
    "gradiente con el numérico."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "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",
    "h = np.tanh(T_tr @ W1 + b1)\n",
    "mascara = (r.random(h.shape) > 0.5) / 0.5\n",
    "h_apagada = h * mascara\n",
    "p = sigmoide(h_apagada @ W2 + b2)\n",
    "d2 = (p - objetivo) / len(objetivo)\n",
    "\n",
    "bien = ((d2 @ W2.T) * mascara) * (1 - h ** 2)\n",
    "mal = (d2 @ W2.T) * (1 - h ** 2)\n",
    "\n",
    "print('gradiente con máscara, media:', round(float(np.abs(T_tr.T @ bien).mean()), 6))\n",
    "print('gradiente sin máscara, media:', round(float(np.abs(T_tr.T @ mal).mean()), 6))\n",
    "print('¿son iguales?', np.allclose(bien, mal))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "gradiente con máscara, media: 0.002287\n",
    "gradiente sin máscara, media: 0.002234\n",
    "¿son iguales? False\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No son iguales, y el fallo es silencioso: la red entrena igual, sin ningún\n",
    "error, corrigiendo neuronas que en esa vuelta estaban apagadas.\n",
    "\n",
    "Es de los errores más comunes al escribir dropout a mano, y la única forma de\n",
    "cazarlo es el gradient checking del capítulo 6 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El error de la dosis imposible\n",
    "\n",
    "Pon un dropout de 1, o sea apagarlo todo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "corre(dropout=1.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: Input contains NaN.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Apagar el 100% de las neuronas deja la división `/ (1 - 1)`, que es\n",
    "dividir entre cero.\n",
    "\n",
    "Numpy no lanza excepción por dividir entre cero en un array: devuelve\n",
    "`inf` con un aviso, y ese `inf` viaja hasta que algo\n",
    "revienta más adelante. Por eso el error que sale no habla de dropout ni de\n",
    "división: habla del sitio donde el `inf` acabó rompiendo algo 🧯\n",
    "\n",
    "Los errores de deep learning casi siempre aparecen lejos de donde nacieron, y\n",
    "esa es la parte que más cuesta al principio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Dropout 0,5 destruye una red de 32 neuronas (0,4964) y ayuda a una de 128. ¿Por qué?\n",
    "\n",
    "a) Porque apagar la mitad de 32 deja demasiado poco para representar nada\n",
    "\n",
    "b) Porque dropout solo funciona en redes grandes por definición\n",
    "\n",
    "c) Porque 0,5 es un valor mal elegido siempre\n",
    "\n",
    "d) Porque la red de 32 necesita más vueltas\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* No es una regla: es lo que pasa cuando lo que queda encendido es insuficiente.\n",
    "\n",
    "*c)* En la de 128 con ese mismo 0,5 la cosa mejora.\n",
    "\n",
    "*d)* Con más vueltas sigue apagando la mitad en cada una.\n",
    "\n",
    "Un freno no es bueno o malo: es bueno o malo para el tamaño de red que tienes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el freno que se queda puesto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Metes dropout, que es lo que todo el mundo recomienda, y al evaluar sale un número más bajo del que esperabas. Se prueba otra vez y sale distinto.\n",
    "\n",
    "```\n",
    "def predice(X):\n",
    "    h = relu(X @ W1 + b1)\n",
    "    h *= (np.random.rand(*h.shape) > 0.5)\n",
    "    return sigmoide(h @ W2 + b2)\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "El dropout se quedó encendido al predecir 🎲 Apagar neuronas al azar es una técnica de **entrenamiento**: sirve para que la red no se apoye en ninguna neurona concreta. Al predecir hay que usarlas todas.\n",
    "\n",
    "La pista está en que dos predicciones sobre la misma fila dan resultados distintos. Un modelo entrenado tiene que ser una función: mismos datos, mismo resultado. Si no lo es, algo aleatorio se quedó dentro.\n",
    "\n",
    "Y hay una segunda mitad que se olvida igual de fácil: al apagar la mitad de las neuronas durante el entrenamiento, la suma que llega a la capa siguiente vale la mitad. Hay que compensarlo, dividiendo entre la probabilidad de quedarse. Si no, las dos fases trabajan con magnitudes distintas y la red aprende con una escala que luego no existe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- ⚖️ Weight decay es una línea (`+ decay * W`) y es el que\n",
    "funciona: la brecha pasa de +0,2636 a -0,0013 y la prueba sube a 0,7207.\n",
    "\n",
    "- 🎯 Y tiene dosis: con 0,001 no hace nada y con 0,1 aprieta de más.\n",
    "\n",
    "- 🎲 Dropout 0,2 ayuda a medias. Dropout 0,5 destruye una red de 32 neuronas\n",
    "(0,4964 en entrenamiento) y funciona bien con 128.\n",
    "\n",
    "- 🖼️ Ensuciar las entradas ayuda poco en tabla. En imágenes es la herramienta\n",
    "principal y se llama aumento de datos.\n",
    "\n",
    "- 🧱 Los frenos no se apilan por si acaso: decay más dropout da lo mismo que\n",
    "decay solo.\n",
    "\n",
    "- 📉 El freno hace lo que dice: el peso medio baja de 0,2415 a 0,0208.\n",
    "\n",
    "- 🚦 La brecha nunca se mira sola: con decay 0,3 sale +0,0000 perfecta y el\n",
    "modelo contesta 0,5000, o sea nada.\n",
    "\n",
    "- 🗄️ Más datos es el freno más eficaz y el único sin contraindicaciones.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Frenar la memorización no es lo mismo que aprender más. Casi siempre es solo lo primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El sobreajuste no es exclusivo de las redes: pasa igual con árboles y con regresiones, y ahí está medido en el [libro de machine learning desde cero](https://missyera.com/guias/machine-learning-desde-cero/) 🌳\n",
    "\n",
    "En el capítulo 11 cambiamos de tipo de dato: las convolucionales, y por fin un\n",
    "problema donde la red gana de calle.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 8 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/frenar-el-sobreajuste/\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
}
