Transformers documentation

Perplexidade de modelos de comprimento fixo

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Perplexidade de modelos de comprimento fixo

A Perplexity (PPL) é uma das métricas mais comuns para avaliar modelos de linguagem. Antes de prosseguir, devemos observar que essa métrica se aplica especificamente a modelos de linguagem clássicos (às vezes chamados de modelos de linguagem autoregressivos ou causais) e não está bem definida para modelos de linguagem mascarados, como o BERT.

A perplexidade é definida como a média negativa log-verossimilhança de uma sequência elevada à potência. Se tivermos uma sequência tokenizada $X = (x_0, x_1, \dots, x_t)$, a perplexidade de XX é, PPL(X)=exp⁡{−1t∑itlog⁡pθ(xi∣x<i)} \text{PPL}(X) = \exp\left\{ -\frac{1}{t}\sum_i^t \log p_\theta (x_i|x_{<i}) \right\}

onde $\log p\theta (x_i|x{<i})$ é a log-verossimilhança do i-ésimo token condicionado aos tokens anteriores $x_{<i}$, de acordo com nosso modelo. Intuitivamente, pode ser interpretado como uma avaliação da capacidade do modelo de prever uniformemente entre o conjunto de tokens especificados em um corpus. É importante observar que isso significa que o procedimento de tokenização tem um impacto direto na perplexidade de um modelo, o que deve sempre ser considerado ao comparar diferentes modelos.

Isso também é equivalente à exponenciação da entropia cruzada entre os dados e as previsões do modelo. Para uma intuição mais aprofundada sobre perplexidade e sua relação com Bits Por Caractere (BPC) e compressão de dados, confira este fantástico post de blog no The Gradient.

Calculando o PPL com modelos de comprimento fixo

Se não fôssemos limitados pelo tamanho de contexto de um modelo, avaliaríamos a perplexidade do modelo fatorando uma sequência de forma autoregressiva e condicionando em toda a subsequência anterior a cada etapa, conforme mostrado abaixo.

Full decomposition of a sequence with unlimited context length

Ao trabalhar com modelos aproximados, no entanto, geralmente temos uma restrição no número de tokens que o modelo pode processar. A maior versão do GPT-2, por exemplo, tem um comprimento fixo de 1024 tokens, portanto, não podemos calcular $p\theta(x_t|x{<t})$ diretamente quando tt é maior que 1024.

Em vez disso, a sequência é tipicamente dividida em subseqüências iguais ao tamanho máximo de entrada do modelo. Se o tamanho máximo de entrada de um modelo for $k$, aproximamos a probabilidade de um token xtx_t condicionando apenas nos k−1k-1 tokens que o precedem, em vez de todo o contexto. Ao avaliar a perplexidade do modelo para uma sequência, uma abordagem tentadora, porém subótima, é dividir a sequência em blocos disjuntos e somar as log-verossimilhanças decompostas de cada segmento independentemente.

Suboptimal PPL not taking advantage of full available context

O cálculo é rápido, pois a perplexidade de cada segmento pode ser computada em uma única passagem para frente, mas serve como uma aproximação ruim da perplexidade totalmente fatorada e geralmente resulta em um PPL maior (pior), pois o modelo terá menos contexto na maioria das etapas de previsão.

Em vez disso, o PPL de modelos de comprimento fixo deve ser avaliado com uma estratégia de janela deslizante. Isso envolve deslizar repetidamente a janela de contexto para que o modelo tenha mais contexto ao fazer cada previsão.

Sliding window PPL taking advantage of all available context

Essa é uma aproximação mais próxima da verdadeira decomposição da probabilidade da sequência e, em geral, resulta em uma pontuação mais favorável. A desvantagem é que ela requer uma passagem de frente separada para cada token no corpus. Um bom compromisso prático é empregar uma janela deslizante com passo, movendo o contexto com passos maiores em vez de deslizar 1 token por vez. Isso permite que o cálculo prossiga muito mais rápido, ao mesmo tempo em que fornece ao modelo um contexto amplo para fazer previsões em cada etapa.

Exemplo: Calculando perplexidade com GPT-2 no 🤗 Transformers

Vamos demonstrar esse processo com o 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)

Vamos carregar o conjunto de dados WikiText-2 e avaliar a perplexidade usando algumas estratégias diferentes de janela deslizante. Como esse conjunto de dados é pequeno e estamos fazendo apenas uma passagem de ida pelo conjunto, podemos simplesmente carregar e codificar o conjunto de dados inteiro na memória.

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")

Com o 🤗 Transformers, podemos simplesmente passar os input_ids como labels para o nosso modelo, e a média da log-verossimilhança negativa para cada token é retornada como a perda. No entanto, com a nossa abordagem de janela deslizante, há sobreposição nos tokens que passamos ao modelo em cada iteração. Não queremos que a log-verossimilhança dos tokens que estamos tratando apenas como contexto seja incluída na nossa perda, então podemos definir esses alvos como -100 para que sejam ignorados. A seguir, um exemplo de como podemos fazer isso com um stride de 512. Isso significa que o modelo terá pelo menos 512 tokens para contexto ao calcular a verossimilhança condicional de qualquer um dos tokens (desde que haja 512 tokens anteriores disponíveis para 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)

Executar isso com o comprimento do stride igual ao comprimento máximo da entrada é equivalente à estratégia subótima, sem janela deslizante, que discutimos acima. Quanto menor for o stride, mais contexto o modelo terá ao fazer cada previsão, e a perplexidade relatada geralmente será melhor.

Quando executamos o código acima com stride = 1024, ou seja, sem sobreposição, o PPL resultante é 19.44, aproximadamente o mesmo que o 19.93 relatado no paper do GPT-2. Ao usar stride = 512 e, assim, empregar nossa estratégia de janela com deslocamento, esse valor cai para 16.44. Isso não apenas resulta em uma pontuação mais favorável, mas é calculado de uma forma mais próxima da verdadeira decomposição autoregressiva da verossimilhança de uma sequência.

Update on GitHub