Transformers 文档

固定长度模型的困惑度

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验

开始使用

固定长度模型的困惑度

困惑度 (PPL) 是评估语言模型最常见的指标之一。在深入研究之前,我们应该注意,该指标专门适用于经典语言模型(有时称为自回归或因果语言模型),对于 BERT 这样的掩码语言模型来说,该指标没有明确的定义(参见 模型概述)。

困惑度定义为序列的平均负对数似然值的指数。如果我们有一个标记化的序列X=(x0,x1,,xt)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\}

其中logpθ(xix<i)\log p_\theta (x_i|x_{<i})是第 i 个标记在前面标记x<ix_{<i}上的对数似然,根据我们的模型。直观地,可以认为是模型在语料库中指定标记集中进行均匀预测的能力的评估。重要的是,这意味着标记化过程对模型的困惑度有直接影响,在比较不同模型时应始终考虑这一点。

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

使用固定长度模型计算困惑度

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

Full decomposition of a sequence with unlimited context length

然而,在使用近似模型时,我们通常对模型可以处理的标记数量有限制。例如,GPT-2 的最大版本具有 1024 个标记的固定长度,因此我们无法计算pθ(xtx<t)p_\theta(x_t|x_{<t})tt大于 1024 时,直接计算。

相反,序列通常被分成与模型最大输入大小相等的子序列。如果模型的最大输入大小是kk,那么我们近似标记的似然xtx_t通过仅条件化k1k-1它之前的标记,而不是整个上下文。在评估模型对序列的困惑度时,一种诱人但次优的方法是将序列分解成不相交的块,并独立地将每个片段的分解对数似然加起来。

Suboptimal PPL not taking advantage of full available context

这计算起来很快,因为每个片段的困惑度可以在一次前向传递中计算出来,但它只是对完全分解的困惑度的糟糕近似,并且通常会产生更高的(更差的)PPL,因为模型在大多数预测步骤中将拥有更少的上下文。

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

Sliding window PPL taking advantage of all available context

这更接近于序列概率的真实分解,并且通常会产生更有利的得分。缺点是它需要对语料库中的每个标记进行单独的前向传递。一个好的实用折衷方案是采用步长滑动窗口,以更大的步长移动上下文,而不是一次滑动一个标记。这允许计算速度更快,同时仍然使模型在每个步骤中都具有较大的上下文来进行预测。

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

让我们用 GPT-2 演示这个过程。

from transformers import GPT2LMHeadModel, GPT2TokenizerFast

device = "cuda"
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)

nlls = []
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

    nlls.append(neg_log_likelihood)

    prev_end_loc = end_loc
    if end_loc == seq_len:
        break

ppl = torch.exp(torch.stack(nlls).mean())

使用与最大输入长度相等的步长运行此操作等同于我们上面讨论的次优的、非滑动窗口策略。步长越小,模型在进行每个预测时拥有的上下文越多,并且报告的困惑度通常会越好。

当我们用 stride = 1024 运行上面的代码时,即没有重叠,得到的 PPL 为 19.44,这与 GPT-2 论文中报告的 19.93 几乎相同。通过使用 stride = 512 并因此采用我们的步长窗口策略,这下降到 16.45。这不仅是一个更有利的得分,而且它是以更接近于序列似然的真实自回归分解的方式计算的。

< > GitHub 上的更新