{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cómo sabe la red hacia dónde corregir\n",
    "\n",
    "La pérdida, la pendiente y el paso. Y por qué sin escalar no existe ningún paso que funcione, que era lo que quedó pendiente.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 5 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/descenso-de-gradiente/\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 4 la red aprendió y yo te dije que no miraras las líneas del\n",
    "\"hacia atrás\". Aquí las miramos 🔍\n",
    "\n",
    "Antes de entrar: **¿alguna vez has dejado algo entrenando toda la noche para descubrir por la mañana que no había aprendido nada?** Aquí está la razón número uno de que pase 📉\n",
    "\n",
    "Y de paso pago lo que quedé debiendo en el capítulo 1, cuando dije que la\n",
    "razón de escalar estaba en el gradiente. Está, y se ve clarísima."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Primero: cuánto te equivocas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L=1n∑i=1n(yi−y^i)2\n",
    "\n",
    "el error medio al cuadrado, que castiga mucho más un fallo grande que dos medianos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para corregir hace falta un número que diga cómo de mal vas. Ese número es la\n",
    "**pérdida**, y para un sí o no se usa esta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\n",
    "\n",
    "def perdida(p, y):\n",
    "    p = np.clip(p, 1e-12, 1 - 1e-12)      # para que log(0) no reviente\n",
    "    return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))\n",
    "\n",
    "y = np.array([1., 1., 0., 0.])\n",
    "print('acertando de lleno  :', round(perdida(np.array([.99, .99, .01, .01]), y), 4))\n",
    "print('sin idea (0,5 a todo):', round(perdida(np.array([.5, .5, .5, .5]), y), 4))\n",
    "print('equivocada de lleno :', round(perdida(np.array([.01, .01, .99, .99]), y), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se llama entropía cruzada y funciona así: **castiga estar seguro y\n",
    "equivocado mucho más que estar dudando**.\n",
    "\n",
    "Fíjate en los números. Dudar cuesta 0,6931, que ya salió en el capítulo 4 y es\n",
    "ln(2). Y estar seguro y equivocado cuesta 4,6052, o sea casi siete veces más.\n",
    "\n",
    "Esa asimetría es a propósito. Un modelo que dice \"70%\" y falla es un modelo\n",
    "que se puede arreglar; uno que dice \"99%\" y falla está roto 😬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Segundo: hacia dónde crece el error"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El **gradiente** es una lista de números, uno por peso, que dice\n",
    "cuánto sube la pérdida si subes ese peso un poquito.\n",
    "\n",
    "Para una neurona con sigmoide y entropía cruzada, sale una fórmula\n",
    "sorprendentemente corta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(0)\n",
    "X = rng.normal(0, 1, (200, 3))\n",
    "y = (rng.random(200) < sigmoide(X @ np.array([1., -0.5, 0.3]))).astype(float)\n",
    "\n",
    "w = np.array([0.2, -0.1, 0.4])\n",
    "b = 0.05\n",
    "\n",
    "error = sigmoide(X @ w + b) - y\n",
    "gradiente = X.T @ error / len(y)\n",
    "print('gradiente:', np.round(gradiente, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa línea, `X.T @ (predicho - real) / n`, es todo el cálculo. Sale\n",
    "de derivar la pérdida y se simplifica muchísimo porque la sigmoide y la entropía\n",
    "cruzada están hechas la una para la otra.\n",
    "\n",
    "Ahora, ¿cómo sabemos que esa fórmula está bien? Se comprueba **moviendo\n",
    "el peso a mano** y midiendo cuánto cambia la pérdida:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def perdida_de(w, b):\n",
    "    return perdida(sigmoide(X @ w + b), y)\n",
    "\n",
    "h = 1e-5\n",
    "numerico = np.zeros(3)\n",
    "for i in range(3):\n",
    "    arriba = w.copy(); arriba[i] += h\n",
    "    abajo = w.copy(); abajo[i] -= h\n",
    "    numerico[i] = (perdida_de(arriba, b) - perdida_de(abajo, b)) / (2 * h)\n",
    "\n",
    "print('por fórmula:', np.round(gradiente, 8))\n",
    "print('moviendo    :', np.round(numerico, 8))\n",
    "print('diferencia  :', float(np.abs(gradiente - numerico).max()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "6,7e-12 de diferencia, o sea que la fórmula está bien 🎯\n",
    "\n",
    "Esto tiene nombre, **gradient checking**, y es lo que se hace\n",
    "cuando escribes una capa nueva a mano y no estás segura de la derivada. Es lento\n",
    "(hay que evaluar la pérdida dos veces por peso) así que solo se usa para\n",
    "comprobar, nunca para entrenar.\n",
    "\n",
    "Te lo enseño porque es la herramienta que convierte \"creo que la derivada está\n",
    "bien\" en \"está bien\", y esa diferencia vale mucho."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tercero: dar el paso"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "w←w−η∂L∂w\n",
    "\n",
    "mueves cada peso un poquito en la dirección contraria a la pendiente, y ese poquito es la tasa de aprendizaje"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El gradiente apunta hacia donde el error *crece*. Así que para mejorar\n",
    "hay que ir al revés:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('pérdida ahora     :', round(perdida_de(w, b), 6))\n",
    "print('un paso a favor   :', round(perdida_de(w - 0.5 * gradiente, b), 6))\n",
    "print('un paso al revés  :', round(perdida_de(w + 0.5 * gradiente, b), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Restando baja de 0,6483 a 0,6285 y sumando sube a 0,6707. Por eso todas las\n",
    "líneas de corrección del libro llevan un menos 🔽\n",
    "\n",
    "Y ese 0,5 que multiplica es el **paso**, o tasa de aprendizaje,\n",
    "que en el capítulo 4 ya vimos que puede arruinarlo todo por los dos lados.\n",
    "\n",
    "Entrenar es repetir esas tres cosas hasta que deje de mejorar. Nada más."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que quedó pendiente en el capítulo 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora sí. Armo un problema con dos columnas de escalas muy distintas, que es\n",
    "lo que pasa siempre en datos de verdad: un monto en cientos de soles y una\n",
    "satisfacción del 1 al 5."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n = 500\n",
    "monto = rng.normal(800, 300, n)\n",
    "satisfaccion = rng.normal(3.5, 1.0, n)\n",
    "z = 0.002 * (monto - 800) + 0.8 * (satisfaccion - 3.5)\n",
    "objetivo = (rng.random(n) < sigmoide(z)).astype(float)\n",
    "\n",
    "crudo = np.column_stack([monto, satisfaccion])\n",
    "escalado = (crudo - crudo.mean(axis=0)) / crudo.std(axis=0)\n",
    "\n",
    "def baja(datos, paso, vueltas=200):\n",
    "    w = np.zeros(datos.shape[1])\n",
    "    b = 0.0\n",
    "    inicial = perdida(sigmoide(datos @ w + b), objetivo)\n",
    "    for _ in range(vueltas):\n",
    "        e = sigmoide(datos @ w + b) - objetivo\n",
    "        w -= paso * (datos.T @ e) / len(objetivo)\n",
    "        b -= paso * e.mean()\n",
    "    return inicial, perdida(sigmoide(datos @ w + b), objetivo)\n",
    "\n",
    "for nombre, datos in [('sin escalar', crudo), ('escalado', escalado)]:\n",
    "    for paso in [0.00001, 0.0001, 0.01, 0.1]:\n",
    "        ini, fin = baja(datos, paso)\n",
    "        estado = 'DIVERGE' if fin > ini else f'{fin:.4f}'\n",
    "        print(f'{nombre:12} paso {paso:<8} {estado}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está la respuesta 😳\n",
    "\n",
    "**Sin escalar, desde 0,0001 la pérdida explota.** El único paso\n",
    "que sobrevive es 0,00001, y con ese la pérdida baja de 0,6931 a 0,6806 en 200\n",
    "vueltas.\n",
    "\n",
    "Con las columnas escaladas, ese mismo paso de 0,00001 apenas mueve nada\n",
    "(0,6930), pero como ahí sí puedes usar 0,1, la pérdida llega a\n",
    "**0,5544**.\n",
    "\n",
    "Compara el avance: sin escalar ganó 0,0125 de pérdida y escalado ganó 0,1387,\n",
    "o sea **once veces más**, en las mismas 200 vueltas.\n",
    "\n",
    "El mecanismo es este. El gradiente lleva la escala de la columna dentro: la\n",
    "del monto sale unas trescientas veces más grande que la de satisfacción. Un paso\n",
    "suficientemente chico para no hacer explotar el peso del monto es\n",
    "**trescientas veces demasiado chico** para mover el de satisfacción.\n",
    "Y no existe un número que sirva para los dos.\n",
    "\n",
    "Escalar es poner las dos columnas en la misma escala para que un solo paso les\n",
    "valga a las dos. Por eso no es una buena práctica: es la condición para que el\n",
    "descenso de gradiente funcione ⚖️\n",
    "\n",
    "Y por eso también el `StandardScaler` va dentro del pipeline desde\n",
    "el capítulo 1."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora sobre las ventas de verdad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo lo anterior fue con datos que fabriqué yo, para poder comparar contra la\n",
    "respuesta verdadera. Vamos a soltar el mismo bucle sobre el CSV de la\n",
    "distribuidora 🐔"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "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 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",
    "ventas = prepara(carga_limpia(URL))\n",
    "Xv = ventas[NUMERICAS + CATEGORICAS]\n",
    "yv = ventas['compro']\n",
    "Xv_tr, Xv_te, yv_tr, yv_te = train_test_split(Xv, yv, test_size=0.25,\n",
    "                                              random_state=42, stratify=yv)\n",
    "\n",
    "sklearn_modelo = 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(Xv_tr, yv_tr)\n",
    "\n",
    "T_tr = sklearn_modelo.named_steps['pre'].transform(Xv_tr)\n",
    "T_te = sklearn_modelo.named_steps['pre'].transform(Xv_te)\n",
    "objetivo_v = yv_tr.values.astype(float)\n",
    "\n",
    "w_mano = np.zeros(T_tr.shape[1])\n",
    "b_mano = 0.0\n",
    "for _ in range(3000):\n",
    "    e = sigmoide(T_tr @ w_mano + b_mano) - objetivo_v\n",
    "    w_mano -= 0.5 * (T_tr.T @ e) / len(objetivo_v)\n",
    "    b_mano -= 0.5 * e.mean()\n",
    "\n",
    "print('nuestro bucle:', round(roc_auc_score(yv_te, sigmoide(T_te @ w_mano + b_mano)), 4))\n",
    "print('scikit-learn :', round(roc_auc_score(\n",
    "    yv_te, sklearn_modelo.predict_proba(Xv_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,7216 el bucle que acabamos de escribir, contra 0,7214 la librería. Con\n",
    "veintiocho columnas de ventas peruanas de verdad, con su suciedad, sus nulos y su\n",
    "OneHotEncoder 🎉\n",
    "\n",
    "Y no es que hayamos empatado por poco: las dos están resolviendo exactamente\n",
    "el mismo problema con el mismo método. La diferencia de dos diezmilésimas es que\n",
    "scikit-learn usa un optimizador más listo que el nuestro y llega a un sitio\n",
    "ligerísimamente distinto.\n",
    "\n",
    "Ese es el punto de haber escrito todo esto a mano. **Ya no hay ninguna\n",
    "parte de una regresión logística que no sepas hacer tú**, y lo que viene\n",
    "en el capítulo 6 es la misma idea con capas 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Ver la pérdida bajar vuelta a vuelta\n",
    "\n",
    "Guarda la pérdida cada 20 vueltas y mírala."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "w = np.zeros(2); b = 0.0\n",
    "for i in range(201):\n",
    "    p = sigmoide(escalado @ w + b)\n",
    "    if i % 40 == 0:\n",
    "        print(f'vuelta {i:3d}  pérdida {perdida(p, objetivo):.4f}')\n",
    "    e = p - objetivo\n",
    "    w -= 0.1 * (escalado.T @ e) / len(objetivo)\n",
    "    b -= 0.1 * e.mean()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "vuelta   0  pérdida 0.6931\n",
    "vuelta  40  pérdida 0.5858\n",
    "vuelta  80  pérdida 0.5639\n",
    "vuelta 120  pérdida 0.5574\n",
    "vuelta 160  pérdida 0.5552\n",
    "vuelta 200  pérdida 0.5544\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Baja rápido al principio y luego se aplana, que es la forma normal de una\n",
    "curva de entrenamiento.\n",
    "\n",
    "Cuando veas una que baja y se queda plana pronto, no está rota: llegó. Y si\n",
    "baja a saltos o sube, el paso es demasiado grande 📉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuánto vale cada peso al final\n",
    "\n",
    "Compara los pesos que encontró con los que usé para\n",
    "fabricar los datos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('pesos encontrados:', np.round(w, 4))\n",
    "print('sesgo            :', round(float(b), 4))\n",
    "print()\n",
    "print('yo generé con 0.002 por sol de monto y 0.8 por punto de satisfacción')\n",
    "print('en unidades escaladas eso es:',\n",
    "      np.round([0.002 * crudo[:, 0].std(), 0.8 * crudo[:, 1].std()], 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "pesos encontrados: [0.6997 1.0115]\n",
    "sesgo            : 0.171\n",
    "\n",
    "yo generé con 0.002 por sol de monto y 0.8 por punto de satisfacción\n",
    "en unidades escaladas eso es: [0.5728 0.8234]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Encontró 0,6997 y 1,0115 donde la respuesta verdadera era 0,5728 y 0,8234. O\n",
    "sea que se pasó, y por bastante: un 22% y un 23% por encima.\n",
    "\n",
    "Este ejercicio solo se puede hacer con datos fabricados, y por eso lo puse:\n",
    "**es la única vez en todo el libro que sabemos la respuesta\n",
    "verdadera** y podemos comprobar si el método la encuentra 🔬\n",
    "\n",
    "Que se pase no significa que el método esté mal. Significa que 500 filas son\n",
    "pocas, y lo comprobamos en el ejercicio 3."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuánto se tarda en llegar de verdad\n",
    "\n",
    "Dale muchas más vueltas y mira si alcanza los pesos\n",
    "reales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for vueltas in [200, 1000, 5000, 20000]:\n",
    "    ww = np.zeros(2); bb = 0.0\n",
    "    for _ in range(vueltas):\n",
    "        e = sigmoide(escalado @ ww + bb) - objetivo\n",
    "        ww -= 0.1 * (escalado.T @ e) / len(objetivo)\n",
    "        bb -= 0.1 * e.mean()\n",
    "    print(f'{vueltas:6d} vueltas: pesos {np.round(ww, 4)}  '\n",
    "          f'pérdida {perdida(sigmoide(escalado @ ww + bb), objetivo):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "   200 vueltas: pesos [0.699  1.0106]  pérdida 0.5544\n",
    "  1000 vueltas: pesos [0.763  1.0924]  pérdida 0.5538\n",
    "  5000 vueltas: pesos [0.763  1.0925]  pérdida 0.5538\n",
    " 20000 vueltas: pesos [0.763  1.0925]  pérdida 0.5538\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 1.000 vueltas ya llegó a 0,7630 y 1,0925, y de ahí no se mueve ni un\n",
    "decimal aunque le des veinte mil. La pérdida se queda clavada en 0,5538.\n",
    "\n",
    "**El 99% del trabajo se hace al principio**, y ahí está la\n",
    "justificación de la parada temprana del capítulo 1 ⏱️\n",
    "\n",
    "Y fíjate que converge a 0,7630 y 1,0925, que *no* son los 0,5728 y\n",
    "0,8234 verdaderos. No le faltan vueltas: es que con 500 filas esos son los pesos\n",
    "que mejor explican los datos que tocaron, y los datos que tocaron no son el\n",
    "mundo. Eso es ruido de muestreo, no un fallo del entrenamiento."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Con más filas, ¿se acerca a la verdad?\n",
    "\n",
    "Fabrica el mismo problema con más datos y mira si los pesos\n",
    "encontrados se pegan a los verdaderos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for filas in [500, 5000, 50000]:\n",
    "    g = np.random.default_rng(0)\n",
    "    m = g.normal(800, 300, filas)\n",
    "    s = g.normal(3.5, 1.0, filas)\n",
    "    obj = (g.random(filas) < sigmoide(0.002 * (m - 800) + 0.8 * (s - 3.5))).astype(float)\n",
    "    c = np.column_stack([m, s])\n",
    "    esc = (c - c.mean(axis=0)) / c.std(axis=0)\n",
    "\n",
    "    ww = np.zeros(2); bb = 0.0\n",
    "    for _ in range(20000):\n",
    "        e = sigmoide(esc @ ww + bb) - obj\n",
    "        ww -= 0.1 * (esc.T @ e) / filas\n",
    "        bb -= 0.1 * e.mean()\n",
    "\n",
    "    verdad = np.array([0.002 * c[:, 0].std(), 0.8 * c[:, 1].std()])\n",
    "    print(f'{filas:6d} filas: encontrado {np.round(ww, 4)}  '\n",
    "          f'verdadero {np.round(verdad, 4)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "   500 filas: encontrado [0.5796 0.6495]  verdadero [0.6082 0.7504]\n",
    "  5000 filas: encontrado [0.635  0.8078]  verdadero [0.5972 0.8005]\n",
    " 50000 filas: encontrado [0.594  0.8179]  verdadero [0.6008 0.7991]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 500 filas se queda en 0,5796 y 0,6495 contra 0,6082 y 0,7504. Con 50.000\n",
    "llega a 0,5940 y 0,8179 contra 0,6008 y 0,7991: prácticamente clavados.\n",
    "\n",
    "Confirmado, entonces: **el desvío del ejercicio anterior era falta de\n",
    "datos, no un fallo del método**. Y esto es lo que hay debajo de todo el\n",
    "libro de machine learning: con pocas filas, lo que encuentras es una mezcla de la\n",
    "verdad y de la suerte que te tocó 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Cuántas vueltas hacen falta con datos de verdad\n",
    "\n",
    "Mira el AUC sobre las ventas según las vueltas que le\n",
    "des."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for vueltas in [100, 500, 1500, 3000]:\n",
    "    ww = np.zeros(T_tr.shape[1]); bb = 0.0\n",
    "    for _ in range(vueltas):\n",
    "        e = sigmoide(T_tr @ ww + bb) - objetivo_v\n",
    "        ww -= 0.5 * (T_tr.T @ e) / len(objetivo_v)\n",
    "        bb -= 0.5 * e.mean()\n",
    "    print(f'{vueltas:5d} vueltas: AUC {roc_auc_score(yv_te, sigmoide(T_te @ ww + bb)):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "  100 vueltas: AUC 0.7189\n",
    "  500 vueltas: AUC 0.7213\n",
    " 1500 vueltas: AUC 0.7216\n",
    " 3000 vueltas: AUC 0.7216\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 100 vueltas ya saca 0,7189 y con 500 llega a 0,7213, que es prácticamente\n",
    "el final. Las 2.500 vueltas siguientes ganan tres diezmilésimas.\n",
    "\n",
    "Es lo mismo que vimos con los datos fabricados y ahora con las ventas\n",
    "reales: **casi todo el aprendizaje pasa al principio**. Si estás\n",
    "esperando a que un entrenamiento largo mejore mucho, casi nunca pasa ⏳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El gradiente cuando ya acertaste\n",
    "\n",
    "Mira cuánto vale el gradiente en un modelo que acierta y en\n",
    "uno que no."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "perfecto = np.array([10., 0., 0.])\n",
    "malo = np.array([-10., 0., 0.])\n",
    "y_facil = (X[:, 0] > 0).astype(float)\n",
    "\n",
    "for nombre, pesos in [('acierta', perfecto), ('se equivoca', malo)]:\n",
    "    e = sigmoide(X @ pesos) - y_facil\n",
    "    g = X.T @ e / len(y_facil)\n",
    "    print(f'{nombre:12} pérdida {perdida(sigmoide(X @ pesos), y_facil):.4f}  '\n",
    "          f'gradiente {np.round(g, 4)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "acierta      pérdida 0.0628  gradiente [-0.0067 -0.0028  0.0041]\n",
    "se equivoca  pérdida 8.0274  gradiente [-0.7963  0.0155  0.0704]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuando acierta, el gradiente es prácticamente cero: no hay hacia dónde\n",
    "corregir y el entrenamiento se para solo.\n",
    "\n",
    "Cuando se equivoca de lleno, el gradiente es enorme y empuja fuerte. El método\n",
    "tiene esa gracia: **se autorregula, corrige mucho cuando va mal y poco\n",
    "cuando va bien** 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Por lotes, que es como se hace de verdad\n",
    "\n",
    "En vez de usar las 500 filas cada vuelta, usa 32 al azar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lote_rng = np.random.default_rng(7)\n",
    "w_lote = np.zeros(2); b_lote = 0.0\n",
    "for i in range(201):\n",
    "    idx = lote_rng.choice(len(objetivo), 32, replace=False)\n",
    "    e = sigmoide(escalado[idx] @ w_lote + b_lote) - objetivo[idx]\n",
    "    w_lote -= 0.1 * (escalado[idx].T @ e) / len(idx)\n",
    "    b_lote -= 0.1 * e.mean()\n",
    "\n",
    "print('con lotes de 32 :', np.round(w_lote, 4),\n",
    "      f'pérdida {perdida(sigmoide(escalado @ w_lote + b_lote), objetivo):.4f}')\n",
    "print('con las 500     :', np.round(w, 4),\n",
    "      f'pérdida {perdida(sigmoide(escalado @ w + b), objetivo):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "con lotes de 32 : [0.7054 1.0285] pérdida 0.5547\n",
    "con las 500     : [0.6997 1.0115] pérdida 0.5543\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi lo mismo, y mirando **una quinceava parte de los datos** en\n",
    "cada vuelta.\n",
    "\n",
    "Eso es el descenso de gradiente por lotes, y es como se entrena todo hoy. Con\n",
    "millones de filas no puedes calcular el gradiente de todas: usas un puñado, que\n",
    "apunta en una dirección parecida y cuesta muchísimo menos 🚀\n",
    "\n",
    "El ruido que mete el lote hasta ayuda, porque a veces empuja fuera de un\n",
    "mínimo local como el que atascó la semilla 0 del capítulo 4."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. El error de pedir un lote más grande que los datos\n",
    "\n",
    "Pide 600 filas de un conjunto de 500."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lote_rng.choice(len(objetivo), 600, replace=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: Cannot take a larger sample than population when replace is False\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"Cannot take a larger sample than population when replace is False\", o sea que\n",
    "no puedes sacar 600 bolas distintas de una bolsa con 500.\n",
    "\n",
    "Parece una tontería y pasa constantemente, porque el tamaño de lote se escribe\n",
    "una vez arriba del cuaderno y luego alguien filtra los datos y se queda con\n",
    "menos filas de las que había 🎒\n",
    "\n",
    "Con `replace=True` sí te deja, y ahí no hay error pero hay filas\n",
    "repetidas dentro del lote. Que a veces está bien (se llama bootstrap) y a veces\n",
    "no es lo que querías."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9. Comprobar el gradiente del sesgo\n",
    "\n",
    "Haz el gradient checking para `b`, que arriba lo\n",
    "salté."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "formula = float((sigmoide(X @ w[:3] if len(w) == 3 else X @ np.array([0.2, -0.1, 0.4]) + b) - y).mean())\n",
    "numerico_b = (perdida_de(np.array([0.2, -0.1, 0.4]), b + h)\n",
    "              - perdida_de(np.array([0.2, -0.1, 0.4]), b - h)) / (2 * h)\n",
    "print('por fórmula:', round(formula, 8))\n",
    "print('moviendo   :', round(numerico_b, 8))\n",
    "print('diferencia :', round(abs(formula - numerico_b), 12))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "por fórmula: 0.03743041\n",
    "moviendo   : 0.03743041\n",
    "diferencia : 0.0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El gradiente del sesgo es sencillamente **la media del error**,\n",
    "sin multiplicar por ninguna columna. Y sale igual que moviéndolo a mano.\n",
    "\n",
    "Tiene sentido si lo piensas: el sesgo entra en la cuenta multiplicado por 1\n",
    "siempre, así que su derivada no lleva ninguna columna dentro 🎈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10. El paso que se va de largo, visto por dentro\n",
    "\n",
    "Imprime la pérdida vuelta a vuelta con un paso enorme."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "w_mal = np.zeros(2); b_mal = 0.0\n",
    "for i in range(8):\n",
    "    p = sigmoide(escalado @ w_mal + b_mal)\n",
    "    print(f'vuelta {i}: pérdida {perdida(p, objetivo):10.4f}  pesos {np.round(w_mal, 3)}')\n",
    "    e = p - objetivo\n",
    "    w_mal -= 60.0 * (escalado.T @ e) / len(objetivo)\n",
    "    b_mal -= 60.0 * e.mean()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "vuelta 0: pérdida     0.6931  pesos [0. 0.]\n",
    "vuelta 1: pérdida     2.1362  pesos [ 7.897 11.991]\n",
    "vuelta 2: pérdida     1.0199  pesos [4.257 4.276]\n",
    "vuelta 3: pérdida     1.6548  pesos [-2.816  0.363]\n",
    "vuelta 4: pérdida     4.7848  pesos [24.973  8.635]\n",
    "vuelta 5: pérdida     2.6199  pesos [10.694 14.776]\n",
    "vuelta 6: pérdida     1.5089  pesos [6.047 7.431]\n",
    "vuelta 7: pérdida     0.6832  pesos [0.707 1.659]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira los pesos: 0 y 0, luego 7,9 y 12, luego 4,3 y 4,3, luego -2,8 y 0,4,\n",
    "luego 25 y 8,6. Van dando tumbos sin asentarse, y la pérdida hace lo mismo: 0,69,\n",
    "2,14, 1,02, 1,65, 4,78.\n",
    "\n",
    "Se pasó del fondo, rebotó en la pared de enfrente, se volvió a pasar. Con este\n",
    "problema tan fácil acaba cayendo cerca por accidente en la vuelta 7, pero eso es\n",
    "suerte y no convergencia.\n",
    "\n",
    "Esa alternancia de signo es la firma de un paso demasiado grande, y se\n",
    "reconoce a simple vista. Si ves la pérdida haciendo eso, **divide el paso\n",
    "entre diez y vuelve a correr**, no toques nada más 🪃"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 11. El nan silencioso del log de cero\n",
    "\n",
    "Quita el `clip` de la función de pérdida y dale\n",
    "una predicción perfecta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def perdida_sin_clip(p, y):\n",
    "    return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))\n",
    "\n",
    "print(perdida_sin_clip(np.array([1.0, 0.0]), np.array([1.0, 0.0])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "nan\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un modelo que acierta con seguridad absoluta hace que `log(1 - 1)`\n",
    "sea `log(0)`, que es menos infinito. Y cero por menos infinito es\n",
    "`nan`.\n",
    "\n",
    "Y fíjate en lo que **no** pasó: no reventó. Numpy avisa con un\n",
    "`RuntimeWarning` y devuelve `nan` tan tranquilo, que es\n",
    "mucho peor que un error. El entrenamiento sigue, ese `nan` entra en el\n",
    "gradiente, y a la vuelta siguiente todos los pesos son `nan`.\n",
    "\n",
    "Si alguna vez ves que de golpe toda tu red se vuelve `nan`, este es\n",
    "el primer sitio donde mirar 🔎\n",
    "\n",
    "Por eso está el `np.clip(p, 1e-12, 1 - 1e-12)` de la primera\n",
    "función del capítulo. Todas las librerías lo hacen y ninguna lo cuenta 🧯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "El descenso de gradiente da pasos cada vez más cortos según se acerca al mínimo. ¿Por qué?\n",
    "\n",
    "a) Porque el paso es proporcional a la pendiente, y la pendiente se aplana\n",
    "\n",
    "b) Porque la tasa de aprendizaje va bajando sola\n",
    "\n",
    "c) Porque el modelo detecta que está cerca\n",
    "\n",
    "d) Porque se va quedando sin datos\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* La tasa es un número fijo que pones tú, salvo que programes lo contrario.\n",
    "\n",
    "*c)* No detecta nada: solo multiplica la pendiente por la tasa.\n",
    "\n",
    "*d)* Ve los mismos datos en cada vuelta.\n",
    "\n",
    "Eso es lo que hace que el descenso frene en vez de pasarse de largo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La pérdida plana que no convergió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Entrenas 20.000 vueltas, imprimes la pérdida cada mil y ves que se queda plana. Convergió, así que paras y apuntas el número.\n",
    "\n",
    "```\n",
    "for i in range(20000):\n",
    "    perdida = paso(lr=0.00001)\n",
    "    if i % 5000 == 0:\n",
    "        print(i, round(perdida, 4))\n",
    "\n",
    "# 0      0.6931\n",
    "# 5000   0.6923\n",
    "# 10000  0.6919\n",
    "# 15000  0.6916\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Una pérdida plana no significa que llegaste: significa que no te estás moviendo, y son cosas distintas 🧊 Ese **0,6931** del que partes es el logaritmo de 2, o sea el modelo diciendo \"cincuenta por ciento\" en todas las filas. Después de veinte mil vueltas estás en 0,6916. Te moviste quince diezmilésimas.\n",
    "\n",
    "La causa no es el modelo: son las escalas. Con una columna que va de 1 a 5 y otra que llega a 4.236, el gradiente de la segunda aplasta al de la primera, así que cualquier paso que no reviente la grande es demasiado pequeño para la chica.\n",
    "\n",
    "Lo que hay que mirar no es si la pérdida está plana, es **contra qué**. Apunta el logaritmo de 2 en la primera línea del cuaderno: mientras tu pérdida esté ahí, no has entrenado nada, has esperado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 📏 La pérdida de entropía cruzada castiga estar seguro y equivocado (4,6052)\n",
    "mucho más que dudar (0,6931).\n",
    "\n",
    "- 🧮 El gradiente de una neurona cabe en una línea:\n",
    "`X.T @ (predicho - real) / n`.\n",
    "\n",
    "- 🔬 Se comprueba moviendo cada peso a mano, y coincide con 6,7e-12 de\n",
    "diferencia.\n",
    "\n",
    "- 🔽 El gradiente apunta hacia donde el error crece, así que se resta.\n",
    "\n",
    "- ⚖️ Sin escalar no hay ningún paso que sirva: desde 0,0001 explota, y con\n",
    "0,00001 la pérdida se mueve una diezmilésima en 200 vueltas.\n",
    "\n",
    "- ⏱️ El 99% del trabajo se hace al principio: entre 5.000 y 20.000 vueltas la\n",
    "pérdida solo baja 0,0005.\n",
    "\n",
    "- 🚀 Con lotes de 32 filas se llega casi al mismo sitio mirando una quinceava\n",
    "parte de los datos.\n",
    "\n",
    "- 🪃 Pesos que saltan de signo son la firma de un paso demasiado grande.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una pérdida plana no dice que llegaste. Dice que no te estás moviendo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las derivadas y las pendientes que salen aquí vienen de la parte matemática, y la trato despacio en el [libro de estadística desde cero](https://missyera.com/guias/estadistica-desde-cero/) 📐\n",
    "\n",
    "En el capítulo 6 llevamos esto a una red con capas, que es donde el cálculo se\n",
    "complica y aparece la retropropagación.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 5 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/descenso-de-gradiente/\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
}
