# Perplexité des modèles de longueur fixe

Perplexity (PPL) est l'une des métriques les plus courantes pour évaluer les modèles de langage. Avant d'entrer dans le détail, il convient de noter que cette métrique s'applique spécifiquement aux modèles de langage classiques (parfois appelés modèles de langage autorégressifs ou causaux) et n'est pas bien définie pour les modèles de langage masqués tels que BERT.

La perplexité est définie comme la moyenne exponentielle de la négative de la log-vraisemblance d'une séquence. Si nous disposons d'une séquence tokenisée $X = (x_0, x_1, \dots, x_t)$, alors la perplexité de $X$ est,

$$ \text{PPL}(X) = \exp\left\{ -\frac{1}{t}\sum_i^t \log p_\theta (x_i|x_{<i})  \right\} $$

où $\log p_\theta (x_i|x_{<i})$ est la log-vraisemblance du i-ème jeton conditionné sur les jetons précédents $x_{<i}$ selon notre modèle. Intuitivement, cela peut être considéré comme une évaluation de la capacité du modèle à prédire uniformément parmi l'ensemble des jetons spécifiés dans un corpus. Il est important de noter que la procédure de tokenisation a un impact direct sur la perplexité d'un modèle, ce qui doit toujours être pris en compte lors de la comparaison de différents modèles.

Cela équivaut également à l’exponentiation de l’entropie croisée entre les données et les prédictions du modèle. Pour mieux comprendre l’intuition derrière la perplexité et sa relation avec les Bits Par Caractère (BPC) et la compression de données, consultez ce [fabuleux article de blog sur The Gradient](https://thegradient.pub/understanding-evaluation-metrics-for-language-models/).

## Calculer le PPL avec des modèles de longueur fixe

Si nous ne étions pas limités par la taille du contexte d'un modèle, nous évaluerions la perplexité du modèle en factorisant une séquence de manière autorégressive et en conditionnant sur toute la sous-séquence précédente à chaque étape, comme illustré ci-dessous.

Lorsqu’on travaille avec des modèles approximatifs, on dispose généralement d’une contrainte sur le nombre de jetons que le modèle peut traiter. La plus grande version de [GPT-2](model_doc/gpt2), par exemple, a une longueur fixe de 1024 jetons, il n’est donc pas possible de calculer $p_\theta(x_t|x_{<t})$ directement lorsque $t$ est supérieur à 1024.

Plutôt, la séquence est généralement découpée en sous-séquences égales à la taille d'entrée maximale du modèle. Si la taille d'entrée maximale d'un modèle est $k$, nous approchons alors la vraisemblance d'un jeton $x_t$ en ne conditionnant que sur les $k-1$ jetons qui le précèdent, plutôt que sur l'ensemble du contexte. Lors de l'évaluation de la perplexité d'un modèle sur une séquence, une approche tentante mais sous-optimale consiste à découper la séquence en segments disjoints et à additionner les log-vraisemblances décomposées de chaque segment indépendamment.

Le calcul est rapide, car la perplexité de chaque segment peut être obtenue en un seul passage avant, mais il constitue une approximation médiocre de la perplexité entièrement factorisée et donne généralement une PPL plus élevée (moins bonne), car le modèle dispose de moins de contexte lors de la plupart des étapes de prédiction.

À la place, le PPL des modèles de longueur fixe doit être évalué avec une stratégie de fenêtre glissante. Il s’agit de faire glisser la fenêtre de contexte de manière répétée afin que le modèle dispose de plus de contexte lors de chaque prédiction.

Il s’agit d’une approximation plus proche de la vraie décomposition de la probabilité de la séquence, ce qui donne généralement un score plus favorable. L’inconvénient est qu’il faut effectuer un passage avant distinct pour chaque jeton du corpus. Un compromis pratique efficace consiste à utiliser une fenêtre glissante à pas, en déplaçant le contexte par des pas plus larges plutôt qu’un jeton à la fois. Cela permet aux calculs de s’exécuter beaucoup plus rapidement tout en offrant au modèle un contexte large pour effectuer des prédictions à chaque étape.

## Exemple : calcul de la perplexité avec GPT-2 dans 🤗 Transformers

Démontrons ce processus avec GPT-2.

```python
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)
```

Nous chargerons le jeu de données WikiText-2 et évaluerons la perplexité à l’aide de plusieurs stratégies de fenêtre glissante différentes. Étant donné que ce jeu de données est petit et que nous effectuons un seul passage avant sur l’ensemble, nous pouvons charger et encoder l’intégralité du jeu de données en mémoire.

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

Avec 🤗 Transformers, il suffit de passer les `input_ids` en tant que `labels` à notre modèle, et la moyenne de la négation de la log-vraisemblance pour chaque jeton est renvoyée en tant que perte. Avec notre approche par fenêtre glissante, il y a toutefois un chevauchement des jetons que nous passons au modèle à chaque itération. Nous ne voulons pas que la log-vraisemblance des jetons que nous traitons simplement comme contexte soit incluse dans notre perte, nous pouvons donc définir ces cibles sur `-100` afin qu’elles soient ignorées. Voici un exemple de la manière de procéder avec un pas de `512`. Cela signifie que le modèle disposera d’au moins 512 jetons de contexte lors du calcul de la vraisemblance conditionnelle de n’importe quel jeton (à condition qu’il y ait 512 jetons précédents disponibles pour servir de condition).

```python
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)
```

L’exécution de cette procédure avec une longueur de pas égale à la longueur maximale de l’entrée est équivalente à la stratégie non glissante et sous-optimale que nous avons discutée plus haut. Plus le pas est petit, plus le modèle dispose de contexte pour effectuer chaque prédiction, et meilleure sera généralement la perplexité rapportée.

Lorsque nous exécutons le code ci-dessus avec `stride = 1024`, c'est-à-dire sans chevauchement, le PPL résultant est de `19.44`, ce qui est à peu près identique aux `19.93` rapportés dans l'article GPT-2. En utilisant `stride = 512` et en appliquant ainsi notre stratégie de fenêtre à pas, cette valeur baisse à `16.44`. Il s'agit non seulement d'un score plus favorable, mais il est calculé d'une manière plus proche de la véritable décomposition autorégressive de la vraisemblance d'une séquence.

