Los modelos de lenguaje grande (LLM) pueden producir resultados variados, creativos y, a veces, sorprendentes incluso cuando se les da el mismo mensaje. Esta aleatoriedad no es un error, sino una característica central de cómo el modelo toma muestras de su siguiente token a partir de una distribución de probabilidad. En este artículo, desglosamos las estrategias de muestreo clave y demostramos cómo parámetros como la temperatura, top-k y top-p influyen en el equilibrio entre coherencia y creatividad.
En este tutorial, adoptamos un enfoque práctico para comprender:
Cómo los logits se convierten en probabilidades Cómo funcionan los muestreos de temperatura, top-k y top-p Cómo las diferentes estrategias de muestreo dan forma a la distribución del siguiente token del modelo
Al final, comprenderá la mecánica detrás de la inferencia de LLM y podrá ajustar la creatividad o el determinismo del resultado.
Empecemos.
Cómo los LLM eligen sus palabras: un recorrido práctico por Logits, Softmax y Sampling
Foto de Colton Duke. Algunos derechos reservados.
Descripción general
Este artículo se divide en cuatro partes; ellos son:
Cómo los logits se convierten en probabilidades Temperatura Muestreo Top-k Muestreo Top-p
Cómo los logits se convierten en probabilidades
Cuando le haces una pregunta a un LLM, genera un vector de logits. Los logits son puntuaciones brutas que el modelo asigna a cada posible siguiente token en su vocabulario.
Si el modelo tiene un vocabulario de tokens $V$, generará un vector de logits $V$ para cada posición de palabra siguiente. Un logit es un número real. Se convierte en probabilidad mediante la función softmax:
$$
p_i = frac{e^{x_i}}{sum_{j=1}^{V} e^{x_j}}
$$
donde $x_i$ es el logit del token $i$ y $p_i$ es la probabilidad correspondiente. Softmax transforma estas puntuaciones brutas en una distribución de probabilidad. Todos los $p_i$ son positivos y su suma es 1.
Supongamos que le damos al modelo este mensaje:
El clima de hoy es tan ___
El modelo considera cada token de su vocabulario como una posible siguiente palabra. Para simplificar, digamos que solo hay 6 tokens en el vocabulario:
wonderful cloudy nice hot gloomy delicious
wonderful cloudy nice hot gloomy delicious
El modelo produce un logit para cada token. A continuación se muestra un conjunto de logits de ejemplo que el modelo podría generar y las probabilidades correspondientes basadas en la función softmax:
Token Logit Probabilidad maravilloso 1,2 0,0457 nublado 2,0 0,1017 agradable 3,5 0,4556 cálido 3,0 0,2764 sombrío 1,8 0,0832 delicioso 1,0 0,0374
Puede confirmar esto utilizando la función softmax de PyTorch:
import torch
import torch.nn.functional as F
vocab = [“wonderful”, “cloudy”, “nice”, “hot”, “gloomy”, “delicious”]
logits = torch.tensor([1.2, 2.0, 3.5, 3.0, 1.8, 1.0])
probs = F.softmax(logits, dim=-1)
print(probs)
# Output:
# tensor([0.0457, 0.1017, 0.4556, 0.2764, 0.0832, 0.0374])
import torch
import torch . nn . functional as F
vocab = [ “wonderful” , “cloudy” , “nice” , “hot” , “gloomy” , “delicious” ]
logits = torch . tensor ( [ 1.2 , 2.0 , 3.5 , 3.0 , 1.8 , 1.0 ] )
probs = F . softmax ( logits , dim = – 1 )
print ( probs )
# Output:
# tensor([0.0457, 0.1017, 0.4556, 0.2764, 0.0832, 0.0374])
Según este resultado, el token con mayor probabilidad es "agradable". Los LLM no siempre seleccionan el token con mayor probabilidad ; en cambio, toman muestras de la distribución de probabilidad para producir un resultado diferente cada vez. En este caso, hay un 46% de probabilidad de ver “agradable”.
Si desea que el modelo dé una respuesta más creativa, ¿cómo puede cambiar la distribución de probabilidad de modo que las respuestas “nublado”, “caliente” y otras también aparezcan con más frecuencia?
Temperatura
La temperatura ($T$) es un parámetro de inferencia del modelo. No es un parámetro del modelo; es un parámetro del algoritmo que genera la salida. Escala logits antes de aplicar softmax:
$$
p_i = frac{e^{x_i / T}}{sum_{j=1}^{V} e^{x_j / T}}
$$
Puede esperar que la distribución de probabilidad sea más determinista si $T<1$, ya que la diferencia entre cada valor de $x_i$ será exagerada. Por otro lado, será más aleatorio si $T>1$, ya que la diferencia entre cada valor de $x_i$ se reducirá.
Ahora, visualicemos este efecto de la temperatura en la distribución de probabilidad:
import matplotlib.pyplot as plt
import torch
import torch.nn.functional as F
vocab = [“wonderful”, “cloudy”, “nice”, “hot”, “gloomy”, “delicious”]
logits = torch.tensor([1.2, 2.0, 3.5, 3.0, 1.8, 1.0]) # (vocab_size,)
scores = logits.unsqueeze(0) # (1, vocab_size)
temperatures = [0.1, 0.5, 1.0, 3.0, 10.0]
fig, ax = plt.subplots(figsize=(10, 6))
for temp in temperatures:
# Apply temperature scaling
scores_processed = scores / temp
# Convert to probabilities
probs = F.softmax(scores_processed, dim=-1)[0]# Muestra de la distribución sampled_idx = torch.multinomial(probs, num_samples=1).item() print(f"Temperature = {temp}, sampled: {vocab[sampled_idx]}") # Traza la distribución de probabilidad ax.plot(vocab, probs.numpy(), Marker="o", label=f"T={temp}") ax.set_title("Efecto de la temperatura") ax.set_ylabel("Probabilidad") ax.legend() plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
importar matplotlib . trazado de datos como pl
importar antorcha
antorcha de importación . nn . funcional como F
vocabulario = [ "maravilloso" , "nublado" , "lindo" , "caliente" , "sombrío" , "delicioso" ]
logits = antorcha . tensor ( [ 1.2 , 2.0 , 3.5 , 3.0 , 1.8 , 1.0 ] ) # (vocab_size,)
montones = logits . descomprimir ( 0 ) # (1, tamaño_vocab)
temperaturas = [ 0.1 , 0,5 , 1.0 , 3.0 , 10.0 ]
higo , hacha = pl . subtramas ( tamaño de figura = ( 10 , 6 ) )
para temperatura en temperaturas :
# Aplicar escala de temperatura
puntuaciones_procesadas = montones / temperatura
# Convertir a probabilidades
problemas = F. softmax ( puntuaciones_procesadas , tenue = – 1 ) [ 0 ]
# Muestra de la distribución
idx_muestreado = antorcha . multinomial ( problemas , núm_muestras = 1 ) . artículo ( )
print ( f "Temperatura = {temp}, muestreado: {vocab[sampled_idx]}" )
# Trazar la distribución de probabilidad
hacha . trama ( vocabulario , problemas . numeroso ( ) , marcador = 'o' , etiqueta = f "T={temp}" )
hacha . set_title ( "Efecto de la temperatura" )
hacha . set_ylabel ( "Probabilidad" )
hacha . leyenda ( )
pl . espectáculo ( )
Este código genera una distribución de probabilidad sobre cada token del vocabulario. Luego toma muestras de una ficha según la probabilidad. La ejecución de este código puede producir el siguiente resultado:
Temperature = 0.1, sampled: nice
Temperature = 0.5, sampled: nice
Temperature = 1.0, sampled: nice
Temperature = 3.0, sampled: wonderful
Temperature = 10.0, sampled: delicious
Temperature = 0.1, sampled: nice
Temperature = 0.5, sampled: nice
Temperature = 1.0, sampled: nice
Temperature = 3.0, sampled: wonderful
Temperature = 10.0, sampled: delicious
y el siguiente gráfico que muestra la distribución de probabilidad para cada temperatura:
El efecto de la temperatura en la distribución de probabilidad resultante.
¡El modelo puede producir el mensaje absurdo "El clima de hoy es tan delicioso" si configura la temperatura en 10!
Muestreo Top-k
La salida del modelo es un vector de logits para cada posición en la secuencia de salida. El algoritmo de inferencia convierte los logits en palabras reales o, en términos de LLM, en tokens.
El método más simple para seleccionar el siguiente token es el muestreo codicioso, que siempre selecciona el token con la mayor probabilidad. Si bien es eficiente, esto a menudo produce resultados repetitivos y predecibles. Otro método consiste en muestrear el token a partir de la distribución de probabilidad softmax derivada de los logits. Sin embargo, debido a que un LLM tiene un vocabulario muy amplio, la inferencia es lenta y existe una pequeña posibilidad de producir tokens sin sentido.
El muestreo Top-$k$ logra un equilibrio entre determinismo y creatividad. En lugar de tomar muestras de todo el vocabulario, restringe el grupo de candidatos a los $k$ tokens y muestras más probables de ese subconjunto. A los tokens fuera de este grupo superior de $k$ se les asigna probabilidad cero y nunca serán elegidos. No sólo acelera la inferencia al reducir el tamaño efectivo del vocabulario, sino que también elimina tokens que no deben seleccionarse.
Al filtrar tokens extremadamente improbables y al mismo tiempo permitir la aleatoriedad entre los más plausibles, el muestreo de $k$ superiores ayuda a mantener la coherencia sin sacrificar la diversidad. Cuando $k=1$, top-$k$ se reduce a un muestreo codicioso.
A continuación se muestra un ejemplo de cómo se puede implementar el muestreo top-$k$:
import matplotlib.pyplot as plt
import torch
import torch.nn.functional as F
vocab = [“wonderful”, “cloudy”, “nice”, “hot”, “gloomy”, “delicious”]
logits = torch.tensor([1.2, 2.0, 3.5, 3.0, 1.8, 1.0]) # (vocab_size,)
scores = logits.unsqueeze(0) # (batch, vocab_size)
k_candidates = [1, 2, 3, 6]
fig, ax = plt.subplots(figsize=(10, 6))
for top_k in k_candidates:
# 1. get the top-k logits
topk_values = torch.topk(scores, top_k)[0]# 2. umbral = logit más pequeño dentro del conjunto top-k umbral = topk_values[…, -1, Ninguno] # (…, 1) # 3. enmascarar todos los logits por debajo del umbral a -inf indices_to_remove = puntuaciones < umbral filtered_scores = puntuaciones.masked_fill(indices_to_remove, -float("inf")) # convertir a probabilidades, aquellos con -inf logits obtener problemas de probabilidad cero = F.softmax(filtered_scores, dim=-1)[0]# muestra de la distribución filtrada sampled_idx = torch.multinomial(probs, num_samples=1).item() print(f"Top-k = {top_k}, sampled: {vocab[sampled_idx]}") # Traza la distribución de probabilidad ax.plot(vocab, probs.numpy(), Marker="o", label=f"Top-k = {top_k}") ax.set_title("Efecto de Muestreo Top-k") ax.set_ylabel("Probabilidad") ax.legend() plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
importar matplotlib . trazado de datos como pl
importar antorcha
antorcha de importación . nn . funcional como F
vocabulario = [ "maravilloso" , "nublado" , "lindo" , "caliente" , "sombrío" , "delicioso" ]
logits = antorcha . tensor ( [ 1.2 , 2.0 , 3.5 , 3.0 , 1.8 , 1.0 ] ) # (vocab_size,)
montones = logits . descomprimir ( 0 ) # (lote, tamaño_vocab)
k_candidatos = [ 1 , 2 , 3 , 6 ]
higo , hacha = pl . subtramas ( tamaño de figura = ( 10 , 6 ) )
para top_k en k_candidatos :
# 1. obtén los logits top-k
valores_topk = antorcha . topk ( puntuaciones , top_k ) [ 0 ]
# 2. umbral = logit más pequeño dentro del conjunto top-k
límite = valores_topk [ . . . , – 1 , Ninguno ] # (…, 1)
# 3. enmascare todos los logits por debajo del umbral a -inf
índices_para_eliminar = montones < límite
puntuaciones_filtradas = puntuaciones . relleno_enmascarado ( índices_a_eliminar , – flotador ( "inf" ) )
# convertir a probabilidades, aquellos con logits -inf obtendrán probabilidad cero
problemas = F. softmax ( puntuaciones_filtradas , tenue = – 1 ) [ 0 ]
# muestra de la distribución filtrada
idx_muestreado = antorcha . multinomial ( problemas , núm_muestras = 1 ) . artículo ( )
print ( f "Top-k = {top_k}, muestreado: {vocab[sampled_idx]}" )
# Trazar la distribución de probabilidad
hacha . trama ( vocabulario , problemas . numeroso ( ) , marcador = 'o' , etiqueta = f "Top-k = {top_k}" )
hacha . set_title ( "Efecto del muestreo Top-k" )
hacha . set_ylabel ( "Probabilidad" )
hacha . leyenda ( )
pl . espectáculo ( )
Este código modifica el ejemplo anterior llenando los logits de algunos tokens con $-infty$ para que la probabilidad de esos tokens sea cero. La ejecución de este código puede producir el siguiente resultado:
Top-k = 1, sampled: nice
Top-k = 2, sampled: nice
Top-k = 3, sampled: hot
Top-k = 6, sampled: delicious
Top-k = 1, sampled: nice
Top-k = 2, sampled: nice
Top-k = 3, sampled: hot
Top-k = 6, sampled: delicious
El siguiente gráfico muestra la distribución de probabilidad después del filtrado superior de $k$:
La distribución de probabilidad después del filtrado top $k$
Puedes ver que para cada $k$, las probabilidades de que haya exactamente $Vk$ tokens son cero. Esos tokens nunca serán elegidos bajo la configuración superior $k$ correspondiente.
Muestreo superior p
El problema con el muestreo de $k$ superiores es que siempre selecciona entre un número fijo de tokens, independientemente de cuánta masa de probabilidad representen colectivamente. El muestreo incluso de los tokens $k$ superiores aún puede permitir que el modelo elija entre la larga cola de opciones de baja probabilidad, lo que a menudo conduce a resultados incoherentes.
El muestreo Top-$p$ (también conocido como muestreo de núcleo) aborda este problema muestreando tokens de acuerdo con su probabilidad acumulativa en lugar de un recuento fijo. Selecciona el conjunto más pequeño de tokens cuya probabilidad acumulada excede un umbral $p$, creando efectivamente un $k$ dinámico para cada posición para filtrar probabilidades de cola no confiables y retener solo los candidatos más plausibles. Cuando el modelo es nítido y alcanza su punto máximo, el $p$ superior produce menos tokens candidatos; cuando la distribución es plana, se expande en consecuencia.
Establecer $p$ cerca de 1,0 se acerca al muestreo completo de todos los tokens. Establecer $p$ en un valor muy pequeño hace que el muestreo sea más conservador. Así es como puedes implementar el muestreo top-$p$:
import matplotlib.pyplot as plt
import torch
import torch.nn.functional as F
vocab = [“wonderful”, “cloudy”, “nice”, “hot”, “gloomy”, “delicious”]
logits = torch.tensor([1.2, 2.0, 3.5, 3.0, 1.8, 1.0]) # (vocab_size,)
scores = logits.unsqueeze(0) # (1, vocab_size)
p_candidates = [0.3, 0.6, 0.8, 0.95, 1.0]
fig, ax = plt.subplots(figsize=(10, 6))
for top_p in p_candidates:
# 1. sort logits in ascending order
sorted_logits, sorted_indices = torch.sort(scores, descending=False)
# 2. compute probabilities of the sorted logits
sorted_probs = F.softmax(sorted_logits, dim=-1)
# 3. cumulative probs from low-prob tokens to high-prob tokens
cumulative_probs = sorted_probs.cumsum(dim=-1)
# 4. remove tokens with cumulative top_p above the threshold (token with 0 are kept)
sorted_indices_to_remove = cumulative_probs <= (1.0 – top_p)
# 5. keep at least 1 token, which is the one with highest probability
sorted_indices_to_remove[…, -1:] = 0
# 6. scatter sorted tensors to original indexing
indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
# 7. mask logits of tokens to remove with -inf
scores_processed = scores.masked_fill(indices_to_remove, -float(“inf”))
# probabilities after top-p filtering, those with -inf logits will get zero probability
probs = F.softmax(scores_processed, dim=-1)[0]# (vocab_size,) # muestra de la distribución del núcleo Choice_idx = torch.multinomial(probs, num_samples=1).item() print(f"Top-p = {top_p}, muestreado: {vocab[choice_idx]}") ax.plot(vocab, probs.numpy(), marcador="o", label=f"Top-p = {top_p}") ax.set_title("Efecto de Muestreo Top-p (Núcleo)") ax.set_ylabel("Probabilidad") ax.legend() plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
importar matplotlib . trazado de datos como pl
importar antorcha
antorcha de importación . nn . funcional como F
vocabulario = [ "maravilloso" , "nublado" , "lindo" , "caliente" , "sombrío" , "delicioso" ]
logits = antorcha . tensor ( [ 1.2 , 2.0 , 3.5 , 3.0 , 1.8 , 1.0 ] ) # (vocab_size,)
montones = logits . descomprimir ( 0 ) # (1, tamaño_vocab)
p_candidatos = [ 0,3 , 0,6 , 0,8 , 0,95 , 1.0 ]
higo , hacha = pl . subtramas ( tamaño de figura = ( 10 , 6 ) )
para top_p en p_candidatos :
# 1. ordenar logits en orden ascendente
logits_ordenados , índices_ordenados = antorcha . ordenar ( puntuaciones , descendente = falso )
# 2. calcular probabilidades de los logits ordenados
problemas_ordenados = F. softmax ( logits_ordenados , tenue = – 1 )
# 3. Problemas acumulativos desde tokens de baja probabilidad hasta tokens de alta probabilidad.
problemas_acumulativos = problemas_ordenados . suma cum ( tenue = – 1 )
# 4. eliminar tokens con top_p acumulativo por encima del umbral (los tokens con 0 se mantienen)
índices_ordenados_para_eliminar = problemas_acumulativos <= ( 1.0 – top_p )
# 5. conserva al menos 1 token, que es el que tiene mayor probabilidad
índices_ordenados_para_eliminar [ . . . , – 1 : ] = 0
# 6. tensores ordenados dispersos a la indexación original
índices_para_eliminar = sorted_indices_to_remove . dispersión ( 1 , índices_ordenados , índices_ordenados_para_eliminar )
# 7. Enmascarar logits de tokens para eliminar con -inf
puntuaciones_procesadas = puntuaciones . relleno_enmascarado ( índices_a_eliminar , – flotador ( "inf" ) )
# probabilidades después del filtrado top-p, aquellas con logits -inf obtendrán probabilidad cero
problemas = F. softmax ( puntuaciones_procesadas , tenue = – 1 ) [ 0 ] # (vocab_size,)
# muestra de la distribución del núcleo
elección_idx = antorcha . multinomial ( problemas , núm_muestras = 1 ) . artículo ( )
print ( f "Top-p = {top_p}, muestra: {vocab[choice_idx]}" )
hacha . trama ( vocabulario , problemas . numeroso ( ) , marcador = 'o' , etiqueta = f "Top-p = {top_p}" )
hacha . set_title ( "Efecto del muestreo Top-p (Núcleo)" )
hacha . set_ylabel ( "Probabilidad" )
hacha . leyenda ( )
pl . espectáculo ( )
La ejecución de este código puede producir el siguiente resultado:
Top-p = 0.3, sampled: nice
Top-p = 0.6, sampled: hot
Top-p = 0.8, sampled: nice
Top-p = 0.95, sampled: hot
Top-p = 1.0, sampled: hot
Top-p = 0.3, sampled: nice
Top-p = 0.6, sampled: hot
Top-p = 0.8, sampled: nice
Top-p = 0.95, sampled: hot
Top-p = 1.0, sampled: hot
y el siguiente gráfico muestra la distribución de probabilidad después del filtrado top-$p$:
La distribución de probabilidad después del filtrado top-$p$
A partir de este gráfico, es menos probable que vea el efecto de $p$ en la cantidad de tokens con probabilidad cero. Este es el comportamiento previsto, ya que depende de la confianza del modelo en el siguiente token.
Lecturas adicionales
A continuación se muestran algunas lecturas adicionales que pueden resultarle útiles:
Resumen
Este artículo demostró cómo las diferentes estrategias de muestreo afectan la elección de la siguiente palabra por parte de un LLM durante la fase de decodificación. Aprendió a seleccionar diferentes valores para los parámetros de muestreo de temperatura, top-$k$ y top-$p$ para diferentes casos de uso de LLM.