{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Qué está pasando cuando le escribes a un modelo de lenguaje\n",
    "\n",
    "Tokens, el bloque completo, el bucle que escribe palabra a palabra y qué hace de verdad la temperatura.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 17 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/del-transformer-al-llm/\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": [
    "Ya tenemos todas las piezas. Aquí las juntamos y respondemos la pregunta con\n",
    "la que mucha gente llega a este libro: **qué está pasando exactamente\n",
    "cuando le escribes a ChatGPT o a Claude** 🤖\n",
    "\n",
    "Y antes de contestarla, dime tú: **¿te has imaginado alguna vez que el modelo \"entiende\" la pregunta y luego busca la respuesta?** Casi todo el mundo se lo imagina así, y no es lo que pasa 💬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Primero: el texto se parte en trozos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 13 quedó un error abierto: pedir el vector de \"huancayo\"\n",
    "reventaba porque esa palabra no estaba en el vocabulario. Así se arregla:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "CONOCIDOS = {'bo', 'de', 'ga', 'hu', 'an', 'ca', 'yo', 'li', 'ma', 'min',\n",
    "             'i', 'mar', 'ket', 'a', 'o', 'e', 'u', 'n', 'r', 's', 't'}\n",
    "\n",
    "def trozos(palabra, conocidos):\n",
    "    salida, i = [], 0\n",
    "    while i < len(palabra):\n",
    "        for j in range(len(palabra), i, -1):        # el trozo más largo que quepa\n",
    "            if palabra[i:j] in conocidos:\n",
    "                salida.append(palabra[i:j])\n",
    "                i = j\n",
    "                break\n",
    "        else:\n",
    "            salida.append('?')\n",
    "            i += 1\n",
    "    return salida\n",
    "\n",
    "for w in ['bodega', 'huancayo', 'minimarket', 'lima']:\n",
    "    print(f'{w:12} -> {trozos(w, CONOCIDOS)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está: \"huancayo\" se parte en hu-an-ca-yo. **No hace falta que la\n",
    "palabra esté en el vocabulario, basta con que estén sus trozos** 🧩\n",
    "\n",
    "Eso es la tokenización por subpalabras, y es por lo que un modelo puede leer\n",
    "un nombre propio peruano que nunca vio, o una palabra inventada, o un error de\n",
    "tipeo.\n",
    "\n",
    "También explica dos cosas que se notan usándolos. Que el español gaste más\n",
    "tokens que el inglés (los vocabularios se arman mirando sobre todo texto en\n",
    "inglés, así que nuestras palabras se parten en más trozos), y que a los modelos\n",
    "les cueste contar letras: ellos no ven letras, ven trozos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Segundo: el bloque completo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "a^=γa−μσ2+ε+β\n",
    "\n",
    "recentra y reescala las activaciones de cada ejemplo, y ese epsilon de abajo es lo que evita dividir entre cero cuando la varianza es minúscula"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La atención del capítulo 16 no va sola. Un bloque de transformer le añade tres\n",
    "cosas, y las tres son necesarias:"
   ]
  },
  {
   "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 norma(X, eps=1e-5):\n",
    "    return (X - X.mean(-1, keepdims=True)) / np.sqrt(X.var(-1, keepdims=True) + eps)\n",
    "\n",
    "rng = np.random.default_rng(7)\n",
    "n, d = 6, 8\n",
    "X = rng.normal(0, 1, (n, d))\n",
    "Wq, Wk, Wv = [rng.normal(0, 0.5, (d, d)) for _ in range(3)]\n",
    "W1 = rng.normal(0, 0.5, (d, 4 * d))\n",
    "W2 = rng.normal(0, 0.5, (4 * d, d))\n",
    "\n",
    "def bloque(X):\n",
    "    A = softmax((X @ Wq) @ (X @ Wk).T / np.sqrt(d))\n",
    "    X = norma(X + A @ (X @ Wv))                       # atención + residual + norma\n",
    "    X = norma(X + np.maximum(0, X @ W1) @ W2)         # feedforward + residual + norma\n",
    "    return X\n",
    "\n",
    "Y = bloque(X)\n",
    "print('entra', X.shape, 'y sale', Y.shape)\n",
    "print('media de cada fila al salir     :', np.round(Y.mean(axis=1), 3))\n",
    "print('desviación de cada fila al salir:', np.round(Y.std(axis=1), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres añadidos y cada uno resuelve un problema concreto 🔧\n",
    "\n",
    "- ➕ El **residual**, ese `X +`, deja pasar la entrada\n",
    "sin tocar al lado de lo que calculó la capa. Así el gradiente del capítulo 6\n",
    "tiene un camino directo hasta abajo sin multiplicarse por nada, y por eso se\n",
    "pueden apilar cien capas.\n",
    "\n",
    "- 📏 La **norma por capa** centra cada fila en media 0 y\n",
    "desviación 1. Mira la salida: todas las medias son 0 y todas las desviaciones 1,\n",
    "exacto.\n",
    "\n",
    "- 🧠 El **feedforward** es la red del capítulo 4 aplicada a cada\n",
    "posición por separado, y suele ser cuatro veces más ancha. Ahí está la mayoría de\n",
    "los pesos de un modelo de lenguaje, aunque la fama se la lleve la atención."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Por qué el residual no es opcional"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "𝐚(l+1)=𝐚(l)+F(𝐚(l))\n",
    "\n",
    "la capa aprende lo que hay que sumarle a la entrada en vez de la salida entera, y por eso el gradiente encuentra siempre un camino corto de vuelta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def bloque_sin_residual(X):\n",
    "    A = softmax((X @ Wq) @ (X @ Wk).T / np.sqrt(d))\n",
    "    X = norma(A @ (X @ Wv))\n",
    "    X = norma(np.maximum(0, X @ W1) @ W2)\n",
    "    return X\n",
    "\n",
    "def parecido(a, b):\n",
    "    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))\n",
    "\n",
    "Z = X.copy()\n",
    "for _ in range(6):\n",
    "    Z = bloque_sin_residual(Z)\n",
    "print('sin residual, tras 6 bloques, parecido entre dos posiciones:',\n",
    "      round(parecido(Z[0], Z[1]), 4))\n",
    "\n",
    "Z = X.copy()\n",
    "for capa in range(6):\n",
    "    Z = bloque(Z)\n",
    "    print(f'con residual, tras {capa + 1} bloques: magnitud media {np.abs(Z).mean():.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin residual, después de seis bloques las dos posiciones tienen un parecido de\n",
    "**1,0**: son el mismo vector. Todas las palabras se volvieron\n",
    "idénticas y el modelo perdió la frase entera.\n",
    "\n",
    "Con residual, la magnitud se queda entre 0,80 y 0,83 bloque tras bloque, sin\n",
    "crecer ni apagarse.\n",
    "\n",
    "Una nota honesta: aquí los pesos son aleatorios, así que ese colapso es más\n",
    "brutal de lo que sería con un modelo entrenado. Lo que la medición enseña de\n",
    "verdad es que **el residual y la norma son lo que mantiene estable una pila\n",
    "de bloques**, y sin ellos las redes profundas de texto no se pueden\n",
    "entrenar 🏗️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tercero: el bucle que escribe"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora la parte que sorprende a casi todo el mundo. Un modelo de lenguaje\n",
    "hace **una sola cosa**: dada una secuencia, poner una probabilidad a\n",
    "cada token posible como siguiente.\n",
    "\n",
    "Escribir es repetir eso: eliges uno, lo pegas al final, y vuelves a\n",
    "preguntar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "COMENTARIOS = [\n",
    "    'el pedido llego completo y a tiempo', 'el pedido llego incompleto y con retraso',\n",
    "    'la bodega pidio arroz y azucar', 'el minimarket pidio aceite y azucar',\n",
    "    'el mayorista pidio arroz en cantidad', 'el pedido de la bodega llego tarde',\n",
    "    'el pedido del minimarket llego tarde', 'la bodega reclamo por el retraso del pedido',\n",
    "    'el minimarket reclamo por el aceite roto', 'el mayorista reclamo por la factura',\n",
    "    'llego todo completo sin reclamo', 'el arroz llego en buen estado',\n",
    "    'el aceite llego en buen estado', 'el azucar llego roto y con retraso',\n",
    "    'la bodega pago la factura a tiempo', 'el minimarket pago la factura con retraso',\n",
    "    'el mayorista pago la factura a tiempo', 'el vendedor visito la bodega el lunes',\n",
    "    'el vendedor visito el minimarket el martes', 'el vendedor visito al mayorista el lunes',\n",
    "    'la bodega compro arroz el lunes', 'el minimarket compro aceite el martes',\n",
    "    'el mayorista compro arroz y aceite', 'nadie visito la bodega esta semana',\n",
    "    'el pedido de arroz llego completo', 'el pedido de aceite llego incompleto',\n",
    "]\n",
    "\n",
    "vocabulario = sorted({p for f in COMENTARIOS for p in f.split()} | {'<fin>'})\n",
    "lugar = {p: i for i, p in enumerate(vocabulario)}\n",
    "V = len(vocabulario)\n",
    "\n",
    "siguiente = np.zeros((V, V))\n",
    "for f in COMENTARIOS:\n",
    "    ps = f.split() + ['<fin>']\n",
    "    for a, b in zip(ps, ps[1:]):\n",
    "        siguiente[lugar[a], lugar[b]] += 1\n",
    "\n",
    "def escribe(inicio, temperatura=1.0, semilla=0, maximo=10):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    salida = [inicio]\n",
    "    for _ in range(maximo):\n",
    "        fila = siguiente[lugar[salida[-1]]]\n",
    "        if fila.sum() == 0:\n",
    "            break\n",
    "        puntajes = np.where(fila > 0, np.log(fila + 1e-9), -1e9) / temperatura\n",
    "        p = np.exp(puntajes - puntajes.max())\n",
    "        p = p / p.sum()\n",
    "        elegida = vocabulario[r.choice(V, p=p)]\n",
    "        if elegida == '<fin>':\n",
    "            break\n",
    "        salida.append(elegida)\n",
    "    return ' '.join(salida)\n",
    "\n",
    "for s in range(4):\n",
    "    print(escribe('el', semilla=s))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es un modelo de lenguaje. Uno malísimo, porque solo mira la palabra\n",
    "anterior en vez de toda la frase con atención, y porque aprendió de 26\n",
    "comentarios en vez de billones de palabras.\n",
    "\n",
    "Pero **el bucle es exactamente el mismo**: mirar, poner\n",
    "probabilidades, elegir, pegar, repetir. Cuando ves a Claude escribir palabra a\n",
    "palabra, es esto, con un transformer en el medio en vez de una tabla 🔁\n",
    "\n",
    "Y de aquí sale algo que conviene entender: el modelo **no tiene un plan\n",
    "de lo que va a decir**. Cada token se elige con lo que hay escrito hasta\n",
    "ese momento. Que las respuestas salgan coherentes es consecuencia de que el\n",
    "contexto incluye todo lo que ya escribió."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La temperatura, que es lo único que ajustas tú"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "palabras = ['completo', 'tarde', 'roto', 'nunca', 'ya']\n",
    "puntajes = np.array([3.2, 2.1, 0.8, -0.5, -1.4])\n",
    "\n",
    "def con_temperatura(z, T):\n",
    "    z = z / T\n",
    "    e = np.exp(z - z.max())\n",
    "    return e / e.sum()\n",
    "\n",
    "print(f\"{'palabra':12}\" + ''.join(f'{f\"T={t}\":>10}' for t in [0.2, 0.5, 1.0, 1.5, 3.0]))\n",
    "for i, p in enumerate(palabras):\n",
    "    print(f'{p:12}' + ''.join(f'{con_temperatura(puntajes, t)[i]:10.4f}'\n",
    "                              for t in [0.2, 0.5, 1.0, 1.5, 3.0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está lo que hace, y es una sola división 🌡️\n",
    "\n",
    "Con T=0,2 la favorita se lleva **0,9959** y las demás quedan en\n",
    "nada: el modelo va a decir siempre lo mismo. Con T=3,0 la favorita baja a\n",
    "**0,3774** y hasta \"ya\", que era la última, se lleva 0,0815.\n",
    "\n",
    "Y fíjate en lo que la temperatura *no* hace: **no cambia el orden\n",
    "ni cambia lo que el modelo sabe**. Completo sigue siendo la más probable\n",
    "en las cinco columnas. Solo cambia cuánto se arriesga al elegir.\n",
    "\n",
    "Por eso \"bájale la temperatura para que no alucine\" funciona a medias: si el\n",
    "modelo tiene mal el dato, con T=0 lo va a decir mal con toda seguridad 😬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los otros dos filtros"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = con_temperatura(puntajes, 1.0)\n",
    "\n",
    "def top_k(p, k):\n",
    "    q = p.copy()\n",
    "    corte = np.sort(q)[::-1][k - 1]\n",
    "    q[q < corte] = 0\n",
    "    return q / q.sum()\n",
    "\n",
    "def top_p(p, umbral):\n",
    "    orden = np.argsort(p)[::-1]\n",
    "    acumulado = np.cumsum(p[orden])\n",
    "    cuantas = int(np.searchsorted(acumulado, umbral)) + 1\n",
    "    q = np.zeros_like(p)\n",
    "    q[orden[:cuantas]] = p[orden[:cuantas]]\n",
    "    return q / q.sum()\n",
    "\n",
    "print('sin filtro:', np.round(p, 4))\n",
    "print('top-k, k=2:', np.round(top_k(p, 2), 4))\n",
    "print('top-p, 0.9:', np.round(top_p(p, 0.9), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El **top-k** se queda con las k mejores y tira el resto. El\n",
    "**top-p** se queda con las que hagan falta para juntar ese\n",
    "porcentaje de probabilidad, así que el corte se mueve solo: si el modelo está\n",
    "segurísimo se queda con una, y si duda se queda con muchas.\n",
    "\n",
    "Aquí los dos dan lo mismo porque con estos cinco números las dos primeras ya\n",
    "suman el 91%. Con un vocabulario de 100.000 tokens la diferencia se nota\n",
    "muchísimo, y por eso el top-p es el que se usa hoy 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que este capítulo NO explica"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y esto es importante para que no salgas de aquí creyendo que ya sabes cómo se\n",
    "hace un ChatGPT.\n",
    "\n",
    "- 📚 **El entrenamiento.** Todo lo de este libro, pero sobre\n",
    "billones de palabras y durante meses, en miles de tarjetas gráficas. La\n",
    "retropropagación es la misma; la escala no.\n",
    "\n",
    "- 🎓 **El ajuste con personas.** Un modelo entrenado solo a\n",
    "predecir la palabra siguiente no contesta preguntas: completa texto. Que sea útil\n",
    "y educado sale de una fase posterior con evaluaciones humanas.\n",
    "\n",
    "- 🧯 **Las barreras.** Lo que decide qué no responde no está en la\n",
    "arquitectura.\n",
    "\n",
    "Lo que sí te llevas es la maquinaria: tokens, atención, bloques apilados y un\n",
    "bucle que elige. Y con eso ya puedes leer un paper y entender de qué habla 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cómo se le baja el peso a un modelo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un modelo son sus pesos, y los pesos se guardan como números decimales de\n",
    "cuatro bytes cada uno. Con miles de millones de pesos, eso es lo que decide si\n",
    "entra en una tarjeta gráfica o no entra.\n",
    "\n",
    "El truco que se usa para bajarlo se llama **cuantización** y es\n",
    "más simple de lo que suena: guardar cada peso con menos precisión. En vez de\n",
    "cuatro bytes, uno, que da 256 valores posibles. Se busca la escala que estira\n",
    "esos 256 escalones sobre el rango real de los pesos, y se redondea:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pesos = np.random.default_rng(0).normal(0, 0.5, 4096).astype(np.float32)\n",
    "\n",
    "escala = np.abs(pesos).max() / 127\n",
    "q = np.round(pesos / escala).astype(np.int8)\n",
    "vuelta = q.astype(np.float32) * escala\n",
    "\n",
    "print('float32 :', pesos.nbytes, 'bytes')\n",
    "print('int8    :', q.nbytes, 'bytes')\n",
    "print('error medio:', round(float(np.abs(pesos - vuelta).mean()), 5))\n",
    "print('el peor    :', round(float(np.abs(pesos - vuelta).max()), 5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La cuarta parte del espacio, y cada peso se movió menos de una centésima. Por\n",
    "eso funciona: **una red no necesita precisión, necesita direcciones**,\n",
    "y redondear un poquito cada peso casi no cambia hacia dónde apunta el conjunto.\n",
    "\n",
    "Cuando veas *GPTQ*, *AWQ* o *GGUF* en la descripción de un\n",
    "modelo descargable, están hablando de esto con más maña: eligen la escala por\n",
    "grupos de pesos en vez de una sola para todos, y miran qué pesos aguantan menos\n",
    "el redondeo. La idea de fondo es la de arriba.\n",
    "\n",
    "Y las otras dos palabras que salen siempre en la misma conversación, para que\n",
    "no te suenen a chino:\n",
    "\n",
    "- **El caché de claves y valores.** En el bucle que escribe, cada\n",
    "palabra nueva vuelve a mirar todas las anteriores. Guardar lo ya calculado en vez\n",
    "de rehacerlo es lo que hace que la respuesta salga fluida en vez de a\n",
    "tirones.\n",
    "\n",
    "- **Los modelos de expertos.** En vez de que todos los pesos\n",
    "participen en cada palabra, el modelo tiene muchos grupos y en cada paso enciende\n",
    "solo unos pocos. Sale un modelo enorme que cuesta como uno mediano cada vez que\n",
    "lo usas. Es lo que quieren decir con *MoE* o *mixture of\n",
    "experts*."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el ajuste con personas, que es lo que lo hace contestar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De la lista de arriba, esta es la que más cambia lo que tú ves, así que la\n",
    "abro un poco más.\n",
    "\n",
    "Un modelo entrenado solo a predecir la palabra siguiente **no contesta\n",
    "preguntas: continúa texto**. Si le escribes \"¿cuál es la capital de Perú?\",\n",
    "una continuación perfectamente razonable es \"¿y la de Chile? ¿y la de Bolivia?\",\n",
    "porque en internet las preguntas suelen venir en listas.\n",
    "\n",
    "Que conteste en vez de continuar sale de una fase posterior en tres pasos:\n",
    "\n",
    "- Personas escriben respuestas buenas a un montón de preguntas, y el modelo se\n",
    "ajusta con esos ejemplos.\n",
    "\n",
    "- Personas comparan respuestas del modelo de a dos y dicen cuál prefieren. Con\n",
    "esas comparaciones se entrena otro modelo, cuyo único trabajo es puntuar qué tan\n",
    "buena es una respuesta.\n",
    "\n",
    "- El modelo grande se sigue ajustando para sacar buena nota con ese\n",
    "puntuador.\n",
    "\n",
    "A eso le dicen **RLHF**, aprendizaje por refuerzo con\n",
    "retroalimentación humana. Es la única parte del libro donde aparece el\n",
    "aprendizaje por refuerzo, que es la tercera familia de la que hablamos en el\n",
    "capítulo 1: ni supervisado ni no supervisado, sino\n",
    "aprender de una señal de qué tan bien lo hiciste.\n",
    "\n",
    "Y explica dos cosas que se notan usándolo. Que el modelo sea educado y\n",
    "prudente no es cosa de la arquitectura: es **gusto de las personas que\n",
    "compararon**. Y que a veces conteste con seguridad antes que decir \"no sé\"\n",
    "también, porque entre dos respuestas la gente tiende a preferir la que suena\n",
    "resuelta 😐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cuántos tokens gasta el español\n",
    "\n",
    "Parte varias palabras y cuenta los trozos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for w in ['lima', 'bodega', 'minimarket', 'huancayo']:\n",
    "    t = trozos(w, CONOCIDOS)\n",
    "    print(f'{w:12} {len(w):2d} letras -> {len(t)} trozos  {t}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "lima          4 letras -> 2 trozos  ['li', 'ma']\n",
    "bodega        6 letras -> 3 trozos  ['bo', 'de', 'ga']\n",
    "minimarket   10 letras -> 4 trozos  ['min', 'i', 'mar', 'ket']\n",
    "huancayo      8 letras -> 4 trozos  ['hu', 'an', 'ca', 'yo']\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro letras dan dos trozos y diez letras dan cuatro. Con un vocabulario de\n",
    "verdad la proporción es mejor, pero la idea es esa.\n",
    "\n",
    "Y tiene consecuencia de plata: los modelos se cobran por token. Si tu texto\n",
    "está en español y el vocabulario se armó mirando inglés, **pagas más por\n",
    "decir lo mismo** 💸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La palabra que no se puede partir\n",
    "\n",
    "Prueba con una palabra cuyos trozos no estén todos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(trozos('zapallo', CONOCIDOS))\n",
    "print(trozos('xyz', CONOCIDOS))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "['?', 'a', '?', 'a', '?', '?', 'o']\n",
    "['?', '?', '?']\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aparecen interrogantes donde no encontró nada. En un tokenizador de verdad eso\n",
    "no pasa nunca, porque el vocabulario incluye todas las letras sueltas: siempre\n",
    "hay una salida, aunque sea letra a letra.\n",
    "\n",
    "Ese es el diseño que hace que nunca falle, a diferencia de los embeddings del\n",
    "capítulo 13, que reventaban con una palabra nueva 🧩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La temperatura sobre el generador\n",
    "\n",
    "Escribe con temperaturas distintas y compara."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for T in [0.2, 1.0, 3.0]:\n",
    "    print(f'T={T}:')\n",
    "    for s in range(3):\n",
    "        print('   ', escribe('el', temperatura=T, semilla=s))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "T=0.2:\n",
    "    el pedido de aceite\n",
    "    el pedido llego completo y azucar\n",
    "    el minimarket el pedido de arroz y azucar\n",
    "T=1.0:\n",
    "    el minimarket el aceite\n",
    "    el minimarket reclamo\n",
    "    el martes\n",
    "T=3.0:\n",
    "    el minimarket el aceite\n",
    "    el mayorista reclamo\n",
    "    el lunes\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las frases cambian, sí, pero no como esperabas: con T=0,2 salen las\n",
    "*más largas*, y con T=3,0 las más cortas y sosas. Justo al revés de la\n",
    "intuición de \"temperatura alta, más creativo\".\n",
    "\n",
    "Y tiene explicación. Con una tabla de 26 comentarios, la mayoría de las\n",
    "palabras tienen una o dos continuaciones posibles, así que la temperatura no\n",
    "cambia cuánto se arriesga: cambia por qué rama se va. Y unas ramas llevan a\n",
    "frases largas y otras a un punto final.\n",
    "\n",
    "La temperatura hace lo que dice el apartado anterior cuando hay muchas\n",
    "continuaciones plausibles, que es lo que pasa con un vocabulario de 100.000\n",
    "tokens. Con cinco opciones, lo que se ve es otra cosa 🌡️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La misma semilla siempre da lo mismo\n",
    "\n",
    "Comprueba que no hay nada mágico ni impredecible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(escribe('la', semilla=3))\n",
    "print(escribe('la', semilla=3))\n",
    "print('¿idénticas?', escribe('la', semilla=3) == escribe('la', semilla=3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "la bodega el pedido del minimarket llego incompleto\n",
    "la bodega el pedido del minimarket llego incompleto\n",
    "¿idénticas? True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Idénticas, siempre. La misma entrada y la misma semilla dan la misma\n",
    "salida.\n",
    "\n",
    "Que un modelo te conteste distinto cada vez no es porque \"piense diferente\":\n",
    "es porque el azar de la elección cambia. Con temperatura 0 y sin muestreo, un\n",
    "modelo de lenguaje es una función determinista 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Cuántos pesos hay en cada sitio\n",
    "\n",
    "Reparte los parámetros de un bloque entre atención y\n",
    "feedforward."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for dim in [8, 768, 4096]:\n",
    "    atencion = 4 * dim * dim              # Wq, Wk, Wv y la de salida\n",
    "    feed = 8 * dim * dim                  # una de d a 4d y otra de 4d a d\n",
    "    print(f'd={dim:5d}: atención {atencion:12,}  feedforward {feed:12,}  '\n",
    "          f'({feed / (atencion + feed):.0%} en el feedforward)')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "d=    8: atención          256  feedforward          512  (67% en el feedforward)\n",
    "d=  768: atención    2,359,296  feedforward    4,718,592  (67% en el feedforward)\n",
    "d= 4096: atención   67,108,864  feedforward  134,217,728  (67% en el feedforward)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos tercios de los pesos de un bloque están en el feedforward, no en la\n",
    "atención.\n",
    "\n",
    "La atención se lleva toda la atención (perdón) porque es la idea nueva, y el\n",
    "grueso de la memoria del modelo está en esas dos matrices anchas que procesan\n",
    "cada posición por separado 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Qué tan grande es la norma por capa\n",
    "\n",
    "Comprueba que deja cada fila en media 0 y desviación 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "raro = np.array([[1000., 1001, 999, 1002],\n",
    "                 [0.001, 0.002, 0.0015, 0.0011]])\n",
    "print('antes, medias      :', np.round(raro.mean(axis=1), 4))\n",
    "print('antes, desviaciones:', np.round(raro.std(axis=1), 4))\n",
    "print('después, medias      :', np.round(norma(raro).mean(axis=1), 4))\n",
    "print('después, desviaciones:', np.round(norma(raro).std(axis=1), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "antes, medias      : [1.0005e+03 1.4000e-03]\n",
    "antes, desviaciones: [1.118e+00 4.000e-04]\n",
    "después, medias      : [ 0. -0.]\n",
    "después, desviaciones: [1.     0.1235]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las dos filas salen en media 0, y ahí funcionó. Pero mira las desviaciones:\n",
    "la primera sale 1 y **la segunda sale 0,1235**.\n",
    "\n",
    "El culpable es el `eps` de la fórmula. La segunda fila tiene una\n",
    "varianza de 1,6e-07, o sea muchísimo más chica que el 1e-05 que le sumamos para\n",
    "no dividir entre cero, así que ese epsilon se come el cálculo y la fila sale\n",
    "aplastada.\n",
    "\n",
    "No es un fallo: es el precio de protegerse de la división entre cero, y solo\n",
    "se nota cuando los números son ridículamente pequeños. Me pareció que valía la\n",
    "pena enseñarlo porque **estas protecciones nunca son gratis**, y\n",
    "esta en concreto está en todas las redes que existen 🧯\n",
    "\n",
    "Por lo demás, la norma por capa es el `StandardScaler` del\n",
    "capítulo 5 aplicado **dentro de la red y a\n",
    "cada fila** en vez de a las\n",
    "columnas antes de empezar. Y por la misma razón: para que el gradiente no tenga\n",
    "que lidiar con escalas distintas ⚖️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El error del token que no existe\n",
    "\n",
    "Pídele al generador que arranque con una palabra que no\n",
    "está en el corpus."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "escribe('hola')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "KeyError: 'hola'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mismo error que cerraba el capítulo 13, y por eso lo repito aquí: el\n",
    "generador trabaja con una tabla de palabras conocidas y \"hola\" no está.\n",
    "\n",
    "La diferencia es que ahora ya sabes la solución. Si en vez de palabras\n",
    "enteras usara los trozos del principio del capítulo, \"hola\" se partiría en\n",
    "pedacitos conocidos y no habría error.\n",
    "\n",
    "**La tokenización por subpalabras no es un detalle de implementación: es\n",
    "lo que hace que el sistema no se rompa con el mundo real** 🧩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "¿Por qué las conexiones residuales no son opcionales en un transformer profundo?\n",
    "\n",
    "a) Porque le dan al gradiente un camino corto de vuelta a las primeras capas\n",
    "\n",
    "b) Porque hacen el modelo más rápido\n",
    "\n",
    "c) Porque reducen el número de parámetros\n",
    "\n",
    "d) Porque evitan el sobreajuste\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Añaden una suma por capa: no ahorran cálculo.\n",
    "\n",
    "*c)* No quitan ni un parámetro.\n",
    "\n",
    "*d)* Ayudan a entrenar, que es otra cosa. Piensa en el capítulo 6.\n",
    "\n",
    "Sin ellas, el gradiente se desvanece igual que con diez capas de sigmoide."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El 92% que el modelo ya se sabía"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Evalúas el modelo con un banco de preguntas conocido, sale un 92% y ese número va a la propuesta. Es un banco público y respetado.\n",
    "\n",
    "```\n",
    "aciertos = 0\n",
    "for pregunta, respuesta in banco_publico:\n",
    "    if modelo(pregunta) == respuesta:\n",
    "        aciertos += 1\n",
    "\n",
    "# 92% de acierto\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Ese banco lleva años publicado en internet, así que casi con seguridad estaba dentro del texto con el que se entrenó el modelo 📚 No estás midiendo si razona: estás midiendo cuánto recuerda de algo que ya leyó.\n",
    "\n",
    "Se llama contaminación del conjunto de prueba y es el problema abierto más incómodo de este campo, porque nadie puede mirar dentro de los datos de entrenamiento de un modelo cerrado para comprobarlo.\n",
    "\n",
    "La salida no es dejar de medir: es medir con **preguntas tuyas, escritas después y que no estén publicadas en ningún sitio**. Cuarenta preguntas de tu negocio, escritas por alguien de tu equipo, dicen mucho más sobre si te sirve que cualquier ranking. Y de paso son las que vas a poder repetir cuando salga la versión siguiente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🧩 El texto se parte en trozos, así que \"huancayo\" se lee como hu-an-ca-yo\n",
    "sin estar en el vocabulario.\n",
    "\n",
    "- ➕ Un bloque es atención más residual más norma más feedforward. Sin\n",
    "residual, seis bloques dejan todas las posiciones con parecido 1,0.\n",
    "\n",
    "- 🧠 Dos tercios de los pesos están en el feedforward, no en la atención.\n",
    "\n",
    "- 🔁 Escribir es un bucle: poner probabilidades, elegir, pegar, repetir. No hay\n",
    "plan previo.\n",
    "\n",
    "- 🌡️ La temperatura es una división: con 0,2 la favorita se lleva 0,9959 y con\n",
    "3,0 baja a 0,3774. No cambia el orden ni lo que el modelo sabe.\n",
    "\n",
    "- 🎚️ Top-k corta por cantidad y top-p por probabilidad acumulada, así que el\n",
    "corte del top-p se mueve según lo seguro que esté.\n",
    "\n",
    "- 🎲 Misma entrada y misma semilla dan la misma salida. Nada de esto es\n",
    "impredecible.\n",
    "\n",
    "- 📚 Y lo que aquí no está: el entrenamiento a escala, el ajuste con personas y\n",
    "las barreras.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un modelo de lenguaje no sabe lo que dice. Sabe qué palabra suele venir después, y eso explica casi todo lo que falla."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si lo que quieres es usar bien estos modelos en el día a día en vez de construirlos, eso es [prompt engineering](https://missyera.com/blog/prompts-que-funcionan/) 💬\n",
    "\n",
    "En el capítulo 18 pasamos al lado práctico: cuándo conviene un prompt, cuándo\n",
    "RAG y cuándo ajustar un modelo, que es la pregunta que llega a las empresas.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 17 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/del-transformer-al-llm/\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
}
