Muestreo: temperatura, top-k y top-p

Muestreo: temperatura, top-k y top-p

30 min de lectura

La lección sobre el entrenamiento dejó el mini-GPT del bloque cargado en el navegador y aprendiendo, y todo lo que se le ha pedido hasta aquí cabe en un verbo: repartir. Lee un prefijo y devuelve 512 logits, y con ellos una distribución para el token siguiente. Escribir con él es repetir cuatro cosas: pasarle el texto, leer esa distribución, quedarse con un token y pegarlo al final. Tres de ellas son mecánicas; la otra, quedarse con un token, es una decisión.

¿Con cuál se queda? Parece un detalle y decide si lo que sale se puede leer. Las dos respuestas que caben en una línea fallan, y por lados opuestos: la favorita en cada posición acaba repitiéndose para siempre, y sortear tal cual, con las probabilidades del modelo, cuela palabras que no existen. Esta lección demuestra por qué, con una cuenta para cada fallo, y construye lo que se usa en su lugar: una temperatura entre los dos extremos y un corte que tira lo que el sorteo no debería tocar. Todo corre sobre el mini-GPT.

Mira una distribución de verdad: la del mini-GPT tras La Nela, el nombre de la protagonista, que reparte la apuesta entre muchas entradas (la favorita, no, no llega al 12 %), y tras La Nela bajó la cabe, que casi no deja dudas. En cada fila, la barra clara es la probabilidad tras la temperatura; la verde intenso, la que de verdad se sortea; lo rayado, lo que un corte tira.

Haz tres cosas. Lleva τ\tau hasta el cero y súbela hasta 2: las barras crecen y encogen, y ninguna cambia de sitio. Pon top-k con k=10k = 10: en el primer contexto tira la mitad de la masa, y en el segundo conserva nueve entradas más, que juntas no llegan al 2 %. Y pon top-p con p=0.9p = 0.9: 44 entradas en el primero, una en el segundo.

Los logits que el mini-GPT produce tras dos contextos, convertidos en la distribución de la que se sortea. La temperatura estira o aplana sin cambiar el orden; top-k y top-p tiran la cola y renormalizan lo que queda.

La temperatura estira la distribución sin cambiar su orden

Fijemos una posición y llamemos z∈R∣V∣\mathbf{z} \in \mathbb{R}^{\lvert V \rvert} a sus logits: el zt\mathbf{z}_t de la lección sobre el modelo de lenguaje causal, sin el subíndice, porque aquí sólo hay una posición en juego. Para un número τ>0\tau > 0, la temperatura, definimos

qv=ezv/τ∑u∈Vezu/τ,q_v = \frac{e^{z_v/\tau}}{\sum_{u \in V} e^{z_u/\tau}},

el softmax de z/τ\mathbf{z}/\tau. (En el curso anterior, τ\tau era el tokenizador.) Con τ=1\tau = 1, q\mathbf{q} es la distribución del modelo; con cualquier otra es una distribución distinta, fabricada con sus logits, y por eso no se llama pθp_\theta. Muestrear es elegir la entrada siguiente al azar, cada vv con probabilidad qvq_v: un sorteo por posición.

Todo lo que hace τ\tau se lee en un cociente. Entre dos entradas uu y vv el denominador se va,

quqv=e(zu−zv)/τ=(ezu−zv)1/τ,\frac{q_u}{q_v} = e^{(z_u - z_v)/\tau} = \left(e^{z_u - z_v}\right)^{1/\tau},

y ezu−zve^{z_u - z_v} es el cociente que da el propio modelo, a τ=1\tau = 1. La temperatura eleva cada cociente a 1/τ1/\tau. Tras La Nela bajó la cabe, el modelo da a za unas 195 veces lo que da a da; a τ=2\tau = 2, la raíz, unas 14 veces, y a τ=0.5\tau = 0.5, el cuadrado, más de treinta mil. Y elevar a una potencia positiva no cambia qué número es mayor.

El orden de las entradas no depende de τ\tau. La favorita sigue siéndolo a cualquier temperatura, y las filas del explorable no se mueven.

Los dos extremos salen de esa misma cuenta. Llamemos v⋆v^{\star} a la favorita, la entrada de logit mayor, y dividamos arriba y abajo entre ezv⋆/τe^{z_{v^{\star}}/\tau}:

qv=e(zv−zv⋆)/τ∑u∈Ve(zu−zv⋆)/τ.q_v = \frac{e^{(z_v - z_{v^{\star}})/\tau}}{\sum_{u \in V} e^{(z_u - z_{v^{\star}})/\tau}}.

Cuando τ→0\tau \to 0, cada exponente con zu<zv⋆z_u < z_{v^{\star}} es un número negativo dividido entre algo que tiende a cero, y se va a −∞-\infty: su término tiende a 00. Sólo sobrevive el de v⋆v^{\star}, que vale e0=1e^{0} = 1 arriba y abajo, así que

lim⁡τ→0qv={1si v=v⋆,0si no.\lim_{\tau \to 0} q_v = \begin{cases} 1 & \text{si } v = v^{\star}, \\ 0 & \text{si no} \end{cases}.

(Si dos entradas empatan en el máximo, se reparten el 11.) Sortear de ahí ya no es sortear: sale v⋆v^{\star} siempre. Quedarse con la favorita en cada posición, la generación voraz (greedy decoding; no la llamamos decodificación porque decodificar ya es pasar de ids a texto), no es otra regla. Es el muestreo a temperatura cero.

En el otro extremo, cuando τ→∞\tau \to \infty cada exponente tiende a 00, cada término a 11, y qv→1/∣V∣q_v \to 1/\lvert V \rvert: el sorteo uniforme, que ignora todo lo que el modelo aprendió. Entre los dos está la familia entera, con τ\tau como único mando: por debajo de 11 la distribución se afila hacia la favorita, por encima se aplana hacia la uniforme.

La generación voraz entra en un ciclo y no sale

La voraz es determinista: elige en función de lo que el mini-GPT lee, que son como mucho los últimos Tctx=64T_{\text{ctx}} = 64 tokens, y cada posición pega la elegida al final y, cuando ya hay 64, suelta el más viejo. Lo que leerá después depende sólo de lo que lee ahora, y el texto entero, sólo del prompt.

De ahí sale el argumento. Si en algún momento lo que lee se repite, los mismos 64 tokens que LL posiciones antes, elige lo mismo que entonces, y en la posición siguiente otra vez, y así para siempre: el texto se vuelve periódico, con periodo LL, y en la regla no hay nada que pueda romperlo. Es un ciclo. Y se cae en uno en cuanto la favorita continúa un fragmento que se repite, como no,, durante 64 posiciones seguidas: a partir de ahí, lo que lee es ese fragmento repetido, idéntico cada LL posiciones.

Lo que la regla no mira es cuánto. arg⁡max⁡\arg\max sólo ve qué entrada tiene el logit mayor, y una favorita del 90 % y una del 30 % dan la misma elección. Un sorteo sí lo ve: si la entrada que continúa el ciclo tiene probabilidad 0.30.3, sale de él siete veces de cada diez, en cada posición. La voraz no tiene puerta. (La búsqueda en haz, beam search, cae en lo mismo, y queda fuera de este curso.)

Sin corte, el sorteo acaba en la cola

El otro extremo falla por el lado contrario. Sortear a τ=1\tau = 1 sin tocar nada no se atasca, pero reparte masa entre cientos de entradas que por separado son improbables: tras La Nela, las 468 que quedan fuera de las 44 más probables suman casi un 10 %. Ninguna saldrá en un sorteo concreto; alguna saldrá, casi seguro, en treinta.

La regla de la cadena lo dice exacto: la probabilidad de escribir nn tokens sin tocar nunca esa cola es el producto de la masa que queda dentro en cada posición, sabido lo anterior. Si cada una dejara fuera un 10 %, para 32 tokens sería

0.932≈0.034.0.9^{32} \approx 0.034.

Treinta y dos tokens sin corte traen, casi seguro, alguno de la cola, y lo que se escribe se queda: el modelo lo lee en todas las posiciones siguientes.

La respuesta es tirar la cola antes de sortear. Ordenemos las probabilidades de mayor a menor, q(1)≥q(2)≥⋯≥q(∣V∣)q_{(1)} \ge q_{(2)} \ge \dots \ge q_{(\lvert V \rvert)} (el paréntesis del subíndice dice «la ii-ésima mayor», no «la entrada ii»), y llamemos masa de las jj primeras a su suma, Mj=∑i=1jq(i)M_j = \sum_{i=1}^{j} q_{(i)}. Un corte se queda con las jj más probables, pone a cero las demás y renormaliza, es decir, divide lo que queda entre su masa para que vuelva a sumar 11:

q~v={qv/Mjsi v estaˊ entre las j maˊs probables,0si no.\tilde{q}_v = \begin{cases} q_v / M_j & \text{si } v \text{ está entre las } j \text{ más probables}, \\ 0 & \text{si no} \end{cases}.

Lo que se sortea es q~\tilde{\mathbf{q}}, y 1−Mj1 - M_j es la masa que el corte tira, la parte rayada del explorable.

Top-p corta a la medida de cada posición

Los dos cortes que se usan sólo difieren en cómo eligen jj. Top-k fija j=kj = k en todas las posiciones. (La kk del curso anterior cortaba el vocabulario; ésta corta una distribución.) Top-p fija la masa: se queda con las más probables hasta reunir al menos pp, que son

kp=min⁡{ j:Mj≥p }k_p = \min\{\, j : M_j \ge p \,\}

entradas, el núcleo (el nombre original es nucleus sampling). La pp de top-p es un número, el umbral; la distribución del modelo sigue siendo pθp_\theta, con su subíndice. Y la entrada que cruza el umbral se queda.

Top-p es, entonces, top-k con una kk que cada posición elige según la forma de su distribución, y eso es lo que top-k no puede hacer. Tras La Nela, el núcleo de 0.90.9 tiene 44 entradas y top-10 tira la mitad de la masa. Tras La Nela bajó la cabe, el núcleo es za sola y top-p no sortea, mientras top-10 conserva nueve entradas que juntas no llegan al 2 % y de vez en cuando escriben cabeda. Una kk fija es pequeña en una posición y grande en la otra.

La temperatura y el corte se combinan, en ese orden: primero τ\tau, después el corte. Como τ\tau no cambia el orden, las entradas que conserva top-k no dependen de ella. El núcleo sí, porque τ\tau mueve la masa.

Voraz, sorteo y núcleo, sobre el mini-GPT

Las dos celdas corren sobre el mini-GPT: minigpt.py y el checkpoint del bloque, sin entrenar nada. Su función muestrear(z, temperatura, top_k, top_p, rng) es lo de arriba: con temperatura=0 devuelve la favorita y, si no, divide, aplica el softmax, corta y sortea. generar la llama una vez por token.

La primera mira una sola posición con lupa: los dos contextos del explorable a tres temperaturas, con sus dos favoritas, la masa que guarda top-10 y el núcleo de 0.90.9. Después genera 32 tokens de forma voraz tras La Nela y tras —¿Qué, apuntando cuánto le daba el modelo a cada favorita.

import json
import numpy as np
from pyodide.http import open_url

exec(open_url("/courses/llm-agents/bpe.py").read()) # codificar, decodificar, vocabulario
exec(open_url("/courses/llm-agents/minigpt.py").read()) # MiniGPT, softmax, muestrear
F = [tuple(par) for par in json.load(open_url("/courses/llm-agents/bpe-merges.json"))]
V = vocabulario(F)
modelo = MiniGPT.cargar(open_url("/courses/llm-agents/minigpt.json").read())

def logits(ids):
"""z: los 512 logits de la última posición, leídos los últimos 64 tokens de ids."""
return modelo.adelante(np.array([ids[-64:]]))[0][0, -1]

for texto in ["La Nela", "La Nela bajó la cabe"]:
z = logits(codificar(texto, F))
print(repr(texto))
for tau in [0.5, 1.0, 2.0]:
q = softmax(z / tau)
M = np.cumsum(np.sort(q)[::-1]) # M[j-1]: la masa de las j más probables
k_p = int(np.searchsorted(M, 0.9)) + 1 # la primera j con M >= 0.9
dos = " ".join("%r %.3f" % (V[i].decode(), q[i]) for i in np.argsort(-q)[:2])
print(" tau = %.1f %s top-10 guarda %.3f núcleo(0.9) = %3d, deja fuera %.3f"
% (tau, dos, M[9], k_p, 1 - M[k_p - 1]))

def voraz(prompt, n=32):
"""La generación voraz: la favorita en cada posición, y cuánto le daba el modelo."""
ids, apuestas = codificar(prompt, F), []
for _ in range(n):
q = softmax(logits(ids))
ids.append(int(np.argmax(q)))
apuestas.append(q[ids[-1]])
return ids, apuestas

ids, apuestas = voraz("La Nela")
print(repr(decodificar(ids, F)))
print("las 6 últimas:", " ".join("%r %.2f" % (V[i].decode(), a) for i, a in zip(ids[-6:], apuestas[-6:])))
print("sorteando a tau = 1, seguir 20 posiciones en el ciclo: %.1e" % np.prod(apuestas[-20:]))
print("coste medio de esos 20 tokens: %.2f por token" % -np.mean(np.log(apuestas[-20:])))
ids, apuestas = voraz("—¿Qué")
print(repr(decodificar(ids, F)))
print("' no', vuelta a vuelta:", " ".join("%.2f" % a for a in apuestas[-14::2]))
numpy

La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador y se reutiliza en todas las lecciones.

Las cuentas de arriba están ahí. A τ=0.5\tau = 0.5, za se queda con todo, y a τ=2\tau = 2 baja a 0.3870.387, catorce veces lo de da. El núcleo de La Nela pasa de 15 a 44 y a 92 entradas mientras top-10 guarda 0.8520.852, 0.5020.502 y 0.2520.252, y el de cabe salta de una a 133, porque τ=2\tau = 2 levanta a la vez las 511 entradas de la cola.

Y el ciclo. El texto voraz dice no debía decirle., abre un diálogo y ya no sale de no y la coma. El modelo apostaba 0.300.30 por no y 0.240.24 por la coma en cada vuelta: nunca estuvo seguro, y un sorteo habría salido siete veces de cada diez. Que siguiera dentro veinte posiciones tiene probabilidad 1.1×10−111.1 \times 10^{-11}, el producto de las veinte apuestas; para la voraz, ese número no existe.

La línea del coste dice algo más. Esos veinte tokens le cuestan al modelo 1.261.26 por token, la media de sus −log⁡-\log, y las páginas de Marianela que no vio le cuestan 3.23.2: el ciclo le parece más probable que la novela. Holtzman y sus coautores vieron lo mismo en GPT-2, un modelo miles de veces mayor. El texto que escribe una persona no es el que el modelo encuentra más probable, y maximizar la probabilidad posición a posición lleva a sitios como éste. Por eso se sortea.

La segunda continuación llega al mismo ciclo por otro camino, y en ella la apuesta sí sube con cada vuelta: de 0.180.18 a 0.310.31 por no, y ahí se queda. Es la realimentación que Holtzman midió en GPT-2, donde cada repetición hace más probable la siguiente hasta rozar el 11. En el mini-GPT es débil, y el argumento de arriba no la necesita: con 0.180.18 la voraz ya estaba atrapada.

La segunda compara tres maneras de elegir, con 32 tokens tras cada uno de tres prompts y dos semillas (una sola para la voraz, que no sortea). Cuenta cuántas de las palabras generadas existen en la novela y cuántas continuaciones repiten algún grupo de cuatro tokens. En un dispositivo lento se detiene antes de que el navegador la corte, y dice dónde.

# Necesita la celda anterior: modelo, codificar, decodificar, F, np.
import re, time

PALABRA = re.compile(r"[^\W\d_]+")
novela = set(PALABRA.findall(quitar_cabecera(open_url("/courses/llm-agents/corpus.txt").read()).lower()))
plazo = time.time() + 8 # el navegador corta a los 10 s

def repite(ids, n=4):
"""True si algún grupo de n tokens seguidos sale dos veces."""
grupos = [tuple(ids[i:i + n]) for i in range(len(ids) - n + 1)]
return len(grupos) != len(set(grupos))

def medir(opciones, semillas):
"""32 tokens por prompt y semilla: una muestra, palabras de la novela y repeticiones."""
reales = total = repetidas = 0
for prompt in ["La Nela", "—¿Qué", "El sol se"]:
for semilla in semillas:
if time.time() > plazo:
return None
ids = codificar(prompt, F)
nuevos = modelo.generar(ids, 32, rng=np.random.default_rng(semilla), **opciones)[len(ids):]
palabras = PALABRA.findall(decodificar(nuevos, F).lower())[:-1] # la última, quizá cortada
reales += sum(w in novela for w in palabras)
total += len(palabras)
repetidas += repite(nuevos)
muestra = decodificar(nuevos, F) if total == len(palabras) else muestra
return muestra, reales, total, repetidas, 3 * len(semillas)

for nombre, opciones, semillas in [("voraz", dict(temperatura=0), [0]),
("tau = 1", dict(temperatura=1.0), [0, 1]),
("top-p 0.9", dict(temperatura=1.0, top_p=0.9), [0, 1])]:
medida = medir(opciones, semillas)
if medida is None:
print("tiempo agotado en", nombre)
break
muestra, reales, total, repetidas, n = medida
print("%-10s %r" % (nombre, muestra))
print("%10s palabras de la novela: %d de %d (%.0f %%) repiten: %d de %d"
% ("", reales, total, 100 * reales / total, repetidas, n))
numpy

La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador y se reutiliza en todas las lecciones.

Los dos fallos, medidos. La voraz escribe casi sólo palabras de la novela, 38 de 39, y sus tres continuaciones repiten, dos de ellas ya en el ciclo de no y la coma. Sortear a τ=1\tau = 1 no repite ninguna y lo paga: 54 de 70, con estreglando y ficill entre las otras. El núcleo de 0.90.9 recupera parte, 59 de 73, sin repetir. El mini-GPT es pequeño y todas las muestras se leen mal; lo que la celda mide es hacia dónde empuja cada regla.

Ahora cambia la última línea de la lista por ("tau = 1.5", dict(temperatura=1.5), [0, 1]): las palabras de la novela bajan a 49 de 70, y la muestra dice lapacuma. Con temperatura=0.5 suben a 78 de 83, y dos de las seis continuaciones vuelven a repetirse: una temperatura baja es una voraz con algo de ruido. Seis continuaciones son pocas para separar dos reglas parecidas; los extremos, en cambio, no dejan duda.

Comprueba tu intuición

Cinco preguntas y un reto: escribir la distribución de la que sortea top-p.

Tras La Nela, a τ=1\tau = 1, el mini-GPT le da 0.1180.118 a no y 0.0450.045 a es. ¿Cuántas veces más probable que es es no a τ=0.5\tau = 0.5? Redondea a un decimal.

Se acepta un margen de ±0.15.

Con los logits de La Nela, subes la temperatura de τ=1\tau = 1 a τ=2\tau = 2. Marca lo que cambia.

Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.

En el ciclo de La Nela, en la primera celda, el mini-GPT le da 0.300.30 a no en cada vuelta: siete de cada diez veces, la masa está en otra parte. ¿Por qué la generación voraz no sale del ciclo?

Sorteas sin corte y, en cada posición, las entradas que quedan fuera del núcleo de 0.90.9 suman un 5 % de la masa. ¿Qué probabilidad hay de escribir 50 tokens sin sacar ni uno de esa cola? Dos decimales.

Se acepta un margen de ±0.01.

Tras La Nela bajó la cabe, a τ=1\tau = 1, za tiene 0.9740.974 y las otras 511 entradas se reparten el resto. ¿Entre cuántas entradas sortea top-p con p=0.9p = 0.9, y entre cuántas top-k con k=10k = 10?

Escribe nucleo(z, p, tau=1.0): la distribución de la que sortea top-p. Recibe los logits z de una posición, aplica la temperatura, se queda con el núcleo de masa p (las entradas más probables hasta reunir al menos p, incluida la que cruza el umbral) y renormaliza. Devuelve un array del tamaño de z, con ceros fuera del núcleo, que suma 1.

La primera comprobación descarga el intérprete de Python (~15 MB); después queda en la caché del navegador. Este desafío se resuelve mejor con un teclado físico: en el móvil puedes leerlo y volver luego.


El mini-GPT ya escribe: cada token sale de un sorteo sobre el núcleo de su distribución, a la temperatura que se elija, y la voraz queda como el caso τ=0\tau = 0, que conviene conocer y no usar. Pero mira lo que hace generar por cada token. Llama a adelante con el texto entero, hasta 64 tokens, se queda con la última fila de logits y tira las demás; en la posición siguiente vuelve a pasar los mismos tokens, más uno.

La máscara de la lección sobre el modelo de lenguaje causal garantiza que, en cada capa, las claves y los valores de esas posiciones no han cambiado: la fila tt sólo depende de lo que hay a su izquierda, y ahí no se ha movido nada. Ese trabajo se repite en cada token y crece con el texto. Guardarlo en lugar de rehacerlo, y contar cuánto cuesta entonces cada token, es la lección siguiente, sobre la caché de claves y valores.

¿Te ha sido útil?
Para profundizar2 fuentes · 2 papers

De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.

  • The Curious Case of Neural Text Degeneration
    paperHoltzman, Buys, Du, Forbes y Choi, 2020ICLR 2020 · arXiv:1904.09751EN

    El artículo de top-p. Su primera figura enseña los dos fallos de esta lección en GPT-2, el ciclo y el galimatías, y mide lo que apunta la primera celda: el texto humano no es el que el modelo encuentra más probable.

  • Hierarchical Neural Story Generation
    paperFan, Lewis y Dauphin, 2018ACL 2018 · arXiv:1805.04833EN

    El que popularizó top-k, con k = 10, para generar cuentos. Lo propone de pasada, como un detalle de cómo generan, y es el punto de partida de la crítica de Holtzman a una k fija.