Transformers documentation

고정 길이 모델의 Perplexity

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

Perplexity (PPL)은 언어 모델을 평가하는 데 가장 일반적으로 사용되는 지표 중 하나입니다. 자세히 살펴보기 전에, 이 지표는 고전적인 언어 모델(때로는 자기회귀(autoregressive) 또는 인과(causal) 언어 모델이라고도 함)에 특히 적용되며, BERT와 같은 마스킹 언어 모델에는 잘 정의되어 있지 않다는 점을 유의해야 합니다.

Perplexity는 시퀀스의 지수화된 평균 음의 로그 가능도(negative log-likelihood)로 정의됩니다. 토큰화된 시퀀스 $X = (x_0, x_1, \dots, x_t)$가 주어지면, 그 perplexity는 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})$는 모델에 따라 이전 토큰 $x_{<i}$를 조건으로 한 i번째 토큰의 로그 가능도(log-likelihood)입니다. 직관적으로, 이는 코퍼스에서 지정된 토큰 집합 내에서 균일하게 예측하는 모델의 능력을 평가하는 것으로 볼 수 있습니다. 중요한 점은 토큰화 절차가 모델의 퍼플렉시티(perplexity)에 직접적인 영향을 미치므로, 서로 다른 모델을 비교할 때 이를 항상 고려해야 한다는 것입니다.

이는 데이터와 모델 예측 간의 교차 엔트로피의 지수화와도 동치입니다. Perplexity에 대한 더 직관적인 이해와 Bits Per Character(BPC) 및 데이터 압축과의 관계에 대해서는 이 The Gradient의 훌륭한 블로그 포스트를 참고하세요.

고정 길이 모델로 PPL 계산

모델의 컨텍스트 크기로 제한되지 않는다면, 아래에 표시된 바와 같이 각 단계에서 이전 전체 부분 시퀀스를 조건으로 삼아 시퀀스를 자기회귀적으로 분해하여 모델의 퍼플렉시티를 평가할 것입니다.

Full decomposition of a sequence with unlimited context length

대체 모델(approximate models)을 다룰 때는 모델이 처리할 수 있는 토큰 수에 대한 제약이 있는 경우가 많습니다. 예를 들어 가장 큰 버전의 GPT-2, 은 고정된 길이가 1024개 토큰이므로\theta(x_t|x{<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

이 방법은 시퀀스 확률의 진정한 분해에 더 가까운 근사치이며, 일반적으로 더 유리한 점수를 제공합니다. 단점으로는 코퍼스 내 각 토큰마다 별도의 forward pass가 필요하다는 것입니다. 좋은 실용적 타협책은 strided sliding window를 사용하는 것입니다. 즉, 1개 토큰씩 이동하는 대신 더 큰 stride로 컨텍스트를 이동합니다. 이를 통해 각 단계에서 모델이 예측을 수행할 수 있는 큰 컨텍스트를 제공하면서도 계산 속도를 훨씬 빠르게 할 수 있습니다.

예시: 🤗 Transformers에서 GPT-2로 perplexity 계산

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 데이터셋을 로드하고, 몇 가지 다른 슬라이딩 윈도우 전략을 사용하여 퍼플렉시티를 평가합니다. 이 데이터셋은 크기가 작고 데이터셋 전체에 대해 포워드 패스만 한 번 수행하므로, 전체 데이터셋을 메모리에 로드하고 인코딩하면 됩니다.

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로 모델에 전달하기만 하면, 각 토큰의 평균 음의 로그 가능도(negative log-likelihood)가 손실(loss)로 반환됩니다. 그러나 슬라이딩 윈도우 방식을 사용하면 각 반복 시 모델에 전달하는 토큰 사이에 중복이 발생합니다. 컨텍스트로만 취급하는 토큰의 로그 가능도가 손실에 포함되는 것을 원하지 않으므로, 이러한 타깃을 -100으로 설정하여 무시되도록 할 수 있습니다. 아래는 스트라이드(stride)가 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 길이를 최대 입력 길이와 동일하게 설정하여 실행하면, 위에서 논의한 비최적(non-sliding-window) 전략과 동일해집니다. stride가 작을수록 모델이 각 예측을 할 때 활용할 수 있는 컨텍스트가 많아지며, 일반적으로 보고되는 perplexity도 더 좋아집니다.

stride = 1024, 즉 겹침이 없는 상태로 위 코드를 실행하면 결과 PPL은 19.44이며, 이는 GPT-2 논문에서 보고된 19.93와 거의 동일합니다. stride = 512를 사용하여 스트라이딩 윈도우 전략을 적용하면 이 값은 16.44로 낮아집니다. 이는 더 유리한 점수일 뿐만 아니라, 시퀀스 가능도의 진정한 자기회귀 분해에 더 가까운 방식으로 계산됩니다.

Update on GitHub