{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Las activaciones\n",
    "\n",
    "Sin función de activación, diez capas son una. Lo comprobamos, y de paso vemos por qué casi nadie usa ya la sigmoide en el medio.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 3 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/activaciones/\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 2 la neurona aplastaba con una sigmoide y no dije por qué. Es\n",
    "que hace falta explicarlo con dos capas delante, y ahora ya podemos 🔧\n",
    "\n",
    "Y te lanzo la pregunta del capítulo por adelantado: **¿qué crees que pasa si apilas cien capas sin nada entre ellas?** Apuesta mentalmente, que se comprueba en dos líneas 🔀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La demostración de que sin activación no sirve de nada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una red es capas apiladas: la salida de una entra en la siguiente. Si\n",
    "quitamos la activación, cada capa es solo una multiplicación de matrices."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "X = rng.normal(0, 1, (5, 3))          # 5 filas con 3 columnas\n",
    "W1 = rng.normal(0, 1, (3, 4))         # primera capa: 3 entradas, 4 neuronas\n",
    "W2 = rng.normal(0, 1, (4, 2))         # segunda capa: 4 entradas, 2 salidas\n",
    "\n",
    "dos_capas = (X @ W1) @ W2\n",
    "una_capa = X @ (W1 @ W2)              # las dos matrices multiplicadas de antemano\n",
    "\n",
    "print('¿dan lo mismo?', np.allclose(dos_capas, una_capa))\n",
    "print('mayor diferencia:', float(np.abs(dos_capas - una_capa).max()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está, y es toda la razón de ser de este capítulo 💡\n",
    "\n",
    "Sin activación, **dos capas son exactamente una**. Y no es que se\n",
    "parezcan: la diferencia es 4,4e-16, o sea el redondeo de la máquina. Puedes\n",
    "apilar cincuenta capas y seguirás teniendo un modelo lineal, con más gasto de\n",
    "electricidad y ninguna capacidad nueva.\n",
    "\n",
    "Lo que rompe esa cadena es meter una función no lineal entre capa y capa. Eso\n",
    "es una **activación**, y su único trabajo es doblar 🌀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las tres que hay que conocer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "σ(z)=11+e−z,σ\\′(z)=σ(z)(1−σ(z))\n",
    "\n",
    "aplasta cualquier número entre 0 y 1, y su derivada nunca pasa de 0,25, que es exactamente por lo que el gradiente se desvanece al apilar capas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\n",
    "\n",
    "def tanh(z):\n",
    "    return np.tanh(z)\n",
    "\n",
    "def relu(z):\n",
    "    return np.maximum(0, z)           # ojo: np.maximum, no max\n",
    "\n",
    "z = np.array([-6.0, -2.0, -0.5, 0.0, 0.5, 2.0, 6.0])\n",
    "print('z       ', z)\n",
    "print('sigmoide', np.round(sigmoide(z), 4))\n",
    "print('tanh    ', np.round(tanh(z), 4))\n",
    "print('relu    ', np.round(relu(z), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelas por lo que hacen con los números:\n",
    "\n",
    "- 🫓 **Sigmoide** aplasta todo entre 0 y 1. Sirve para una salida\n",
    "que quieras leer como probabilidad.\n",
    "\n",
    "- ⚖️ **Tanh** aplasta entre -1 y 1, o sea que queda centrada en\n",
    "cero. Es la sigmoide estirada.\n",
    "\n",
    "- ✂️ **ReLU** es lo más tonto que te puedas imaginar: si es\n",
    "negativo, cero; si no, tal cual. Y es la que se usa en casi todo.\n",
    "\n",
    "Que la más simple sea la que ganó no es casualidad, y se ve mirando otra\n",
    "cosa 👀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que de verdad decide: la pendiente"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ReLU(z)=max(0,z),ReLU\\′(z)={1z>00z≤0\n",
    "\n",
    "deja pasar lo positivo y corta lo negativo, y su derivada vale uno o cero, que es lo que impide que el gradiente se encoja al bajar de capa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Del capítulo 2 nos quedó pendiente que una neurona saturada deja de aprender.\n",
    "Aquí está el porqué, con las tres al lado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('pendiente de la sigmoide', np.round(sigmoide(z) * (1 - sigmoide(z)), 4))\n",
    "print('pendiente de tanh       ', np.round(1 - tanh(z) ** 2, 4))\n",
    "print('pendiente de relu       ', np.round((z > 0).astype(float), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta tabla explica veinte años de historia del deep learning, así que vale la\n",
    "pena pararse 🛑\n",
    "\n",
    "**La sigmoide no pasa nunca de 0,25**, y eso es en su mejor\n",
    "momento, justo en el centro. En z=6 vale 0,0025.\n",
    "\n",
    "**La de ReLU vale 1** en toda la mitad positiva. Ni encoge ni\n",
    "crece.\n",
    "\n",
    "Y ahora la parte que importa: en el capítulo 5 vamos a ver que para entrenar\n",
    "una red, las pendientes de todas las capas **se multiplican entre\n",
    "sí**. Con sigmoides, diez capas te dan como mucho 0,25 elevado a diez, que\n",
    "es 0,00000095. El aviso de corrección que llega a la primera capa es\n",
    "prácticamente cero y esa capa no aprende nunca.\n",
    "\n",
    "Eso tiene nombre, **gradiente desvanecido**, y fue lo que tuvo\n",
    "parado el campo durante años. Con ReLU se multiplica por 1 diez veces y no pasa\n",
    "nada 🎉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que ReLU rompe a cambio"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nada es gratis. Mira su pendiente en la mitad negativa: cero, y no cero\n",
    "pequeño sino cero exacto.\n",
    "\n",
    "Una neurona cuya suma sale siempre negativa devuelve siempre cero, y como su\n",
    "pendiente también es cero, no recibe ninguna corrección. Está muerta y no\n",
    "revive:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_g = rng.normal(0, 1, (200, 4))\n",
    "W = rng.normal(0, 1, (4, 8))\n",
    "\n",
    "for sesgo in [0.0, -5.0]:\n",
    "    H = relu(X_g @ W + np.full(8, sesgo))\n",
    "    muertas = int((H.sum(axis=0) == 0).sum())\n",
    "    print(f'sesgo {sesgo:5}: {muertas} de 8 neuronas nunca se activan, '\n",
    "          f'{(H == 0).mean():.4f} de la capa son ceros')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con el sesgo en cero, ninguna muere y el 49,94% de la capa son ceros, que es\n",
    "lo normal y hasta conveniente. Con el sesgo en -5, **mueren 6 de 8**\n",
    "y el 99,69% de la capa es cero. Esa capa ya no hace nada 💀\n",
    "\n",
    "Por eso importa cómo se arrancan los pesos, que es un tema entero, y por eso\n",
    "existen variantes como `LeakyReLU`, que en vez de cero devuelve\n",
    "`0,01 * z` en el lado negativo para dejar un hilito de pendiente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La de la salida es otra cosa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "pk=ezk∑jezj\n",
    "\n",
    "convierte una lista de números en probabilidades que suman uno, exagerando las diferencias porque la exponencial crece rapidísimo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí se confunde mucha gente. La activación de las capas del medio y la de la\n",
    "salida se eligen por razones distintas:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Dónde | Qué se usa | Por qué |\n",
    "|---|---|---|\n",
    "| Capas del medio | ReLU | Para que la pendiente no se apague al multiplicarse |\n",
    "| Salida, sí o no | Sigmoide | Para leer el número como probabilidad |\n",
    "| Salida, varias clases | Softmax | Para que las probabilidades sumen 1 |\n",
    "| Salida, un número | Ninguna | Un precio o una cantidad no se aplasta |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La softmax es la sigmoide cuando hay más de dos opciones, y tiene una trampa\n",
    "que vas a encontrarte:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def softmax_ingenua(z):\n",
    "    e = np.exp(z)\n",
    "    return e / e.sum()\n",
    "\n",
    "def softmax(z):\n",
    "    z = z - z.max()                   # esta línea es toda la diferencia\n",
    "    e = np.exp(z)\n",
    "    return e / e.sum()\n",
    "\n",
    "grandes = np.array([1000.0, 1.0, 2.0])\n",
    "print('ingenua:', softmax_ingenua(grandes))\n",
    "print('estable:', np.round(softmax(grandes), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La ingenua devuelve `nan`, porque `np.exp(1000)` se sale\n",
    "del rango de un número de computadora y luego infinito dividido infinito no es\n",
    "nada.\n",
    "\n",
    "Restar el máximo antes no cambia el resultado matemático (compruébalo con\n",
    "lápiz: se cancela arriba y abajo) y hace que el mayor exponente sea siempre\n",
    "`exp(0) = 1`. Todas las librerías lo hacen por dentro, y si alguna vez\n",
    "escribes la tuya, acuérdate 🧯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y en nuestros datos, ¿cuál gana?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya sabes que aquí las redes pierden, pero la comparación entre activaciones\n",
    "sigue siendo interesante:"
   ]
  },
  {
   "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.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.neural_network import MLPClassifier\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v['ciudad'] = (v['ciudad'].str.strip().str.lower()\n",
    "                   .str.normalize('NFKD')\n",
    "                   .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "    for col in ['fecha', 'fecha_ultima_compra']:\n",
    "        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "def prepara(v):\n",
    "    v = v.sort_values(['cliente_id', 'fecha']).copy()\n",
    "    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)\n",
    "    v['sin_descuento'] = v['descuento'].isna().astype(int)\n",
    "    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)\n",
    "    v['precio_unitario'] = v['monto'] / v['unidades']\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "def arma(modelo):\n",
    "    return Pipeline([\n",
    "        ('pre', ColumnTransformer([\n",
    "            ('num', Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                              ('e', StandardScaler())]), NUMERICAS),\n",
    "            ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                              ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),\n",
    "        ])),\n",
    "        ('mod', modelo),\n",
    "    ])\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos['compro']\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,\n",
    "                                          random_state=42, stratify=y)\n",
    "\n",
    "for activacion in ['logistic', 'tanh', 'relu', 'identity']:\n",
    "    m = arma(MLPClassifier(hidden_layer_sizes=(16,), activation=activacion,\n",
    "                           max_iter=300, random_state=42)).fit(X_tr, y_tr)\n",
    "    print(f'{activacion:10} {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí está el remate del capítulo, que no me lo esperaba cuando lo corrí 😳\n",
    "\n",
    "`identity` significa **sin activación ninguna**, o sea\n",
    "justo lo que acabamos de demostrar que colapsa en un modelo lineal. Y gana:\n",
    "0,7230, por encima de las tres que doblan.\n",
    "\n",
    "No se contradice con nada. Lo que dice es que **en estos datos la\n",
    "relación es lineal**, así que doblar la frontera no ayuda y solo añade\n",
    "formas de equivocarse. Y 0,7230 es prácticamente el 0,7214 de la regresión\n",
    "logística, que es exactamente lo que tiene que pasar: son el mismo modelo.\n",
    "\n",
    "Doblar es una capacidad, no una mejora 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Comprobar el colapso con más capas\n",
    "\n",
    "Apila cinco capas sin activación y mira si sigue siendo\n",
    "una."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "M = rng.normal(0, 1, (5, 3))          # cinco filas de tres columnas\n",
    "capas = [rng.normal(0, 1, (3, 6)), rng.normal(0, 1, (6, 6)),\n",
    "         rng.normal(0, 1, (6, 6)), rng.normal(0, 1, (6, 4)),\n",
    "         rng.normal(0, 1, (4, 2))]\n",
    "\n",
    "salida = M\n",
    "for W in capas:\n",
    "    salida = salida @ W\n",
    "\n",
    "junta = capas[0]\n",
    "for W in capas[1:]:\n",
    "    junta = junta @ W\n",
    "\n",
    "print('cinco capas =', salida.shape, '  una sola matriz =', junta.shape)\n",
    "print('¿dan lo mismo?', np.allclose(salida, M @ junta))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cinco capas = (5, 2)   una sola matriz = (3, 2)\n",
    "¿dan lo mismo? True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cinco capas, 3 por 6 por 6 por 6 por 4 por 2, y todo eso se reduce a una\n",
    "matriz de 3 por 2.\n",
    "\n",
    "Si alguna vez alguien te enseña una arquitectura con muchas capas y ninguna\n",
    "activación, ya sabes lo que estás mirando: una regresión lineal cara 💸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuánto se apaga la pendiente al apilar\n",
    "\n",
    "Multiplica la mejor pendiente posible de cada activación\n",
    "tantas veces como capas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for capas_n in [1, 2, 5, 10, 20]:\n",
    "    print(f'{capas_n:2d} capas   sigmoide {0.25 ** capas_n:.3e}   '\n",
    "          f'tanh {1.0 ** capas_n:.4f}   relu {1.0 ** capas_n:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    " 1 capas   sigmoide 2.500e-01   tanh 1.0000   relu 1.0000\n",
    " 2 capas   sigmoide 6.250e-02   tanh 1.0000   relu 1.0000\n",
    " 5 capas   sigmoide 9.766e-04   tanh 1.0000   relu 1.0000\n",
    "10 capas   sigmoide 9.537e-07   tanh 1.0000   relu 1.0000\n",
    "20 capas   sigmoide 9.095e-13   tanh 1.0000   relu 1.0000\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con veinte capas de sigmoide, el aviso que llega a la primera se multiplicó\n",
    "por 9,095e-13, o sea nueve billonésimas. Y eso **siendo generosas**,\n",
    "porque 0,25 es su máximo y casi nunca está ahí.\n",
    "\n",
    "Con tanh y ReLU el mejor caso es 1, así que no se apaga. Por eso las redes\n",
    "profundas de verdad no existieron hasta que se dejó de usar sigmoide en el\n",
    "medio 📉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Dibujar la frontera que dobla\n",
    "\n",
    "Vuelve al problema del círculo del capítulo 1 y compara con\n",
    "activación y sin ella."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "P = rng.normal(0, 1, (3000, 2))\n",
    "etiqueta = ((P[:, 0] ** 2 + P[:, 1] ** 2) > 2).astype(int)\n",
    "A_tr, A_te, b_tr, b_te = train_test_split(P, etiqueta, test_size=0.25,\n",
    "                                          random_state=42, stratify=etiqueta)\n",
    "\n",
    "for activacion in ['identity', 'tanh', 'relu']:\n",
    "    m = MLPClassifier(hidden_layer_sizes=(16,), activation=activacion,\n",
    "                      max_iter=500, random_state=42).fit(A_tr, b_tr)\n",
    "    print(f'{activacion:10} {roc_auc_score(b_te, m.predict_proba(A_te)[:, 1]):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "identity   0.5063\n",
    "tanh       0.9998\n",
    "relu       0.9995\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí sí se separan de verdad: `identity` se queda en 0,5063, que\n",
    "es azar, y las dos que doblan pasan de 0,999.\n",
    "\n",
    "Este ejercicio y el de arriba son el mismo experimento con datos distintos, y\n",
    "por eso los puse los dos. **La activación no mejora modelos: los hace\n",
    "capaces de otra cosa**. Si esa otra cosa no está en tus datos, no\n",
    "gana 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. ReLU deja medio cerebro apagado y está bien\n",
    "\n",
    "Cuenta qué porcentaje de la capa son ceros en una red\n",
    "entrenada de verdad."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "red = arma(MLPClassifier(hidden_layer_sizes=(16,), activation='relu',\n",
    "                         max_iter=300, random_state=42)).fit(X_tr, y_tr)\n",
    "T = red.named_steps['pre'].transform(X_te)\n",
    "oculta = relu(T @ red.named_steps['mod'].coefs_[0] + red.named_steps['mod'].intercepts_[0])\n",
    "\n",
    "print('ceros en la capa oculta:', round(float((oculta == 0).mean()), 4))\n",
    "print('neuronas muertas del todo:', int((oculta.sum(axis=0) == 0).sum()), 'de 16')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ceros en la capa oculta: 0.4933\n",
    "neuronas muertas del todo: 0 de 16\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El 49,33% de la capa son ceros y no hay ninguna neurona muerta del todo.\n",
    "\n",
    "Esos ceros no son desperdicio: significan que **cada neurona se\n",
    "especializó en una parte de los datos** y se calla en el resto. A esa\n",
    "propiedad se le llama activación dispersa y es parte de por qué ReLU funciona\n",
    "tan bien 🧩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La softmax de verdad, con tres opciones\n",
    "\n",
    "Úsala sobre un caso con la forma de un problema real."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "puntajes = np.array([2.1, 0.4, -1.3])       # Mayorista, Horeca, Bodega\n",
    "p = softmax(puntajes)\n",
    "for nombre, valor in zip(['Mayorista', 'Horeca', 'Bodega'], p):\n",
    "    print(f'{nombre:10} {valor:.4f}')\n",
    "print('suman:', round(float(p.sum()), 10))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "Mayorista  0.8223\n",
    "Horeca     0.1502\n",
    "Bodega     0.0274\n",
    "suman: 1.0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Suman 1 exactamente, que es lo que la hace útil: son probabilidades de verdad\n",
    "y compiten entre ellas.\n",
    "\n",
    "Fíjate en que la diferencia entre 2,1 y 0,4 (1,7 puntos) se convierte en\n",
    "0,8223 contra 0,1502, o sea cinco veces y media. **La softmax exagera las\n",
    "diferencias**, y eso es justo lo que la temperatura de un modelo de\n",
    "lenguaje ajusta, que lo vemos en el capítulo 17 🌡️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Escribir LeakyReLU en una línea\n",
    "\n",
    "Hazla tú y compárala con la ReLU normal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def leaky_relu(z, fuga=0.01):\n",
    "    return np.where(z > 0, z, fuga * z)\n",
    "\n",
    "print('z          ', z)\n",
    "print('relu       ', np.round(relu(z), 4))\n",
    "print('leaky      ', np.round(leaky_relu(z), 4))\n",
    "print('pendiente  ', np.round(np.where(z > 0, 1.0, 0.01), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "z           [-6.  -2.  -0.5  0.   0.5  2.   6. ]\n",
    "relu        [0.  0.  0.  0.  0.5 2.  6. ]\n",
    "leaky       [-6.e-02 -2.e-02 -5.e-03  0.e+00  5.e-01  2.e+00  6.e+00]\n",
    "pendiente   [0.01 0.01 0.01 0.01 1.   1.   1.  ]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La diferencia son unos decimalitos en el lado negativo, y esos decimalitos son\n",
    "la diferencia entre una neurona muerta y una neurona que puede volver.\n",
    "\n",
    "Que una idea tan pequeña tenga nombre propio y papers te dice algo bonito del\n",
    "campo: **casi todo el deep learning son ideas simples puestas en el sitio\n",
    "correcto** 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El error de escribir ReLU con el max de Python\n",
    "\n",
    "Prueba a usar `max` en vez de\n",
    "`np.maximum`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "max(0, z)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El `max` de Python compara dos cosas y devuelve la mayor, así que\n",
    "necesita saber si `z` es mayor que 0. Y `z` son siete\n",
    "números: unos lo son y otros no.\n",
    "\n",
    "El mensaje es de los mejores que da numpy porque te dice hasta la salida:\n",
    "*\"Use a.any() or a.all()\"*. Aunque aquí ninguna de las dos es lo que\n",
    "quieres 🙃\n",
    "\n",
    "`np.maximum` con \"n\" al final es otra función: compara elemento por\n",
    "elemento y devuelve un array. La confusión entre `max` y\n",
    "`np.maximum` es un clásico y ahora ya no te va a pasar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "En el capítulo 3 gana identity, o sea ninguna activación, con 0,7230. ¿Qué significa eso?\n",
    "\n",
    "a) Que en esta tabla no hay nada no lineal que aprender\n",
    "\n",
    "b) Que identity es la mejor activación\n",
    "\n",
    "c) Que la prueba está mal hecha\n",
    "\n",
    "d) Que hay que probar más activaciones\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Es la mejor aquí. En el XOR del capítulo 4 no aprende absolutamente nada.\n",
    "\n",
    "*c)* Está hecha igual que las otras y con la misma partición.\n",
    "\n",
    "*d)* Se probaron cuatro. El resultado no es que falte una: es qué dice de los datos.\n",
    "\n",
    "La activación no se elige por moda: se elige por lo que hay en los datos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La activación de salida que no reparte nada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Te suena que la capa de salida lleva softmax, así que lo pones. La red entrena, no da ningún error y el acierto sale razonable.\n",
    "\n",
    "```\n",
    "salida = softmax(h @ W2 + b2)\n",
    "\n",
    "print('salida.shape:', salida.shape)\n",
    "print('primeras:', salida[:3].ravel())\n",
    "\n",
    "# salida.shape: (750, 1)\n",
    "# primeras: [1. 1. 1.]\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Softmax reparte 1 entre todas las salidas que hay. Si hay **una sola**, le toca el 1 entero, siempre, pase lo que pase 🥧 El modelo está diciendo \"seguro que sí\" en las 750 filas y por eso su acierto es exactamente el porcentaje de la clase mayoritaria.\n",
    "\n",
    "Y no falla, que es lo peor: devuelve un array de la forma correcta, con números entre 0 y 1 que suman 1. Todo lo que comprobarías a ojo pasa.\n",
    "\n",
    "La regla es de las pocas que se pueden memorizar sin culpa: **softmax cuando hay varias clases y una neurona por clase, sigmoide cuando hay dos clases y una sola neurona**. Con dos clases también vale softmax si pones dos neuronas de salida, y entonces sí reparte."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🌀 Sin activación, dos capas son exactamente una: la diferencia es\n",
    "4,4e-16.\n",
    "\n",
    "- 📉 La pendiente de la sigmoide no pasa de 0,25 y se multiplica entre capas.\n",
    "Veinte capas la dejan en 9,095e-13.\n",
    "\n",
    "- ✂️ ReLU tiene pendiente 1 en todo el lado positivo, y por eso las redes\n",
    "profundas se pudieron entrenar.\n",
    "\n",
    "- 💀 Y a cambio mata neuronas: con un sesgo de -5 murieron 6 de 8.\n",
    "\n",
    "- 🎚️ La activación del medio se elige por el gradiente; la de la salida, por\n",
    "lo que quieres leer.\n",
    "\n",
    "- 🧯 La softmax se escribe restando el máximo, o devuelve `nan` con\n",
    "números grandes.\n",
    "\n",
    "- 😳 En nuestros datos gana `identity`, o sea ninguna activación,\n",
    "con 0,7230. Doblar es una capacidad y no una mejora.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin la parte que dobla, cien capas son una sola. Y eso se comprueba en dos líneas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y si quieres el vocabulario suelto de todo esto, lo tengo definido en dos líneas por término en el [glosario de IA](https://missyera.com/glosario-ia/) 📖\n",
    "\n",
    "En el capítulo 4 apilamos la primera red de verdad y le damos el problema que\n",
    "una sola neurona no puede resolver.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 3 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/activaciones/\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
}
