学習者言語の分析(応用)1(第6回)¶

6.1 導入
6.2 背景
6.3 サプライザルの計算
6.4 計算してみよう

6.1 導入¶

  • 以下の2つの文を読んでみてください。
  1. The children went outside to play.
  2. The children went outside to iron.
  • 2の文の"to iron"(アイロンをかけるために)を読んだときに、1の文では感じなかった違和感を感じたり、「え?」と目を止め前の単語を読み直したりすることがあります。
  • 人は文(文章)読むとき、単語をひとつずつ理解しているのではなく、それまでの単語から次にくる単語を予測しながら読んでいると言えます。
  • 予想通りの単語が出現したときと予想外の単語が出現したときで文理解の処理にかかる時間が異なることがあります。
  • この「文脈における単語の意外さ(予想の外れ具合)」を数値化した指標をサプライザル(surprisal)と呼びます。

6.2 サプライザルに関する背景¶

6.2.1 情報理論¶

  • クロード・シャノンは「情報量」を次のように定義しました。
  • 例えば、以下の2つの文が表している出来事に関して、「通常起こり得る」度合いを評価し、その確率が大きい出来事(当然起きること)が起きた場合の情報量は小さく、その確率が小さい出来事(滅多に起きないこと)が起きた場合の情報量は大きいとします。
  1. 母親が息子に晩御飯を作ってあげた。
  2. 息子が母親に晩御飯を作ってあげた。
  • ある出来事$x$が起きる確率を$P(x)$とすると、その情報量($I$)は以下の式で定義される。 $$ I = -log_2P(x) $$
  • 上の式は以下のようにも書き換えられます。 $$ I = log_2(\frac{1}{P}) $$
  • この値は以下のように変化します。 $$ P = \frac{1}{2} \Rightarrow log_2(2) = 1$$ $$ P = \frac{1}{4} \Rightarrow log_2(4) = 2$$ $$ P = \frac{1}{8} \Rightarrow log_2(8) = 3$$
  • これは$P$の分母が2の何乗かを表しています。
  • 「これは『ビット』である」と説明されることがありますが、なんのことか分からないので、「起きた現象を確認するための手間の回数」ぐらいに考えてください。
    • コインの裏表が出る確率$P = \frac{1}{2}$で「出たのは表?」と1回きいて、その答えによって起きた現象が何か確認できます。
    • トランプのスートを当てる$P = \frac{1}{4}$にはまず「赤?」ときいて、その後に「ハート?」のようにきくと確定します。

6.2.2 Hale (2001)のA Probabilistic Earley Parser as a Psycholinguistic Model¶

  • Hale (2001)がシャノンが定義した情報量を人間の文理解に応用した。
  • 「文において次の単語$w$のサプライザルが高いほど、人間がその単語を処理する時間は長くなる」
  • 一般に「認知的負荷が高まる」と言われる。
  • 当該の単語を注視する時間が長くなる、当該の文を読み終えるまでの時間が長くなることを根拠に「認知的負荷が高まっている」とするのが一般的である。

6.3 サプライザルの計算¶

  • 文におけるある単語のサプライザル($S(w_t)$)は、与えられた文脈(単語列)の下で、次の単語が出現する条件付き確率をもとに計算する。

$$ S(w_t) = -log_2P(w_t|w_1,w_2,...w_{t-1}) $$

  • $P(w_t|w_1,w_2,...w_{t-1})$は、その文脈において、当該の単語が出現する確率なので、これは、膨大な文章を集めてその確率を近似的に求める。
  • この計算に、大規模言語モデル(Large Language Model: LLM)が利用できる。
  • 今のLLMは少し違いますが、GPT-2などはまさに「文脈から次に出現する確率が最も高い単語を出力する」ことができるように学習されているため、上述の条件付き確率を取り出す最適なツールである。

6.4 計算してみよう¶

  • 以下のセルではGPT-2を利用してサプライザルを計算する関数を定義しています。
In [1]:
import math
import re
import torch
from transformers import GPT2LMHeadModel, GPT2TokenizerFast

# モデルの準備
MODEL_NAME = "gpt2"
tokenizer = GPT2TokenizerFast.from_pretrained(MODEL_NAME)
model = GPT2LMHeadModel.from_pretrained(MODEL_NAME)
model.eval()

# 文を受け取り、単語ごとのサプライザル (単位: bit) を辞書形式で返す関数
def get_word_surprisals(sentence: str) -> dict:
    
    inputs = tokenizer(sentence, return_tensors="pt")
    input_ids = inputs["input_ids"]

    # モデルに文を渡し、各トークン位置での全語彙に対する予測スコア(logits)を取得
    with torch.no_grad():
        outputs = model(**inputs)
        logits = outputs.logits

    # 「t番目までの文脈」から「t+1番目の単語」を予測するため、1つずらす(Shift)
    shift_logits = logits[:, :-1, :].contiguous()
    shift_labels = input_ids[:, 1:].contiguous()

    # Log-Softmaxで対数確率 ln(P) を求め、実際に出現した単語の確率を抽出
    log_probs = torch.nn.functional.log_softmax(shift_logits, dim=-1)
    target_log_probs = log_probs.gather(2, shift_labels.unsqueeze(-1)).squeeze(-1)[0]

    # 自然対数 ln(P) を底2の対数 -log2(P) に変換して bits 単位のサプライザルにする
    token_surprisals = [-lp.item() / math.log(2) for lp in target_log_probs]

    # トークン(サブワード)を元の単語単位にまとめる
    encodings = tokenizer(sentence, return_offsets_mapping=True)
    offset_mapping = encodings["offset_mapping"]

    word_matches = list(re.finditer(r"\b[\w'-]+\b", sentence))
    result = {}

    for match in word_matches:
        w_start, w_end = match.span()
        word = match.group()
        total_bits = 0.0
        matched = False

        # offset_mapping[0] は文頭(先行文脈なし)のため、2番目のトークン以降を照合
        for i, (t_start, t_end) in enumerate(offset_mapping[1:], start=0):
            if max(w_start, t_start) < min(w_end, t_end):
                total_bits += token_surprisals[i]
                matched = True

        result[word] = round(total_bits, 2) if matched else None

    return result
  • sentenceに適当な文を入力して実行してみてください。
In [5]:
sentence = "There are two man at the door."
surprisals = get_word_surprisals(sentence)

print(f"{'Word':<10} | {'Surprisal (bits)':<18} | {'Graph'}")
print("-" * 45)
for word, val in surprisals.items():
    if val is not None:
        bar = "■" * int(val)
        print(f"{word:<10} | {val:<18.2f} | {bar}")
    else:
        print(f"{word:<10} | {'(Initial word)':<18} | ")
Word       | Surprisal (bits)   | Graph
---------------------------------------------
There      | (Initial word)     | 
are        | 4.97               | ■■■■
two        | 3.95               | ■■■
man        | 14.90              | ■■■■■■■■■■■■■■
at         | 8.72               | ■■■■■■■■
the        | 1.23               | ■
door       | 5.55               | ■■■■■
  • この例では文法的に誤りである"man"(正しくは"men")のサプライザルが大きくなっている。
  • 一方で以下のような時制の間違いではこのようなわかりやすい結果を得ることができない。
In [10]:
# 実験:文法的に不自然な箇所を持つ文で試す
sentence = "She goes there yesterday"
surprisals = get_word_surprisals(sentence)

# 結果を見やすく整形して出力
print(f"{'Word':<10} | {'Surprisal (bits)':<18} | {'Graph'}")
print("-" * 45)
for word, val in surprisals.items():
    if val is not None:
        bar = "■" * int(val)
        print(f"{word:<10} | {val:<18.2f} | {bar}")
    else:
        print(f"{word:<10} | {'(Initial word)':<18} | ")
Word       | Surprisal (bits)   | Graph
---------------------------------------------
She        | (Initial word)     | 
goes       | 10.68              | ■■■■■■■■■■
there      | 8.60               | ■■■■■■■■
yesterday  | 12.81              | ■■■■■■■■■■■■