Transformers 文件
固定長度模型的困惑度
並獲得增強的文件體驗
開始使用
固定長度模型的困惑度
困惑度 (PPL) 是評估語言模型最常見的指標之一。在深入探討之前,我們應該注意此指標特別適用於經典語言模型(有時稱為自迴歸或因果語言模型),對於 BERT 等掩碼語言模型則不明確(參見模型摘要)。
困惑度定義為序列的平均負對數似然的指數。如果我們有一個分詞序列則的困惑度為:
其中是第 i 個詞元在給定前序詞元下的對數似然。直觀地說,這可以看作是對模型在語料庫中指定詞元集之間進行一致預測能力的評估。重要的是,這意味著分詞過程直接影響模型的困惑度,在比較不同模型時應始終考慮到這一點。
這也等同於資料和模型預測之間交叉熵的指數化。有關困惑度及其與每字元位數 (BPC) 和資料壓縮關係的更多直觀解釋,請查閱這篇關於 The Gradient 的精彩博文。
計算固定長度模型的 PPL
如果我們不受模型上下文大小的限制,我們將透過自迴歸分解序列並在每一步基於整個前置子序列進行條件化來評估模型的困惑度,如下所示。

然而,在處理近似模型時,我們通常會受到模型可處理的詞元數量的限制。GPT-2 的最大版本(例如)的固定長度為 1024 個詞元,因此我們無法直接計算當大於 1024 時。
相反,序列通常被分成等於模型最大輸入大小的子序列。如果模型的最大輸入大小是,那麼我們透過僅以其前面的個詞元為條件來近似詞元的似然,而不是以整個上下文為條件。在評估模型的序列困惑度時,一種誘人但次優的方法是將序列分解成不相交的塊,並獨立地將每個片段的分解對數似然相加。

這種方法計算速度快,因為每個片段的困惑度可以在一次前向傳播中計算出來,但它對完全因子化的困惑度近似不佳,並且通常會產生更高的(更差的)PPL,因為模型在大多數預測步驟中上下文較少。
相反,固定長度模型的 PPL 應該使用滑動視窗策略進行評估。這涉及到重複滑動上下文視窗,以便模型在進行每次預測時都有更多的上下文。

這更接近於序列機率的真實分解,通常會產生更有利的分數。缺點是它需要對語料庫中的每個詞元進行單獨的前向傳播。一個好的實用折衷方案是採用跨步滑動視窗,透過更大的步幅移動上下文,而不是一次滑動一個詞元。這使得計算速度大大加快,同時仍為模型提供了大量的上下文,以便在每個步驟進行預測。
示例:使用 🤗 Transformers 中的 GPT-2 計算困惑度
我們來演示一下用 GPT-2 進行這個過程。
from transformers import GPT2LMHeadModel, GPT2TokenizerFast
from accelerate.test_utils.testing import get_backend
device, _, _ = get_backend() # automatically detects the underlying device type (CUDA, CPU, XPU, MPS, etc.)
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("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`,與 GPT-2 論文中報告的 `19.93` 大致相同。透過使用 `stride = 512` 並因此採用我們的跨步視窗策略,這個值降至 `16.44`。這不僅是一個更有利的分數,而且其計算方式也更接近於序列似然的真實自迴歸分解。
< > 在 GitHub 上更新