Transformers documentation
Perplejidad de modelos de longitud fija
Perplejidad de modelos de longitud fija
La perplexidad (PPL) es una de las métricas más comunes para evaluar modelos de lenguaje. Antes de profundizar, debemos señalar que esta métrica se aplica específicamente a los modelos de lenguaje clásicos (a veces denominados modelos de lenguaje autoregresivos o causales) y no está bien definida para los modelos de lenguaje enmascarados, como BERT.
La perplexity se define como la media de la log-verosimilitud negativa de una secuencia elevada a la exponencial. Si tenemos una secuencia tokenizada $X = (x_0, x_1, \dots, x_t)$, la perplexity de es:
donde $\log p\theta (x_i|x{<i})$ es la verosimilitud logarítmica del token i-ésimo condicionado en los tokens precedentes $x_{<i}$ según nuestro modelo. Intuitivamente, puede interpretarse como una evaluación de la capacidad del modelo para predecir de manera uniforme entre el conjunto de tokens especificados en un corpus. Es importante destacar que esto significa que el procedimiento de tokenización tiene un impacto directo en la perplejidad de un modelo, lo cual debe tenerse siempre en cuenta al comparar diferentes modelos.
Esto también es equivalente a la potenciación de la entropía cruzada entre los datos y las predicciones del modelo. Para obtener más intuición sobre la perplejidad y su relación con los Bits Por Carácter (BPC) y la compresión de datos, consulta esta fantástica publicación en The Gradient.
Calcular el PPL con modelos de longitud fija
Si no estuviéramos limitados por el tamaño de contexto de un modelo, evaluaríamos la perplejidad del modelo factorizando de forma autoregresiva una secuencia y condicionando en cada paso sobre toda la subsecuencia anterior, como se muestra a continuación.
Sin embargo, al trabajar con modelos aproximados, normalmente existe una restricción sobre el número de tokens que el modelo puede procesar. La versión más grande de GPT-2, por ejemplo, tiene una longitud fija de 1024 tokens, por lo que no podemos calcular $p\theta(x_t|x{<t})$ directamente cuando es mayor que 1024.
En su lugar, la secuencia se divide típicamente en subsecuencias iguales al tamaño máximo de entrada del modelo. Si el tamaño máximo de entrada de un modelo es $k$, aproximamos la probabilidad de un token condicionando únicamente en los tokens que lo preceden, en lugar de en todo el contexto. Al evaluar la perplejidad de una secuencia, un enfoque tentador pero subóptimo es dividir la secuencia en fragmentos disjuntos y sumar las log-verosimilitudes descompuestas de cada segmento de forma independiente.
Esto se calcula rápidamente, ya que la perplejidad de cada segmento puede calcularse en una sola pasada hacia adelante, pero sirve como una aproximación deficiente de la perplejidad totalmente factorizada y generalmente arroja un PPL más alto (peor), porque el modelo tendrá menos contexto en la mayoría de los pasos de predicción.
En su lugar, la PPL de los modelos de longitud fija debe evaluarse con una estrategia de ventana deslizante. Esto implica desplazar repetidamente la ventana de contexto para que el modelo tenga más contexto al realizar cada predicción.
Esta es una aproximación más cercana a la descomposición verdadera de la probabilidad de la secuencia y, por lo general, produce una puntuación más favorable. El inconveniente es que requiere una pasada de adelante (forward pass) separada para cada token del corpus. Un buen compromiso práctico es emplear una ventana deslizante con paso (strided sliding window), moviendo el contexto con pasos más grandes en lugar de deslizarlo un token a la vez. Esto permite que el cálculo progrese mucho más rápido, al tiempo que proporciona al modelo un contexto amplio para realizar predicciones en cada paso.
Ejemplo: Cálculo de la perplejidad con GPT-2 en 🤗 Transformers
Vamos a demostrar este proceso con GPT-2.
from transformers import GPT2LMHeadModel, GPT2TokenizerFast
from accelerate import Accelerator
device = Accelerator().device
model_id = "openai-community/gpt2-large"
model = GPT2LMHeadModel.from_pretrained(model_id).to(device)
tokenizer = GPT2TokenizerFast.from_pretrained(model_id)Cargaremos el conjunto de datos WikiText-2 y evaluaremos la perplejidad utilizando varias estrategias de ventana deslizante. Dado que este conjunto de datos es pequeño y solo realizamos una pasada de adelante sobre el conjunto, podemos cargar y codificar todo el conjunto de datos en memoria.
from datasets import load_dataset
test = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split="test")
encodings = tokenizer("\n\n".join(test["text"]), return_tensors="pt")Con 🤗 Transformers, simplemente podemos pasar input_ids como labels a nuestro modelo, y la media de la log-verosimilitud negativa de cada token se devuelve como pérdida. Sin embargo, con nuestro enfoque de ventana deslizante, hay solapamiento en los tokens que pasamos al modelo en cada iteración. No queremos que la log-verosimilitud de los tokens que solo estamos tratando como contexto se incluya en nuestra pérdida, por lo que podemos establecer estos objetivos en -100 para que se ignoren. A continuación se muestra un ejemplo de cómo podríamos hacer esto con un desplazamiento de 512. Esto significa que el modelo tendrá al menos 512 tokens de contexto al calcular la verosimilitud condicional de cualquier token (siempre que haya 512 tokens anteriores disponibles sobre los que condicionar).
import torch
from tqdm import tqdm
max_length = model.config.n_positions
stride = 512
seq_len = encodings.input_ids.size(1)
nll_sum = 0.0
n_tokens = 0
prev_end_loc = 0
for begin_loc in tqdm(range(0, seq_len, stride)):
end_loc = min(begin_loc + max_length, seq_len)
trg_len = end_loc - prev_end_loc # may be different from stride on last loop
input_ids = encodings.input_ids[:, begin_loc:end_loc].to(device)
target_ids = input_ids.clone()
target_ids[:, :-trg_len] = -100
with torch.no_grad():
outputs = model(input_ids, labels=target_ids)
# loss is calculated using CrossEntropyLoss which averages over valid labels
# N.B. the model only calculates loss over trg_len - 1 labels, because it internally shifts the labels
# to the left by 1.
neg_log_likelihood = outputs.loss
# Accumulate the total negative log-likelihood and the total number of tokens
num_valid_tokens = (target_ids != -100).sum().item() # number of valid tokens in target_ids
batch_size = target_ids.size(0)
num_loss_tokens = num_valid_tokens - batch_size # subtract batch_size due to internal label shift
nll_sum += neg_log_likelihood * num_loss_tokens
n_tokens += num_loss_tokens
prev_end_loc = end_loc
if end_loc == seq_len:
break
avg_nll = nll_sum / n_tokens # average negative log-likelihood per token
ppl = torch.exp(avg_nll)Ejecutar esto con la longitud del desplazamiento igual a la longitud máxima de entrada es equivalente a la estrategia no óptima, sin ventana deslizante, que discutimos anteriormente. Cuanto menor sea el desplazamiento, más contexto tendrá el modelo para realizar cada predicción, y la perplejidad informada suele ser mejor.
Cuando ejecutamos lo anterior con stride = 1024, es decir, sin solapamiento, el PPL resultante es 19.44, que es aproximadamente el mismo que el 19.93 reportado en el artículo de GPT-2. Al utilizar stride = 512 y, por lo tanto, emplear nuestra estrategia de ventana con solapamiento, este valor baja a 16.44. Esto no solo es una puntuación más favorable, sino que se calcula de una manera más cercana a la verdadera descomposición autorregresiva de la verosimilitud de una secuencia.