{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Normalizar por lotes para que la red no se atasque\n",
    "\n",
    "Cuatro capas que no aprenden y tres líneas que las destraban. Y el error de producción que esas mismas tres líneas provocan.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 10 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/normalizar-por-lotes/\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": [
    "Este capítulo empieza con una red que no aprende nada. Y no le pasa nada\n",
    "raro: le pasa lo que le pasa a casi todas las primeras veces 🫠\n",
    "\n",
    "En el capítulo 4 vimos que apilar capas es lo que permite\n",
    "resolver lo que una sola no puede. Vamos a llevarlo al extremo: cuatro capas\n",
    "ocultas sobre los dígitos, que debería ir mejor que una, y a mirar qué\n",
    "ocurre.\n",
    "\n",
    "Antes de correrlo, piénsalo tú un segundo: **¿qué le pasa a un número\n",
    "cuando lo multiplicas cuatro veces seguidas por una matriz?** Ahí está\n",
    "toda la respuesta 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las cuatro capas, y lo que se les va de las manos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from sklearn.datasets import load_digits\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "d = load_digits()\n",
    "X = d.data / 16.0\n",
    "y = d.target\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",
    "print('para entrenar:', X_tr.shape)\n",
    "print('para probar  :', X_te.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los mismos dígitos de 8 por 8 del capítulo 11. Ahora los\n",
    "pesos, y aquí viene el truco del capítulo: los voy a poder multiplicar por un\n",
    "número para desajustarlos a propósito."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def pesos_al_azar(escala, capas=4, H=32, semilla=0):\n",
    "    # La red del capitulo: cuatro capas ocultas y una de salida, inicializadas\n",
    "    # como en el proyecto final pero multiplicadas por `escala` para poder\n",
    "    # desajustarlas a proposito.\n",
    "    rng = np.random.default_rng(semilla)\n",
    "    dims = [64] + [H] * capas + [10]\n",
    "    return [rng.normal(0, escala * np.sqrt(2 / dims[i]), (dims[i], dims[i + 1]))\n",
    "            for i in range(len(dims) - 1)]\n",
    "\n",
    "for escala in [1.0, 4.0]:\n",
    "    A = X_tr\n",
    "    anchos = []\n",
    "    for W in pesos_al_azar(escala)[:4]:\n",
    "        A = np.maximum(0, A @ W)\n",
    "        anchos.append(round(float(A.std()), 3))\n",
    "    print(f'pesos x{escala}, desviacion capa por capa:', anchos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la segunda fila 😳\n",
    "\n",
    "Con los pesos cuatro veces más grandes, los números que salen de cada capa se\n",
    "van multiplicando: 1,1 después 3,4 después 16,1 y al final 52,1. Cuatro capas y\n",
    "ya son cincuenta veces más anchos de lo que entraron.\n",
    "\n",
    "La primera fila también dice algo, aunque más callado: 0,281 y va bajando\n",
    "hasta 0,204. Se encoge. En una red de cuarenta capas eso llega a cero y se\n",
    "llama gradiente que se desvanece, que es exactamente el problema del capítulo\n",
    "15 pero en vertical en vez de en el tiempo.\n",
    "\n",
    "A esto se le llama **desplazamiento interno**: cada capa aprende\n",
    "mirando lo que le manda la de abajo, y lo que le manda la de abajo cambia de\n",
    "escala mientras las dos aprenden. Es como aprender a cocinar mientras alguien te\n",
    "cambia la unidad de la balanza cada rato 🍳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La idea, que son tres líneas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si el problema es que los números se van de escala, la solución es\n",
    "devolverlos a su sitio antes de pasar a la siguiente capa. Restar la media y\n",
    "dividir entre la desviación, que es exactamente lo que se le hace a una columna\n",
    "de `monto` antes de meterla en un modelo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Z = X_tr @ pesos_al_azar(4.0)[0]\n",
    "print('sin normalizar: media', round(float(Z.mean()), 3),\n",
    "      ' desviacion', round(float(Z.std()), 3))\n",
    "Zn = (Z - Z.mean(0)) / np.sqrt(Z.var(0) + 1e-5)\n",
    "print('normalizado   : media', round(float(Zn.mean()), 3),\n",
    "      ' desviacion', round(float(Zn.std()), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Media 0 y desviación 1, que es donde las activaciones del capítulo\n",
    "3 se portan bien 📏\n",
    "\n",
    "El `+ 1e-5` no es decoración: si una neurona sale siempre igual su\n",
    "varianza es cero, y sin ese numerito estarías dividiendo entre cero. Se llama\n",
    "épsilon y va en todas las implementaciones de verdad.\n",
    "\n",
    "Y lo importante, que es lo que le da nombre: **la media y la desviación\n",
    "se calculan sobre el lote**, o sea sobre las filas que están pasando\n",
    "ahora mismo por la red. Por eso se llama normalización por lotes y no\n",
    "normalización a secas 🔧"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Gamma y beta, o cómo deshacerlo si conviene"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí hay una objeción buena, y si se te ocurrió a ti, muy bien: si forzamos\n",
    "todas las capas a media 0 y desviación 1, le estamos quitando libertad a la red.\n",
    "¿Y si a esa capa le venía bien salir con media 3?\n",
    "\n",
    "Por eso después de normalizar se vuelve a estirar y a correr, con dos números\n",
    "que la red aprende sola:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "salida = gamma * normalizado + beta\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`gamma` dice cuánto estirar y `beta` cuánto correr. Y\n",
    "son parámetros como los pesos: empiezan en 1 y en 0, y el descenso de gradiente\n",
    "del capítulo 5 los mueve.\n",
    "\n",
    "O sea que la red puede deshacer la normalización si le conviene. La\n",
    "diferencia es que ahora **lo elige** en vez de sufrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La red entera, con y sin"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es larga porque está escrita a mano, como todo en este libro. Las líneas de\n",
    "la normalización son cinco y están marcadas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def softmax(z):\n",
    "    z = z - z.max(axis=1, keepdims=True)\n",
    "    e = np.exp(z)\n",
    "    return e / e.sum(axis=1, keepdims=True)\n",
    "\n",
    "\n",
    "def entrena(con_norma, escala=1.0, capas=4, H=32, vueltas=300, paso=0.5, semilla=0):\n",
    "    W = pesos_al_azar(escala, capas, H, semilla)\n",
    "    b = [np.zeros(H) for _ in range(capas)] + [np.zeros(10)]\n",
    "    g = [np.ones(H) for _ in range(capas)]        # gamma, cuanto estirar\n",
    "    be = [np.zeros(H) for _ in range(capas)]      # beta, cuanto correr\n",
    "    mu_g = [np.zeros(H) for _ in range(capas)]    # las medias que se guardan\n",
    "    var_g = [np.ones(H) for _ in range(capas)]\n",
    "    Y = np.eye(10)[y_tr]\n",
    "    n = len(X_tr)\n",
    "    for it in range(vueltas):\n",
    "        A, guardado = [X_tr], []\n",
    "        for i in range(capas):\n",
    "            Z = A[-1] @ W[i] + b[i]\n",
    "            if con_norma:\n",
    "                mu, var = Z.mean(0), Z.var(0) + 1e-5\n",
    "                mu_g[i] = 0.9 * mu_g[i] + 0.1 * mu        # media movil\n",
    "                var_g[i] = 0.9 * var_g[i] + 0.1 * var\n",
    "                Zn = (Z - mu) / np.sqrt(var)\n",
    "                guardado.append((var, Zn))\n",
    "                Z = g[i] * Zn + be[i]\n",
    "            else:\n",
    "                guardado.append(None)\n",
    "            A.append(np.maximum(0, Z))\n",
    "        P = softmax(A[-1] @ W[-1] + b[-1])\n",
    "        if it == 0:\n",
    "            primera = -np.mean(np.log(P[np.arange(n), y_tr] + 1e-12))\n",
    "        dZ = (P - Y) / n\n",
    "        gW = [None] * len(W)\n",
    "        gb = [None] * len(b)\n",
    "        gW[-1] = A[-1].T @ dZ\n",
    "        gb[-1] = dZ.sum(0)\n",
    "        dA = dZ @ W[-1].T\n",
    "        for i in range(capas - 1, -1, -1):\n",
    "            dZi = dA * (A[i + 1] > 0)\n",
    "            if con_norma:\n",
    "                var, Zn = guardado[i]\n",
    "                g[i] -= paso * (dZi * Zn).sum(0)\n",
    "                be[i] -= paso * dZi.sum(0)\n",
    "                dZn = dZi * g[i]\n",
    "                dZi = (dZn - dZn.mean(0) - Zn * (dZn * Zn).mean(0)) / np.sqrt(var)\n",
    "            gW[i] = A[i].T @ dZi\n",
    "            gb[i] = dZi.sum(0)\n",
    "            dA = dZi @ W[i].T\n",
    "        for i in range(len(W)):\n",
    "            W[i] -= paso * gW[i]\n",
    "            b[i] -= paso * gb[i]\n",
    "    ultima = -np.mean(np.log(P[np.arange(n), y_tr] + 1e-12))\n",
    "    return dict(W=W, b=b, g=g, be=be, mu=mu_g, var=var_g, capas=capas,\n",
    "                con_norma=con_norma, primera=primera, ultima=ultima)\n",
    "\n",
    "\n",
    "def aplica(m, M, guardadas=True):\n",
    "    for i in range(m['capas']):\n",
    "        Z = M @ m['W'][i] + m['b'][i]\n",
    "        if m['con_norma']:\n",
    "            mu = m['mu'][i] if guardadas else Z.mean(0)\n",
    "            var = m['var'][i] if guardadas else Z.var(0) + 1e-5\n",
    "            Z = m['g'][i] * (Z - mu) / np.sqrt(var) + m['be'][i]\n",
    "        M = np.maximum(0, Z)\n",
    "    return softmax(M @ m['W'][-1] + m['b'][-1])\n",
    "\n",
    "\n",
    "print(f'{\"pesos\":>8} {\"norma\":>8} {\"perdida\":>18} {\"acierto\":>9}')\n",
    "for escala in [1.0, 4.0]:\n",
    "    for con in [False, True]:\n",
    "        m = entrena(con, escala)\n",
    "        acierto = (aplica(m, X_te).argmax(1) == y_te).mean()\n",
    "        print(f'{escala:8.1f} {\"si\" if con else \"no\":>8} '\n",
    "              f'{m[\"primera\"]:8.3f} -> {m[\"ultima\"]:6.3f} {acierto:9.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está todo el capítulo en cuatro filas 📊\n",
    "\n",
    "Con los pesos bien puestos, la norma sube de 0,8467 a 0,9622. Está bien, pero\n",
    "no es el titular.\n",
    "\n",
    "El titular es la tercera fila: con los pesos desajustados la red\n",
    "**no aprende nada**. Pérdida 2,302, que es exactamente lo que da\n",
    "contestar al azar entre diez dígitos, y acierto 0,1022, que es uno de cada diez.\n",
    "Trescientas vueltas para quedarse donde empezó.\n",
    "\n",
    "Y la cuarta fila es la misma red, con los mismos pesos rotos, y las tres\n",
    "líneas puestas: 0,9044. De tirar el modelo a la basura a un modelo que sirve 💪\n",
    "\n",
    "Eso es lo que hace la normalización por lotes de verdad. No es que mejore un\n",
    "poco: es que hace entrenable lo que no lo era. Por eso está en casi toda red que\n",
    "veas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Normalizar por lotes no mejora una red que ya entrena. Hace entrenar a una que no podía."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora el problema que trae debajo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vuelve a leer la idea: la media y la desviación se calculan sobre el lote.\n",
    "\n",
    "Eso está muy bien mientras entrenas, porque siempre tienes mil filas\n",
    "pasando a la vez. Pero cuando pongas el modelo a trabajar te va a llegar\n",
    "**un pedido, uno solo**, y un lote de una fila tiene media él mismo\n",
    "y desviación cero 😰\n",
    "\n",
    "Por eso en el código de arriba hay dos líneas que todavía no expliqué:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "mu_g[i] = 0.9 * mu_g[i] + 0.1 * mu\n",
    "var_g[i] = 0.9 * var_g[i] + 0.1 * var\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Son **medias móviles**. Durante el entrenamiento se va guardando\n",
    "un promedio de las medias y las varianzas de todos los lotes que pasaron, y esos\n",
    "números guardados son los que se usan después, cuando toca predecir.\n",
    "\n",
    "Mira si da igual o no da igual:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = entrena(True)\n",
    "print('con las medias guardadas    :',\n",
    "      round(float((aplica(m, X_te).argmax(1) == y_te).mean()), 4))\n",
    "print('con las del lote de prueba  :',\n",
    "      round(float((aplica(m, X_te, guardadas=False).argmax(1) == y_te).mean()), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi lo mismo, ¿no? 0,9622 contra 0,96. Aquí es donde uno cierra el archivo y\n",
    "se va tranquilo 🙃\n",
    "\n",
    "Y aquí es donde te la pegas. Porque en producción no llegan 450 filas juntas:\n",
    "llega una."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('lo que de verdad son           :', y_te[:10].tolist())\n",
    "print('todas juntas, medias guardadas :', aplica(m, X_te).argmax(1)[:10].tolist())\n",
    "print('fila por fila, medias guardadas:',\n",
    "      [int(aplica(m, X_te[i:i + 1]).argmax(1)[0]) for i in range(10)])\n",
    "print('fila por fila, medias del lote :',\n",
    "      [int(aplica(m, X_te[i:i + 1], guardadas=False).argmax(1)[0]) for i in range(10)])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres. Diez veces 😵\n",
    "\n",
    "Con las medias del propio lote, la red contesta **3 a todo**. Y\n",
    "tiene sentido: si el lote es una fila sola, normalizarla contra sí misma la deja\n",
    "en ceros, así que las diez filas entran a la red convertidas en la misma cosa y\n",
    "salen con la misma respuesta.\n",
    "\n",
    "La línea de arriba es la que salva: con las medias guardadas, fila por fila\n",
    "da **exactamente lo mismo** que en lote. Idéntico, y fíjate que\n",
    "incluye el mismo fallo, ese 8 donde había un 9. Eso es lo que uno quiere de un\n",
    "modelo en producción: que conteste igual esté solo o acompañado, aunque se\n",
    "equivoque 🎯\n",
    "\n",
    "Y este es el motivo por el que en PyTorch existen `model.train()` y\n",
    "`model.eval()`, y por el que olvidarse del segundo es de los errores\n",
    "más repetidos del mundo entero. No es un capricho de la librería: es esta\n",
    "diferencia."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error que sí revienta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada capa tiene su propio `gamma` y su propio `beta`, y\n",
    "son del ancho de esa capa. Si te confundes de capa al guardarlos o al cargarlos,\n",
    "sale esto:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    gamma_de_la_primera = np.ones(32)\n",
    "    Z_de_una_capa_de_64 = np.zeros((100, 64))\n",
    "    gamma_de_la_primera * Z_de_una_capa_de_64\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "ValueError: operands could not be broadcast together with shapes (32,) (100,64)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este es el error bueno, el que te avisa 🚩\n",
    "\n",
    "El malo es el de arriba, el de las medias, que no revienta: te devuelve\n",
    "predicciones perfectamente formadas y todas iguales."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y en tu trabajo, ¿dónde toca esto?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te lo aterrizo con la distribuidora del libro, que es de donde salen las\n",
    "ventas, el canal y la ciudad de todos los capítulos 💼"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Lo que estás haciendo | ¿Normalizar por lotes? |\n",
    "|---|---|\n",
    "| Una red de dos capas sobre tu tabla de ventas | Ni te molestes. Con dos capas no hay nada que se descontrole, y antes revisa el capítulo 1 |\n",
    "| Leer el monto de una factura escaneada, con una red de verdad | Sí, y ya viene puesta. Toda arquitectura moderna de imágenes la trae |\n",
    "| Servir ese modelo pedido por pedido desde una API | Aquí es donde importa este capítulo. Modo evaluación siempre, y compruébalo mandando la misma fila sola y acompañada |\n",
    "| Predecir el stock de la semana con una red recurrente | Ahí se usa la prima hermana, la normalización por capa, que es la del capítulo 17 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La tercera fila es la que yo pondría en el checklist de todo el mundo: antes\n",
    "de dar por bueno un despliegue, manda una fila sola y compárala con la misma\n",
    "fila dentro de un lote. Si no dan lo mismo, tienes esto 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La prima hermana, en dos líneas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Porque te la vas a encontrar y conviene que no te sorprenda. La normalización\n",
    "por capa hace la misma cuenta pero **al revés**: en vez de promediar\n",
    "cada neurona a lo largo de las filas del lote, promedia todas las neuronas de una\n",
    "fila."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "por lotes:  media de cada neurona, a lo largo de las filas del lote\n",
    "por capa :  media de todas las neuronas, dentro de una sola fila\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La diferencia práctica es enorme: la de capa no depende del lote, así que no\n",
    "tiene el problema de arriba y no necesita guardar medias. Por eso es la que usan\n",
    "los transformers del capítulo 17, donde cada frase\n",
    "tiene un largo distinto y hacer lotes es un lío 🧩\n",
    "\n",
    "Si quieres el detalle formal de las dos, el paper original de la de lotes es\n",
    "[Batch Normalization](https://arxiv.org/abs/1502.03167) (2015) y el de la de\n",
    "capa es [Layer Normalization](https://arxiv.org/abs/1607.06450) (2016). Los dos se\n",
    "leen bien.\n",
    "\n",
    "Y una nota honesta sobre el porqué: durante años se explicó que la\n",
    "normalización funciona porque quita el desplazamiento interno, que es como te lo\n",
    "conté al principio. En 2018 salió\n",
    "[How Does Batch Normalization Help\n",
    "Optimization?](https://arxiv.org/abs/1805.11604), que mide eso mismo y encuentra que la explicación no se\n",
    "sostiene: lo que la normalización hace es suavizar el terreno por donde baja el\n",
    "gradiente. Funciona igual de bien; lo que cambió es el porqué 📚"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Un equipo despliega una red con normalización por lotes para leer el monto de las facturas que llegan escaneadas. La prueban con un lote de mil facturas viejas, sale 0,96 y la ponen en producción. A la semana, la mitad de los montos leídos están mal y nadie entiende por qué.\n",
    "\n",
    "```\n",
    "# el servicio, que atiende una factura por peticion\n",
    "def lee_monto(imagen):\n",
    "    lote = imagen.reshape(1, -1)      # una sola factura\n",
    "    return red.predict(lote)\n",
    "\n",
    "# y asi se probo antes de subirlo\n",
    "print(red.score(mil_facturas_viejas, montos_reales))   # 0.96\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Se probó con mil facturas a la vez y se usa de una en una. Con normalización por lotes eso **no es lo mismo**: si la red sigue calculando la media y la desviación del lote que le llega, un lote de una fila se normaliza contra sí mismo y sale convertido en ceros, así que todas las facturas le parecen la misma. En el capítulo está medido: diez filas distintas y la red contesta 3 a las diez. La corrección es usar las medias móviles guardadas durante el entrenamiento, que en PyTorch es `model.eval()` y en Keras se hace solo al llamar a `predict`. Y la prueba que lo hubiera cazado antes de subirlo cabe en dos líneas: manda una fila sola, mándala dentro de un lote, y comprueba que dan lo mismo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis. El 4 es el que yo haría antes de subir cualquier modelo a producción 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Sube la escala hasta romperlo del todo\n",
    "\n",
    "Prueba con `escala` 8 y 16, con norma y sin."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for escala in [1.0, 4.0, 8.0, 16.0]:\n",
    "    for con in [False, True]:\n",
    "        m = entrena(con, escala)\n",
    "        acierto = (aplica(m, X_te).argmax(1) == y_te).mean()\n",
    "        print(f'x{escala:<5} {\"con\" if con else \"sin\":>4} norma: {acierto:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La pregunta que contesta: ¿hay un punto donde ni la normalización salva la\n",
    "red? Búscalo, que saber dónde se rompe una herramienta vale más que saber que\n",
    "funciona."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Quita gamma y beta\n",
    "\n",
    "Deja `g` fijo en 1 y `be` fijo en 0,\n",
    "o sea normaliza y no dejes que la red lo deshaga."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se hace borrando las dos líneas que los actualizan. Y ojo con lo que\n",
    "esperas: el acierto baja poco, porque en una red chiquita como esta la libertad\n",
    "que dan importa menos de lo que suena. Vale igual medirlo, que es la única forma\n",
    "de saber cuánto aporta cada pieza."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Normaliza solo la primera capa\n",
    "\n",
    "En vez de las cuatro, normaliza únicamente la capa 0 y mide."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si el problema fuera solo de la entrada, con esto bastaría. Si el problema es\n",
    "que la escala se va acumulando capa a capa, no va a bastar. El resultado te dice\n",
    "cuál de las dos historias es la verdadera, y ese es el tipo de experimento que\n",
    "convierte una explicación en un hecho 🔬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La prueba de las dos formas\n",
    "\n",
    "La que te salva del error de la trampa. Escríbela como\n",
    "función y déjala puesta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def coinciden(m, M):\n",
    "    juntas = aplica(m, M).argmax(1)\n",
    "    solas = np.array([aplica(m, M[i:i + 1]).argmax(1)[0] for i in range(len(M))])\n",
    "    return (juntas == solas).mean()\n",
    "\n",
    "print('con medias guardadas:', coinciden(m, X_te[:50]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tiene que dar 1.0 exacto. Si da menos, tu modelo contesta distinto según con\n",
    "quién viaje, y eso en producción es un problema silencioso."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Lotes pequeños\n",
    "\n",
    "Entrena con lotes de 8 filas en vez de con las 1.347 de\n",
    "golpe y compara."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con lotes chicos la media y la desviación se calculan sobre poquitas filas,\n",
    "así que son ruidosas. Ese ruido tiene dos caras: molesta al entrenar y de paso\n",
    "regulariza, que es lo del capítulo 8. Mide las dos\n",
    "caras antes de opinar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Ordena las cajas al revés\n",
    "\n",
    "Mueve la normalización a después de la ReLU en vez de antes\n",
    "y mira si cambia algo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El paper original la pone antes de la activación y es lo que hicimos aquí.\n",
    "Mucha implementación moderna la pone después y funciona igual de bien. Es una de\n",
    "esas discusiones donde la respuesta honesta es medir en tu caso, no citar a\n",
    "nadie 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tu modelo con normalización por lotes da 0,96 en la prueba y en producción contesta casi siempre lo mismo. ¿Qué miras primero?\n",
    "\n",
    "a) Si al predecir está usando las medias guardadas o las del lote que llega\n",
    "\n",
    "b) Si los datos de producción vienen de otra distribución\n",
    "\n",
    "c) Si la red está sobreajustada\n",
    "\n",
    "d) Si hay que entrenar más vueltas\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Puede pasar y hay que mirarlo. Pero mira antes lo que se rompe también con los mismos datos de la prueba, mandados de uno en uno.\n",
    "\n",
    "*c)* Una red sobreajustada falla, sí, pero falla variado. Contestar casi siempre lo mismo apunta a que todas las entradas están llegando convertidas en la misma cosa.\n",
    "\n",
    "*d)* Si en la prueba daba 0,96, entrenada está. El problema apareció entre la prueba y la producción, así que ahí hay que mirar.\n",
    "\n",
    "Y la comprobación cuesta dos líneas: la misma fila sola y dentro de un lote, y que den lo mismo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 📏 Normalizar por lotes es restar la media y dividir entre la desviación\n",
    "**del lote**, dentro de la red y en cada capa.\n",
    "\n",
    "- 💪 Sobre estos dígitos, con los pesos desajustados, la red pasa de 0,1022 a\n",
    "0,9044. No mejora: hace posible lo que no lo era.\n",
    "\n",
    "- 🎛️ `gamma` y `beta` le devuelven a la red la libertad\n",
    "que la normalización le quita, y los aprende sola.\n",
    "\n",
    "- ⚠️ Al predecir hay que usar las medias guardadas. Con las del lote, diez\n",
    "filas distintas devuelven 3 diez veces.\n",
    "\n",
    "- 🧩 En texto se usa la prima hermana, la normalización por capa, que no\n",
    "depende del lote.\n",
    "\n",
    "En el capítulo 11 está la otra pieza que trae toda red de\n",
    "imágenes, y el vocabulario suelto está en el\n",
    "[glosario de IA](https://missyera.com/glosario-ia/) 📖\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 10 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/normalizar-por-lotes/\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
}
