{
 "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 práctica 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",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"dnVlbHRhICAgIDAgIHDDqXJkaWRhIDAuNzcxNgp2dWVsdGEgIDUwMCAgcMOpcmRpZGEgMC40MTYzCnZ1ZWx0YSAxMDAwICBww6lyZGlkYSAwLjM3OTEKdnVlbHRhIDE1MDAgIHDDqXJkaWRhIDAuMzU3Ngp2dWVsdGEgMjAwMCAgcMOpcmRpZGEgMC4zMjM5\",\n",
    "    2: \"Zm9ybWEgZGUgZDE6ICg1MCwgNCkgICh1bmEgZmlsYSBwb3IgZGF0bywgdW5hIGNvbHVtbmEgcG9yIG5ldXJvbmEpCgpjdWxwYSBtZWRpYSBxdWUgcmVjaWJlIGNhZGEgdW5hIGRlIGxhcyA0IG5ldXJvbmFzIG9jdWx0YXM6ClswLjAwMzE3OSAwLjAwMTkzOSAwLjAwMDU5OCAwLjAwMTQ4NF0KCmN1bHBhIG1lZGlhIGRlIGxhIGNhcGEgZGUgc2FsaWRhOiAwLjAwOTc4NA==\",\n",
    "    3: \"Y29uIHBlbmRpZW50ZSwgZXJyb3I6IDEuMTQwNDUxMzQ0NDcyMzgxOGUtMTAKc2luIHBlbmRpZW50ZSwgZXJyb3I6IDAuMDMxMjk1OTMyMzI0OTY1Nw==\",\n",
    "    4: \"Y3VscGEgbWVkaWEgZW4gbGEgY2FwYSAzIChzYWxpZGEpOiAwLjAxMDE3Mzc2CmN1bHBhIG1lZGlhIGVuIGxhIGNhcGEgMiAgICAgICAgIDogMC4wMDI2NTQ1CmN1bHBhIG1lZGlhIGVuIGxhIGNhcGEgMSAgICAgICAgIDogMC4wMDExNjI4\",\n",
    "    5: \"cGVuZGllbnRlIG3DoXhpbWEgZGUgbGEgc2lnbW9pZGU6IDAuMjUKcGVuZGllbnRlIG3DoXhpbWEgZGUgdGFuaCAgICAgICA6IDEuMAoKIDIgY2FwYXM6IHNpZ21vaWRlIDYuMjUwZS0wMiAgIHRhbmggMS4wMDBlKzAwCiA1IGNhcGFzOiBzaWdtb2lkZSA5Ljc2NmUtMDQgICB0YW5oIDEuMDAwZSswMAoxMCBjYXBhczogc2lnbW9pZGUgOS41MzdlLTA3ICAgdGFuaCAxLjAwMGUrMDAKMjAgY2FwYXM6IHNpZ21vaWRlIDkuMDk1ZS0xMyAgIHRhbmggMS4wMDBlKzAw\",\n",
    "    6: \"cGVzb3MgZGUgZXN0YSByZWQgZGltaW51dGE6IDIxCmV2YWx1YWNpb25lcyBwYXJhIGNvbXByb2JhcjogNDIKCmVuIHVuYSByZWQgY29uIHVuIG1pbGzDs24gZGUgcGVzb3Mgc2Vyw61hbjogMjAwMDAwMCBldmFsdWFjaW9uZXMKbWllbnRyYXMgcXVlIGxhIHJldHJvcHJvcGFnYWNpw7NuIGN1ZXN0YSBjb21vIFVOQSBwYXNhZGEgaGFjaWEgYWRlbGFudGU=\",\n",
    "    7: \"bnVlc3RyYSByZWQgZGUgMiBjYXBhczogMC42NjUxCmxvZ8Otc3RpY2EgZGUgc2tsZWFybiAgOiAwLjcyMTQ=\",\n",
    "    8: \"TmFtZUVycm9yOiBuYW1lICdkMicgaXMgbm90IGRlZmluZWQ=\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "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": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "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": [
    "%%revisa 8\n",
    "# tu turno"
   ]
  },
  {
   "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"
   ]
  },
  {
   "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?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "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
}
