{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Apilar capas\n",
    "\n",
    "El XOR, que paró el campo veinte años. Y la diferencia entre poder representar algo y llegar a encontrarlo.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 4 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/apilar-capas/\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": [
    "Hasta aquí tenemos una neurona (capítulo 2) y sabemos por qué hace falta\n",
    "doblar (capítulo 3). Toca apilar 🧱\n",
    "\n",
    "Una pregunta para ti: **¿cuántas veces has probado algo una vez, te ha salido bien y lo has dado por bueno?** Este capítulo tiene una medición que me curó de eso 🎲\n",
    "\n",
    "Y para que se vea la diferencia hace falta un problema que una sola neurona\n",
    "**no pueda**. El clásico se llama XOR, tiene cuatro filas y paró la\n",
    "investigación en redes neuronales durante casi veinte años."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tres problemas de cuatro filas"
   ]
  },
  {
   "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",
    "X = np.array([[0., 0.],\n",
    "              [0., 1.],\n",
    "              [1., 0.],\n",
    "              [1., 1.]])\n",
    "\n",
    "Y_AND = np.array([0., 0., 0., 1.])      # los dos\n",
    "Y_OR = np.array([0., 1., 1., 1.])       # alguno de los dos\n",
    "Y_XOR = np.array([0., 1., 1., 0.])      # uno pero no los dos\n",
    "\n",
    "for nombre, objetivo in [('AND', Y_AND), ('OR', Y_OR), ('XOR', Y_XOR)]:\n",
    "    print(nombre, objetivo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Llévalo a algo tuyo para que no sea abstracto: imagina que la primera columna\n",
    "es \"le hicimos descuento\" y la segunda \"lo visitó un vendedor\".\n",
    "\n",
    "El AND sería \"compra solo si le hacemos las dos cosas\". El OR, \"compra si le\n",
    "hacemos cualquiera de las dos\". Y el XOR sería \"compra si le haces una, pero si\n",
    "le haces las dos se siente presionado y no compra\" 🤔\n",
    "\n",
    "Los tres suenan igual de razonables. Y solo dos se pueden aprender con una\n",
    "neurona."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Una neurona, entrenada 5.000 vueltas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def una_neurona(objetivo, vueltas=5000, paso=0.5):\n",
    "    rng = np.random.default_rng(0)\n",
    "    w = rng.normal(0, 0.5, 2)\n",
    "    b = 0.0\n",
    "    for _ in range(vueltas):\n",
    "        p = sigmoide(X @ w + b)\n",
    "        error = p - objetivo\n",
    "        w -= paso * X.T @ error / len(X)\n",
    "        b -= paso * error.mean()\n",
    "    return sigmoide(X @ w + b)\n",
    "\n",
    "for nombre, objetivo in [('AND', Y_AND), ('OR', Y_OR), ('XOR', Y_XOR)]:\n",
    "    p = una_neurona(objetivo)\n",
    "    aciertos = int(((p >= 0.5) == objetivo).sum())\n",
    "    print(f'{nombre:4} {np.round(p, 4)}  acierta {aciertos} de 4')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AND y OR salen perfectos. Y el XOR devuelve **0,5 en los cuatro\n",
    "casos**, que es la forma que tiene una neurona de decir \"no tengo ni\n",
    "idea\" 🤷‍♀️\n",
    "\n",
    "Y no es que le faltaran vueltas. Puedes dejarla un millón y va a seguir\n",
    "diciendo 0,5, porque el problema no es de esfuerzo.\n",
    "\n",
    "Una neurona traza una recta y decide de qué lado cae cada punto. Coloca los\n",
    "cuatro puntos del XOR en un papel: los que valen 1 están en las esquinas (0,1) y\n",
    "(1,0), o sea en diagonal. No hay ninguna recta que deje esas dos de un lado y las\n",
    "otras dos del otro. Ninguna 📐\n",
    "\n",
    "Eso lo demostraron Minsky y Papert en 1969, y el campo se quedó helado. La\n",
    "salida ya se conocía, pero faltaba saber cómo entrenarla, que es el capítulo 6."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dos capas, escritas enteras"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Metemos una capa en medio: las entradas van a cuatro neuronas ocultas, y esas\n",
    "cuatro van a la salida."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def red_dos_capas(semilla, ocultas=4, vueltas=5000, paso=0.5):\n",
    "    rng = np.random.default_rng(semilla)\n",
    "    W1 = rng.normal(0, 1, (2, ocultas))\n",
    "    b1 = np.zeros(ocultas)\n",
    "    W2 = rng.normal(0, 1, (ocultas, 1))\n",
    "    b2 = np.zeros(1)\n",
    "    objetivo = Y_XOR.reshape(-1, 1)\n",
    "\n",
    "    for _ in range(vueltas):\n",
    "        # hacia adelante\n",
    "        h = np.tanh(X @ W1 + b1)\n",
    "        p = sigmoide(h @ W2 + b2)\n",
    "        # hacia atrás (el capítulo 6 explica de dónde sale cada línea)\n",
    "        d2 = (p - objetivo) / len(X)\n",
    "        dW2 = h.T @ d2\n",
    "        db2 = d2.sum(axis=0)\n",
    "        d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "        dW1 = X.T @ d1\n",
    "        db1 = d1.sum(axis=0)\n",
    "        # corregir\n",
    "        W2 -= paso * dW2\n",
    "        b2 -= paso * db2\n",
    "        W1 -= paso * dW1\n",
    "        b1 -= paso * db1\n",
    "\n",
    "    perdida = -np.mean(objetivo * np.log(p) + (1 - objetivo) * np.log(1 - p))\n",
    "    return p.ravel(), float(perdida)\n",
    "\n",
    "p, perdida = red_dos_capas(semilla=1)\n",
    "print('la red dice :', np.round(p, 4))\n",
    "print('lo que quería:', Y_XOR)\n",
    "print('pérdida      :', round(perdida, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está 🎉\n",
    "\n",
    "0,0001 y 0,0016 donde tenía que decir 0, y 0,9989 en los dos que tenían que\n",
    "ser 1. Con cuatro neuronas en medio y veinte líneas de numpy.\n",
    "\n",
    "Lo que hicieron esas cuatro neuronas es doblar el espacio hasta que los cuatro\n",
    "puntos sí se puedan separar con una recta. Cada una traza su propia raya, y la\n",
    "capa de salida combina las cuatro. Eso es todo lo que hace una capa oculta.\n",
    "\n",
    "No te preocupes por las líneas del \"hacia atrás\", que son el capítulo 6\n",
    "entero. Por ahora quédate con que hay una parte que calcula y otra que corrige 🔄"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora lo que casi nadie cuenta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese resultado salió con la semilla 1. Probemos con diez arranques distintos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for semilla in range(10):\n",
    "    p, perdida = red_dos_capas(semilla)\n",
    "    aciertos = int(((p >= 0.5).astype(int) == Y_XOR).sum())\n",
    "    print(f'semilla {semilla}: {np.round(p, 4)}  pérdida {perdida:.4f}  '\n",
    "          f'acierta {aciertos}/4')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nueve de diez lo resuelven. Y **la semilla 0 se queda atascada**:\n",
    "devuelve 0,4995 y 0,5005 en dos de los cuatro, acierta 2 de 4, y su pérdida se\n",
    "queda en 0,3474 en vez de bajar a 0,0009.\n",
    "\n",
    "Y no le faltan vueltas ni le falta capacidad. La misma red, el mismo código,\n",
    "las mismas 5.000 vueltas. Lo único distinto son los números con los que\n",
    "arrancó 🎲\n",
    "\n",
    "Esto tiene nombre: se quedó en un **mínimo local**. Encontró una\n",
    "combinación de pesos desde la que cualquier cambio pequeño empeora, así que se\n",
    "queda ahí aunque exista una solución mucho mejor en otro sitio.\n",
    "\n",
    "Guárdate esta distinción porque vale para todo el libro: **que una red\n",
    "pueda representar la solución no significa que la vaya a encontrar**. Son\n",
    "dos cosas distintas y la segunda es la difícil."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Más neuronas no es más capacidad: es más suerte"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En teoría con dos neuronas ocultas basta para el XOR. Vamos a ver qué pasa en\n",
    "la práctica:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for ocultas in [2, 4, 8, 16]:\n",
    "    convergen = sum(\n",
    "        1 for s in range(10)\n",
    "        if ((red_dos_capas(s, ocultas=ocultas)[0] >= 0.5).astype(int) == Y_XOR).sum() == 4\n",
    "    )\n",
    "    print(f'{ocultas:2d} neuronas ocultas: convergen {convergen} de 10 arranques')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí está el resultado que a mí me parece el más bonito del capítulo 💡\n",
    "\n",
    "Con dos neuronas convergen 5 de 10, con cuatro 9 de 10, y con ocho o dieciséis\n",
    "las diez.\n",
    "\n",
    "Con dos ya alcanza para representar el XOR. Lo que cambia al poner más no es\n",
    "lo que la red *puede*: es la probabilidad de que el entrenamiento\n",
    "**encuentre** una solución en vez de atascarse. Más neuronas son más\n",
    "caminos hacia abajo.\n",
    "\n",
    "Por eso en la práctica las redes se hacen más grandes de lo que la teoría\n",
    "pide. No es derroche: es comprar probabilidad de converger 🛣️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Y hay algún XOR en los datos de la distribuidora?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Buena pregunta, porque si no lo hay, este capítulo es una clase de teoría y ya\n",
    "está. Vamos a mirarlo 🔎\n",
    "\n",
    "El equivalente sería: que hacer descuento ayude cuando el cliente está poco\n",
    "satisfecho y **estorbe** cuando está contento. O sea que el efecto\n",
    "del descuento cambie de signo según la otra columna."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\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)\n",
    "ventas['con_descuento'] = (ventas['descuento'].fillna(0) > 0).astype(int)\n",
    "ventas['satisfecho'] = (ventas['satisfaccion']\n",
    "                        .fillna(ventas['satisfaccion'].median()) >= 4).astype(int)\n",
    "\n",
    "print(ventas.groupby(['con_descuento', 'satisfecho'])['compro']\n",
    "      .agg(['size', 'mean']).round(4).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí están las cuatro casillas, igual que el XOR pero con 3.000 filas\n",
    "detrás.\n",
    "\n",
    "Y **no hay XOR**. Mira los dos saltos: sin satisfacción, el\n",
    "descuento sube la compra de 0,4031 a 0,5535, o sea quince puntos. Con\n",
    "satisfacción, la sube de 0,6250 a 0,6813, o sea cinco puntos y medio.\n",
    "\n",
    "El descuento ayuda *siempre*. Ayuda más cuando el cliente está poco\n",
    "contento, que tiene todo el sentido comercial, pero nunca cambia de signo. Eso no\n",
    "es un XOR: es un efecto que suma con distinta fuerza.\n",
    "\n",
    "Y ahí tienes explicado, por fin, todo lo que veníamos midiendo. En el\n",
    "capítulo 1 la red perdía y en el\n",
    "3 ganaba `identity` porque **en estos\n",
    "datos no hay ninguna esquina cruzada que doblar**. La capacidad de la red\n",
    "está ahí y no hay dónde gastarla 💡\n",
    "\n",
    "Esta comprobación de cuatro casillas es la que yo haría antes de proponer una\n",
    "red para datos en tabla. Si las cuatro casillas suman, la logística te vale."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Rescatar la semilla atascada\n",
    "\n",
    "Dale a la semilla 0 más vueltas, más paso y más neuronas, y\n",
    "mira cuál la salva."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('base           ', np.round(red_dos_capas(0)[0], 4))\n",
    "print('4 veces vueltas', np.round(red_dos_capas(0, vueltas=20000)[0], 4))\n",
    "print('paso más grande', np.round(red_dos_capas(0, paso=2.0)[0], 4))\n",
    "print('8 neuronas     ', np.round(red_dos_capas(0, ocultas=8)[0], 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "base            [7.000e-04 4.995e-01 9.995e-01 5.005e-01]\n",
    "4 veces vueltas [1.000e-04 4.999e-01 9.999e-01 5.001e-01]\n",
    "paso más grande [2.000e-04 4.999e-01 9.999e-01 5.001e-01]\n",
    "8 neuronas      [2.000e-04 9.994e-01 9.996e-01 6.000e-04]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuadruplicar las vueltas no la salva: sigue en 0,5. El paso más grande\n",
    "tampoco. Lo que la salva es **darle más neuronas**.\n",
    "\n",
    "Es la lección de arriba en forma de ejercicio: cuando una red se atasca, la\n",
    "respuesta casi nunca es entrenar más rato 🕐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Qué aprendió cada neurona oculta\n",
    "\n",
    "Mira lo que sale de la capa del medio para los cuatro\n",
    "casos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(1)\n",
    "W1 = rng.normal(0, 1, (2, 4)); b1 = np.zeros(4)\n",
    "W2 = rng.normal(0, 1, (4, 1)); b2 = np.zeros(1)\n",
    "objetivo = Y_XOR.reshape(-1, 1)\n",
    "for _ in range(5000):\n",
    "    h = np.tanh(X @ W1 + b1)\n",
    "    p = sigmoide(h @ W2 + b2)\n",
    "    d2 = (p - objetivo) / len(X)\n",
    "    d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "    W2 -= 0.5 * (h.T @ d2); b2 -= 0.5 * d2.sum(axis=0)\n",
    "    W1 -= 0.5 * (X.T @ d1); b1 -= 0.5 * d1.sum(axis=0)\n",
    "\n",
    "print('capa oculta, una fila por caso:')\n",
    "print(np.round(np.tanh(X @ W1 + b1), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "capa oculta, una fila por caso:\n",
    "[[ 0.012 -0.06  -0.684 -0.777]\n",
    " [ 0.972  0.972 -1.     0.955]\n",
    " [ 0.975  0.977  0.949 -1.   ]\n",
    " [ 1.     1.    -0.98  -0.981]]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada columna es una neurona y cada fila uno de los cuatro casos. Fíjate en que\n",
    "las filas del medio (las que tienen que dar 1) se parecen entre sí y se\n",
    "diferencian de las otras dos.\n",
    "\n",
    "Eso es exactamente el trabajo de la capa oculta: **convertir un problema\n",
    "que no se separa con una recta en uno que sí**. La capa de salida, que es\n",
    "una sola neurona, ya solo tiene que trazar esa recta 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Una neurona con la pista dada a mano\n",
    "\n",
    "Añade tú la columna que hace falta y usa una sola\n",
    "neurona."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_ayudada = np.column_stack([X, X[:, 0] * X[:, 1]])   # el producto de las dos\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "w = rng.normal(0, 0.5, 3); b = 0.0\n",
    "for _ in range(5000):\n",
    "    p = sigmoide(X_ayudada @ w + b)\n",
    "    error = p - Y_XOR\n",
    "    w -= 0.5 * X_ayudada.T @ error / len(X)\n",
    "    b -= 0.5 * error.mean()\n",
    "\n",
    "print('con la columna extra:', np.round(sigmoide(X_ayudada @ w + b), 4))\n",
    "print('acierta', int(((sigmoide(X_ayudada @ w + b) >= 0.5) == Y_XOR).sum()), 'de 4')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "con la columna extra: [0.0148 0.9901 0.9901 0.0066]\n",
    "acierta 4 de 4\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro de cuatro con una sola neurona, porque le di masticado lo que la capa\n",
    "oculta habría tenido que descubrir sola.\n",
    "\n",
    "Y este es el mismo experimento del capítulo 1 con los cuadrados del círculo,\n",
    "por si no había quedado claro: **la capa oculta es feature engineering\n",
    "automático**. Cuando sabes qué columna falta, no necesitas la capa 🧩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Cuántos pesos hay en juego\n",
    "\n",
    "Cuenta los parámetros de cada versión."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for ocultas in [2, 4, 8, 16]:\n",
    "    pesos = 2 * ocultas + ocultas + ocultas * 1 + 1\n",
    "    print(f'{ocultas:2d} ocultas: {pesos:3d} pesos para aprender 4 filas')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    " 2 ocultas:   9 pesos para aprender 4 filas\n",
    " 4 ocultas:  17 pesos para aprender 4 filas\n",
    " 8 ocultas:  33 pesos para aprender 4 filas\n",
    "16 ocultas:  65 pesos para aprender 4 filas\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con dieciséis neuronas hay 65 parámetros para aprender cuatro filas. O sea\n",
    "dieciséis pesos por fila 😅\n",
    "\n",
    "En cualquier otro contexto eso sería sobreajuste puro. Aquí no importa porque\n",
    "las cuatro filas *son* el universo entero: no hay ningún caso nuevo que\n",
    "pueda llegar. Con datos de verdad esto se paga, y es el capítulo 8."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La red que se rinde por el paso\n",
    "\n",
    "Prueba pasos muy chicos y muy grandes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for paso in [0.001, 0.05, 0.5, 5.0, 50.0]:\n",
    "    p, perdida = red_dos_capas(1, paso=paso)\n",
    "    print(f'paso {paso:6}: pérdida {perdida:.4f}  {np.round(p, 3)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "paso  0.001: pérdida 0.6382  [0.438 0.631 0.451 0.513]\n",
    "paso   0.05: pérdida 0.0114  [0.002 0.988 0.987 0.018]\n",
    "paso    0.5: pérdida 0.0009  [0.    0.999 0.999 0.002]\n",
    "paso    5.0: pérdida 0.0000  [0. 1. 1. 0.]\n",
    "paso   50.0: pérdida 55.8202  [0. 0. 0. 0.]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los tres del medio funcionan y los dos extremos fallan, cada uno a su\n",
    "manera.\n",
    "\n",
    "Con 0,001 la pérdida se queda en 0,6382 y las salidas rondan el 0,5: no le dio\n",
    "tiempo a moverse del punto de partida en 5.000 vueltas. Ese punto de partida vale\n",
    "0,6931, que no es un número cualquiera: es el logaritmo natural de 2, lo que vale\n",
    "la pérdida cuando el modelo dice 0,5 a todo. Cuando lo veas en un entrenamiento\n",
    "de verdad, sabes que no ha aprendido nada todavía 🎯\n",
    "\n",
    "Con 50 pasa lo contrario y es peor: la pérdida sale **55,82** y\n",
    "las cuatro salidas se van a 0. El paso era tan grande que en vez de bajar por la\n",
    "pendiente se pasó de largo, rebotó y se disparó. Eso se llama divergir, y una\n",
    "pérdida que crece en vez de bajar es su síntoma 💥\n",
    "\n",
    "Entre 0,05 y 5 hay dos órdenes de magnitud que funcionan, y fuera de ahí no\n",
    "funciona nada. Elegir el paso es el ajuste que más importa de una red, y es el\n",
    "capítulo 5."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Tres capas en vez de dos\n",
    "\n",
    "Mete una capa más y mira si ayuda."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def red_tres_capas(semilla, vueltas=5000, paso=0.5):\n",
    "    rng = np.random.default_rng(semilla)\n",
    "    W1 = rng.normal(0, 1, (2, 4)); b1 = np.zeros(4)\n",
    "    W2 = rng.normal(0, 1, (4, 4)); b2 = np.zeros(4)\n",
    "    W3 = rng.normal(0, 1, (4, 1)); b3 = np.zeros(1)\n",
    "    objetivo = Y_XOR.reshape(-1, 1)\n",
    "    for _ in range(vueltas):\n",
    "        h1 = np.tanh(X @ W1 + b1)\n",
    "        h2 = np.tanh(h1 @ W2 + b2)\n",
    "        p = sigmoide(h2 @ W3 + b3)\n",
    "        d3 = (p - objetivo) / len(X)\n",
    "        d2 = (d3 @ W3.T) * (1 - h2 ** 2)\n",
    "        d1 = (d2 @ W2.T) * (1 - h1 ** 2)\n",
    "        W3 -= paso * (h2.T @ d3); b3 -= paso * d3.sum(axis=0)\n",
    "        W2 -= paso * (h1.T @ d2); b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (X.T @ d1); b1 -= paso * d1.sum(axis=0)\n",
    "    return p.ravel()\n",
    "\n",
    "convergen = sum(1 for s in range(10)\n",
    "                if ((red_tres_capas(s) >= 0.5).astype(int) == Y_XOR).sum() == 4)\n",
    "print('con tres capas convergen', convergen, 'de 10')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "con tres capas convergen 10 de 10\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Diez de diez, mejor que las nueve de dos capas. Aunque el XOR no necesita\n",
    "profundidad para nada: se resuelve con una capa oculta y punto.\n",
    "\n",
    "Lo que ganó aquí la capa extra es lo mismo que ganaban las neuronas de más:\n",
    "caminos. Y fíjate cómo crece el código hacia atrás, que es una línea más por\n",
    "capa. Ese patrón es literalmente todo el deep learning 🔁"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El error de olvidar el reshape\n",
    "\n",
    "Pásale el objetivo plano en vez de en columna."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(1)\n",
    "W1 = rng.normal(0, 1, (2, 4)); b1 = np.zeros(4)\n",
    "W2 = rng.normal(0, 1, (4, 1)); b2 = np.zeros(1)\n",
    "\n",
    "h = np.tanh(X @ W1 + b1)\n",
    "p = sigmoide(h @ W2 + b2)\n",
    "print('p tiene forma  :', p.shape)\n",
    "print('Y_XOR tiene    :', Y_XOR.shape)\n",
    "print('la resta sale  :', (p - Y_XOR).shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "p tiene forma  : (4, 1)\n",
    "Y_XOR tiene    : (4,)\n",
    "la resta sale  : (4, 4)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí está el peligro, que no da error: restar algo de (4, 1) menos algo de\n",
    "(4,) devuelve **(4, 4)**. Numpy estira las dos formas hasta que\n",
    "encajan, y en vez de cuatro errores tienes dieciséis.\n",
    "\n",
    "El entrenamiento sigue corriendo, la pérdida baja, todo parece bien y el\n",
    "resultado está mal. Es de los errores más caros que existen porque no avisa 😨\n",
    "\n",
    "Por eso el `reshape(-1, 1)` del código de arriba no era manía. Mi\n",
    "regla: **imprime `.shape` de las dos cosas antes de cada\n",
    "resta**, sobre todo si una viene de pandas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. Buscar la esquina cruzada en tres pares de columnas\n",
    "\n",
    "Repite la comprobación de las cuatro casillas con otros\n",
    "pares y mira si alguno cruza."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['visita_repetida'] = (ventas.sort_values(['cliente_id', 'fecha'])\n",
    "                             .groupby('cliente_id').cumcount() > 0).astype(int)\n",
    "ventas['mayorista'] = (ventas['segmento'] == 'Mayorista').astype(int)\n",
    "\n",
    "for otra in ['visita_repetida', 'mayorista']:\n",
    "    tabla = ventas.groupby(['con_descuento', otra])['compro'].mean().unstack()\n",
    "    salto_sin = tabla.loc[1, 0] - tabla.loc[0, 0]\n",
    "    salto_con = tabla.loc[1, 1] - tabla.loc[0, 1]\n",
    "    print(f'{otra:18} el descuento suma {salto_sin:+.4f} cuando es 0 '\n",
    "          f'y {salto_con:+.4f} cuando es 1')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "visita_repetida    el descuento suma +0.1175 cuando es 0 y +0.1235 cuando es 1\n",
    "mayorista          el descuento suma +0.1368 cuando es 0 y +0.0682 cuando es 1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los dos saltos salen positivos en los dos pares. Ni uno cruza.\n",
    "\n",
    "Con `mayorista` el descuento suma 0,1368 a los que no lo son y\n",
    "0,0682 a los que sí, o sea el doble en unos que en otros. Otra vez intensidad y\n",
    "no signo.\n",
    "\n",
    "Y con `visita_repetida` los dos saltos son casi idénticos (0,1175 y\n",
    "0,1235), o sea que ahí ni siquiera hay interacción: son dos efectos que van cada\n",
    "uno por su lado y se suman.\n",
    "\n",
    "Tres pares mirados y ninguna esquina cruzada. En un negocio de distribución\n",
    "tiene sentido: **rebajar el precio no hace que nadie deje de\n",
    "comprar** 🧾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9. El error de cambiar el tamaño en un solo sitio\n",
    "\n",
    "Pon ocho neuronas en la primera capa y déjate las cuatro de\n",
    "la segunda, que es lo que pasa cuando editas rápido."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(1)\n",
    "W1 = rng.normal(0, 1, (2, 8)); b1 = np.zeros(8)     # ocho ahora\n",
    "W2 = rng.normal(0, 1, (4, 1)); b2 = np.zeros(1)     # y aquí seguían cuatro\n",
    "\n",
    "h = np.tanh(X @ W1 + b1)\n",
    "h @ W2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 4 is different from 8)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"size 4 is different from 8\", que es el mismo error de formas del capítulo 2 y\n",
    "te va a acompañar toda la vida.\n",
    "\n",
    "Y menos mal que revienta. La capa oculta tiene que salir con la misma forma\n",
    "con la que entra en la siguiente, y ese encadenado es lo único que hay que\n",
    "cuidar al montar una red a mano 🔗\n",
    "\n",
    "Cuando uses una librería, esto se lo lleva ella: le dices\n",
    "`hidden_layer_sizes=(8, 4)` y las formas las cuadra sola. Es de las\n",
    "pocas cosas que agradezco de verdad de las librerías 😅"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "El XOR converge en 9 de 10 arranques con 4 neuronas, en 5 de 10 con 2 y en 10 de 10 con 8. ¿Qué dan las neuronas de más?\n",
    "\n",
    "a) Caminos: más arranques distintos acaban encontrando la solución\n",
    "\n",
    "b) Capacidad: pueden aprender funciones más complicadas\n",
    "\n",
    "c) Velocidad de entrenamiento\n",
    "\n",
    "d) Precisión en la respuesta\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Con 2 ya hay capacidad suficiente para el XOR. Lo que falla es otra cosa.\n",
    "\n",
    "*c)* Con más neuronas cada vuelta cuesta más, no menos.\n",
    "\n",
    "*d)* Cuando converge, converge igual de bien con 2 que con 8.\n",
    "\n",
    "Más neuronas no dan capacidad: dan suerte."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y la arquitectura que eliges por suerte"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Pruebas dos arquitecturas para el XOR, la de dos neuronas ocultas y la de ocho, y te quedas con la de dos porque es más pequeña y resolvió igual.\n",
    "\n",
    "```\n",
    "red2 = entrena(ocultas=2, semilla=0)\n",
    "red8 = entrena(ocultas=8, semilla=0)\n",
    "\n",
    "print('2 ocultas:', red2.acierto)   # 1.0\n",
    "print('8 ocultas:', red8.acierto)   # 1.0\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Las dos resolvieron, sí, pero con **una sola semilla cada una**. Y en este mismo capítulo está medido lo que pasa al repetirlo: con ocho neuronas ocultas convergen **diez de diez arranques**, y con dos convergen **cinco de diez** 🎲\n",
    "\n",
    "O sea que acabas de elegir la arquitectura que falla la mitad de las veces, y lo has hecho con evidencia real: tu prueba salió bien. Simplemente te tocó uno de los cinco arranques buenos.\n",
    "\n",
    "Una red se evalúa **con varias semillas y se reporta el reparto**, no una corrida. Es la misma disciplina de la validación cruzada, pero aquí lo que varía no es el reparto de los datos: es el punto del que arranca el entrenamiento."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 📐 Una neurona traza una recta. El XOR no se separa con una recta, así que\n",
    "devuelve 0,5 en los cuatro casos por muchas vueltas que le des.\n",
    "\n",
    "- 🧱 Una capa oculta de cuatro neuronas lo resuelve, y se escribe en veinte\n",
    "líneas de numpy.\n",
    "\n",
    "- 🎲 Pero no siempre: de diez arranques convergen nueve, y el que falla se\n",
    "queda en un mínimo local con pérdida 0,3468.\n",
    "\n",
    "- 🛣️ Con dos neuronas convergen 5 de 10, con cuatro 9 y con ocho las diez. Más\n",
    "neuronas no dan más capacidad, dan más caminos.\n",
    "\n",
    "- 🧩 La capa oculta es feature engineering automático: si le das a mano la\n",
    "columna que falta, una sola neurona resuelve el XOR.\n",
    "\n",
    "- 🎯 Una pérdida de 0,6931 es ln(2), o sea el modelo diciendo 0,5 a todo.\n",
    "\n",
    "- 😨 Restar un (4, 1) menos un (4,) devuelve un (4, 4) sin avisar.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una corrida no es un resultado. Y en redes, dos corridas tampoco."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo de medir con varias semillas en vez de con una es la misma disciplina de la validación cruzada, y está en el [libro de machine learning desde cero](https://missyera.com/guias/machine-learning-desde-cero/) 🎲\n",
    "\n",
    "En el capítulo 5 dejamos de mover los pesos a mano: descenso de gradiente, que\n",
    "es cómo la red decide hacia dónde corregir.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 4 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/apilar-capas/\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
}
