{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reutilizar lo que otra red ya aprendió\n",
    "\n",
    "Congelar la capa oculta de una red entrenada y medir qué pasa. Salió lo contrario de lo que yo esperaba.\n",
    "\n",
    "Cuaderno de práctica del capítulo 19 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/transferencia/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este capítulo lo escribí esperando una cosa y salió otra. Te cuento las dos,\n",
    "porque la segunda enseña más 🔄\n",
    "\n",
    "La idea de partida es la que sostiene casi todo el deep learning que se usa\n",
    "hoy: **nadie entrena desde cero**. Se agarra una red que ya aprendió\n",
    "sobre millones de imágenes y se le enseña lo tuyo encima, con cien fotos en vez\n",
    "de un millón.\n",
    "\n",
    "Antes de medirlo, piénsalo tú: **¿qué es exactamente lo que una red\n",
    "aprende y se puede prestar?** No son las respuestas, porque las clases son\n",
    "otras. Es la capa del medio 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué es lo que se presta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 4 vimos que una capa oculta transforma el\n",
    "espacio hasta que el problema se pueda separar. Esa transformación es lo\n",
    "valioso: aprender a ver bordes, curvas y trazos cuesta muchos datos, y una vez\n",
    "aprendida sirve para reconocer cosas que la red nunca vio.\n",
    "\n",
    "Vamos a montarlo con los dígitos del capítulo 11, partidos\n",
    "en dos mitades: la red aprende con los dígitos del 0 al 4, y después le pedimos\n",
    "que reconozca del 5 al 9 con poquísimos ejemplos."
   ]
  },
  {
   "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\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.neural_network import MLPClassifier\n",
    "\n",
    "d = load_digits()\n",
    "X = d.data / 16.0\n",
    "y = d.target\n",
    "X_viejas, y_viejas = X[y <= 4], y[y <= 4]        # las que tienen muchas fotos\n",
    "X_nuevas, y_nuevas = X[y >= 5], y[y >= 5]        # las que casi no tienen\n",
    "print('fotos de las clases viejas:', len(X_viejas))\n",
    "print('fotos de las clases nuevas:', len(X_nuevas))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa partición imita la situación real: tienes un montón de datos de una cosa\n",
    "y cuatro fotos de la que te interesa 📸"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "base = MLPClassifier(hidden_layer_sizes=(64,), max_iter=600,\n",
    "                     random_state=0).fit(X_viejas, y_viejas)\n",
    "print('acierto de la base en sus propias clases:', round(base.score(X_viejas, y_viejas), 4))\n",
    "print('pesos de la capa oculta:', base.coefs_[0].shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La base aprendió sus cinco dígitos perfecto. Y lo que nos vamos a llevar\n",
    "prestado es esa matriz de 64 por 64, que es su capa del medio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prestarla son dos líneas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def rasgos(M):\n",
    "    # La capa oculta de la base, usada como extractor de caracteristicas.\n",
    "    return np.maximum(0, M @ base.coefs_[0] + base.intercepts_[0])\n",
    "\n",
    "print('64 pixeles se vuelven', rasgos(X_nuevas[:1]).shape[1], 'numeros aprendidos')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es todo. Multiplicar por los pesos que la base aprendió y aplicar su\n",
    "activación, o sea la misma cuenta del capítulo 2 pero parando a\n",
    "mitad de camino 🔧\n",
    "\n",
    "El `np.maximum(0, ...)` es la ReLU del capítulo\n",
    "3: `MLPClassifier` la usa por defecto, así que hay\n",
    "que aplicar la misma o los números no significarían lo mismo.\n",
    "\n",
    "A esto se le llama **congelar** la base: sus pesos no se vuelven\n",
    "a tocar, solo se usan. Encima ponemos un modelo barato, que con pocos datos es\n",
    "justo lo que hay que poner."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la medición"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f'{\"por clase\":>10} {\"pixeles\":>10} {\"transferencia\":>15}')\n",
    "for pocas in [2, 3, 5, 10]:\n",
    "    pix, tra = [], []\n",
    "    for semilla in range(5):\n",
    "        i_tr, i_te = train_test_split(np.arange(len(X_nuevas)), train_size=pocas * 5,\n",
    "                                      random_state=semilla, stratify=y_nuevas)\n",
    "        Xt, yt = X_nuevas[i_tr], y_nuevas[i_tr]\n",
    "        Xe, ye = X_nuevas[i_te], y_nuevas[i_te]\n",
    "        pix.append(accuracy_score(ye, LogisticRegression(max_iter=2000)\n",
    "                                  .fit(Xt, yt).predict(Xe)))\n",
    "        tra.append(accuracy_score(ye, LogisticRegression(max_iter=2000)\n",
    "                                  .fit(rasgos(Xt), yt).predict(rasgos(Xe))))\n",
    "    print(f'{pocas:10} {np.mean(pix):10.4f} {np.mean(tra):15.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pierde. En los cuatro tamaños 🫠\n",
    "\n",
    "Con dos fotos por clase, los píxeles crudos sacan 0,8032 y la transferencia\n",
    "0,6781. Doce puntos de diferencia, y encima la brecha no se cierra al darle más\n",
    "datos.\n",
    "\n",
    "Y no es de una partición con suerte: cada fila es el promedio de cinco\n",
    "particiones distintas, que es la lección del capítulo 4\n",
    "aplicada aquí."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La explicación fácil, y por qué es falsa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La primera explicación que se le ocurre a cualquiera, y a mí la primera, es\n",
    "esta: *la base se entrenó para distinguir del 0 al 4, así que tiró todo lo\n",
    "que no le servía para eso, incluido justo lo que separa al 5 del 8*.\n",
    "\n",
    "Suena impecable. Vamos a comprobarla, que para eso están los datos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.feature_selection import mutual_info_classif\n",
    "\n",
    "i_pix = mutual_info_classif(X_nuevas, y_nuevas, random_state=0).sum()\n",
    "i_ras = mutual_info_classif(rasgos(X_nuevas), y_nuevas, random_state=0).sum()\n",
    "print(f'informacion sobre la clase nueva en los 64 pixeles : {i_pix:.4f}')\n",
    "print(f'informacion en los 64 rasgos de la base            : {i_ras:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es falsa 😮\n",
    "\n",
    "Los rasgos de la base tienen **más** información sobre las clases\n",
    "nuevas que los píxeles crudos, no menos. 11,83 contra 11,16.\n",
    "\n",
    "O sea que el problema no es que se perdiera nada. Es que la información quedó\n",
    "**ordenada para la tarea vieja**: repartida entre las 64 neuronas de\n",
    "una forma que le sirve a una recta para separar del 0 al 4, y que a una recta\n",
    "para separar del 5 al 9 no le sirve igual.\n",
    "\n",
    "Y esto explica de paso por qué en la práctica **casi nadie congela la\n",
    "base entera**: se ajusta, o sea se sigue entrenando desde esos pesos en\n",
    "vez de arrancar de cero. Eso permite reordenar la representación en lugar de\n",
    "tener que apañárselas con el orden viejo 🔧"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La transferencia no falla por perder información. Falla cuando la información queda ordenada para otra pregunta."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error que sale al cambiar la entrada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y antes de cerrar, el error que te va a salir el primer día. Imagina que\n",
    "tienes fotos de productos del almacén y decides recortarlas a otro tamaño para\n",
    "que pesen menos:"
   ]
  },
  {
   "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",
    "    recortadas = X_nuevas[:, :30]      # nos quedamos con 30 pixeles en vez de 64\n",
    "    rasgos(recortadas)\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: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 64 is different from 30)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*size 64 is different from 30*. La base aprendió una matriz de 64 por\n",
    "64 y no sabe multiplicar otra cosa.\n",
    "\n",
    "Esto sí revienta, y por eso es el bueno de los dos errores. El peligroso es el\n",
    "de la trampa de más abajo, que **no** revienta 🚩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y en un negocio de verdad, ¿dónde entra esto?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te lo aterrizo con la distribuidora del libro, que es donde a mí me llega esta\n",
    "pregunta 💼"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Lo que te piden | Qué haría yo |\n",
    "|---|---|\n",
    "| Clasificar fotos de productos del stock por categoría | Modelo preentrenado congelado, y encima una regresión. Es el caso de manual y ahí sí gana |\n",
    "| Sacar el monto y el número de pedido de facturas escaneadas | Empieza por un lector de texto ya hecho. Entrenar uno tuyo es meses |\n",
    "| Predecir qué cliente va a comprar, con tu tabla de ventas | Nada de esto. Vuelve al capítulo 1 y usa una logística |\n",
    "| Clasificar los reclamos que llegan por WhatsApp | Un modelo de lenguaje ya entrenado. Aquí prestar es casi obligatorio |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en la tercera fila, que es la que más veces me toca decir en voz alta:\n",
    "si tu dato es una tabla de ventas con ciudad, canal y segmento, la transferencia\n",
    "no aplica porque no hay nada que prestar 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Entonces, ¿cuándo sí sirve?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La condición es una y se deduce de todo lo anterior: **la transferencia\n",
    "gana cuando aprender los rasgos es la parte cara**.\n",
    "\n",
    "Aquí no lo es. Una imagen de 8 por 8 son 64 números y ya son una\n",
    "representación decente: cada píxel dice algo directo. No hay gran cosa que\n",
    "aprender antes de clasificar, así que prestada no aporta y encima estorba."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Tu dato | ¿Sirve prestar? |\n",
    "|---|---|\n",
    "| Imágenes de 8x8 como estas | No. Los píxeles crudos ya sirven, medido arriba |\n",
    "| Fotos de verdad, 224x224 a color | Sí, y muchísimo. Son 150.000 números y aprender a verlos cuesta millones de ejemplos |\n",
    "| Texto | Sí. Aprender qué significa una palabra es el trabajo entero, y es lo que hace el capítulo 13 |\n",
    "| Tu tabla de ventas | No. Vuelve al capítulo 1 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y una cosa que hay que decir con todas sus letras: **este libro no puede\n",
    "enseñarte la parte donde la transferencia gana**. Para eso hace falta un\n",
    "modelo preentrenado sobre millones de fotos, que pesa cientos de megas y hay que\n",
    "descargarlo. Lo que sí puedes hacer tú, cuando tengas internet y ganas, es\n",
    "buscar `timm` o los modelos de\n",
    "[Hugging Face](https://huggingface.co/models) y repetir exactamente este\n",
    "montaje con uno de ellos 🌐\n",
    "\n",
    "Lo que sí te llevas de aquí es lo que no cambia con el modelo: qué es lo que\n",
    "se presta, cómo se enchufa, y cómo se comprueba si sirvió."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El caso donde esto se usa todos los días: el sentimiento de un texto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De la tabla de arriba, la fila del texto es la que más veces vas a ver\n",
    "aplicada, y tiene nombre propio: **análisis de sentimiento**. Saber\n",
    "si un reclamo llega enojado, si una reseña es buena o mala, si el comentario del\n",
    "cliente pide una llamada urgente.\n",
    "\n",
    "Y se resuelve exactamente con el montaje de este capítulo, cambiando las fotos\n",
    "por frases. La base preentrenada, en vez de una red que vio millones de imágenes,\n",
    "es una que leyó millones de textos: la más nombrada se llama\n",
    "**BERT**. Encima se le pone un clasificador chiquito con tus\n",
    "etiquetas, que suelen ser unas cientos, y listo.\n",
    "\n",
    "La diferencia con lo que acabamos de medir es la que decide todo, y ya está\n",
    "dicha arriba: **acá aprender los rasgos sí es la parte cara**. Una\n",
    "imagen de 8 por 8 son 64 números que ya significan algo por sí solos. Una frase\n",
    "es una fila de palabras que no significan nada hasta que alguien aprendió con\n",
    "quién anda cada una, que es el trabajo entero del capítulo\n",
    "13 y el que no puedes pagar con doscientos reclamos\n",
    "tuyos.\n",
    "\n",
    "Por eso en imágenes de 8 por 8 la transferencia estorbó y en texto es casi\n",
    "obligatoria. No son dos resultados contradictorios: es la misma regla aplicada a\n",
    "dos datos distintos 🔄\n",
    "\n",
    "Dos avisos si te toca evaluarlo en el trabajo. El primero es que el\n",
    "sentimiento es más difícil de lo que suena, porque la ironía y la queja educada\n",
    "se escriben con palabras amables, y eso no lo arregla un modelo más grande.\n",
    "\n",
    "El segundo es de la casa: si la base leyó sobre todo inglés y tus reclamos\n",
    "llegan en castellano peruano con jerga y sin tildes, la parte prestada sirve\n",
    "menos de lo que promete el folleto. Se comprueba igual que todo lo de este\n",
    "capítulo, midiendo contra la versión sin prestar nada 📋"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Un equipo reutiliza una red preentrenada para clasificar fotos de productos. Preparan las imágenes así y el modelo sale con un acierto malísimo que nadie sabe explicar.\n",
    "\n",
    "```\n",
    "# la base se entreno con imagenes normalizadas a media 0 y desviacion 1\n",
    "base = carga_modelo_preentrenado()\n",
    "\n",
    "fotos = carga_fotos()          # valores de 0 a 255\n",
    "rasgos = base.extrae(fotos)    # se le pasan tal cual\n",
    "\n",
    "modelo = LogisticRegression().fit(rasgos, etiquetas)\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis. El 3 es el que contesta la pregunta que quedó abierta 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Rómpelo con el preprocesamiento\n",
    "\n",
    "Pásale a `rasgos()` los píxeles sin dividir entre\n",
    "16 y mira cuánto cae."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Una base más chica\n",
    "\n",
    "Repite todo con `hidden_layer_sizes=(16,)` en la\n",
    "base y compara."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Ajustar en vez de congelar\n",
    "\n",
    "La pregunta que el capítulo deja abierta. En vez de congelar\n",
    "la base, arranca de sus pesos y sigue entrenando."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Cambia qué mitad es la vieja\n",
    "\n",
    "Entrena la base con el 5 al 9 y transfiere al 0 al 4."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. ¿Y si la base ve todas las clases?\n",
    "\n",
    "Entrena la base con los diez dígitos y después transfiere al\n",
    "5 al 9."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuántas fotos necesitas para empatar\n",
    "\n",
    "Sigue subiendo el número de fotos por clase y busca dónde\n",
    "las dos curvas se cruzan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tienes 300 fotos de piezas de tu almacén y quieres clasificarlas. ¿Por dónde empiezas?\n",
    "\n",
    "a) Por un modelo preentrenado de imágenes, congelado, y encima una regresión\n",
    "\n",
    "b) Por entrenar una red desde cero con las 300 fotos\n",
    "\n",
    "c) Por los píxeles crudos y una regresión, como en este capítulo\n",
    "\n",
    "d) Por etiquetar 10.000 fotos más"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🔄 Prestar una red es usar su capa oculta como entrada de otro modelo, y son\n",
    "dos líneas.\n",
    "\n",
    "- 📉 Sobre estos dígitos pierde en los cuatro tamaños, y con dos fotos por\n",
    "clase pierde por doce puntos.\n",
    "\n",
    "- 🔬 Y no por perder información: los rasgos prestados tienen más (11,83\n",
    "contra 11,16). Queda ordenada para la pregunta vieja.\n",
    "\n",
    "- 🎯 Gana cuando aprender a ver es la parte cara. Con 64 píxeles no lo es.\n",
    "\n",
    "- ⚠️ Y el error que no avisa es olvidarse del preprocesamiento con el que se\n",
    "entrenó la base.\n",
    "\n",
    "Si quieres el vocabulario suelto de todo esto, está definido en dos líneas por\n",
    "término en el [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 19 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/transferencia/\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
}
