Transformers 文档
固定长度模型的困惑度
并获得增强的文档体验
开始使用
固定长度模型的困惑度
困惑度 (PPL) 是评估语言模型最常用的指标之一。在深入探讨之前,我们应该注意到,该指标专门适用于经典语言模型(有时称为自回归或因果语言模型),并且对于像 BERT 这样的掩码语言模型没有明确定义(参见模型概述)。
困惑度定义为序列的指数化平均负对数似然。如果我们有一个分词序列,那么是,
其中是第 i 个 token 以之前的 token 为条件的对数似然根据我们的模型。直观地说,它可以被认为是评估模型在语料库中指定 token 集中均匀预测能力的一种方式。重要的是,这意味着分词过程对模型的困惑度有直接影响,在比较不同模型时应始终考虑这一点。
这也等同于数据和模型预测之间交叉熵的指数化。要更直观地了解困惑度及其与每字符比特数 (BPC) 和数据压缩的关系,请查看 The Gradient 上的这篇精彩博客文章。
计算固定长度模型的 PPL
如果我们不受模型上下文大小的限制,我们将通过自回归分解序列并在每一步以前面的整个子序列为条件来评估模型的困惑度,如下所示。

但是,当使用近似模型时,我们通常对模型可以处理的 token 数量有限制。例如,最大版本的 GPT-2 的固定长度为 1024 个 token,因此我们无法计算直接计算当大于 1024 时的情况。
相反,序列通常被分成等于模型最大输入大小的子序列。如果模型的最大输入大小为,然后我们近似一个 token 的可能性方法是仅以它之前的 token 而不是整个上下文为条件。当评估模型序列的困惑度时,一种诱人但欠佳的方法是将序列分成不相交的块,并独立地将每个分段的分解对数似然相加。

这计算速度很快,因为每个分段的困惑度可以在一次前向传递中计算出来,但它不能很好地近似完全分解的困惑度,并且通常会产生更高的(更差的)PPL,因为在大多数预测步骤中模型将具有较少的上下文。
相反,固定长度模型的 PPL 应该使用滑动窗口策略进行评估。这涉及到重复滑动上下文窗口,以便模型在进行每次预测时具有更多的上下文。

这更接近于序列概率的真实分解,并且通常会产生更有利的分数。缺点是它需要对语料库中的每个 token 进行单独的前向传递。一个好的实用折衷方案是采用带步幅的滑动窗口,以较大的步幅移动上下文,而不是一次滑动 1 个 token。这使得计算可以更快地进行,同时仍然使模型在每个步骤都有较大的上下文来进行预测。
示例:在 🤗 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
传递给我们的模型,并且每个 token 的平均负对数似然将作为损失返回。但是,使用我们的滑动窗口方法,我们在每次迭代中传递给模型的 token 存在重叠。我们不希望将我们仅作为上下文处理的 token 的对数似然包含在我们的损失中,因此我们可以将这些目标设置为 -100
,以便忽略它们。以下是如何使用步幅 512
执行此操作的示例。这意味着当计算任何一个 token 的条件似然时,模型将至少有 512 个 token 作为上下文(前提是有 512 个前面的 token 可用于条件化)。
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
并因此采用我们的带步幅窗口策略,PPL 降至 16.44
。这不仅是一个更有利的分数,而且是以更接近序列似然的真实自回归分解的方式计算出来的。