Transformers documentation
고정 길이 모델의 Perplexity
고정 길이 모델의 Perplexity
Perplexity (PPL)은 언어 모델을 평가하는 데 가장 일반적으로 사용되는 지표 중 하나입니다. 자세히 살펴보기 전에, 이 지표는 고전적인 언어 모델(때로는 자기회귀(autoregressive) 또는 인과(causal) 언어 모델이라고도 함)에 특히 적용되며, BERT와 같은 마스킹 언어 모델에는 잘 정의되어 있지 않다는 점을 유의해야 합니다.
Perplexity는 시퀀스의 지수화된 평균 음의 로그 가능도(negative log-likelihood)로 정의됩니다. 토큰화된 시퀀스 $X = (x_0, x_1, \dots, x_t)$가 주어지면, 그 perplexity는 입니다.
여기서 $\log p\theta (x_i|x{<i})$는 모델에 따라 이전 토큰 $x_{<i}$를 조건으로 한 i번째 토큰의 로그 가능도(log-likelihood)입니다. 직관적으로, 이는 코퍼스에서 지정된 토큰 집합 내에서 균일하게 예측하는 모델의 능력을 평가하는 것으로 볼 수 있습니다. 중요한 점은 토큰화 절차가 모델의 퍼플렉시티(perplexity)에 직접적인 영향을 미치므로, 서로 다른 모델을 비교할 때 이를 항상 고려해야 한다는 것입니다.
이는 데이터와 모델 예측 간의 교차 엔트로피의 지수화와도 동치입니다. Perplexity에 대한 더 직관적인 이해와 Bits Per Character(BPC) 및 데이터 압축과의 관계에 대해서는 이 The Gradient의 훌륭한 블로그 포스트를 참고하세요.
고정 길이 모델로 PPL 계산
모델의 컨텍스트 크기로 제한되지 않는다면, 아래에 표시된 바와 같이 각 단계에서 이전 전체 부분 시퀀스를 조건으로 삼아 시퀀스를 자기회귀적으로 분해하여 모델의 퍼플렉시티를 평가할 것입니다.
대체 모델(approximate models)을 다룰 때는 모델이 처리할 수 있는 토큰 수에 대한 제약이 있는 경우가 많습니다. 예를 들어 가장 큰 버전의 GPT-2, 은 고정된 길이가 1024개 토큰이므로\theta(x_t|x{<t})$를 직접 계산할 수 없습니다. 가 1024보다 클 때
대신, 시퀀스는 일반적으로 모델의 최대 입력 크기와 동일한 길이의 부분 시퀀스로 나뉩니다. 모델의 최대 입력 크기가 $k$인 경우, 토큰 의 확률은 전체 컨텍스트가 아니라 바로 앞의 토큰에만 조건을 붙여 근사합니다. 모델의 시퀀스 퍼플렉시티를 평가할 때, 유혹적이지만 비최적적인 접근 방식은 시퀀스를 서로 겹치지 않는 청크로 나누고 각 세그먼트의 분해된 로그 가능도를 독립적으로 합산하는 것입니다.
각 세그먼트의 퍼플렉시티는 하나의 포워드 패스로 계산할 수 있어 계산 속도가 빠르지만, 완전 인자화 퍼플렉시티에 대한 근사치로는 부적합하며, 대부분의 예측 단계에서 모델이 더 적은 컨텍스트를 갖게 되어 일반적으로 더 높은(더 나쁜) PPL을 반환합니다.
대신 고정 길이 모델의 PPL은 슬라이딩 윈도우 전략으로 평가해야 합니다. 이는 컨텍스트 윈도우를 반복적으로 슬라이딩하여 모델이 각 예측을 수행할 때 더 많은 컨텍스트를 갖도록 하는 방식입니다.
이 방법은 시퀀스 확률의 진정한 분해에 더 가까운 근사치이며, 일반적으로 더 유리한 점수를 제공합니다. 단점으로는 코퍼스 내 각 토큰마다 별도의 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로 낮아집니다. 이는 더 유리한 점수일 뿐만 아니라, 시퀀스 가능도의 진정한 자기회귀 분해에 더 가까운 방식으로 계산됩니다.