Transformers 文档

固定长度模型的困惑度 (Perplexity)

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

固定长度模型的困惑度

困惑度 (Perplexity, PPL) 是评估语言模型最常用的指标之一。在深入探讨之前,我们需要注意该指标专门适用于经典语言模型(有时称为自回归或因果语言模型),而对于像 BERT 这样的掩码语言模型则没有明确定义(参见 模型总结)。

困惑度被定义为一个序列的平均负对数似然的指数。如果我们有一个分词后的序列 $X = (x_0, x_1, \dots, x_t)$,那么XX其困惑度为:PPL(X)=exp{1titlogpθ(xix<i)} \text{PPL}(X) = \exp\left\{ -\frac{1}{t}\sum_i^t \log p_\theta (x_i|x_{<i}) \right\}

其中 $\log p\theta (x_i|x_{<i})$ 是根据我们的模型,在给定前序 token $x_{<i}$ 的条件下,第 $i$ 个 token 的对数似然。直观地说,它可以被视为衡量模型在语料库中指定 token 集合中进行均匀预测的能力。重要的是,这意味着分词过程会对模型的困惑度产生直接影响,在比较不同模型时应始终考虑到这一点。

这也等同于数据与模型预测之间交叉熵的指数。关于困惑度及其与每字符位数 (BPC) 和数据压缩关系的更多直观解释,请查看这篇 来自 The Gradient 的精彩博文

使用固定长度模型计算 PPL

如果我们不受模型上下文大小的限制,我们将通过自回归地分解一个序列,并在每一步都以整个前序子序列为条件来评估模型的困惑度,如下所示。

Full decomposition of a sequence with unlimited context length

然而,在处理近似模型时,我们通常会对模型能处理的 token 数量有一个限制。例如,最大版本的 GPT-2 的固定长度为 1024 个 token,因此当tt大于 1024 时,我们无法直接计算 $p\theta(x_t|x{<t})$。

相反,序列通常被分解为等于模型最大输入大小的子序列。如果模型的最大输入大小为 $k$,那么我们通过仅以xtx_t其之前的k1k-1个 token 为条件来近似一个 token 的似然,而不是使用整个上下文。在评估序列的模型困惑度时,一种诱人但非最优的方法是将序列分解为不相交的块,并独立地将每个分段的分解对数似然相加。

Suboptimal PPL not taking advantage of full available context

这种方法的计算速度很快,因为每个分段的困惑度可以通过一次前向传播完成,但它对完全分解困惑度的近似效果较差,且通常会产生较高(较差)的 PPL,因为模型在大多数预测步骤中拥有的上下文较少。

相反,固定长度模型的 PPL 应该使用滑动窗口策略进行评估。这涉及重复地滑动上下文窗口,以便模型在进行每次预测时拥有更多的上下文。

Sliding window PPL taking advantage of all available context

这更接近于序列概率的真实分解,且通常会产生更理想的分数。缺点是它需要为语料库中的每个 token 进行一次单独的前向传播。一个良好的实际折中方案是采用步进滑动窗口 (strided sliding window),即以较大的步长移动上下文,而不是一次滑动 1 个 token。这使得计算速度大幅提升,同时仍能为模型在每一步预测提供较大的上下文。

示例:使用 🤗 Transformers 中的 GPT-2 计算困惑度

让我们用 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("wikitext", "wikitext-2-raw-v1", split="test")
encodings = tokenizer("\n\n".join(test["text"]), return_tensors="pt")

使用 🤗 Transformers,我们可以简单地将 input_ids 作为 labels 传递给模型,每个 token 的平均负对数似然(negative log-likelihood)将作为损失(loss)返回。然而,在使用滑动窗口方法时,我们在每次迭代传递给模型的 token 之间存在重叠。我们不希望将那些仅被视作上下文的 token 的对数似然包含在损失中,因此我们可以将这些目标设置为 -100,以便它们被忽略。以下是一个使用步长(stride)为 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)

如果将步长设置为等于最大输入长度,则等同于我们上述讨论的次优且非滑动窗口的策略。步长越小,模型在进行每次预测时拥有的上下文就越多,通常报告的困惑度(perplexity)也会越好。

当我们运行上述代码且 stride = 1024(即没有重叠)时,得到的 PPL 为 19.44,这与 GPT-2 论文中报告的 19.93 差不多。通过使用 stride = 512 并采用我们的滑动窗口策略,该数值下降到了 16.44。这不仅是一个更理想的分数,而且其计算方式更接近序列似然的真实自回归分解。

在 GitHub 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.