Transformers documentation

Переплетность моделей фиксированной длины

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

and get access to the augmented documentation experience

to get started

Переплетность моделей фиксированной длины

Perplexity (PPL) — одна из наиболее распространённых метрик для оценки языковых моделей. Прежде чем переходить к деталям, стоит отметить, что данная метрика применима именно к классическим языковым моделям (иногда их называют авторегрессионными или каузальными языковыми моделями) и не имеет чёткого определения для маскированных языковых моделей, таких как BERT.

Переплексия определяется как экспонента среднего отрицательного логарифмического правдоподобия последовательности. Если у нас есть токенизированная последовательность $X = (x_0, x_1, \dots, x_t)$, то переплексия 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\}

где $\log p\theta (x_i|x{<i})$ — это логарифм правдоподобия i-го токена, обусловленного предшествующими токенами $x_{<i}$, согласно нашей модели. Интуитивно это можно рассматривать как оценку способности модели предсказывать равномерно среди множества заданных токенов в корпусе. Важно, что это означает: процедура токенизации напрямую влияет на перплексность модели, что всегда следует учитывать при сравнении различных моделей.

Это также эквивалентно возведению в степень кросс-энтропии между данными и предсказаниями модели. Чтобы лучше понять perplexity и её связь с Bits Per Character (BPC) и сжатием данных, посмотрите этот прекрасный пост на The Gradient.

Вычисление PPL с моделями фиксированной длины

Если бы мы не были ограничены размером контекста модели, мы бы оценивали перплексность модели, авторегрессивно разлагая последовательность и на каждом шаге используя в качестве условия всю предшествующую подпоследовательность, как показано ниже.

Full decomposition of a sequence with unlimited context length

Однако при работе с приближёнными моделями обычно существует ограничение на количество токенов, которые модель может обработать. Самая крупная версия GPT-2, например, имеет фиксированную длину в 1024 токена, поэтому мы не можем напрямую вычислить $p\_{\theta}(x_t|x_{<t})$ при{<t tt больше 1024.

Вместо этого последовательность обычно разбивается на подпоследовательности, равные максимальному размеру входных данных модели. Если максимальный размер входных данных модели равен $k$, то вероятность токена xtx_t условно оценивается только по k−1k-1 токенам, предшествующим ему, а не по всему контексту. При оценке переплексии модели для последовательности соблазнительным, но неоптимальным подходом является разбиение последовательности на непересекающиеся фрагменты и суммирование разложенных лог-вероятностей каждого сегмента независимо.

Suboptimal PPL not taking advantage of full available context

Это вычисляется быстро, поскольку перплексия каждого сегмента может быть вычислена за один прямой проход, но это плохая аппроксимация полностью факторизованной перплексии и, как правило, даёт более высокое (худшее) значение PPL, так как у модели будет меньше контекста на большинстве шагов предсказания.

Вместо этого PPL моделей с фиксированной длиной следует оценивать с использованием стратегии скользящего окна. Это предполагает многократное смещение контекстного окна, чтобы при каждом предсказании модель имела больший объём контекста.

Sliding window PPL taking advantage of all available context

Это более точное приближение к истинному разложению вероятности последовательности, как правило, дающее более высокий показатель. Недостаток в том, что для каждого токена в корпусе требуется отдельный прямой проход. Хороший практический компромисс — использовать скользящее окно с шагом, сдвигая контекст на больший шаг, а не на 1 токен за раз. Это позволяет значительно ускорить вычисления, при этом сохраняя для модели большой контекст для формирования прогнозов на каждом шаге.

Пример: вычисление перплекса с помощью GPT-2 в 🤗 Transformers

Давайте продемонстрируем этот процесс на примере 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)

Мы загрузим набор данных WikiText-2 и оценим perplexity, используя несколько различных стратегий скользящего окна. Поскольку этот набор данных небольшой, а мы выполняем лишь один прямой проход по нему, мы можем просто загрузить и закодировать весь набор данных в оперативную память.

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

В 🤗 Transformers можно просто передать input_ids в качестве labels нашей модели, и в качестве потерь будет возвращаться среднее отрицательное логарифмическое правдоподобие для каждого токена. Однако при использовании скользящего окна между токенами, которые мы передаём модели на каждой итерации, есть перекрытие. Мы не хотим, чтобы логарифмическое правдоподобие для токенов, которые мы используем только как контекст, учитывалось в наших потерях, поэтому мы можем установить для этих целей значение -100, чтобы они игнорировались. Ниже приведён пример того, как это можно сделать с шагом 512. Это означает, что при вычислении условного правдоподобия для любого токена модель будет иметь как минимум 512 токенов в качестве контекста (при условии, что доступно 512 предшествующих токенов, на которые можно опираться).

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)

Запуск с длиной шага, равной максимальной длине входных данных, эквивалентен неоптимальной стратегии без скользящего окна, о которой мы говорили выше. Чем меньше шаг, тем больше контекста у модели при формировании каждого прогноза и, как правило, тем ниже будет значение перплексии.

При запуске приведённого выше кода с stride = 1024, то есть без перекрытия, полученное значение PPL составляет 19.44, что примерно соответствует значению 19.93, указанному в статье о GPT-2. При использовании stride = 512 и, соответственно, стратегии скользящего окна это значение снижается до 16.44. Это не только более предпочтительный результат, но и вычисленный способом, который ближе к истинному авторегрессионному разложению вероятности последовательности.

Update on GitHub