Transformers documentation

固定长度模型的困惑度

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

固定长度模型的困惑度

困惑度(PPL)是评估语言模型最常用的指标之一。在深入探讨之前,我们应当指出,该指标专门适用于经典语言模型(有时也称为自回归或因果语言模型),而对于 BERT 这类掩码语言模型,其定义并不明确。

困惑度(Perplexity)定义为序列的平均负对数似然的指数。如果有一个分词后的序列 $X = (x_0, x_1, \dots, x_t)$,那么 XX 的困惑度为 PPL(X)=exp⁡{−1t∑itlog⁡pθ(xi∣x<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})$ 表示根据我们的模型,第 i 个 token 在前序 token $x_{<i}$ 条件下的对数似然。直观地说,它可以被理解为对模型在语料库中指定的 token 集合上进行均匀预测能力的评估。重要的是,这意味着分词过程会直接影响模型的困惑度,因此在比较不同模型时,这一点始终需要考虑在内。

这也等价于数据与模型预测之间交叉熵的指数运算。如需进一步了解困惑度及其与每字符比特数(BPC)和数据压缩之间的关系,请参阅这篇关于 The Gradient 的精彩博客文章。

使用固定长度模型计算 PPL

如果不受模型上下文大小的限制,我们会通过自回归方式对序列进行分解,并在每一步中以整个前缀子序列作为条件,来评估模型的困惑度,如下图所示。

Full decomposition of a sequence with unlimited context length

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

通常,序列会被拆分为长度等于模型最大输入大小的子序列。如果模型的最大输入大小为 $k$,那么我们仅基于其前面的 xtx_t 个 token 来近似 token k−1k-1 的对数似然,而不是基于整个上下文。在评估模型对某个序列的困惑度时,一种诱人但次优的做法是将序列拆分为互不重叠的块,并独立累加每个片段分解后的对数似然。

Suboptimal PPL not taking advantage of full available context

由于每个片段的困惑度只需一次前向传播即可计算,因此该方法计算速度很快,但它对完全分解困惑度的近似效果较差,并且通常会得出更高(更差)的 PPL,因为在大多数预测步骤中,模型可用的上下文较少。

相反,固定长度模型的 PPL 应采用滑动窗口策略进行评估。该策略通过反复滑动上下文窗口,使模型在进行每次预测时都能拥有更多的上下文信息。

Sliding window PPL taking advantage of all available context

这是序列概率真实分解的更精确近似,通常能带来更优的分数。其缺点在于,需要对语料库中的每个 token 执行一次独立的前向传播。一个良好的实用折中方案是采用带步长的滑动窗口,以较大的步长移动上下文,而非每次仅滑动 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("Salesforce/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 并采用我们的滑动窗口策略,该值降至 16.44。这不仅是一个更优的分数,而且其计算方式更接近序列似然的真实自回归分解。

Update on GitHub