{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Prompt, RAG o ajuste fino\n",
    "\n",
    "La pregunta que llega a las empresas, con un buscador de RAG escrito a mano y sus dos formas de fallar medidas.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 18 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/prompt-rag-o-ajuste/\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 es el capítulo que menos código tiene y el que más veces me han\n",
    "preguntado en reuniones 💼\n",
    "\n",
    "Y te devuelvo la pregunta: **¿el problema que tienes en la cabeza es que el modelo no sabe algo tuyo, o que no responde como tú quieres?** De esa respuesta salen dos caminos que cuestan muy distinto 💭\n",
    "\n",
    "La pregunta llega siempre igual: \"queremos un asistente que responda sobre\n",
    "nuestros documentos, ¿hay que entrenar un modelo?\". Y la respuesta casi siempre\n",
    "es que no."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las tres opciones, y qué arregla cada una"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Si el problema es | La herramienta | Cuesta |\n",
    "|---|---|---|\n",
    "| El modelo puede hacerlo y no le explicaste bien | Prompt | Minutos, gratis |\n",
    "| Le falta información que solo tienes tú | RAG | Días, barato |\n",
    "| Le falta una forma de responder difícil de explicar | Ajuste fino | Semanas, caro |\n",
    "| Le falta capacidad de razonar | Otro modelo | Cambiar de proveedor |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en la primera columna, porque ahí está todo. **La pregunta no es\n",
    "qué herramienta es mejor: es qué te falta**.\n",
    "\n",
    "El error que más veo es usar ajuste fino para meter información. Ajustar un\n",
    "modelo con tus documentos *no* es una forma de que se los aprenda: es una\n",
    "forma de que aprenda a sonar como ellos. Si lo que quieres es que cite tu\n",
    "política de devoluciones correctamente, eso es RAG 📄"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## RAG, que es lo que casi siempre toca"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "RAG son tres pasos: buscar los documentos que tengan que ver con la pregunta,\n",
    "pegarlos en el prompt, y pedirle al modelo que responda usando eso.\n",
    "\n",
    "El paso interesante es el primero, y se puede escribir a mano. Vamos con ocho\n",
    "documentos de una distribuidora:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "DOCS = [\n",
    "    'la politica de devoluciones acepta productos sin abrir hasta 15 dias despues de la entrega',\n",
    "    'el flete es gratis para pedidos mayores a 2000 soles en lima y arequipa',\n",
    "    'los mayoristas tienen 30 dias de credito y las bodegas pagan al contado',\n",
    "    'el horario de atencion es de lunes a viernes de 8 a 6 y sabados de 9 a 1',\n",
    "    'para reclamos por producto roto hay que enviar foto dentro de las 48 horas',\n",
    "    'el descuento por volumen empieza en 10 por ciento a partir de 50 unidades',\n",
    "    'no se aceptan devoluciones de productos refrigerados por politica sanitaria',\n",
    "    'las entregas en cusco y piura demoran 3 dias habiles adicionales',\n",
    "]\n",
    "\n",
    "def tokens(t):\n",
    "    return t.lower().split()\n",
    "\n",
    "vocabulario = sorted({w for d in DOCS for w in tokens(d)})\n",
    "lugar = {w: i for i, w in enumerate(vocabulario)}\n",
    "N = len(DOCS)\n",
    "\n",
    "cuentas = np.zeros((N, len(vocabulario)))\n",
    "for i, d in enumerate(DOCS):\n",
    "    for w in tokens(d):\n",
    "        cuentas[i, lugar[w]] += 1\n",
    "\n",
    "en_cuantos = (cuentas > 0).sum(axis=0)\n",
    "raras = np.log((1 + N) / (1 + en_cuantos)) + 1      # las palabras raras pesan más\n",
    "X = cuentas * raras\n",
    "X = X / np.linalg.norm(X, axis=1, keepdims=True)\n",
    "\n",
    "print('documentos:', N, ' palabras distintas:', len(vocabulario))\n",
    "print('la palabra más rara pesa', round(float(raras.max()), 3),\n",
    "      'y la más común', round(float(raras.min()), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es TF-IDF y es de 1972. La idea: **una palabra que sale en todos los\n",
    "documentos no distingue nada, y una que sale en uno solo distingue\n",
    "muchísimo**. Por eso \"de\" pesa poco y \"refrigerados\" pesa mucho."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def busca(pregunta, k=2):\n",
    "    v = np.zeros(len(vocabulario))\n",
    "    for w in tokens(pregunta):\n",
    "        if w in lugar:\n",
    "            v[lugar[w]] += 1\n",
    "    v = v * raras\n",
    "    if np.linalg.norm(v) == 0:\n",
    "        return []\n",
    "    v = v / np.linalg.norm(v)\n",
    "    parecidos = X @ v\n",
    "    return [(float(parecidos[i]), DOCS[i]) for i in np.argsort(-parecidos)[:k]]\n",
    "\n",
    "for p in ['cuanto demora una entrega en cusco', 'cuanto credito tienen los mayoristas']:\n",
    "    print('P:', p)\n",
    "    for s, d in busca(p):\n",
    "        print(f'   {s:.3f}  {d[:66]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Funciona 🎉\n",
    "\n",
    "La pregunta de Cusco trae el documento de las entregas con 0,320 y la del\n",
    "crédito trae el correcto con 0,611. Eso pegado en un prompt es un asistente que\n",
    "responde sobre tus documentos, sin entrenar nada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Falla número 1: no encuentra lo que sí está"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('P: puedo devolver un producto refrigerado')\n",
    "for s, d in busca('puedo devolver un producto refrigerado'):\n",
    "    print(f'   {s:.3f}  {d[:66]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El documento correcto es el de \"no se aceptan devoluciones de productos\n",
    "refrigerados\", y saca **0,000**. Cero. Ni aparece.\n",
    "\n",
    "Y trae el de los productos rotos, que no tiene nada que ver 😖\n",
    "\n",
    "La razón es de una tontez que da rabia: la pregunta dice\n",
    "*devolver* y el documento dice *devoluciones*. Dice\n",
    "*refrigerado* y el documento dice *refrigerados*. Para un buscador\n",
    "de palabras exactas, son palabras distintas.\n",
    "\n",
    "Este es **el motivo por el que existe la búsqueda por embeddings**:\n",
    "con los vectores del capítulo 13, \"devolver\" y \"devoluciones\" caerían cerca. Pero\n",
    "mira primero la solución barata:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def raiz(w):\n",
    "    for terminacion in ['ciones', 'cion', 'ados', 'adas', 'ando', 'idos',\n",
    "                        'idas', 'ar', 'er', 'ir', 'os', 'as', 'es', 'o', 'a', 's']:\n",
    "        if len(w) > 4 and w.endswith(terminacion):\n",
    "            return w[:-len(terminacion)]\n",
    "    return w\n",
    "\n",
    "def tokens_raiz(t):\n",
    "    return [raiz(w) for w in t.lower().split()]\n",
    "\n",
    "vocab2 = sorted({w for d in DOCS for w in tokens_raiz(d)})\n",
    "lugar2 = {w: i for i, w in enumerate(vocab2)}\n",
    "cuentas2 = np.zeros((N, len(vocab2)))\n",
    "for i, d in enumerate(DOCS):\n",
    "    for w in tokens_raiz(d):\n",
    "        cuentas2[i, lugar2[w]] += 1\n",
    "raras2 = np.log((1 + N) / (1 + (cuentas2 > 0).sum(axis=0))) + 1\n",
    "X2 = cuentas2 * raras2\n",
    "X2 = X2 / np.linalg.norm(X2, axis=1, keepdims=True)\n",
    "\n",
    "def busca_raiz(pregunta, k=2):\n",
    "    v = np.zeros(len(vocab2))\n",
    "    for w in tokens_raiz(pregunta):\n",
    "        if w in lugar2:\n",
    "            v[lugar2[w]] += 1\n",
    "    v = v * raras2\n",
    "    if np.linalg.norm(v) == 0:\n",
    "        return []\n",
    "    parecidos = X2 @ (v / np.linalg.norm(v))\n",
    "    return [(float(parecidos[i]), DOCS[i]) for i in np.argsort(-parecidos)[:k]]\n",
    "\n",
    "print('P: puedo devolver un producto refrigerado')\n",
    "for s, d in busca_raiz('puedo devolver un producto refrigerado'):\n",
    "    print(f'   {s:.3f}  {d[:66]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Arreglado: ahora el documento correcto sale primero con 0,261 🎯\n",
    "\n",
    "Y lo arregló una función de ocho líneas que corta terminaciones, no un modelo\n",
    "de mil millones de parámetros. **Antes de traer artillería, prueba con lo\n",
    "barato**, que en recuperación de texto suele llegar bastante lejos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Falla número 2: siempre devuelve algo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('P: aceptan tarjeta de credito')\n",
    "for s, d in busca_raiz('aceptan tarjeta de credito'):\n",
    "    print(f'   {s:.3f}  {d[:66]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta es la peligrosa de verdad 🚨\n",
    "\n",
    "En los ocho documentos **no hay nada sobre formas de pago**. La\n",
    "respuesta correcta es \"no lo sé\". Y el buscador devuelve, con toda confianza, el\n",
    "documento de las devoluciones de refrigerados con 0,300.\n",
    "\n",
    "Ahora imagina el sistema completo: eso se pega en el prompt con un \"responde\n",
    "usando estos documentos\", y el modelo hace lo que sabe hacer, que es escribir\n",
    "algo plausible con lo que tiene delante. El resultado es una respuesta inventada\n",
    "con una fuente al lado, que es peor que no responder.\n",
    "\n",
    "La solución no es más modelo. Es **un umbral**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "UMBRAL = 0.35\n",
    "\n",
    "def responde(pregunta):\n",
    "    encontrados = busca_raiz(pregunta, k=1)\n",
    "    if not encontrados or encontrados[0][0] < UMBRAL:\n",
    "        return 'No tengo ese dato en los documentos.'\n",
    "    return f'Según los documentos: {encontrados[0][1]}'\n",
    "\n",
    "for p in ['cuanto credito tienen los mayoristas', 'aceptan tarjeta de credito',\n",
    "          'a que hora abren los sabados']:\n",
    "    print(f'P: {p}')\n",
    "    print(f'R: {responde(p)}')\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres líneas y el sistema aprende a decir que no sabe 🤐\n",
    "\n",
    "Pero mira la tercera respuesta, que es la que me interesa: **\"a que hora\n",
    "abren los sabados\" también se queda sin contestar**, y ese dato sí está en\n",
    "los documentos.\n",
    "\n",
    "O sea que el umbral acaba de cambiar un error por otro: antes inventaba, ahora\n",
    "se calla cuando sabía. No hay forma de tener las dos cosas, y elegir dónde\n",
    "ponerlo es una decisión de negocio, no técnica.\n",
    "\n",
    "Es la misma idea del capítulo 13 del libro de machine learning, la predicción\n",
    "conforme: **un sistema que sabe cuándo callarse vale más que uno que\n",
    "siempre contesta**, y por eso yo prefiero perder una respuesta a inventar\n",
    "una.\n",
    "\n",
    "Y el umbral se elige como todo en este libro: con ejemplos. Juntas treinta\n",
    "preguntas de las que tu equipo recibe, marcas cuáles tienen respuesta en los\n",
    "documentos y cuáles no, y buscas el corte. Igualito que el capítulo 11 del libro\n",
    "anterior."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuándo sí toca ajustar el modelo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para que no parezca que estoy en contra. El ajuste fino sirve, y sirve para\n",
    "tres cosas concretas:\n",
    "\n",
    "- ✍️ **Una forma de escribir muy particular** que no se puede\n",
    "explicar en un prompt pero sí enseñar con doscientos ejemplos.\n",
    "\n",
    "- 🏷️ **Una tarea muy repetida y muy acotada**, donde ajustar un\n",
    "modelo chiquito sale mucho más barato por consulta que llamar a uno grande.\n",
    "\n",
    "- 🗣️ **Un vocabulario propio** que el modelo no maneja: jerga\n",
    "técnica, códigos internos, nombres de producto.\n",
    "\n",
    "Y no sirve para meter información. Repito eso porque es el error caro: el\n",
    "ajuste fino enseña **comportamiento, no hechos**. Ajustar con tus\n",
    "documentos hace que el modelo suene como ellos, no que los recuerde 📚\n",
    "\n",
    "Y sobre el costo, una cosa que cambió bastante: hoy casi nadie ajusta el\n",
    "modelo entero. Se usa **LoRA**, que en vez de mover los millones de\n",
    "pesos originales le pega unas matrices chiquitas al lado y entrena solo esas.\n",
    "\n",
    "La idea es la del capítulo 2 de este libro: si una matriz grande se puede\n",
    "aproximar multiplicando dos flacas, entrenas las dos flacas. Con eso un ajuste\n",
    "que pedía varias tarjetas gráficas cabe en una, y el resultado que guardas pesa\n",
    "megas en vez de gigas.\n",
    "\n",
    "Baja el costo, no cambia el diagnóstico: **LoRA barato sigue siendo\n",
    "ajuste fino**, y sigue sin servir para meter información 🧵"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Qué palabras pesan más\n",
    "\n",
    "Mira el peso que TF-IDF le da a cada palabra."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pesos = sorted(zip(raras, vocabulario))\n",
    "print('las que menos distinguen:')\n",
    "for p, w in pesos[:5]:\n",
    "    print(f'   {w:14} {p:.3f}')\n",
    "print('las que más distinguen:')\n",
    "for p, w in pesos[-5:]:\n",
    "    print(f'   {w:14} {p:.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "las que menos distinguen:\n",
    "   de             1.251\n",
    "   y              1.588\n",
    "   a              1.811\n",
    "   dias           1.811\n",
    "   el             1.811\n",
    "las que más distinguen:\n",
    "   soles          2.504\n",
    "   tienen         2.504\n",
    "   unidades       2.504\n",
    "   viernes        2.504\n",
    "   volumen        2.504\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Abajo del todo están \"de\", \"a\", \"y\", \"el\": salen en casi todos los documentos y\n",
    "no sirven para distinguir nada.\n",
    "\n",
    "Arriba están las palabras que salen en un solo documento. Y fíjate que nadie\n",
    "escribió una lista de palabras vacías: **el método las descubre solo**,\n",
    "contando 🔢"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Buscar por trozo en vez de por documento\n",
    "\n",
    "Los documentos de verdad son largos. Pártelos en pedazos y\n",
    "mira qué cambia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "largo = ('la politica de devoluciones acepta productos sin abrir hasta 15 dias '\n",
    "         'despues de la entrega pero no se aceptan devoluciones de productos '\n",
    "         'refrigerados por politica sanitaria y para reclamos por producto roto '\n",
    "         'hay que enviar foto dentro de las 48 horas')\n",
    "\n",
    "palabras = largo.split()\n",
    "trozos = [' '.join(palabras[i:i + 12]) for i in range(0, len(palabras), 12)]\n",
    "for i, t in enumerate(trozos):\n",
    "    print(f'trozo {i}: {t[:64]}')\n",
    "print()\n",
    "print('un documento de', len(palabras), 'palabras se volvió', len(trozos), 'trozos')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "trozo 0: la politica de devoluciones acepta productos sin abrir hasta 15 \n",
    "trozo 1: de la entrega pero no se aceptan devoluciones de productos refri\n",
    "trozo 2: politica sanitaria y para reclamos por producto roto hay que env\n",
    "trozo 3: dentro de las 48 horas\n",
    "\n",
    "un documento de 41 palabras se volvió 4 trozos\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Partirlo importa muchísimo. Si metes el documento entero, la parte de los\n",
    "refrigerados queda diluida entre todo lo demás y el parecido baja.\n",
    "\n",
    "Cómo partir es la decisión más práctica de un RAG: trozos chicos son precisos\n",
    "y pierden contexto, trozos grandes tienen contexto y diluyen. Igual que el\n",
    "pooling del capítulo 11, es un compromiso sin respuesta única ✂️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuántos documentos meter en el prompt\n",
    "\n",
    "Mira qué trae con k=1, k=3 y k=8."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for k in [1, 3, 8]:\n",
    "    resultados = busca_raiz('cuanto demora una entrega en cusco', k=k)\n",
    "    utiles = sum(1 for s, _ in resultados if s > 0.1)\n",
    "    print(f'k={k}: {len(resultados)} documentos, {utiles} con parecido mayor a 0,1')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "k=1: 1 documentos, 1 con parecido mayor a 0,1\n",
    "k=3: 3 documentos, 3 con parecido mayor a 0,1\n",
    "k=8: 8 documentos, 4 con parecido mayor a 0,1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con k=8 traes los ocho documentos y solo cuatro tienen algún parecido. Los\n",
    "otros cuatro son ruido pegado en el prompt.\n",
    "\n",
    "Y el ruido no es gratis: ocupa contexto (que cuesta plata, capítulo 17) y\n",
    "además distrae al modelo. Yo empiezo con k=3 y lo subo solo si mido que hace\n",
    "falta 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Elegir el umbral con ejemplos\n",
    "\n",
    "Arma una lista de preguntas con y sin respuesta y busca el\n",
    "corte."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CON_RESPUESTA = ['cuanto credito tienen los mayoristas',\n",
    "                 'cuanto demora una entrega en cusco',\n",
    "                 'a que hora abren los sabados',\n",
    "                 'desde cuantas unidades hay descuento']\n",
    "SIN_RESPUESTA = ['aceptan tarjeta de credito',\n",
    "                 'tienen sucursal en tacna',\n",
    "                 'quien es el gerente general']\n",
    "\n",
    "for umbral in [0.2, 0.35, 0.5]:\n",
    "    aciertos_con = sum(1 for p in CON_RESPUESTA\n",
    "                       if busca_raiz(p, 1) and busca_raiz(p, 1)[0][0] >= umbral)\n",
    "    aciertos_sin = sum(1 for p in SIN_RESPUESTA\n",
    "                       if not busca_raiz(p, 1) or busca_raiz(p, 1)[0][0] < umbral)\n",
    "    print(f'umbral {umbral}: contesta bien {aciertos_con}/4  '\n",
    "          f'se calla bien {aciertos_sin}/3')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "umbral 0.2: contesta bien 4/4  se calla bien 0/3\n",
    "umbral 0.35: contesta bien 2/4  se calla bien 3/3\n",
    "umbral 0.5: contesta bien 2/4  se calla bien 3/3\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 0,2 contesta las cuatro que sabe y no se calla en **ninguna**\n",
    "de las tres que no sabe: inventa las tres. Con 0,35 se calla en las tres, y a\n",
    "cambio pierde dos que sí sabía.\n",
    "\n",
    "No hay ningún umbral que acierte las siete. Es exactamente la matriz de\n",
    "confusión del capítulo 8 del libro de machine learning, con otros nombres, y se\n",
    "elige igual: **según cuál de los dos errores te cuesta más caro**.\n",
    "\n",
    "En un asistente que habla con clientes, inventar una política de devoluciones\n",
    "cuesta muchísimo más que decir \"consulta con un asesor\". Por eso yo me quedaría\n",
    "con 0,35 aunque pierda dos 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La pregunta con palabras que no existen\n",
    "\n",
    "Pregunta algo cuyas palabras no salen en ningún\n",
    "documento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(busca_raiz('hay wifi en el almacen'))\n",
    "print(responde('hay wifi en el almacen'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "[(0.2168163190237303, 'el flete es gratis para pedidos mayores a 2000 soles en lima y arequipa'), (0.21189158919116474, 'el descuento por volumen empieza en 10 por ciento a partir de 50 unidades')]\n",
    "No tengo ese dato en los documentos.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ni \"wifi\" ni \"almacen\" están en ningún documento, y aun así el buscador\n",
    "devuelve dos cosas con 0,2168 y 0,2119.\n",
    "\n",
    "¿De dónde sale ese parecido? De \"hay\", \"en\" y \"el\", que sí están. Palabras que\n",
    "no significan nada y que aun así arrastran puntaje 😑\n",
    "\n",
    "El `responde` lo salva porque 0,2168 no llega al umbral de 0,35. Y\n",
    "esa es toda la defensa que tienes: **el buscador nunca te va a decir que no\n",
    "encontró nada**, así que la decisión de callarse la tomas tú, con un\n",
    "número."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Lo que cuesta cada opción\n",
    "\n",
    "Pon los números al lado para la conversación de\n",
    "presupuesto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "opciones = [\n",
    "    ('prompt', 0, 'horas', 'cambias el texto y ya'),\n",
    "    ('RAG', 0, 'días', 'hay que indexar y mantener los documentos'),\n",
    "    ('ajuste fino', 200, 'semanas', 'hacen falta ejemplos etiquetados'),\n",
    "]\n",
    "for nombre, ejemplos, tiempo, nota in opciones:\n",
    "    print(f'{nombre:12} {ejemplos:4d} ejemplos  {tiempo:8}  {nota}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "prompt          0 ejemplos  horas     cambias el texto y ya\n",
    "RAG             0 ejemplos  días      hay que indexar y mantener los documentos\n",
    "ajuste fino   200 ejemplos  semanas   hacen falta ejemplos etiquetados\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa tabla es la que yo llevo a la reunión donde alguien dice \"entrenemos un\n",
    "modelo con nuestros datos\".\n",
    "\n",
    "No para decir que no, sino para que la decisión se tome sabiendo. Casi siempre\n",
    "hay una versión con prompt que se prueba esta semana, y si esa no alcanza, ya se\n",
    "verá 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El documento vacío que envenena el índice\n",
    "\n",
    "Mete un documento en blanco al índice y mira qué sale."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vacio = np.zeros((1, len(vocabulario)))\n",
    "normalizado = vacio / np.linalg.norm(vacio, axis=1, keepdims=True)\n",
    "print('nan que quedaron:', int(np.isnan(normalizado).sum()), 'de', normalizado.size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "nan que quedaron: 77 de 77\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un documento vacío tiene norma cero, así que normalizarlo es dividir entre\n",
    "cero. Y numpy **no revienta**: avisa con un\n",
    "`RuntimeWarning` y devuelve `nan` en todas las casillas.\n",
    "\n",
    "Y a partir de ahí ese `nan` contamina todos los parecidos, así que\n",
    "el buscador deja de funcionar para *todas* las preguntas por culpa de un\n",
    "documento en blanco.\n",
    "\n",
    "Pasa constantemente cuando indexas una carpeta de verdad: un PDF escaneado sin\n",
    "texto, un archivo vacío, una página que solo tiene una imagen. **Filtra los\n",
    "documentos sin palabras antes de indexar** 🧹"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. El error de cruzar dos índices\n",
    "\n",
    "Arma la consulta con un vocabulario y búscala en el índice\n",
    "del otro, que es lo que pasa al reindexar y olvidarse de algo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "consulta_vieja = np.zeros(len(vocabulario))\n",
    "consulta_vieja[lugar['credito']] = 1\n",
    "X2 @ consulta_vieja"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 77 is different from 75)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El índice con raíces tiene menos columnas que el original, porque al cortar\n",
    "terminaciones varias palabras se juntaron en una. Las formas no cuadran y\n",
    "revienta.\n",
    "\n",
    "Menos mal. El caso de verdad peligroso es cuando *sí* cuadran: si\n",
    "añades un documento a la lista y no reconstruyes la matriz, no hay error de\n",
    "ninguna clase y ese documento **simplemente no aparece nunca en ningún\n",
    "resultado**, para siempre 😶\n",
    "\n",
    "Por eso el índice y la lista de documentos se construyen juntos, en la misma\n",
    "función, y nunca por separado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "El sistema de RAG no encuentra un documento que sí está en la base. ¿Cuál es el primer sospechoso?\n",
    "\n",
    "a) Cómo se partió el texto en trozos\n",
    "\n",
    "b) El modelo de lenguaje\n",
    "\n",
    "c) El tamaño de la base de documentos\n",
    "\n",
    "d) La temperatura\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* El modelo no puede citar lo que no le llegó. El fallo es anterior.\n",
    "\n",
    "*c)* Con más documentos empeora, pero el documento ya no se encontraba con los que hay.\n",
    "\n",
    "*d)* La temperatura cambia cómo escribe, no qué recupera.\n",
    "\n",
    "Un trozo mal cortado deja la respuesta partida entre dos, y ninguno de los dos se parece a la pregunta."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el ajuste fino que no enseña lo que crees"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Montas RAG, funciona, y el cliente pide además que el modelo \"sepa\" sus procedimientos internos. Le haces un ajuste fino con los mil documentos.\n",
    "\n",
    "```\n",
    "ajustado = ajusta(modelo_base,\n",
    "                  documentos_internos,\n",
    "                  epocas=3)\n",
    "\n",
    "# \"ahora ya conoce nuestros procesos\"\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "El ajuste fino enseña **formas**, no datos 🎓 Enseña a responder con cierto tono, cierto formato o cierta estructura. Lo que no hace es meter mil documentos en la cabeza del modelo de manera que los pueda citar.\n",
    "\n",
    "Lo que sale es lo peor de los dos mundos: recuerda algunos trozos, se inventa el resto con muchísima seguridad, y ya no puedes señalar de qué documento salió cada frase. Con RAG podías, porque el documento estaba delante en el momento de responder.\n",
    "\n",
    "La regla que uso y que casi nadie quiere oír: **si la respuesta tiene que poder citarse, es RAG**. El ajuste fino entra cuando el problema es cómo suena la respuesta, no qué información lleva dentro. Y cuando hacen falta los dos, el ajuste va encima del RAG, nunca en su lugar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🎯 La pregunta no es qué herramienta es mejor, es qué te falta.\n",
    "\n",
    "- 📚 Ajustar un modelo con tus documentos hace que suene como ellos, no que\n",
    "los recuerde. Para información, RAG.\n",
    "\n",
    "- 🔢 TF-IDF es de 1972, se escribe en diez líneas y descubre solo qué palabras\n",
    "no distinguen nada.\n",
    "\n",
    "- 😖 Falla 1: \"devolver\" no encuentra \"devoluciones\", y el documento correcto\n",
    "saca 0,000. Lo arregla una función de ocho líneas que corta terminaciones.\n",
    "\n",
    "- 🚨 Falla 2: siempre devuelve algo. Para \"aceptan tarjeta de credito\", que no\n",
    "está en ningún documento, trae uno con 0,300.\n",
    "\n",
    "- 🤐 Se arregla con un umbral, y el umbral se elige con ejemplos, igual que\n",
    "todo en el libro anterior.\n",
    "\n",
    "- ✂️ Cómo partir los documentos es la decisión más práctica de un RAG: chico es\n",
    "preciso y pierde contexto, grande diluye.\n",
    "\n",
    "- 🧹 Y un documento vacío en el índice rompe el buscador entero con un\n",
    "`nan`.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la respuesta tiene que poder citarse, es RAG. El ajuste fino enseña formas, no datos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta conversación es la que tengo con empresas casi todas las semanas. Si es la tuya, en [consultoría en IA](https://missyera.com/consultoria-ia/) cuento cómo la aterrizo 💼\n",
    "\n",
    "En el capítulo 21 juntamos el libro en un proyecto de punta a punta.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preguntas frecuentes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Qué es prompt engineering?Escribirle a un modelo de lenguaje de forma que devuelva lo que necesitas. No es magia ni son fórmulas secretas: es decir qué quieres, para quién, con qué datos y en qué formato.\n",
    "\n",
    "¿Qué es RAG?Darle al modelo tus documentos en el momento de preguntar, para que conteste con ellos en vez de con lo que recuerda. Es lo que se usa cuando la respuesta tiene que salir de información tuya.\n",
    "\n",
    "¿Cuándo uso RAG y cuándo ajuste fino?RAG cuando lo que falta es información. Ajuste fino cuando lo que falta es la forma: un tono, un formato o una tarea muy repetida. Casi siempre el problema es de información.\n",
    "\n",
    "¿Qué es el ajuste fino de un modelo?Seguir entrenando un modelo ya entrenado con ejemplos tuyos. Cuesta más que RAG, hay que rehacerlo cuando cambian los datos, y por eso se deja para cuando RAG ya no alcanza."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 18 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/prompt-rag-o-ajuste/\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
}
