Skip to content

English · Español

02 — Truncado top-k y top-p (nucleus)

🇪🇸 Temperatura escala la distribución; truncado la recorta. Top-k mantiene los k tokens con mayor logit. Top-p mantiene el conjunto más pequeño cuya masa cumulativa supera p. Top-k es fijo; top-p adapta. Ambos se combinan con temperatura.

Top-k

Dados logits \(z \in \mathbb{R}^V\) y \(k \in \{1, \ldots, V\}\):

  1. Encuentra los \(k\) logits más grandes; llama a sus índices \(\mathcal{S}_k\).
  2. Pon a cero (asigna \(-\infty\)) todos los logits que no estén en \(\mathcal{S}_k\).
  3. Aplica softmax al resultado.
\[q_i^{(k)} = \begin{cases} \frac{\exp(z_i)}{\sum_{j \in \mathcal{S}_k} \exp(z_j)} & \text{si } i \in \mathcal{S}_k \\ 0 & \text{en caso contrario} \end{cases}\]

Fácil de razonar: "el modelo elige entre sus \(k\) mejores predicciones". Fácil de fijar: \(k = 40\) o \(k = 50\) son defaults habituales en la literatura.

Modo de fallo: cuando la distribución es plana (el modelo está incierto), top-\(k\) sigue eligiendo exactamente \(k\) tokens — incluyendo algunos con probabilidad muy baja. Esto desperdicia la señal de "incertidumbre": una distribución plana debería producir salidas más diversas, no los mismos \(k\) tokens que se sitúan al frente.

Top-p (nucleus)

Holtzman et al. 2020 ("The Curious Case of Neural Text Degeneration") introdujo nucleus sampling para abordar el problema del top-k con distribuciones planas.

Dados logits \(z\) y \(p \in (0, 1]\):

  1. Calcula \(q = \text{softmax}(z)\).
  2. Ordena los tokens por probabilidad descendente: \(q_{i_1} \ge q_{i_2} \ge \ldots \ge q_{i_V}\).
  3. Encuentra el más pequeño \(K^*\) tal que \(\sum_{r=1}^{K^*} q_{i_r} \ge p\).
  4. Pon a cero los tokens \(i_{K^* + 1}, \ldots, i_V\); renormaliza los restantes.

El conjunto \(\{i_1, \ldots, i_{K^*}\}\) es el nucleus. Su tamaño se adapta:

  • Distribución picuda (p. ej., q_{i_1} = 0.9): \(K^*\) podría ser 1 o 2. Top-\(p\) se comporta como greedy o casi-greedy.
  • Distribución plana (p. ej., uniforme): \(K^*\) podría ser cientos. Top-\(p\) muestrea de un grupo amplio.

Valores habituales: \(p = 0.9\) o \(p = 0.95\). A veces \(p = 0.92\) para más diversidad.

Top-k vs top-p, lado a lado

Logits sobre 5 tokens Top-k (k=2) Top-p (p=0.9)
[5, 4, 0, 0, 0] (picuda) {5, 4} {5} (porque 5 por sí solo excede p=0.9 de masa)
[2, 1.9, 1.8, 1.7, 0] (plana) {2, 1.9} {2, 1.9, 1.8, 1.7} (hacen falta ~todos para llegar a 0.9)

Top-p es el mejor default cuando la confianza del modelo varía. Top-k es más barato de computar (sin ordenar la distribución completa, solo un top-k parcial).

Para nuestro vocabulario pequeño (\(V = 64\)), el coste del sort es despreciable. Usa top-p.

Combinar temperatura con truncado

Puedes aplicar la temperatura primero (sobre todos los logits) y después truncar. El orden importa porque la temperatura cambia qué tokens están en el conjunto "top" si el truncado es por rango (top-k); para top-p, dado que softmax es monótona, el orden relativo de los tokens se preserva pero la distribución de masa cumulativa cambia.

Receta habitual: Temperature(τ=0.7) ∘ TopP(p=0.9). Aplica temperatura, luego trunca, luego muestrea.

def temperature_then_topp(logits, tau, p, rng):
    scaled = logits / tau
    probs = softmax(scaled)
    sorted_probs, sorted_idx = sort_descending(probs)
    cumsum = np.cumsum(sorted_probs)
    cutoff = np.searchsorted(cumsum, p) + 1  # smallest K* such that sum >= p
    nucleus = set(sorted_idx[:cutoff])
    truncated = np.where(in_set(np.arange(len(probs)), nucleus), probs, 0)
    truncated /= truncated.sum()             # renormalise
    return rng.choice(len(probs), p=truncated)

Propiedad: top-p = 1.0 es un no-op

Para \(p = 1.0\), el nucleus es el vocabulario completo; la distribución no cambia. Este es un sanity test útil: TopP(p=1.0) compuesto con cualquier cosa debería producir salidas idénticas a la versión sin truncar.

El lab 02 lo testeará. Si tu implementación de top-p produce resultados distintos en \(p = 1.0\), tienes un off-by-one en la suma cumulativa o en la comprobación del umbral.

Propiedad: top-k = 1 ≈ greedy

Si k = 1, solo sobrevive el token argmax, y muestrear de una distribución de un solo token es determinista. Así que TopK(k=1) + sample() es greedy decoding.

(Sutil: si hay empates en los logits del argmax, top-k=1 con un sort estable elige uno; argmax típicamente elige el primer índice. Pueden diferir en el desempate. Raro en la práctica.)

Una trampa: top-p con distribuciones picudas

Si el modelo está muy confiado (q_{i_1} = 0.99), entonces \(K^* = 1\) incluso para \(p = 0.95\). Top-p colapsa a greedy siempre que el modelo está más que \(p\)-confiado.

Este es el comportamiento correcto. El compromiso es: top-p es "diverso-cuando-incierto, greedy-cuando-confiado". Top-k es "siempre-k-opciones", que puede ser despilfarrador o erróneo según la situación.

Para nuestro Mini-GPT entrenado, en prompts como "Tomorrow she", la distribución del modelo sobre el siguiente token suele estar picuda hacia will o is. Así que el muestreo top-p=0.9 podría siempre elegir will en ese prompt — y no verías diversidad. Eso es el modelo siendo confiado, no el sampler estando roto.

Lo que este archivo NO cubre

  • Typical sampling (Meister et al. 2023). Un truncado más sofisticado. Fuera de alcance.
  • Min-p sampling (Nguyen et al. 2023). Otra variante. Fuera de alcance.
  • Beam search. Búsqueda heurística sobre \(k\) haces; mejor para traducción, no para generación abierta.

Siguiente: 03-cost-model.md