{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Repartir la culpa hacia atrás\n",
    "\n",
    "La regla de la cadena con capas, comprobada peso a peso. Y el gradiente desvanecido, por fin medido.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 6 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/retropropagacion/\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 5 calculamos el gradiente de una neurona suelta. Aquí está el\n",
    "problema nuevo: en una red con capas, **los pesos del medio no tocan la\n",
    "salida directamente**, así que no está claro cuánta culpa tienen 🤔\n",
    "\n",
    "La respuesta se llama retropropagación, y la escribimos entera.\n",
    "\n",
    "Antes, una pregunta: **¿cómo repartirías tú la culpa de un error entre diez personas que trabajaron en cadena?** Lo que hace una red es exactamente eso, y por eso se entiende sin fórmulas 🔁"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La idea, sin fórmulas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "∂L∂w=∂L∂a·∂a∂z·∂z∂w\n",
    "\n",
    "la regla de la cadena, que es toda la retropropagación: para saber cuánta culpa tiene un peso, multiplicas las derivadas de todo lo que hay entre ese peso y el error"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Imagínate una cadena de tres personas que se pasan un pedido: la primera lo\n",
    "apunta, la segunda lo prepara y la tercera lo entrega. Llega mal.\n",
    "\n",
    "La tercera sabe exactamente en qué se equivocó, porque el cliente se lo dijo.\n",
    "La segunda no habló con el cliente: se entera por la tercera, y solo de la parte\n",
    "que le tocaba. Y la primera se entera por la segunda 📦\n",
    "\n",
    "Eso es la retropropagación. El error viaja hacia atrás y cada capa recibe\n",
    "**la culpa que le llega de la siguiente, ajustada por cuánto influía\n",
    "ella**.\n",
    "\n",
    "Y \"cuánto influía\" es justamente la pendiente de su activación, que es por lo\n",
    "que el capítulo 3 pasó tanto rato con las derivadas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Escrita entera, con nombres"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "δ(L)=∇aL⊙f\\′(z(L)),δ(l)=(W(l+1)⊤δ(l+1))⊙f\\′(z(l))\n",
    "\n",
    "la culpa de la última capa se calcula directo, y la de cada capa anterior sale de repartir hacia atrás la de la siguiente con la misma matriz de pesos, pero transpuesta"
   ]
  },
  {
   "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)\n",
    "    return float(-np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)))\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "X = rng.normal(0, 1, (50, 3))\n",
    "y = (rng.random(50) < 0.5).astype(float).reshape(-1, 1)\n",
    "\n",
    "W1 = rng.normal(0, 0.5, (3, 4)); b1 = np.zeros(4)\n",
    "W2 = rng.normal(0, 0.5, (4, 1)); b2 = np.zeros(1)\n",
    "\n",
    "# hacia adelante\n",
    "h = np.tanh(X @ W1 + b1)          # lo que sale de la capa oculta\n",
    "p = sigmoide(h @ W2 + b2)         # la predicción\n",
    "\n",
    "# hacia atrás\n",
    "d2 = (p - y) / len(X)             # la culpa de la capa de salida\n",
    "dW2 = h.T @ d2                    # cómo cambiar sus pesos\n",
    "db2 = d2.sum(axis=0)\n",
    "\n",
    "d1 = (d2 @ W2.T) * (1 - h ** 2)   # la culpa que le llega a la oculta\n",
    "dW1 = X.T @ d1                    # cómo cambiar los suyos\n",
    "db1 = d1.sum(axis=0)\n",
    "\n",
    "print('pérdida:', round(perdida(p, y), 6))\n",
    "print('formas :', dW1.shape, db1.shape, dW2.shape, db2.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Son seis líneas y merecen que las leas una por una 👀\n",
    "\n",
    "**`d2 = (p - y) / len(X)`**. La culpa de la última\n",
    "capa, que es el error a secas. Salió del capítulo 5.\n",
    "\n",
    "**`dW2 = h.T @ d2`**. Para saber cuánto cambiar un\n",
    "peso, se multiplica la culpa por *lo que entró* por ese peso. Un peso que\n",
    "recibió un número grande tiene más responsabilidad.\n",
    "\n",
    "**`d1 = (d2 @ W2.T) * (1 - h ** 2)`**. Esta es la\n",
    "línea del capítulo. Tiene dos partes:\n",
    "\n",
    "- 📨 `d2 @ W2.T` reparte la culpa de la salida entre las cuatro\n",
    "neuronas ocultas, en proporción al peso con que cada una contribuyó.\n",
    "\n",
    "- 🎚️ `* (1 - h ** 2)` es la pendiente de la tanh, y ajusta esa\n",
    "culpa por cuánto podía esa neurona haber cambiado algo.\n",
    "\n",
    "Ahí está todo. Una neurona saturada tiene pendiente casi cero, así que su\n",
    "parte de la culpa se multiplica por casi cero y no aprende. **Es\n",
    "literalmente la misma multiplicación de la que hablábamos en el capítulo 3**,\n",
    "ahora escrita 🎯\n",
    "\n",
    "Y `dW1 = X.T @ d1` es la misma forma que `dW2`: la culpa\n",
    "por lo que entró. El patrón se repite igual por cada capa que añadas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Y cómo sabemos que está bien?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Igual que en el capítulo 5, pero ahora para los cuatro grupos de pesos:\n",
    "moviendo cada uno a mano y midiendo cuánto cambia la pérdida."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def adelante(W1, b1, W2, b2):\n",
    "    hh = np.tanh(X @ W1 + b1)\n",
    "    return perdida(sigmoide(hh @ W2 + b2), y)\n",
    "\n",
    "def gradiente_numerico(parametro, cual, eps=1e-6):\n",
    "    g = np.zeros_like(parametro)\n",
    "    it = np.nditer(parametro, flags=['multi_index'])\n",
    "    for _ in it:\n",
    "        i = it.multi_index\n",
    "        arriba = parametro.copy(); arriba[i] += eps\n",
    "        abajo = parametro.copy(); abajo[i] -= eps\n",
    "        args = {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}\n",
    "        args[cual] = arriba; La = adelante(**args)\n",
    "        args[cual] = abajo; Lb = adelante(**args)\n",
    "        g[i] = (La - Lb) / (2 * eps)\n",
    "    return g\n",
    "\n",
    "for nombre, analitico, original in [('W2', dW2, W2), ('b2', db2, b2),\n",
    "                                    ('W1', dW1, W1), ('b1', db1, b1)]:\n",
    "    n = gradiente_numerico(original, nombre)\n",
    "    print(f'{nombre}: mayor diferencia {float(np.abs(analitico - n).max()):.3e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los cuatro coinciden hasta 1e-10 🎉\n",
    "\n",
    "Esto es lo que quería que tuvieras: **no hay que creerse la\n",
    "retropropagación, se comprueba**. Y cuando escribas una capa rara y no te\n",
    "salgan las cuentas, este es el método que te dice si el fallo está en la derivada\n",
    "o en otro sitio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El gradiente desvanecido, por fin medido"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 3 dije que las pendientes se multiplican entre capas y que con\n",
    "sigmoides se apagan. Vamos a verlo de verdad, midiendo qué tamaño tiene el\n",
    "gradiente en cada capa según la profundidad:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gradientes_por_profundidad(n_capas, activacion):\n",
    "    r = np.random.default_rng(1)\n",
    "    Xg = np.random.default_rng(0).normal(0, 1, (200, 4))\n",
    "    yg = (np.random.default_rng(0).random(200) < 0.5).astype(float).reshape(-1, 1)\n",
    "\n",
    "    Ws = [r.normal(0, 1, (4, 4)) for _ in range(n_capas)] + [r.normal(0, 1, (4, 1))]\n",
    "    bs = [np.zeros(4) for _ in range(n_capas)] + [np.zeros(1)]\n",
    "\n",
    "    hs = [Xg]\n",
    "    for W, b in zip(Ws[:-1], bs[:-1]):\n",
    "        z = hs[-1] @ W + b\n",
    "        hs.append(sigmoide(z) if activacion == 'sigmoide' else np.maximum(0, z))\n",
    "\n",
    "    pg = sigmoide(hs[-1] @ Ws[-1] + bs[-1])\n",
    "    d = (pg - yg) / len(Xg)\n",
    "\n",
    "    tamanos = []\n",
    "    for i in range(len(Ws) - 1, -1, -1):\n",
    "        tamanos.append(float(np.abs(hs[i].T @ d).mean()))\n",
    "        if i > 0:\n",
    "            if activacion == 'sigmoide':\n",
    "                d = (d @ Ws[i].T) * hs[i] * (1 - hs[i])\n",
    "            else:\n",
    "                d = (d @ Ws[i].T) * (hs[i] > 0)\n",
    "    return tamanos[::-1]\n",
    "\n",
    "for activacion in ['sigmoide', 'relu']:\n",
    "    for capas in [2, 5, 10]:\n",
    "        g = gradientes_por_profundidad(capas, activacion)\n",
    "        print(f'{activacion:9} {capas:2d} capas: primera {g[0]:.3e}   última {g[-1]:.3e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con sigmoide y diez capas, el gradiente que llega a la primera es\n",
    "**7,604e-08** y el de la última es **3,802e-02**. Son\n",
    "quinientas mil veces de diferencia 😱\n",
    "\n",
    "Traducido: mientras la última capa aprende a paso normal, la primera se mueve\n",
    "quinientas mil veces más despacio. Con cualquier paso razonable, esa primera\n",
    "capa no cambia nunca. Está ahí, ocupa memoria, y sigue con los pesos\n",
    "aleatorios del principio.\n",
    "\n",
    "Con ReLU y diez capas: 4,330e-04 en la primera y 1,895e-04 en la última. El\n",
    "mismo orden de magnitud, y encima la primera sale *mayor*. La primera capa\n",
    "aprende igual que la última 🎉\n",
    "\n",
    "Eso es exactamente lo que destrabó el campo. No hizo falta un algoritmo nuevo:\n",
    "hizo falta cambiar una función por otra más tonta cuya pendiente no encoge.\n",
    "\n",
    "Y fíjate en un detalle honesto de la tabla: con dos capas la sigmoide da\n",
    "2,336e-03 en la primera, que comparado con el 1,375e-01 de la última son dos\n",
    "órdenes de magnitud y no cinco. **El problema no es la\n",
    "sigmoide, es la sigmoide con profundidad**. Por eso funcionaba bien en los\n",
    "años 90, cuando las redes tenían dos capas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Entrenar la red con esas seis líneas\n",
    "\n",
    "Mete el bucle alrededor y mira bajar la pérdida."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A1 = rng.normal(0, 0.5, (3, 4)); c1 = np.zeros(4)\n",
    "A2 = rng.normal(0, 0.5, (4, 1)); c2 = np.zeros(1)\n",
    "\n",
    "for i in range(2001):\n",
    "    hh = np.tanh(X @ A1 + c1)\n",
    "    pp = sigmoide(hh @ A2 + c2)\n",
    "    if i % 500 == 0:\n",
    "        print(f'vuelta {i:4d}  pérdida {perdida(pp, y):.4f}')\n",
    "    e2 = (pp - y) / len(X)\n",
    "    e1 = (e2 @ A2.T) * (1 - hh ** 2)\n",
    "    A2 -= 0.5 * (hh.T @ e2); c2 -= 0.5 * e2.sum(axis=0)\n",
    "    A1 -= 0.5 * (X.T @ e1); c1 -= 0.5 * e1.sum(axis=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "vuelta    0  pérdida 0.7716\n",
    "vuelta  500  pérdida 0.4163\n",
    "vuelta 1000  pérdida 0.3791\n",
    "vuelta 1500  pérdida 0.3576\n",
    "vuelta 2000  pérdida 0.3239\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Baja de 0,7716 a 0,3239 y sigue bajando. Y ahora fíjate en lo importante:\n",
    "el `y` de este ejemplo lo generé **tirando una moneda**.\n",
    "No hay absolutamente nada que aprender.\n",
    "\n",
    "O sea que esa pérdida que baja tan bonito es memorización pura. Con 50 filas y\n",
    "21 pesos, la red se aprende el ruido de carrerilla, y una curva de entrenamiento\n",
    "preciosa no significa nada por sí sola.\n",
    "\n",
    "Ese es el capítulo 8 en un ejercicio, y es la razón de que exista el conjunto\n",
    "de prueba 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Ver la culpa que le llega a cada neurona\n",
    "\n",
    "Imprime `d1` y mira su forma y su tamaño."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('forma de d1:', d1.shape, ' (una fila por dato, una columna por neurona)')\n",
    "print()\n",
    "print('culpa media que recibe cada una de las 4 neuronas ocultas:')\n",
    "print(np.round(np.abs(d1).mean(axis=0), 6))\n",
    "print()\n",
    "print('culpa media de la capa de salida:', round(float(np.abs(d2).mean()), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "forma de d1: (50, 4)  (una fila por dato, una columna por neurona)\n",
    "\n",
    "culpa media que recibe cada una de las 4 neuronas ocultas:\n",
    "[0.003179 0.001939 0.000598 0.001484]\n",
    "\n",
    "culpa media de la capa de salida: 0.009784\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada neurona oculta recibe una cantidad de culpa distinta, según con qué peso\n",
    "contribuyó a la salida.\n",
    "\n",
    "Y fíjate en la comparación con la de salida: ya en una sola capa hacia atrás\n",
    "la culpa se hizo más pequeña. Multiplica eso diez veces y tienes el problema de\n",
    "arriba 📉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Qué pasa si te olvidas la pendiente\n",
    "\n",
    "Quita el `* (1 - h ** 2)` y compara el gradiente\n",
    "con el numérico."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "d1_mal = d2 @ W2.T                 # sin multiplicar por la pendiente\n",
    "dW1_mal = X.T @ d1_mal\n",
    "\n",
    "correcto = gradiente_numerico(W1, 'W1')\n",
    "print('con pendiente, error:', float(np.abs(dW1 - correcto).max()))\n",
    "print('sin pendiente, error:', float(np.abs(dW1_mal - correcto).max()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "con pendiente, error: 1.1404513444723818e-10\n",
    "sin pendiente, error: 0.0312959323249657\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El correcto se equivoca en 1,1e-10 y el otro en 0,0313, o sea trescientos\n",
    "millones de veces más.\n",
    "\n",
    "Y esto no da ningún error: la red entrena, la pérdida baja algo, y los pesos\n",
    "van a un sitio que no es. Es el tipo de fallo que solo caza el gradient checking,\n",
    "y por eso existe 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Tres capas, el mismo patrón\n",
    "\n",
    "Añade una capa y comprueba que la retropropagación es la\n",
    "misma línea repetida."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "V1 = rng.normal(0, 0.5, (3, 4)); e1b = np.zeros(4)\n",
    "V2 = rng.normal(0, 0.5, (4, 4)); e2b = np.zeros(4)\n",
    "V3 = rng.normal(0, 0.5, (4, 1)); e3b = np.zeros(1)\n",
    "\n",
    "k1 = np.tanh(X @ V1 + e1b)\n",
    "k2 = np.tanh(k1 @ V2 + e2b)\n",
    "q = sigmoide(k2 @ V3 + e3b)\n",
    "\n",
    "g3 = (q - y) / len(X)\n",
    "g2 = (g3 @ V3.T) * (1 - k2 ** 2)\n",
    "g1 = (g2 @ V2.T) * (1 - k1 ** 2)\n",
    "\n",
    "print('culpa media en la capa 3 (salida):', round(float(np.abs(g3).mean()), 8))\n",
    "print('culpa media en la capa 2         :', round(float(np.abs(g2).mean()), 8))\n",
    "print('culpa media en la capa 1         :', round(float(np.abs(g1).mean()), 8))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "culpa media en la capa 3 (salida): 0.01017376\n",
    "culpa media en la capa 2         : 0.0026545\n",
    "culpa media en la capa 1         : 0.0011628\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una línea por capa, siempre igual: reparte con la matriz de la siguiente y\n",
    "multiplica por la pendiente de esta.\n",
    "\n",
    "Ese patrón se repite tanto que se acabó automatizando, y a eso se le llama\n",
    "diferenciación automática. Es lo que hacen PyTorch y TensorFlow por dentro, y es\n",
    "básicamente esta línea con contabilidad 🧾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La sigmoide es peor que tanh incluso con dos capas\n",
    "\n",
    "Compara las pendientes máximas de las dos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('pendiente máxima de la sigmoide:', 0.25)\n",
    "print('pendiente máxima de tanh       :', 1.0)\n",
    "print()\n",
    "for capas in [2, 5, 10, 20]:\n",
    "    print(f'{capas:2d} capas: sigmoide {0.25 ** capas:.3e}   tanh {1.0 ** capas:.3e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "pendiente máxima de la sigmoide: 0.25\n",
    "pendiente máxima de tanh       : 1.0\n",
    "\n",
    " 2 capas: sigmoide 6.250e-02   tanh 1.000e+00\n",
    " 5 capas: sigmoide 9.766e-04   tanh 1.000e+00\n",
    "10 capas: sigmoide 9.537e-07   tanh 1.000e+00\n",
    "20 capas: sigmoide 9.095e-13   tanh 1.000e+00\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por eso en los años 90, cuando todavía no existía ReLU, la recomendación era\n",
    "usar tanh en vez de sigmoide en las capas del medio.\n",
    "\n",
    "Con la sigmoide reservada para la salida, que es donde sí la quieres porque\n",
    "devuelve algo entre 0 y 1. Cada función en su sitio 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuánto cuesta comprobar el gradiente\n",
    "\n",
    "Cuenta las evaluaciones que hace el gradient checking."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pesos_totales = W1.size + b1.size + W2.size + b2.size\n",
    "print('pesos de esta red diminuta:', pesos_totales)\n",
    "print('evaluaciones para comprobar:', pesos_totales * 2)\n",
    "print()\n",
    "print('en una red con un millón de pesos serían:', 1_000_000 * 2, 'evaluaciones')\n",
    "print('mientras que la retropropagación cuesta como UNA pasada hacia adelante')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "pesos de esta red diminuta: 21\n",
    "evaluaciones para comprobar: 42\n",
    "\n",
    "en una red con un millón de pesos serían: 2000000 evaluaciones\n",
    "mientras que la retropropagación cuesta como UNA pasada hacia adelante\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuarenta y dos evaluaciones para una red de 21 pesos, y dos millones para una\n",
    "de un millón.\n",
    "\n",
    "Ahí está por qué la retropropagación fue un hallazgo y no una obviedad:\n",
    "**calcula todos los gradientes de golpe por el precio de una pasada**.\n",
    "Sin ella, entrenar redes grandes sería imposible, y el gradient checking se queda\n",
    "como herramienta de depuración sobre un puñado de pesos 🧮"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Soltarla sobre las ventas de la distribuidora\n",
    "\n",
    "Entrena la red de dos capas, escrita por nosotras, sobre el\n",
    "CSV de verdad. Es un adelanto del capítulo 7."
   ]
  },
  {
   "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",
    "ventas = carga_limpia(URL).sort_values(['cliente_id', 'fecha']).copy()\n",
    "ventas['sin_compra_previa'] = ventas['fecha_ultima_compra'].isna().astype(int)\n",
    "ventas['sin_descuento'] = ventas['descuento'].isna().astype(int)\n",
    "ventas['sin_satisfaccion'] = ventas['satisfaccion'].isna().astype(int)\n",
    "ventas['precio_unitario'] = ventas['monto'] / ventas['unidades']\n",
    "ventas['visita_numero'] = ventas.groupby('cliente_id').cumcount() + 1\n",
    "\n",
    "NUM = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "       'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CAT = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "Xv_tr, Xv_te, yv_tr, yv_te = train_test_split(\n",
    "    ventas[NUM + CAT], ventas['compro'], test_size=0.25,\n",
    "    random_state=42, stratify=ventas['compro'])\n",
    "\n",
    "logistica = Pipeline([\n",
    "    ('pre', ColumnTransformer([\n",
    "        ('num', Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                          ('e', StandardScaler())]), NUM),\n",
    "        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CAT),\n",
    "    ])),\n",
    "    ('mod', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "]).fit(Xv_tr, yv_tr)\n",
    "\n",
    "Tv_tr = logistica.named_steps['pre'].transform(Xv_tr)\n",
    "Tv_te = logistica.named_steps['pre'].transform(Xv_te)\n",
    "obj = yv_tr.values.astype(float).reshape(-1, 1)\n",
    "\n",
    "r = np.random.default_rng(0)\n",
    "M1 = r.normal(0, 0.1, (Tv_tr.shape[1], 8)); n1 = np.zeros(8)\n",
    "M2 = r.normal(0, 0.1, (8, 1)); n2 = np.zeros(1)\n",
    "\n",
    "for _ in range(2000):\n",
    "    hh = np.tanh(Tv_tr @ M1 + n1)\n",
    "    pp = sigmoide(hh @ M2 + n2)\n",
    "    f2 = (pp - obj) / len(obj)\n",
    "    f1 = (f2 @ M2.T) * (1 - hh ** 2)\n",
    "    M2 -= 0.5 * (hh.T @ f2); n2 -= 0.5 * f2.sum(axis=0)\n",
    "    M1 -= 0.5 * (Tv_tr.T @ f1); n1 -= 0.5 * f1.sum(axis=0)\n",
    "\n",
    "pred = sigmoide(np.tanh(Tv_te @ M1 + n1) @ M2 + n2).ravel()\n",
    "print('nuestra red de 2 capas:', round(roc_auc_score(yv_te, pred), 4))\n",
    "print('logística de sklearn  :', round(roc_auc_score(\n",
    "    yv_te, logistica.predict_proba(Xv_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "nuestra red de 2 capas: 0.6651\n",
    "logística de sklearn  : 0.7214\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Funciona: 0,6651 con una red que escribimos nosotras, sobre las ventas de\n",
    "verdad, con sus nulos y su OneHotEncoder 🎉\n",
    "\n",
    "Y pierde contra la logística, que saca 0,7214. Que es exactamente lo que el\n",
    "capítulo 1 anunció y lo que el capítulo 4 explicó: en estos datos no hay ninguna\n",
    "esquina cruzada, así que la capa oculta no tiene dónde ayudar y solo añade\n",
    "2.000 vueltas de tropiezos.\n",
    "\n",
    "Lo importante de este ejercicio no es el número: es que ya **puedes\n",
    "entrenar una red sobre datos reales sin importar ninguna librería de deep\n",
    "learning**. En el capítulo 7 la ponemos a punto 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. El error de retropropagar en el orden equivocado\n",
    "\n",
    "Calcula `d1` antes que `d2`, que es\n",
    "lo que sale si escribes el bloque de arriba abajo sin pensar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "del d2\n",
    "d1_temprano = (d2 @ W2.T) * (1 - h ** 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "NameError: name 'd2' is not defined\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Obvio dicho así, y a la vez es el fallo conceptual más común cuando alguien\n",
    "escribe su primera red: **hacia atrás significa hacia atrás**. La\n",
    "última capa se calcula primero porque es la única que sabe algo sin depender de\n",
    "nadie.\n",
    "\n",
    "Si te sale este error, casi siempre es que ordenaste el bloque como el de\n",
    "adelante 🔄"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Con diez capas de sigmoide, el gradiente que llega a la primera es 7,6e-08 y el de la última 3,8e-02. ¿Qué consecuencia tiene?\n",
    "\n",
    "a) Que la primera capa casi no aprende, por muchas vueltas que des\n",
    "\n",
    "b) Que hay que bajar la tasa de aprendizaje\n",
    "\n",
    "c) Que la red está mal inicializada\n",
    "\n",
    "d) Que hacen falta más datos\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Bajarla encoge todos los pasos, incluidos los que ya eran diminutos.\n",
    "\n",
    "*c)* La inicialización ayuda, pero el problema está en la derivada de la sigmoide, que nunca pasa de 0,25.\n",
    "\n",
    "*d)* Con más datos el gradiente sigue encogiéndose igual al bajar de capa.\n",
    "\n",
    "Con ReLU los gradientes de las diez capas quedan en el mismo orden de magnitud."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el gradiente que crece con el lote"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Sumas los gradientes de todas las filas del lote, que es lo que dice la fórmula, y das el paso. Entrena, la pérdida baja, todo normal.\n",
    "\n",
    "```\n",
    "dW = np.zeros_like(W)\n",
    "for x, objetivo in lote:\n",
    "    dW += gradiente(x, objetivo)\n",
    "\n",
    "W -= lr * dW\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Falta dividir entre el tamaño del lote 🧮 Sumando, el gradiente crece con el número de filas, así que tu paso real no es `lr`: es `lr` multiplicado por cuántas filas metiste.\n",
    "\n",
    "Con lotes de 32 funciona, subes a 256 para ir más rápido y de pronto la pérdida explota. Vas a pasarte una tarde buscando el error en la red, y el error está en que acabas de multiplicar por ocho la velocidad de aprendizaje sin tocar `lr`.\n",
    "\n",
    "Se arregla con `dW / len(lote)`, y entonces el paso significa lo mismo con cualquier tamaño de lote. Es la diferencia entre `sum` y `mean`, y explica una cantidad de horas perdidas que no te imaginas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 📦 La última capa sabe su error directamente; cada capa anterior lo recibe a\n",
    "través de la siguiente.\n",
    "\n",
    "- 🧮 Son seis líneas, y el patrón se repite igual por cada capa que añadas.\n",
    "\n",
    "- 🎚️ `d1 = (d2 @ W2.T) * pendiente`: repartir y ajustar por cuánto\n",
    "podía esa neurona cambiar algo.\n",
    "\n",
    "- 🔬 Los cuatro gradientes coinciden con el numérico a 1e-10. No hay que\n",
    "creérselo, se comprueba.\n",
    "\n",
    "- 😱 Con diez capas de sigmoide, el gradiente de la primera es 500.000 veces\n",
    "menor que el de la última.\n",
    "\n",
    "- 🎉 Con ReLU y diez capas están en el mismo orden de magnitud.\n",
    "\n",
    "- 🕰️ El problema no es la sigmoide: es la sigmoide con profundidad. Con dos\n",
    "capas va bien, y por eso funcionaba en los años 90.\n",
    "\n",
    "- 🧾 Comprobar el gradiente cuesta dos evaluaciones por peso; la\n",
    "retropropagación los saca todos por el precio de una pasada.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La retropropagación no es magia. Es contabilidad: cada capa cobra la culpa que le llega de la siguiente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las derivadas y la regla de la cadena que hay debajo de esto las trato despacio en el [libro de estadística desde cero](https://missyera.com/guias/estadistica-desde-cero/) 📐\n",
    "\n",
    "En el capítulo 7 juntamos todo y entrenamos una red completa sobre las ventas\n",
    "de la distribuidora, escrita por nosotras de principio a fin.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 6 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/retropropagacion/\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
}
