{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# La regla de la cadena es la retropropagación\n",
    "\n",
    "Qué dice la regla de la cadena, cómo se aplica eslabón por eslabón sobre una regresión logística, y por qué eso es exactamente lo que hace una red.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 15 de **Matemática para machine learning**, 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/matematica-para-machine-learning/la-regla-de-la-cadena/\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": [
    "## Antes de empezar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# Este capitulo abre el archivo por su nombre, sin URL, que es lo que se hace\n",
    "# cuando lo tienes al lado. Aqui se baja primero para que exista.\n",
    "for nombre in ['ventas-miss-yera.csv']:\n",
    "    urllib.request.urlretrieve(f\"https://missyera.com/static/datasets/{nombre}\", nombre)\n",
    "    print(nombre, \"lista\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El capítulo que explica la retropropagación 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te lo digo de entrada para que sepas dónde estás: **la\n",
    "retropropagación no es un algoritmo aparte**. Es la regla de la cadena,\n",
    "que es una regla de derivadas de toda la vida, aplicada con orden.\n",
    "\n",
    "Cuando termines este capítulo, \"Repartir la culpa hacia atrás\" de mi libro de\n",
    "deep learning se lee distinto."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué dice"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si una cosa depende de otra que depende de otra, la derivada del todo es el\n",
    "producto de las derivadas de cada paso."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "dLdw=dLdp·dpdz·dzdw\n",
    "\n",
    "la regla de la cadena dice que la derivada de algo encadenado es el producto de las derivadas de cada eslabón, cada una calculada donde le toca"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El ejemplo de siempre: si un coche va al doble de rápido que otro, y ese otro\n",
    "al triple que un tercero, el primero va seis veces más rápido que el tercero. Se\n",
    "multiplica."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Volvemos a abrir el archivo, con la columna que se predice"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Acá necesito una columna binaria, así que meto `compro`, que vale\n",
    "1 si la venta se cerró y 0 si no."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "df = pd.read_csv('ventas-miss-yera.csv')\n",
    "for c in ('unidades', 'monto', 'satisfaccion'):\n",
    "    df[c] = pd.to_numeric(df[c], errors='coerce')\n",
    "\n",
    "d = df[['unidades', 'monto', 'satisfaccion', 'compro']].dropna()\n",
    "X = d[['unidades', 'monto', 'satisfaccion']].to_numpy()\n",
    "t = d['compro'].to_numpy().astype(float)\n",
    "\n",
    "Z = (X - X.mean(axis=0)) / X.std(axis=0)\n",
    "A = np.column_stack([np.ones(len(Z)), Z])\n",
    "print('A.shape:', A.shape)\n",
    "print('compro 1:', int(t.sum()), '| compro 0:', int((1 - t).sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La cadena de tres eslabones"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una regresión logística hace tres cosas seguidas:\n",
    "\n",
    "- **z**: el producto punto del capítulo 4, que da un\n",
    "número sin límites.\n",
    "\n",
    "- **p**: pasarlo por la sigmoide, que lo aplasta entre 0 y 1 para\n",
    "que se pueda leer como probabilidad.\n",
    "\n",
    "- **L**: la pérdida, que compara esa probabilidad con lo que pasó\n",
    "de verdad."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\n",
    "\n",
    "w = np.array([0.0, 0.1, -0.2, 0.3])\n",
    "z = A @ w\n",
    "p = sigmoide(z)\n",
    "L = float(-(t * np.log(p) + (1 - t) * np.log(1 - p)).mean())\n",
    "\n",
    "print('z[:3]  :', np.round(z[:3], 4))\n",
    "print('p[:3]  :', np.round(p[:3], 4))\n",
    "print('perdida:', round(L, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres eslabones: los pesos hacen la z, la z hace la p, la p hace la pérdida. Y\n",
    "lo que quiero saber es cuánto cambia la pérdida si muevo un peso, o sea el\n",
    "gradiente del capítulo 14."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bajamos por la cadena, eslabón por eslabón"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dL_dp = (-(t / p) + (1 - t) / (1 - p)) / len(t)\n",
    "dp_dz = p * (1 - p)\n",
    "dL_dz = dL_dp * dp_dz\n",
    "grad = A.T @ dL_dz\n",
    "\n",
    "print('gradiente por la cadena:', np.round(grad, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo de arriba abajo, que es como se hace de verdad:\n",
    "\n",
    "- `dL_dp`: cuánto cambia la pérdida si cambia la probabilidad.\n",
    "\n",
    "- `dp_dz`: cuánto cambia la probabilidad si cambia z. Esa es la\n",
    "derivada de la sigmoide.\n",
    "\n",
    "- `dL_dz`: los dos multiplicados. Primer eslabón encadenado.\n",
    "\n",
    "- `A.T @ dL_dz`: el último eslabón, que es cuánto cambia z si\n",
    "cambia el peso, y como z es un producto punto, eso son los propios datos.\n",
    "\n",
    "Eso es retropropagación. Empiezas por el final, calculas la derivada de un\n",
    "eslabón, la multiplicas por lo que traías, y sigues bajando 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "σ′(z)=σ(z)(1−σ(z))\n",
    "\n",
    "la derivada de la sigmoide se escribe con la propia sigmoide, y vale como mucho un cuarto, justo en el medio"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las tres comprobaciones"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "corto = A.T @ (p - t) / len(t)\n",
    "print('con la formula corta:', np.round(corto, 6))\n",
    "print('iguales:', np.allclose(grad, corto))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa fórmula corta es la que sale en los libros: predicción menos realidad, por\n",
    "los datos. Parece magia y no lo es: es la cadena de arriba simplificada a mano.\n",
    "Los términos de la pérdida y de la sigmoide se cancelan.\n",
    "\n",
    "Por eso la sigmoide y esa pérdida van juntas. No es costumbre, es que al\n",
    "juntarlas la cadena se simplifica 🌟"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "h = 1e-6\n",
    "def perdida(ww):\n",
    "    pp = sigmoide(A @ ww)\n",
    "    return float(-(t * np.log(pp) + (1 - t) * np.log(1 - pp)).mean())\n",
    "\n",
    "numerico = []\n",
    "for i in range(len(w)):\n",
    "    mas, menos = w.copy(), w.copy()\n",
    "    mas[i] += h\n",
    "    menos[i] -= h\n",
    "    numerico.append((perdida(mas) - perdida(menos)) / (2 * h))\n",
    "\n",
    "print('gradiente numerico  :', np.round(numerico, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres caminos, el mismo vector. El gradient checking del capítulo 13 otra vez, y ahora sobre algo que sí se parece a un\n",
    "modelo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora el problema que hundió al deep learning veinte años"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for z_ in (0.0, 5.0, -5.0, 10.0):\n",
    "    s = sigmoide(z_)\n",
    "    print('z =', z_, '-> derivada', round(float(s * (1 - s)), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La derivada de la sigmoide vale **como mucho 0.25**, y eso solo\n",
    "justo en el medio. En cuanto te alejas se desploma.\n",
    "\n",
    "Ahora acuérdate de que la cadena **multiplica**. Diez capas con\n",
    "sigmoide, cada una aportando 0.0066, dejan un gradiente de diez elevado a menos\n",
    "veintiuno.\n",
    "\n",
    "Traducido: **las primeras capas no aprenden nada**. El gradiente\n",
    "que les llega es cero para todos los efectos.\n",
    "\n",
    "Eso se llama el problema del gradiente que se desvanece, y fue lo que tuvo\n",
    "frenadas a las redes profundas durante años. La solución que se acabó imponiendo\n",
    "es la ReLU, cuya derivada vale uno pelado en la mitad positiva, así que la cadena\n",
    "no la apaga. Toda esa historia está contada en mi libro de deep learning; acá\n",
    "solo quería que vieras **de dónde sale el número** 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- La regla de la cadena multiplica las derivadas de cada eslabón.\n",
    "\n",
    "- La retropropagación es esa regla, empezando por el final.\n",
    "\n",
    "- La derivada de la sigmoide se escribe con la propia sigmoide.\n",
    "\n",
    "- La fórmula corta \"predicción menos realidad\" es la cadena simplificada.\n",
    "\n",
    "- La sigmoide y su pérdida van juntas porque al juntarlas se cancelan cosas.\n",
    "\n",
    "- Factores menores que uno multiplicados muchas veces apagan el gradiente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "En una red profunda, la derivada de la sigmoide vale 0.0066 en varias capas seguidas. ¿Qué le pasa al gradiente que llega a las primeras?\n",
    "\n",
    "a) Se apaga, porque la cadena multiplica números chiquitos una y otra vez\n",
    "\n",
    "b) Se dispara, porque hay muchos factores acumulados\n",
    "\n",
    "c) Se queda igual, porque la regla de la cadena suma en vez de multiplicar\n",
    "\n",
    "d) No le pasa nada, porque cada capa calcula su gradiente por separado\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Se dispararía si los factores fueran mayores que uno. Con 0.0066 pasa lo contrario: cada capa lo divide por unas 150.\n",
    "\n",
    "*c)* La regla de la cadena multiplica. Si sumara, no habría problema de gradientes que se apagan y la historia del deep learning sería otra.\n",
    "\n",
    "*d)* No es por separado: el de una capa se calcula a partir del de la siguiente, y por eso arrastra todos los factores de arriba.\n",
    "\n",
    "Diez capas con 0.0066 dejan un gradiente de diez elevado a menos veintiuno. Ese es el problema del gradiente que se desvanece, y es la razón de que hoy se use ReLU 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. La cadena con números a mano\n",
    "\n",
    "Una sola fila, tres eslabones, sin vectores."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\n",
    "\n",
    "z0 = 0.2637\n",
    "p0 = sigmoide(z0)\n",
    "t0 = 1.0\n",
    "\n",
    "dL_dp = -(t0 / p0)\n",
    "dp_dz = p0 * (1 - p0)\n",
    "print('dL_dp    :', round(dL_dp, 6))\n",
    "print('dp_dz    :', round(dp_dz, 6))\n",
    "print('dL_dz    :', round(dL_dp * dp_dz, 6))\n",
    "print('directo  :', round(p0 - t0, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "dL_dp    : -1.768204\n",
    "dp_dz    : 0.245704\n",
    "dL_dz    : -0.434454\n",
    "directo  : -0.434454\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos derivadas feas multiplicadas dan una resta simple. Cuando en un paper\n",
    "veas que la derivada de la logística es \"p menos t\", ya sabes qué se canceló\n",
    "para llegar ahí."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Diez capas de sigmoide\n",
    "\n",
    "Multiplica el factor diez veces y mira el desastre."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "factor = 0.006648\n",
    "g = 1.0\n",
    "for capa in range(1, 11):\n",
    "    g = g * factor\n",
    "    if capa in (1, 3, 5, 10):\n",
    "        print('capa', capa, '-> gradiente', g)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "capa 1 -> gradiente 0.006648\n",
    "capa 3 -> gradiente 2.9381436979200003e-07\n",
    "capa 5 -> gradiente 1.2985391681147732e-11\n",
    "capa 10 -> gradiente 1.6862039711282078e-22\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Diez elevado a menos veintidós. En coma flotante eso sigue siendo un número,\n",
    "pero al sumárselo a un peso no cambia ni el último decimal. La capa está viva y\n",
    "congelada a la vez."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La ReLU no apaga nada\n",
    "\n",
    "Compara las dos derivadas encadenadas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def d_relu(z):\n",
    "    return 1.0 if z > 0 else 0.0\n",
    "\n",
    "def d_sig(z):\n",
    "    s = 1 / (1 + np.exp(-z))\n",
    "    return s * (1 - s)\n",
    "\n",
    "gs, gr = 1.0, 1.0\n",
    "for _ in range(10):\n",
    "    gs = gs * d_sig(5.0)\n",
    "    gr = gr * d_relu(5.0)\n",
    "print('sigmoide, 10 capas:', gs)\n",
    "print('relu, 10 capas    :', gr)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sigmoide, 10 capas: 1.6863477168817078e-22\n",
    "relu, 10 capas    : 1.0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Uno pelado después de diez capas. Ese es el motivo entero de que la ReLU se\n",
    "comiera el mundo, y ahora ya sabes que no fue una corazonada: fue una\n",
    "multiplicación. Ojo con la otra cara, que la ReLU apaga del todo la mitad\n",
    "negativa, y ese es otro problema con su propio nombre."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 15 de **Matemática para machine learning**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/matematica-para-machine-learning/la-regla-de-la-cadena/\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
}
