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 上更新