{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Una red que reconoce dígitos\n",
    "\n",
    "Todo el libro en un archivo, sin importar ninguna librería de deep learning. Y por fin la red gana.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 21 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/proyecto-final/\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": [
    "Trece capítulos diciendo dónde la red pierde. Este es donde gana 🎉\n",
    "\n",
    "Y una pregunta antes de empezar: **¿en qué dígito crees que se va a equivocar más?** Apuesta ahora, porque al final vamos a mirar los once errores uno por uno 🔢\n",
    "\n",
    "Vamos a escribir una red completa, de principio a fin, sin importar ni una\n",
    "librería de deep learning. Y va a reconocer números escritos a mano."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El encargo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1.797 imágenes de dígitos de 8 por 8, las mismas del capítulo 11. Hay que\n",
    "decir qué número es cada una, de 0 a 9.\n",
    "\n",
    "Y ojo con eso último, porque es nuevo: **hasta ahora todo el libro fue\n",
    "sí o no**. Aquí hay diez respuestas posibles, y eso cambia dos piezas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from sklearn.datasets import load_digits\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import accuracy_score, confusion_matrix\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "digitos = load_digits()\n",
    "X = digitos.images.reshape(len(digitos.images), -1) / 16.0\n",
    "y = digitos.target\n",
    "\n",
    "i_tr, i_te = train_test_split(np.arange(len(X)), test_size=0.25,\n",
    "                              random_state=42, stratify=y)\n",
    "X_tr, X_te = X[i_tr], X[i_te]\n",
    "y_tr, y_te = y[i_tr], y[i_te]\n",
    "\n",
    "Y = np.zeros((len(y_tr), 10))              # el objetivo, una columna por dígito\n",
    "Y[np.arange(len(y_tr)), y_tr] = 1\n",
    "\n",
    "print('entreno con', len(X_tr), 'imágenes de', X.shape[1], 'píxeles')\n",
    "print('el objetivo pasa de', y_tr.shape, 'a', Y.shape)\n",
    "print('la primera imagen es un', y_tr[0], 'y su fila objetivo es', Y[0].astype(int))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese `Y` con una columna por dígito es el one-hot del capítulo 13,\n",
    "ahora aplicado a la *respuesta* en vez de a las entradas. La red va a\n",
    "sacar diez números y compararlos con esa fila 🔟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las dos piezas nuevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para diez clases cambia la activación de salida y cambia la pérdida:"
   ]
  },
  {
   "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",
    "def perdida(P, Y):\n",
    "    return float(-np.mean(np.sum(Y * np.log(np.clip(P, 1e-12, 1)), axis=1)))\n",
    "\n",
    "ejemplo = softmax(np.array([[2.0, 0.5, -1.0]]))\n",
    "print('tres puntajes se vuelven:', np.round(ejemplo, 4), ' suman', round(float(ejemplo.sum()), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La **softmax** del capítulo 3 en vez de la sigmoide, para que las\n",
    "diez salidas sumen 1 y se puedan leer como probabilidades que compiten.\n",
    "\n",
    "Y la **entropía cruzada categórica**, que es la del capítulo 5\n",
    "generalizada: en vez de mirar una probabilidad, mira la de la clase correcta.\n",
    "\n",
    "Y aquí está lo bonito: aunque las dos fórmulas cambien,\n",
    "**`P - Y` sigue siendo el gradiente**, exactamente igual\n",
    "que con una sola salida. La pareja softmax y entropía cruzada se simplifica igual\n",
    "de bien que sigmoide y entropía cruzada. Por eso van siempre juntas 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La red entera"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L=−∑kyklogpk\n",
    "\n",
    "solo mira la probabilidad que el modelo le dio a la clase correcta, y la castiga con un logaritmo que se dispara cuando esa probabilidad es chica"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def entrena(ocultas=64, vueltas=1000, paso=0.5, semilla=0, decay=0.0):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    entradas = X_tr.shape[1]\n",
    "    # arranque con la escala de He, que es la que va con ReLU\n",
    "    W1 = r.normal(0, np.sqrt(2 / entradas), (entradas, ocultas))\n",
    "    b1 = np.zeros(ocultas)\n",
    "    W2 = r.normal(0, np.sqrt(2 / ocultas), (ocultas, 10))\n",
    "    b2 = np.zeros(10)\n",
    "\n",
    "    for _ in range(vueltas):\n",
    "        h = np.maximum(0, X_tr @ W1 + b1)          # ReLU, capítulo 3\n",
    "        P = softmax(h @ W2 + b2)\n",
    "        d2 = (P - Y) / len(Y)                      # capítulo 5\n",
    "        d1 = (d2 @ W2.T) * (h > 0)                 # capítulo 6\n",
    "        W2 -= paso * (h.T @ d2 + decay * W2)       # capítulo 8\n",
    "        b2 -= paso * d2.sum(axis=0)\n",
    "        W1 -= paso * (X_tr.T @ d1 + decay * W1)\n",
    "        b1 -= paso * d1.sum(axis=0)\n",
    "    return W1, b1, W2, b2\n",
    "\n",
    "def predice(Z, W1, b1, W2, b2):\n",
    "    return softmax(np.maximum(0, Z @ W1 + b1) @ W2 + b2)\n",
    "\n",
    "W1, b1, W2, b2 = entrena()\n",
    "p_tr = predice(X_tr, W1, b1, W2, b2).argmax(axis=1)\n",
    "p_te = predice(X_te, W1, b1, W2, b2).argmax(axis=1)\n",
    "print('acierto entrenando:', round(accuracy_score(y_tr, p_tr), 4))\n",
    "print('acierto en prueba :', round(accuracy_score(y_te, p_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está, y con eso ya terminó el libro en lo esencial 💛\n",
    "\n",
    "Una red de una capa oculta, escrita en veinte líneas, reconoce números escritos\n",
    "a mano con casi 98% de acierto. Sin TensorFlow, sin PyTorch, sin nada.\n",
    "\n",
    "Fíjate en el arranque de los pesos, que es lo único que no habíamos visto:\n",
    "`sqrt(2 / entradas)`. Se llama inicialización de He y está pensada para\n",
    "ReLU. Si arrancas con la escala 0,1 de los capítulos anteriores, con 64 entradas\n",
    "por neurona la suma sale demasiado chica y la red tarda mucho más en arrancar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora sí: contra la logística"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logistica = LogisticRegression(max_iter=3000).fit(X_tr, y_tr)\n",
    "print('logística:', round(accuracy_score(y_te, logistica.predict(X_te)), 4))\n",
    "print('nuestra red:', round(accuracy_score(y_te, p_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,9756 la red contra 0,9622 la logística 🏆\n",
    "\n",
    "Después de trece capítulos perdiendo, gana. Y gana en el sitio donde el\n",
    "capítulo 1 dijo que ganaría: **cuando el dato tiene estructura**.\n",
    "\n",
    "Es un punto y tres décimas de diferencia, que parece poco y no lo es: sobre 450\n",
    "imágenes son seis errores menos. En un sistema que procesa cheques o formularios,\n",
    "eso es la diferencia entre revisar seis cosas a mano o no.\n",
    "\n",
    "Y ahora fíjate en algo que en el capítulo 7 era la señal de alarma: el\n",
    "acierto en entrenamiento es **1,0000**, o sea perfecto. Allá eso\n",
    "venía con una prueba de 0,5870 y significaba memorización pura.\n",
    "\n",
    "Aquí viene con 0,9756 en prueba. La red se aprendió las 1.347 imágenes\n",
    "*y además* generaliza.\n",
    "\n",
    "Eso es lo que cambia cuando el dato tiene estructura: no es que memorizar deje\n",
    "de ocurrir, es que lo que aprende de memoria **también sirve para las\n",
    "imágenes nuevas**. Por eso aquí no hicieron falta ni parada temprana ni\n",
    "frenos 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## En qué se equivoca"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "M = confusion_matrix(y_te, p_te)\n",
    "print('errores:', int((p_te != y_te).sum()), 'de', len(y_te))\n",
    "print()\n",
    "for i in range(10):\n",
    "    for j in range(10):\n",
    "        if i != j and M[i, j] >= 2:\n",
    "            print(f'   confundió el {i} con el {j}: {M[i, j]} veces')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once errores en 450 imágenes. Y los que repite son el 8 con el 1 (tres veces)\n",
    "y el 0 con el 4 (dos veces).\n",
    "\n",
    "El 8 con el 1 tiene sentido: en 8 por 8 píxeles, un 8 mal escrito es un trazo\n",
    "vertical con dos bultitos, y un 1 es un trazo vertical. El 0 con el 4 es más\n",
    "raro, y es lo que yo iría a mirar imagen por imagen antes de entregar\n",
    "esto 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Sabe cuándo duda?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La pregunta del capítulo 13 del libro de machine learning, ahora aquí:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "probabilidades = predice(X_te, W1, b1, W2, b2)\n",
    "confianza = probabilidades.max(axis=1)\n",
    "fallo = p_te != y_te\n",
    "\n",
    "print('confianza media cuando acierta:', round(float(confianza[~fallo].mean()), 4))\n",
    "print('confianza media cuando falla  :', round(float(confianza[fallo].mean()), 4))\n",
    "print()\n",
    "for corte in [0.5, 0.9, 0.99]:\n",
    "    seguros = confianza >= corte\n",
    "    print(f'si solo contesto con confianza >= {corte}: '\n",
    "          f'contesto {seguros.mean():.4f} de las veces y acierto '\n",
    "          f'{accuracy_score(y_te[seguros], p_te[seguros]):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sí sabe 🎯\n",
    "\n",
    "Cuando acierta, su confianza media es 0,9742. Cuando falla, 0,7159. Son\n",
    "veintiséis puntos de diferencia, y eso convierte la confianza en una señal\n",
    "utilizable.\n",
    "\n",
    "Y la tabla de abajo es el producto de verdad: **pidiéndole confianza de\n",
    "al menos 0,9 contesta el 91,78% de las veces y en esas acierta el\n",
    "99,52%**. El 8% restante se manda a una persona.\n",
    "\n",
    "Ese es exactamente el diseño de un sistema de lectura de formularios que\n",
    "funciona en producción. No uno que acierta siempre: uno que sabe cuáles pasar a\n",
    "revisión 🤝"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y esto para qué le sirve a una distribuidora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Porque el libro empezó con las ventas de una distribuidora peruana y termina\n",
    "con dígitos, y conviene cerrar el círculo 🔄\n",
    "\n",
    "En el capítulo 1 medimos que sobre esa tabla de 3.000 ventas la red sacaba\n",
    "0,5834 y la regresión logística 0,7214. Aquí sobre imágenes la red saca 0,9756 y\n",
    "la logística 0,9622. **El mismo tipo de modelo, resultados invertidos**,\n",
    "y la diferencia no está en el modelo: está en si el dato tiene forma.\n",
    "\n",
    "Así que la pregunta útil no es \"¿usamos deep learning?\", es **\"¿qué\n",
    "tenemos que no entre en una tabla?\"**. En una distribuidora eso suele\n",
    "existir y suele estar tirado:\n",
    "\n",
    "- 📝 Pedidos anotados a mano por el vendedor, que alguien transcribe.\n",
    "\n",
    "- 🧾 Facturas y guías escaneadas de las que hay que sacar números.\n",
    "\n",
    "- 📸 Fotos de anaquel para ver si el producto está donde debería.\n",
    "\n",
    "- 💬 Comentarios y reclamos de clientes en texto libre.\n",
    "\n",
    "Los tres primeros son este capítulo con imágenes más grandes. El cuarto son\n",
    "los capítulos 13, 16 y 18.\n",
    "\n",
    "Y para lo que sí entra en una tabla, o sea a quién visitar y quién va a\n",
    "comprar, la respuesta sigue siendo el libro anterior: una regresión logística,\n",
    "que se entrena en dos décimas de segundo y se explica leyendo sus\n",
    "coeficientes 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que se entrega"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como en el proyecto final del libro de machine learning, esto no se entrega en\n",
    "un cuaderno:\n",
    "\n",
    "- 💾 Los cuatro arrays (W1, b1, W2, b2) guardados con\n",
    "`np.savez`. Pesan poco y no dependen de ninguna librería.\n",
    "\n",
    "- 📄 Una página con qué predice, con qué datos, cuánto acierta (0,9756), contra\n",
    "qué se compara (0,9622) y en qué se equivoca (8 con 1, 0 con 4).\n",
    "\n",
    "- 🚦 El umbral de confianza elegido y qué porcentaje manda a revisión.\n",
    "\n",
    "- 📐 La lista de tamaños de entrada, porque el capítulo 11 nos enseñó que ahí es\n",
    "donde revientan las cosas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Guardar el modelo y volver a cargarlo\n",
    "\n",
    "Guarda los pesos y comprueba que predice igual."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import tempfile\n",
    "\n",
    "ruta = os.path.join(tempfile.gettempdir(), 'red-digitos.npz')\n",
    "np.savez(ruta, W1=W1, b1=b1, W2=W2, b2=b2)\n",
    "print('pesa', round(os.path.getsize(ruta) / 1024, 1), 'KB')\n",
    "\n",
    "cargado = np.load(ruta)\n",
    "p2 = predice(X_te, cargado['W1'], cargado['b1'],\n",
    "             cargado['W2'], cargado['b2']).argmax(axis=1)\n",
    "print('predice igual:', bool((p2 == p_te).all()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "pesa 38.5 KB\n",
    "predice igual: True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Treinta y ocho kilobytes y medio, y predice exactamente lo mismo.\n",
    "\n",
    "Cuatro arrays de numpy en un archivo. Sin dependencias, sin versión de\n",
    "librería que se rompa dentro de dos años, sin nada. Es de las cosas que más\n",
    "tranquila me dejan de haber escrito todo a mano 💾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuántas neuronas y cuántas vueltas\n",
    "\n",
    "Barre las dos cosas y busca el punto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for ocultas in [16, 64, 128]:\n",
    "    ws = entrena(ocultas=ocultas)\n",
    "    pr = predice(X_te, *ws).argmax(axis=1)\n",
    "    print(f'{ocultas:3d} neuronas: {accuracy_score(y_te, pr):.4f}')\n",
    "print()\n",
    "for vueltas in [200, 1000, 3000]:\n",
    "    ws = entrena(vueltas=vueltas)\n",
    "    pr = predice(X_te, *ws).argmax(axis=1)\n",
    "    print(f'{vueltas:5d} vueltas: {accuracy_score(y_te, pr):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    " 16 neuronas: 0.9556\n",
    " 64 neuronas: 0.9756\n",
    "128 neuronas: 0.9711\n",
    "\n",
    "  200 vueltas: 0.9622\n",
    " 1000 vueltas: 0.9756\n",
    " 3000 vueltas: 0.9711\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 16 neuronas ya saca 0,9556 y con 128 baja un poquito respecto a 64 (0,9711\n",
    "contra 0,9756). Y las vueltas mejoran hasta las 1.000 y con 3.000 vuelve a bajar\n",
    "a 0,9711.\n",
    "\n",
    "Compáralo con el capítulo 7, donde cada botón que subías empeoraba. Aquí los\n",
    "botones se comportan: suben, llegan a un punto y se quedan. **Esa es la\n",
    "diferencia entre un problema donde la red tiene algo que aprender y uno donde\n",
    "no** 📈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Sin la inicialización de He\n",
    "\n",
    "Arranca con la escala 0,1 de los capítulos anteriores y\n",
    "compara."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def entrena_escala(escala, vueltas=1000, ocultas=64):\n",
    "    r = np.random.default_rng(0)\n",
    "    W1 = r.normal(0, escala, (X_tr.shape[1], ocultas)); b1 = np.zeros(ocultas)\n",
    "    W2 = r.normal(0, escala, (ocultas, 10)); b2 = np.zeros(10)\n",
    "    for _ in range(vueltas):\n",
    "        h = np.maximum(0, X_tr @ W1 + b1)\n",
    "        P = softmax(h @ W2 + b2)\n",
    "        d2 = (P - Y) / len(Y)\n",
    "        d1 = (d2 @ W2.T) * (h > 0)\n",
    "        W2 -= 0.5 * (h.T @ d2); b2 -= 0.5 * d2.sum(axis=0)\n",
    "        W1 -= 0.5 * (X_tr.T @ d1); b1 -= 0.5 * d1.sum(axis=0)\n",
    "    return accuracy_score(y_te, softmax(np.maximum(0, X_te @ W1 + b1) @ W2 + b2).argmax(axis=1))\n",
    "\n",
    "for escala in [0.01, 0.1, float(np.sqrt(2 / 64))]:\n",
    "    print(f'escala {escala:.4f}: {entrena_escala(escala):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "escala 0.0100: 0.9644\n",
    "escala 0.1000: 0.9689\n",
    "escala 0.1768: 0.9756\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 0,01 se queda en 0,9644, con 0,1 en 0,9689 y con la de He (0,1768) llega\n",
    "a 0,9756.\n",
    "\n",
    "Un punto por elegir bien los números con los que arrancas, antes de haber\n",
    "entrenado nada. Es el ajuste del que menos se habla y el que decide si el\n",
    "entrenamiento arranca o se arrastra 🎬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Ver los errores uno por uno\n",
    "\n",
    "Imprime en texto las imágenes que falló."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fallos = np.where(p_te != y_te)[0]\n",
    "for k in fallos[:2]:\n",
    "    imagen = (X_te[k].reshape(8, 8) * 16).astype(int)\n",
    "    print(f'era un {y_te[k]} y dijo {p_te[k]} con confianza '\n",
    "          f'{confianza[k]:.3f}')\n",
    "    for fila in imagen:\n",
    "        print('   ' + ''.join('#' if v > 8 else ('+' if v > 3 else '.') for v in fila))\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "era un 8 y dijo 1 con confianza 0.880\n",
    "   ..+##+..\n",
    "   ..+###..\n",
    "   ..+##...\n",
    "   ..+#+...\n",
    "   ..+#....\n",
    "   ..+#+...\n",
    "   ..###...\n",
    "   ..+##...\n",
    "\n",
    "era un 0 y dijo 4 con confianza 0.839\n",
    "   ...##...\n",
    "   ...##+..\n",
    "   ..##.#..\n",
    "   ..#+.#..\n",
    "   ..##.#..\n",
    "   ..#+##..\n",
    "   ..###+..\n",
    "   ...##...\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Míralas con calma, que se entiende la confusión: son trazos que a un ojo humano\n",
    "también le costarían en 8 por 8 píxeles.\n",
    "\n",
    "Este ejercicio es el que más recomiendo de todo el libro. **Mirar los\n",
    "errores uno por uno te dice si el modelo está roto o si el problema es\n",
    "difícil**, y son dos situaciones que piden cosas muy distintas 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El umbral que decide cuánto revisa una persona\n",
    "\n",
    "Arma la tabla de decisión con el costo de cada opción."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f'{\"corte\":>6} {\"contesta\":>10} {\"acierta\":>9} {\"revisan\":>9} {\"errores\":>9}')\n",
    "for corte in [0.0, 0.8, 0.95, 0.99, 0.999]:\n",
    "    seguros = confianza >= corte\n",
    "    if seguros.sum() == 0:\n",
    "        continue\n",
    "    acierto = accuracy_score(y_te[seguros], p_te[seguros])\n",
    "    print(f'{corte:6.3f} {seguros.mean():10.4f} {acierto:9.4f} '\n",
    "          f'{(~seguros).sum():9d} {int((p_te[seguros] != y_te[seguros]).sum()):9d}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    " corte   contesta   acierta   revisan   errores\n",
    " 0.000     1.0000    0.9756         0        11\n",
    " 0.800     0.9511    0.9860        22         6\n",
    " 0.950     0.8756    0.9949        56         2\n",
    " 0.990     0.7867    0.9972        96         1\n",
    " 0.999     0.5644    1.0000       196         0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con corte 0,999 no pasa **ni un solo error** automático, y a\n",
    "cambio una persona revisa 196 de las 450. Con corte 0 no revisa nadie y pasan\n",
    "los 11.\n",
    "\n",
    "Y mira el punto de en medio: con 0,95 revisan 56 y pasan 2 errores. Ese es el\n",
    "tipo de fila que se lleva a la reunión.\n",
    "\n",
    "Cuál eliges depende de lo que cueste cada error y de lo que cueste cada\n",
    "revisión, exactamente como el capítulo 9 del libro de machine learning. Aquí lo\n",
    "único nuevo es que la palanca es la confianza y no el umbral de una\n",
    "probabilidad 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Todo lo que usamos, capítulo por capítulo\n",
    "\n",
    "Repasa qué pieza salió de dónde."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PIEZAS = [\n",
    "    ('la neurona: multiplicar, sumar, aplastar', 2),\n",
    "    ('ReLU en la capa oculta', 3),\n",
    "    ('la capa oculta que dobla', 4),\n",
    "    ('la pérdida y el descenso de gradiente', 5),\n",
    "    ('la retropropagación con (P - Y)', 6),\n",
    "    ('mirar entrenamiento y prueba a la vez', 7),\n",
    "    ('el weight decay, aquí sin necesitarlo', 8),\n",
    "    ('los píxeles como dato con forma', 11),\n",
    "    ('el one-hot del objetivo', 13),\n",
    "    ('la softmax estable', 17),\n",
    "]\n",
    "for pieza, capitulo in PIEZAS:\n",
    "    print(f'capítulo {capitulo:2d}: {pieza}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "capítulo  2: la neurona: multiplicar, sumar, aplastar\n",
    "capítulo  3: ReLU en la capa oculta\n",
    "capítulo  4: la capa oculta que dobla\n",
    "capítulo  5: la pérdida y el descenso de gradiente\n",
    "capítulo  6: la retropropagación con (P - Y)\n",
    "capítulo  7: mirar entrenamiento y prueba a la vez\n",
    "capítulo  8: el weight decay, aquí sin necesitarlo\n",
    "capítulo 11: los píxeles como dato con forma\n",
    "capítulo 13: el one-hot del objetivo\n",
    "capítulo 17: la softmax estable\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Diez piezas y ninguna es de librería 🧱\n",
    "\n",
    "Si llegaste hasta aquí escribiéndolas, ya no hay ninguna parte de una red\n",
    "neuronal que tengas que dar por buena porque alguien lo dijo. Y eso, cuando te\n",
    "toque depurar una que no entrena, vale muchísimo 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El error del one-hot al revés\n",
    "\n",
    "Compara la predicción con las etiquetas sin convertirlas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "P = predice(X_te, W1, b1, W2, b2)\n",
    "P - y_te"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: operands could not be broadcast together with shapes (450,10) (450,) \n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las predicciones son de 450 por 10 y las etiquetas de 450, así que numpy no\n",
    "sabe cómo alinearlas.\n",
    "\n",
    "Y este error es un regalo, porque el silencioso está a un paso: si en vez de\n",
    "450 etiquetas tuvieras 10, numpy las estiraría sin quejarse y estarías restando\n",
    "cualquier cosa. Es el mismo peligro del capítulo 4, ahora con diez\n",
    "columnas 📐\n",
    "\n",
    "La regla se repite: **imprime las dos formas antes de restar**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "La red del proyecto final saca resultados parecidos a la logística sobre la misma tabla. ¿Qué se entrega?\n",
    "\n",
    "a) Los dos, con lo que cuesta mantener cada uno al lado\n",
    "\n",
    "b) La red, porque es lo que se pidió\n",
    "\n",
    "c) La logística, y se tira el trabajo de la red\n",
    "\n",
    "d) Un promedio de los dos\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Lo que se pidió es resolver el problema. Entregar lo más caro sin ventaja no es resolverlo.\n",
    "\n",
    "*c)* El trabajo de la red es lo que permite afirmar que no aporta.\n",
    "\n",
    "*d)* Promediar dos modelos parecidos añade mantenimiento sin añadir información.\n",
    "\n",
    "Saber cuándo NO usar una red es parte de saber usarla."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El 97,56% que se cae al salir a la calle"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "La red saca 97,56% con los dígitos del conjunto, así que se conecta a la aplicación del celular para que la gente fotografíe los albaranes.\n",
    "\n",
    "```\n",
    "foto = camara.capturar()\n",
    "digito = red.predice(foto)\n",
    "\n",
    "# en el conjunto: 97,56%\n",
    "# en las fotos reales: 40 y pico\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Los dígitos con los que entrenó vienen todos centrados, del mismo tamaño, en blanco y negro y sin fondo 📸 Una foto de un albarán tiene sombra, el papel torcido, tinta azul y el número descuadrado. Para la red es otro problema.\n",
    "\n",
    "Y no falla avisando. Devuelve un dígito con toda la confianza del mundo, porque un softmax siempre reparte 1 entre las diez opciones: no tiene forma de decir \"esto no se parece a nada de lo que vi\".\n",
    "\n",
    "Antes de conectar nada se prueba con **veinte fotos hechas donde se va a usar**, con el celular que se va a usar. Veinte fotos y media hora te ahorran el proyecto. Y si el número se cae, eso no es un fracaso del modelo: es la medida de cuánto se parecen tus datos de entrenamiento a tu mundo real."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🏆 Una red escrita entera en numpy clasifica dígitos al 97,56% contra el\n",
    "96,22% de una regresión logística. Por fin gana, y gana donde el capítulo 1\n",
    "dijo.\n",
    "\n",
    "- 🔟 Para diez clases cambian dos piezas: softmax en la salida y entropía\n",
    "cruzada categórica. Y el gradiente sigue siendo `P - Y`.\n",
    "\n",
    "- 🧠 El acierto en entrenamiento es 1,0000 y aquí eso no es alarma: con\n",
    "estructura de verdad, lo que aprende de memoria también sirve para lo nuevo.\n",
    "\n",
    "- 🎬 La inicialización de He vale un punto entero de acierto, y es de lo que\n",
    "menos se habla.\n",
    "\n",
    "- 🎯 Su confianza media es 0,9742 cuando acierta y 0,7159 cuando falla: el\n",
    "modelo sabe cuándo duda.\n",
    "\n",
    "- 🤝 Con confianza mínima de 0,9 contesta el 91,78% de las veces y acierta el\n",
    "99,52%. Y con 0,999 no pasa ni un error, revisando 196 de 450.\n",
    "\n",
    "- 💾 Se entrega en cuatro arrays de numpy, sin dependencias que se rompan.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un modelo no falla cuando se equivoca. Falla cuando se equivoca con toda la confianza del mundo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y para seguir, el paso natural no es más deep learning: es saber evaluar bien lo que construyes, que es el [libro de machine learning desde cero](https://missyera.com/guias/machine-learning-desde-cero/) 🎯\n",
    "\n",
    "En el capítulo 22 te dejo dónde seguir, qué librerías tocan ahora y qué cosas\n",
    "este libro no cubre.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## De entender la red a poner una a trabajar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sabes qué hace una capa, por qué la retropropagación funciona y cuándo un transformer es exagerado para tu problema. Ese criterio es la parte difícil y ya la tienes 🐣\n",
    "\n",
    "La parte que queda es la menos glamorosa y la que decide si el proyecto sirve: medir si el sistema hace lo que prometió, con qué se compara, y qué haces el día que empieza a responder cualquier cosa. En [el curso de ingeniería de IA en producción](https://missyera.com/cursos/ingenieria-de-ia/) eso es el hilo de las cuatro semanas.\n",
    "\n",
    "Y ojo con algo que veo seguido: mucha gente sale de un libro como este queriendo entrenar su propia red desde cero, y casi nunca es lo que toca. Elegir bien qué modelo usar y saber medirlo rinde mucho más que entrenar el tuyo, y esa decisión también es parte del curso."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 21 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/proyecto-final/\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
}
