Transformers documentation
Độ bất ngờ của các mô hình có độ dài cố định
Độ bất ngờ của các mô hình có độ dài cố định
Perplexity (PPL) là một trong những chỉ số phổ biến nhất để đánh giá các mô hình ngôn ngữ. Trước khi đi sâu hơn, chúng ta cần lưu ý rằng chỉ số này áp dụng cụ thể cho các mô hình ngôn ngữ cổ điển (đôi khi được gọi là mô hình ngôn ngữ tự hồi quy hoặc nhân quả) và không được định nghĩa rõ ràng cho các mô hình ngôn ngữ có mặt nạ như BERT.
Perplexity được định nghĩa là trung bình log-likelihood âm của một chuỗi được mũ hóa. Nếu chúng ta có một chuỗi đã token hóa $X = (x_0, x_1, \dots, x_t)$, thì perplexity của là,
trong đó $\log p\theta (x_i|x{<i})$ là log-likelihood của token thứ i có điều kiện trên các token đi trước $x_{<i}$ theo mô hình của chúng ta. Về mặt trực quan, nó có thể được hiểu là một đánh giá về khả năng dự đoán đều giữa tập hợp các token được chỉ định trong một kho ngữ liệu. Quan trọng là, điều này có nghĩa là quy trình tokenization có tác động trực tiếp đến perplexity của mô hình, điều này luôn phải được xem xét khi so sánh các mô hình khác nhau.
Điều này cũng tương đương với việc lũy thừa của entropy chéo giữa dữ liệu và dự đoán của mô hình. Để có thêm trực giác về perplexity và mối quan hệ của nó với Bits Per Character (BPC) và nén dữ liệu, hãy xem bài đăng tuyệt vời này trên The Gradient.
Tính PPL với các mô hình có độ dài cố định
Nếu không bị giới hạn bởi kích thước ngữ cảnh của mô hình, chúng ta sẽ đánh giá độ bất ngờ (perplexity) của mô hình bằng cách phân rã tự hồi quy một chuỗi và điều kiện hóa dựa trên toàn bộ tiểu chuỗi đi trước ở mỗi bước, như minh họa bên dưới.
Tuy nhiên, khi làm việc với các mô hình xấp xỉ, chúng ta thường có ràng buộc về số lượng token mà mô hình có thể xử lý. Phiên bản lớn nhất của GPT-2, chẳng hạn, có độ dài cố định là 1024 token, do đó chúng ta không thể tính trực tiếp $p\theta(x_t|x{<t})$ khi lớn hơn 1024.
Thay vào đó, chuỗi thường được chia thành các chuỗi con có độ dài bằng kích thước đầu vào tối đa của mô hình. Nếu kích thước đầu vào tối đa của mô hình là $k$, chúng ta sẽ xấp xỉ xác suất của một token bằng cách chỉ điều kiện trên token đi trước nó thay vì toàn bộ ngữ cảnh. Khi đánh giá độ perplexity của mô hình đối với một chuỗi, một cách tiếp cận hấp dẫn nhưng không tối ưu là chia chuỗi thành các khối rời rạc và cộng dồn log-likelihood đã phân rã của từng đoạn một cách độc lập.
Cách này tính toán nhanh vì độ bất ngờ (perplexity) của mỗi đoạn có thể được tính trong một lần truyền thẳng duy nhất, nhưng nó là một xấp xỉ kém của độ bất ngờ được phân rã hoàn toàn và thường cho ra PPL cao hơn (tệ hơn) vì mô hình sẽ có ít ngữ cảnh hơn ở hầu hết các bước dự đoán.
Thay vào đó, PPL của các mô hình có độ dài cố định nên được đánh giá bằng chiến lược cửa sổ trượt. Chiến lược này bao gồm việc trượt lặp đi lặp lại cửa sổ ngữ cảnh để mô hình có thêm ngữ cảnh khi đưa ra mỗi dự đoán.
Đây là một phép xấp xỉ sát hơn với sự phân rã thực sự của xác suất chuỗi và thường sẽ cho điểm đánh giá thuận lợi hơn. Nhược điểm là phương pháp này yêu cầu một lần truyền tiến riêng biệt cho từng token trong tập dữ liệu. Một sự thỏa hiệp thực tế tốt là sử dụng cửa sổ trượt có bước nhảy, di chuyển ngữ cảnh theo các bước nhảy lớn hơn thay vì trượt từng 1 token một. Điều này cho phép quá trình tính toán diễn ra nhanh hơn nhiều trong khi vẫn cung cấp cho mô hình một ngữ cảnh lớn để đưa ra dự đoán ở mỗi bước.
Ví dụ: Tính độ bất ngờ (perplexity) với GPT-2 trong 🤗 Transformers
Hãy cùng minh họa quy trình này với 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)Chúng ta sẽ tải bộ dữ liệu WikiText-2 và đánh giá độ perplexity bằng một vài chiến lược sliding-window khác nhau. Vì bộ dữ liệu này nhỏ và chúng ta chỉ thực hiện một lần forward pass trên tập dữ liệu, chúng ta có thể tải và mã hóa toàn bộ bộ dữ liệu trong bộ nhớ.
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")Với 🤗 Transformers, chúng ta chỉ cần truyền input_ids làm labels cho mô hình, và trung bình của log-likelihood âm cho từng token sẽ được trả về dưới dạng loss. Tuy nhiên, với cách tiếp cận cửa sổ trượt của chúng ta, có sự chồng chéo giữa các token được truyền vào mô hình ở mỗi lần lặp. Chúng ta không muốn log-likelihood của các token chỉ được coi là ngữ cảnh bị tính vào loss, vì vậy chúng ta có thể đặt các target này thành -100 để chúng bị bỏ qua. Dưới đây là một ví dụ về cách thực hiện điều này với stride là 512. Điều này có nghĩa là mô hình sẽ có ít nhất 512 token làm ngữ cảnh khi tính toán likelihood có điều kiện của bất kỳ token nào (miễn là có 512 token trước đó để làm điều kiện).
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)Chạy với độ dài bước nhảy bằng với độ dài đầu vào tối đa là tương đương với chiến lược không tối ưu, không dùng cửa sổ trượt mà chúng ta đã thảo luận ở trên. Bước nhảy càng nhỏ, mô hình càng có nhiều ngữ cảnh hơn để đưa ra mỗi dự đoán, và độ bất ngờ (perplexity) được báo cáo thường sẽ tốt hơn.
Khi chúng ta chạy đoạn mã trên với stride = 1024, tức là không có phần chồng lấn, PPL thu được là 19.44, gần như tương đương với con số 19.93 được báo cáo trong bài báo về GPT-2. Bằng cách sử dụng stride = 512 và do đó áp dụng chiến lược cửa sổ trượt của chúng ta, giá trị này giảm xuống còn 16.44. Đây không chỉ là một điểm số thuận lợi hơn, mà còn được tính toán theo cách gần với sự phân rã tự hồi quy thực sự của khả năng xảy ra của một chuỗi hơn.