Transformers documentation

固定長モデルのPerplexity

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

固定長モデルのPerplexity

Perplexity (PPL)は、言語モデルを評価するための最も一般的な指標の一つです。詳細に入る前に、この指標は古典的な言語モデル(自己回帰型または因果言語モデルと呼ばれることもあります)に特化して適用されるものであり、BERTのようなマスク言語モデルには明確に定義されていない点に注意が必要です。

Perplexity は、シーケンスの平均負対数尤度の指数として定義されます。トークン化されたシーケンス $X = (x_0, x_1, \dots, x_t)$ がある場合、その perplexity は 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})$は、モデルに従い、先行トークン $x_{<i}$ を条件とした第 i トークンの対数尤度です。直感的には、コーパス内の指定されたトークンの集合の中で均等に予測するモデルの能力の評価と考えることができます。重要なのは、トークン化の手順がモデルのパープレキシティに直接影響を与えるため、異なるモデルを比較する際には常にこれを考慮に入れる必要があるということです。

これは、データとモデルの予測との間の交差エントロピーの指数化と同等です。パープレキシティ、およびそれが1文字あたりのビット数(BPC)やデータ圧縮とどのように関係しているかについての直感的な理解を深めるには、The Gradientの素晴らしいブログ記事をご覧ください。

固定長モデルでのPPLの計算

モデルのコンテキストサイズによる制約がなければ、以下の図に示すように、シーケンスを自己回帰的に分解し、各ステップでそれ以前の部分シーケンス全体を条件として与えることで、モデルのパープレキシティを評価します。

Full decomposition of a sequence with unlimited context length

ただし、近似モデルを扱う場合、モデルが処理できるトークン数に制約があるのが一般的です。 GPT-2, の最大サイズは、例えば、1024トークンという固定長を持つため、\theta(x_t|x{<t})$ を直接計算することはできません。 tt が 1024 を超える場合です。

代わりに、シーケンスは通常、モデルの最大入力サイズに等しい部分シーケンスに分割されます。モデルの最大入力サイズが $k$ の場合、トークン xtx_t の尤度を、直前の k−1k-1 トークンのみに条件付け、全体のコンテキストではなくそれに基づいて近似します。モデルのシーケンスに対するパープレキシティを評価する際、誘惑的ではあるが最適ではないアプローチとして、シーケンスを互いに重複しないチャンクに分割し、各セグメントの分解された対数尤度を独立に合計する方法があります。

Suboptimal PPL not taking advantage of full available context

これは、各セグメントのパープレキシティが1回のフォワードパスで計算できるため高速に計算できますが、完全に因子分解されたパープレキシティの近似としては不十分であり、予測ステップの大部分でモデルが得られるコンテキストが少なくなるため、通常はより高い(悪い)PPLを返します。

代わりに、固定長モデルのPPLは、スライディングウィンドウ戦略を用いて評価すべきです。これにより、コンテキストウィンドウを繰り返しスライドさせ、各予測を行う際にモデルがより多くのコンテキストを利用できるようにします。

Sliding window PPL taking advantage of all available context

これは、シーケンス確率の真の分解に近づいた近似であり、通常により有利なスコアが得られます。その代わりとして、コーパス内の各トークンごとに個別のフォワードパスが必要です。実用的な妥協案としては、1トークンずつスライドさせるのではなく、より大きなストライドでコンテキストを移動させるストライド付きスライディングウィンドウを用いることが挙げられます。これにより、各ステップでモデルが予測を行う際に大きなコンテキストを維持したまま、計算を大幅に高速化できます。

例:🤗 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 データセットを読み込み、いくつかの異なるスライディングウィンドウ戦略を用いてパープレキシティを評価します。このデータセットは小さく、セット全体に対してフォワードパスを1回だけ実行するだけなので、メモリ内にデータセット全体を読み込んでエンコードできます。

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 として渡すだけで、各トークンの平均負対数尤度が損失として返されます。ただし、スライディングウィンドウ方式では、各イテレーションでモデルに渡すトークン間に重複が生じます。コンテキストとしてのみ扱うトークンの尤度を損失に含めたくないため、これらのターゲットを -100 に設定して無視させることができます。以下は、ストライドを 512 に設定した場合の例です。これにより、任意の 1 トークンの条件付き尤度を計算する際に、モデルは少なくとも 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まで低下します。これはより好ましいスコアであるだけでなく、シーケンス尤度の真の自己回帰分解に近い方法で計算されています。

Update on GitHub