Capítulo 17 de 22 12 secciones 22 min

Compartir

Qué está pasando cuando le escribes a un modelo de lenguaje

Tokens, el bloque completo, el bucle que escribe palabra a palabra y qué hace de verdad la temperatura.

Esta es la pregunta con la que muchísima gente llega a este libro, y me la hacen en todas las reuniones. Un modelo de lenguaje parte el texto en tokens, los pasa por unas decenas de bloques de atención y saca una probabilidad para cada token posible como siguiente. Elige uno, lo pega al final y repite. No hay nada más, y eso explica casi todo lo que falla 💬

Ya tenemos todas las piezas. Aquí las juntamos y respondemos la pregunta con la que mucha gente llega a este libro: qué está pasando exactamente cuando le escribes a ChatGPT o a Claude 🤖

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 💬

Primero: el texto se parte en trozos

En el capítulo 13 quedó un error abierto: pedir el vector de "huancayo" reventaba porque esa palabra no estaba en el vocabulario. Así se arregla:

import numpy as np

CONOCIDOS = {'bo', 'de', 'ga', 'hu', 'an', 'ca', 'yo', 'li', 'ma', 'min',
             'i', 'mar', 'ket', 'a', 'o', 'e', 'u', 'n', 'r', 's', 't'}

def trozos(palabra, conocidos):
    salida, i = [], 0
    while i < len(palabra):
        for j in range(len(palabra), i, -1):        # el trozo más largo que quepa
            if palabra[i:j] in conocidos:
                salida.append(palabra[i:j])
                i = j
                break
        else:
            salida.append('?')
            i += 1
    return salida

for w in ['bodega', 'huancayo', 'minimarket', 'lima']:
    print(f'{w:12} -> {trozos(w, CONOCIDOS)}')
bodega       -> ['bo', 'de', 'ga']
huancayo     -> ['hu', 'an', 'ca', 'yo']
minimarket   -> ['min', 'i', 'mar', 'ket']
lima         -> ['li', 'ma']

Ahí está: "huancayo" se parte en hu-an-ca-yo. No hace falta que la palabra esté en el vocabulario, basta con que estén sus trozos 🧩

Eso es la tokenización por subpalabras, y es por lo que un modelo puede leer un nombre propio peruano que nunca vio, o una palabra inventada, o un error de tipeo.

También explica dos cosas que se notan usándolos. Que el español gaste más tokens que el inglés (los vocabularios se arman mirando sobre todo texto en inglés, así que nuestras palabras se parten en más trozos), y que a los modelos les cueste contar letras: ellos no ven letras, ven trozos.

Un bloque de transformer de abajo arriba: entrada con embeddings y posición, atención multicabeza, suma y normalización, red feed forward, otra suma y normalización, y salida. Dos conexiones residuales saltan por encima de los dos subbloques.
Un modelo grande es este bloque repetido decenas de veces, nada más. Las flechas punteadas de la derecha son las conexiones residuales: suman la entrada a la salida para que el gradiente tenga un camino directo hasta abajo, que es lo que permite apilar tantos.

Segundo: el bloque completo

a^=γaμσ2+ε+β

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

La atención del capítulo 16 no va sola. Un bloque de transformer le añade tres cosas, y las tres son necesarias:

def softmax(z):
    z = z - z.max(axis=-1, keepdims=True)
    e = np.exp(z)
    return e / e.sum(axis=-1, keepdims=True)

def norma(X, eps=1e-5):
    return (X - X.mean(-1, keepdims=True)) / np.sqrt(X.var(-1, keepdims=True) + eps)

rng = np.random.default_rng(7)
n, d = 6, 8
X = rng.normal(0, 1, (n, d))
Wq, Wk, Wv = [rng.normal(0, 0.5, (d, d)) for _ in range(3)]
W1 = rng.normal(0, 0.5, (d, 4 * d))
W2 = rng.normal(0, 0.5, (4 * d, d))

def bloque(X):
    A = softmax((X @ Wq) @ (X @ Wk).T / np.sqrt(d))
    X = norma(X + A @ (X @ Wv))                       # atención + residual + norma
    X = norma(X + np.maximum(0, X @ W1) @ W2)         # feedforward + residual + norma
    return X

Y = bloque(X)
print('entra', X.shape, 'y sale', Y.shape)
print('media de cada fila al salir     :', np.round(Y.mean(axis=1), 3))
print('desviación de cada fila al salir:', np.round(Y.std(axis=1), 3))
entra (6, 8) y sale (6, 8)
media de cada fila al salir     : [ 0.  0.  0. -0. -0.  0.]
desviación de cada fila al salir: [1. 1. 1. 1. 1. 1.]

Tres añadidos y cada uno resuelve un problema concreto 🔧

  • ➕ El residual, ese X +, deja pasar la entrada sin tocar al lado de lo que calculó la capa. Así el gradiente del capítulo 6 tiene un camino directo hasta abajo sin multiplicarse por nada, y por eso se pueden apilar cien capas.
  • 📏 La norma por capa centra cada fila en media 0 y desviación 1. Mira la salida: todas las medias son 0 y todas las desviaciones 1, exacto.
  • 🧠 El feedforward es la red del capítulo 4 aplicada a cada posición por separado, y suele ser cuatro veces más ancha. Ahí está la mayoría de los pesos de un modelo de lenguaje, aunque la fama se la lleve la atención.

Por qué el residual no es opcional

𝐚(l+1)=𝐚(l)+F(𝐚(l))

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

def bloque_sin_residual(X):
    A = softmax((X @ Wq) @ (X @ Wk).T / np.sqrt(d))
    X = norma(A @ (X @ Wv))
    X = norma(np.maximum(0, X @ W1) @ W2)
    return X

def parecido(a, b):
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

Z = X.copy()
for _ in range(6):
    Z = bloque_sin_residual(Z)
print('sin residual, tras 6 bloques, parecido entre dos posiciones:',
      round(parecido(Z[0], Z[1]), 4))

Z = X.copy()
for capa in range(6):
    Z = bloque(Z)
    print(f'con residual, tras {capa + 1} bloques: magnitud media {np.abs(Z).mean():.4f}')
sin residual, tras 6 bloques, parecido entre dos posiciones: 1.0
con residual, tras 1 bloques: magnitud media 0.8264
con residual, tras 2 bloques: magnitud media 0.8187
con residual, tras 3 bloques: magnitud media 0.8034
con residual, tras 4 bloques: magnitud media 0.8156
con residual, tras 5 bloques: magnitud media 0.8210
con residual, tras 6 bloques: magnitud media 0.8247

Sin residual, después de seis bloques las dos posiciones tienen un parecido de 1,0: son el mismo vector. Todas las palabras se volvieron idénticas y el modelo perdió la frase entera.

Con residual, la magnitud se queda entre 0,80 y 0,83 bloque tras bloque, sin crecer ni apagarse.

Una nota honesta: aquí los pesos son aleatorios, así que ese colapso es más brutal de lo que sería con un modelo entrenado. Lo que la medición enseña de verdad es que el residual y la norma son lo que mantiene estable una pila de bloques, y sin ellos las redes profundas de texto no se pueden entrenar 🏗️

Pérdida frente a cómputo con los dos ejes en escala logarítmica: los puntos caen sobre una recta que baja, y un punto marcado señala dónde estamos hoy en esa recta.
Los dos ejes son logarítmicos, y ahí está la trampa: que se vea como una recta significa que para bajar el error lo mismo que bajó antes hay que multiplicar la inversión otra vez. Eso explica los presupuestos de los que entrenan modelos, y explica también por qué a ti te conviene usar el suyo.

Tercero: el bucle que escribe

Y ahora la parte que sorprende a casi todo el mundo. Un modelo de lenguaje hace una sola cosa: dada una secuencia, poner una probabilidad a cada token posible como siguiente.

Escribir es repetir eso: eliges uno, lo pegas al final, y vuelves a preguntar.

COMENTARIOS = [
    'el pedido llego completo y a tiempo', 'el pedido llego incompleto y con retraso',
    'la bodega pidio arroz y azucar', 'el minimarket pidio aceite y azucar',
    'el mayorista pidio arroz en cantidad', 'el pedido de la bodega llego tarde',
    'el pedido del minimarket llego tarde', 'la bodega reclamo por el retraso del pedido',
    'el minimarket reclamo por el aceite roto', 'el mayorista reclamo por la factura',
    'llego todo completo sin reclamo', 'el arroz llego en buen estado',
    'el aceite llego en buen estado', 'el azucar llego roto y con retraso',
    'la bodega pago la factura a tiempo', 'el minimarket pago la factura con retraso',
    'el mayorista pago la factura a tiempo', 'el vendedor visito la bodega el lunes',
    'el vendedor visito el minimarket el martes', 'el vendedor visito al mayorista el lunes',
    'la bodega compro arroz el lunes', 'el minimarket compro aceite el martes',
    'el mayorista compro arroz y aceite', 'nadie visito la bodega esta semana',
    'el pedido de arroz llego completo', 'el pedido de aceite llego incompleto',
]

vocabulario = sorted({p for f in COMENTARIOS for p in f.split()} | {'<fin>'})
lugar = {p: i for i, p in enumerate(vocabulario)}
V = len(vocabulario)

siguiente = np.zeros((V, V))
for f in COMENTARIOS:
    ps = f.split() + ['<fin>']
    for a, b in zip(ps, ps[1:]):
        siguiente[lugar[a], lugar[b]] += 1

def escribe(inicio, temperatura=1.0, semilla=0, maximo=10):
    r = np.random.default_rng(semilla)
    salida = [inicio]
    for _ in range(maximo):
        fila = siguiente[lugar[salida[-1]]]
        if fila.sum() == 0:
            break
        puntajes = np.where(fila > 0, np.log(fila + 1e-9), -1e9) / temperatura
        p = np.exp(puntajes - puntajes.max())
        p = p / p.sum()
        elegida = vocabulario[r.choice(V, p=p)]
        if elegida == '<fin>':
            break
        salida.append(elegida)
    return ' '.join(salida)

for s in range(4):
    print(escribe('el', semilla=s))
el minimarket el aceite
el minimarket reclamo
el martes
el arroz en cantidad

Eso es un modelo de lenguaje. Uno malísimo, porque solo mira la palabra anterior en vez de toda la frase con atención, y porque aprendió de 26 comentarios en vez de billones de palabras.

Pero el bucle es exactamente el mismo: mirar, poner probabilidades, elegir, pegar, repetir. Cuando ves a Claude escribir palabra a palabra, es esto, con un transformer en el medio en vez de una tabla 🔁

Y de aquí sale algo que conviene entender: el modelo no tiene un plan de lo que va a decir. Cada token se elige con lo que hay escrito hasta ese momento. Que las respuestas salgan coherentes es consecuencia de que el contexto incluye todo lo que ya escribió.

La temperatura, que es lo único que ajustas tú

palabras = ['completo', 'tarde', 'roto', 'nunca', 'ya']
puntajes = np.array([3.2, 2.1, 0.8, -0.5, -1.4])

def con_temperatura(z, T):
    z = z / T
    e = np.exp(z - z.max())
    return e / e.sum()

print(f"{'palabra':12}" + ''.join(f'{f"T={t}":>10}' for t in [0.2, 0.5, 1.0, 1.5, 3.0]))
for i, p in enumerate(palabras):
    print(f'{p:12}' + ''.join(f'{con_temperatura(puntajes, t)[i]:10.4f}'
                              for t in [0.2, 0.5, 1.0, 1.5, 3.0]))
palabra          T=0.2     T=0.5     T=1.0     T=1.5     T=3.0
completo        0.9959    0.8931    0.6857    0.5514    0.3774
tarde           0.0041    0.0990    0.2282    0.2648    0.2616
roto            0.0000    0.0073    0.0622    0.1113    0.1696
nunca           0.0000    0.0005    0.0170    0.0468    0.1100
ya              0.0000    0.0001    0.0069    0.0257    0.0815

Ahí está lo que hace, y es una sola división 🌡️

Con T=0,2 la favorita se lleva 0,9959 y las demás quedan en nada: el modelo va a decir siempre lo mismo. Con T=3,0 la favorita baja a 0,3774 y hasta "ya", que era la última, se lleva 0,0815.

Y fíjate en lo que la temperatura no hace: no cambia el orden ni cambia lo que el modelo sabe. Completo sigue siendo la más probable en las cinco columnas. Solo cambia cuánto se arriesga al elegir.

Por eso "bájale la temperatura para que no alucine" funciona a medias: si el modelo tiene mal el dato, con T=0 lo va a decir mal con toda seguridad 😬

Los otros dos filtros

p = con_temperatura(puntajes, 1.0)

def top_k(p, k):
    q = p.copy()
    corte = np.sort(q)[::-1][k - 1]
    q[q < corte] = 0
    return q / q.sum()

def top_p(p, umbral):
    orden = np.argsort(p)[::-1]
    acumulado = np.cumsum(p[orden])
    cuantas = int(np.searchsorted(acumulado, umbral)) + 1
    q = np.zeros_like(p)
    q[orden[:cuantas]] = p[orden[:cuantas]]
    return q / q.sum()

print('sin filtro:', np.round(p, 4))
print('top-k, k=2:', np.round(top_k(p, 2), 4))
print('top-p, 0.9:', np.round(top_p(p, 0.9), 4))
sin filtro: [0.6857 0.2282 0.0622 0.017  0.0069]
top-k, k=2: [0.7503 0.2497 0.     0.     0.    ]
top-p, 0.9: [0.7503 0.2497 0.     0.     0.    ]

El top-k se queda con las k mejores y tira el resto. El top-p se queda con las que hagan falta para juntar ese porcentaje de probabilidad, así que el corte se mueve solo: si el modelo está segurísimo se queda con una, y si duda se queda con muchas.

Aquí los dos dan lo mismo porque con estos cinco números las dos primeras ya suman el 91%. Con un vocabulario de 100.000 tokens la diferencia se nota muchísimo, y por eso el top-p es el que se usa hoy 🎚️

Recompensa a lo largo de los episodios de entrenamiento por refuerzo: una línea muy ruidosa con una media móvil encima que sube deprisa al principio, se estanca en una meseta y vuelve a subir hasta aplanarse.
Así se ve por dentro el paso que convierte un modelo bruto en uno con el que se puede conversar. La recompensa la ponen personas comparando respuestas, y la meseta del medio es normal: el modelo se atasca en una estrategia hasta que encuentra otra mejor.

Lo que este capítulo NO explica

Y esto es importante para que no salgas de aquí creyendo que ya sabes cómo se hace un ChatGPT.

  • 📚 El entrenamiento. Todo lo de este libro, pero sobre billones de palabras y durante meses, en miles de tarjetas gráficas. La retropropagación es la misma; la escala no.
  • 🎓 El ajuste con personas. Un modelo entrenado solo a predecir la palabra siguiente no contesta preguntas: completa texto. Que sea útil y educado sale de una fase posterior con evaluaciones humanas.
  • 🧯 Las barreras. Lo que decide qué no responde no está en la arquitectura.

Lo que sí te llevas es la maquinaria: tokens, atención, bloques apilados y un bucle que elige. Y con eso ya puedes leer un paper y entender de qué habla 💛

Cómo se le baja el peso a un modelo

Un modelo son sus pesos, y los pesos se guardan como números decimales de cuatro bytes cada uno. Con miles de millones de pesos, eso es lo que decide si entra en una tarjeta gráfica o no entra.

El truco que se usa para bajarlo se llama cuantización y es más simple de lo que suena: guardar cada peso con menos precisión. En vez de cuatro bytes, uno, que da 256 valores posibles. Se busca la escala que estira esos 256 escalones sobre el rango real de los pesos, y se redondea:

pesos = np.random.default_rng(0).normal(0, 0.5, 4096).astype(np.float32)

escala = np.abs(pesos).max() / 127
q = np.round(pesos / escala).astype(np.int8)
vuelta = q.astype(np.float32) * escala

print('float32 :', pesos.nbytes, 'bytes')
print('int8    :', q.nbytes, 'bytes')
print('error medio:', round(float(np.abs(pesos - vuelta).mean()), 5))
print('el peor    :', round(float(np.abs(pesos - vuelta).max()), 5))
float32 : 16384 bytes
int8    : 4096 bytes
error medio: 0.00385
el peor    : 0.00767

La cuarta parte del espacio, y cada peso se movió menos de una centésima. Por eso funciona: una red no necesita precisión, necesita direcciones, y redondear un poquito cada peso casi no cambia hacia dónde apunta el conjunto.

Cuando veas GPTQ, AWQ o GGUF en la descripción de un modelo descargable, están hablando de esto con más maña: eligen la escala por grupos de pesos en vez de una sola para todos, y miran qué pesos aguantan menos el redondeo. La idea de fondo es la de arriba.

Y las otras dos palabras que salen siempre en la misma conversación, para que no te suenen a chino:

  • El caché de claves y valores. En el bucle que escribe, cada palabra nueva vuelve a mirar todas las anteriores. Guardar lo ya calculado en vez de rehacerlo es lo que hace que la respuesta salga fluida en vez de a tirones.
  • Los modelos de expertos. En vez de que todos los pesos participen en cada palabra, el modelo tiene muchos grupos y en cada paso enciende solo unos pocos. Sale un modelo enorme que cuesta como uno mediano cada vez que lo usas. Es lo que quieren decir con MoE o mixture of experts.

Y el ajuste con personas, que es lo que lo hace contestar

De la lista de arriba, esta es la que más cambia lo que tú ves, así que la abro un poco más.

Un modelo entrenado solo a predecir la palabra siguiente no contesta preguntas: continúa texto. Si le escribes "¿cuál es la capital de Perú?", una continuación perfectamente razonable es "¿y la de Chile? ¿y la de Bolivia?", porque en internet las preguntas suelen venir en listas.

Que conteste en vez de continuar sale de una fase posterior en tres pasos:

  • Personas escriben respuestas buenas a un montón de preguntas, y el modelo se ajusta con esos ejemplos.
  • Personas comparan respuestas del modelo de a dos y dicen cuál prefieren. Con esas comparaciones se entrena otro modelo, cuyo único trabajo es puntuar qué tan buena es una respuesta.
  • El modelo grande se sigue ajustando para sacar buena nota con ese puntuador.

A eso le dicen RLHF, aprendizaje por refuerzo con retroalimentación humana. Es la única parte del libro donde aparece el aprendizaje por refuerzo, que es la tercera familia de la que hablamos en el capítulo 1: ni supervisado ni no supervisado, sino aprender de una señal de qué tan bien lo hiciste.

Y explica dos cosas que se notan usándolo. Que el modelo sea educado y prudente no es cosa de la arquitectura: es gusto de las personas que compararon. Y que a veces conteste con seguridad antes que decir "no sé" también, porque entre dos respuestas la gente tiende a preferir la que suena resuelta 😐

Ejercicios

1. Cuántos tokens gasta el español

Parte varias palabras y cuenta los trozos.

for w in ['lima', 'bodega', 'minimarket', 'huancayo']:
    t = trozos(w, CONOCIDOS)
    print(f'{w:12} {len(w):2d} letras -> {len(t)} trozos  {t}')
lima          4 letras -> 2 trozos  ['li', 'ma']
bodega        6 letras -> 3 trozos  ['bo', 'de', 'ga']
minimarket   10 letras -> 4 trozos  ['min', 'i', 'mar', 'ket']
huancayo      8 letras -> 4 trozos  ['hu', 'an', 'ca', 'yo']

Cuatro letras dan dos trozos y diez letras dan cuatro. Con un vocabulario de verdad la proporción es mejor, pero la idea es esa.

Y tiene consecuencia de plata: los modelos se cobran por token. Si tu texto está en español y el vocabulario se armó mirando inglés, pagas más por decir lo mismo 💸

2. La palabra que no se puede partir

Prueba con una palabra cuyos trozos no estén todos.

print(trozos('zapallo', CONOCIDOS))
print(trozos('xyz', CONOCIDOS))
['?', 'a', '?', 'a', '?', '?', 'o']
['?', '?', '?']

Aparecen interrogantes donde no encontró nada. En un tokenizador de verdad eso no pasa nunca, porque el vocabulario incluye todas las letras sueltas: siempre hay una salida, aunque sea letra a letra.

Ese es el diseño que hace que nunca falle, a diferencia de los embeddings del capítulo 13, que reventaban con una palabra nueva 🧩

3. La temperatura sobre el generador

Escribe con temperaturas distintas y compara.

for T in [0.2, 1.0, 3.0]:
    print(f'T={T}:')
    for s in range(3):
        print('   ', escribe('el', temperatura=T, semilla=s))
T=0.2:
    el pedido de aceite
    el pedido llego completo y azucar
    el minimarket el pedido de arroz y azucar
T=1.0:
    el minimarket el aceite
    el minimarket reclamo
    el martes
T=3.0:
    el minimarket el aceite
    el mayorista reclamo
    el lunes

Las frases cambian, sí, pero no como esperabas: con T=0,2 salen las más largas, y con T=3,0 las más cortas y sosas. Justo al revés de la intuición de "temperatura alta, más creativo".

Y tiene explicación. Con una tabla de 26 comentarios, la mayoría de las palabras tienen una o dos continuaciones posibles, así que la temperatura no cambia cuánto se arriesga: cambia por qué rama se va. Y unas ramas llevan a frases largas y otras a un punto final.

La temperatura hace lo que dice el apartado anterior cuando hay muchas continuaciones plausibles, que es lo que pasa con un vocabulario de 100.000 tokens. Con cinco opciones, lo que se ve es otra cosa 🌡️

4. La misma semilla siempre da lo mismo

Comprueba que no hay nada mágico ni impredecible.

print(escribe('la', semilla=3))
print(escribe('la', semilla=3))
print('¿idénticas?', escribe('la', semilla=3) == escribe('la', semilla=3))
la bodega el pedido del minimarket llego incompleto
la bodega el pedido del minimarket llego incompleto
¿idénticas? True

Idénticas, siempre. La misma entrada y la misma semilla dan la misma salida.

Que un modelo te conteste distinto cada vez no es porque "piense diferente": es porque el azar de la elección cambia. Con temperatura 0 y sin muestreo, un modelo de lenguaje es una función determinista 🎲

5. Cuántos pesos hay en cada sitio

Reparte los parámetros de un bloque entre atención y feedforward.

for dim in [8, 768, 4096]:
    atencion = 4 * dim * dim              # Wq, Wk, Wv y la de salida
    feed = 8 * dim * dim                  # una de d a 4d y otra de 4d a d
    print(f'd={dim:5d}: atención {atencion:12,}  feedforward {feed:12,}  '
          f'({feed / (atencion + feed):.0%} en el feedforward)')
d=    8: atención          256  feedforward          512  (67% en el feedforward)
d=  768: atención    2,359,296  feedforward    4,718,592  (67% en el feedforward)
d= 4096: atención   67,108,864  feedforward  134,217,728  (67% en el feedforward)

Dos tercios de los pesos de un bloque están en el feedforward, no en la atención.

La atención se lleva toda la atención (perdón) porque es la idea nueva, y el grueso de la memoria del modelo está en esas dos matrices anchas que procesan cada posición por separado 🧠

6. Qué tan grande es la norma por capa

Comprueba que deja cada fila en media 0 y desviación 1.

raro = np.array([[1000., 1001, 999, 1002],
                 [0.001, 0.002, 0.0015, 0.0011]])
print('antes, medias      :', np.round(raro.mean(axis=1), 4))
print('antes, desviaciones:', np.round(raro.std(axis=1), 4))
print('después, medias      :', np.round(norma(raro).mean(axis=1), 4))
print('después, desviaciones:', np.round(norma(raro).std(axis=1), 4))
antes, medias      : [1.0005e+03 1.4000e-03]
antes, desviaciones: [1.118e+00 4.000e-04]
después, medias      : [ 0. -0.]
después, desviaciones: [1.     0.1235]

Las dos filas salen en media 0, y ahí funcionó. Pero mira las desviaciones: la primera sale 1 y la segunda sale 0,1235.

El culpable es el eps de la fórmula. La segunda fila tiene una varianza de 1,6e-07, o sea muchísimo más chica que el 1e-05 que le sumamos para no dividir entre cero, así que ese epsilon se come el cálculo y la fila sale aplastada.

No es un fallo: es el precio de protegerse de la división entre cero, y solo se nota cuando los números son ridículamente pequeños. Me pareció que valía la pena enseñarlo porque estas protecciones nunca son gratis, y esta en concreto está en todas las redes que existen 🧯

Por lo demás, la norma por capa es el StandardScaler del capítulo 5 aplicado dentro de la red y a cada fila en vez de a las columnas antes de empezar. Y por la misma razón: para que el gradiente no tenga que lidiar con escalas distintas ⚖️

7. El error del token que no existe

Pídele al generador que arranque con una palabra que no está en el corpus.

escribe('hola')
KeyError: 'hola'

Mismo error que cerraba el capítulo 13, y por eso lo repito aquí: el generador trabaja con una tabla de palabras conocidas y "hola" no está.

La diferencia es que ahora ya sabes la solución. Si en vez de palabras enteras usara los trozos del principio del capítulo, "hola" se partiría en pedacitos conocidos y no habría error.

La tokenización por subpalabras no es un detalle de implementación: es lo que hace que el sistema no se rompa con el mundo real 🧩

Comprueba que lo tienes

¿Por qué las conexiones residuales no son opcionales en un transformer profundo?

  • Porque le dan al gradiente un camino corto de vuelta a las primeras capas
  • Porque hacen el modelo más rápido
  • Porque reducen el número de parámetros
  • Porque evitan el sobreajuste

El 92% que el modelo ya se sabía

La trampa

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.

aciertos = 0
for pregunta, respuesta in banco_publico:
    if modelo(pregunta) == respuesta:
        aciertos += 1

# 92% de acierto
Qué está mal

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ó.

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.

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.

Lo que te llevas

  • 🧩 El texto se parte en trozos, así que "huancayo" se lee como hu-an-ca-yo sin estar en el vocabulario.
  • ➕ Un bloque es atención más residual más norma más feedforward. Sin residual, seis bloques dejan todas las posiciones con parecido 1,0.
  • 🧠 Dos tercios de los pesos están en el feedforward, no en la atención.
  • 🔁 Escribir es un bucle: poner probabilidades, elegir, pegar, repetir. No hay plan previo.
  • 🌡️ La temperatura es una división: con 0,2 la favorita se lleva 0,9959 y con 3,0 baja a 0,3774. No cambia el orden ni lo que el modelo sabe.
  • 🎚️ Top-k corta por cantidad y top-p por probabilidad acumulada, así que el corte del top-p se mueve según lo seguro que esté.
  • 🎲 Misma entrada y misma semilla dan la misma salida. Nada de esto es impredecible.
  • 📚 Y lo que aquí no está: el entrenamiento a escala, el ajuste con personas y las barreras.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Un modelo de lenguaje no sabe lo que dice. Sabe qué palabra suele venir después, y eso explica casi todo lo que falla.

Si lo que quieres es usar bien estos modelos en el día a día en vez de construirlos, eso es prompt engineering 💬

En el capítulo 18 pasamos al lado práctico: cuándo conviene un prompt, cuándo RAG y cuándo ajustar un modelo, que es la pregunta que llega a las empresas.

Que tengas lindo día! 🌸

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

Los términos que salen en este capítulo

Están todos en el glosario de IA, con su definición corta.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?