言語モデル(Language Model)は、テキストの確率分布をモデル化する機械学習モデルです。GPT、BERT、LLaMAなど、現代の大規模言語モデル(LLM)の基盤となる概念です。
言語モデルの性能評価には、パープレキシティ(Perplexity)という指標が広く用いられています。本記事では、言語モデルの基礎からパープレキシティの数学的定義、そしてPythonでの計算方法まで解説します。
本記事の内容
- 言語モデルの定義と役割
- N-gramモデルからニューラル言語モデルへの発展
- パープレキシティの数学的定義
- パープレキシティの直感的理解
- 交差エントロピーとの関係
- Pythonでの計算と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
言語モデルとは
定義
言語モデルは、トークン列(単語列)の確率分布 $P(w_1, w_2, \ldots, w_n)$ を推定するモデルです。
連鎖律(Chain Rule)により、この同時確率は条件付き確率の積に分解できます。
$$ P(w_1, w_2, \ldots, w_n) = \prod_{i=1}^{n} P(w_i \mid w_1, w_2, \ldots, w_{i-1}) $$
つまり、言語モデルは「これまでの単語列が与えられたとき、次の単語は何か」を予測するモデルと見なせます。
言語モデルの用途
1. 自然さの評価
文の確率を計算することで、その文が「自然か」を評価できます。
- $P(\text{“今日は天気が良い”}) > P(\text{“今日は天気が良いる”})$
2. テキスト生成
条件付き確率 $P(w_t \mid w_1, \ldots, w_{t-1})$ からサンプリングすることで、文章を生成できます。
3. 特徴抽出
言語モデルの中間表現を、下流タスク(分類、翻訳など)の特徴量として使用できます。
N-gram言語モデル
概要
N-gram言語モデルは、直前の $N-1$ 個の単語のみを条件として次の単語の確率を推定します(マルコフ仮定)。
$$ P(w_i \mid w_1, \ldots, w_{i-1}) \approx P(w_i \mid w_{i-N+1}, \ldots, w_{i-1}) $$
例: 2-gram(Bigram)モデル
$$ P(w_i \mid w_1, \ldots, w_{i-1}) \approx P(w_i \mid w_{i-1}) $$
最尤推定
N-gram確率は、コーパスの頻度から推定されます。
$$ P(w_i \mid w_{i-N+1}, \ldots, w_{i-1}) = \frac{C(w_{i-N+1}, \ldots, w_i)}{C(w_{i-N+1}, \ldots, w_{i-1})} $$
ここで $C(\cdot)$ は出現回数(count)です。
例: “I love cats” のBigram確率
$$ P(\text{cats} \mid \text{love}) = \frac{C(\text{love cats})}{C(\text{love})} $$
限界
- データスパースネス: 長いN-gramは出現頻度が低く、確率推定が不安定
- 文脈の制限: 固定長の文脈しか考慮できない
- 語彙外問題: 未知の単語や単語列に確率0が割り当てられる
ニューラル言語モデル
概要
ニューラル言語モデルは、ニューラルネットワークを用いて条件付き確率を推定します。
$$ P(w_t \mid w_1, \ldots, w_{t-1}) = f_\theta(\bm{h}_{t-1}) $$
ここで $\bm{h}_{t-1}$ は文脈の隠れ状態、$f_\theta$ は出力層(通常はsoftmax)です。
RNN言語モデル
RNN(LSTM, GRU含む)を用いた言語モデルでは、隠れ状態が逐次的に更新されます。
$$ \bm{h}_t = \text{RNN}(\bm{h}_{t-1}, \bm{e}_{w_t}) $$
$$ P(w_{t+1} \mid w_1, \ldots, w_t) = \text{softmax}(\bm{W}_o \bm{h}_t + \bm{b}_o) $$
Transformer言語モデル
Transformer(GPTなど)では、Self-Attentionにより全ての過去のトークンを同時に参照します。
$$ P(w_t \mid w_1, \ldots, w_{t-1}) = \text{softmax}\left(\bm{W}_o \cdot \text{TransformerDecoder}(\bm{e}_{w_1}, \ldots, \bm{e}_{w_{t-1}})\right) $$
因果マスクにより、未来のトークンへの注意は遮断されます。
パープレキシティ
定義
パープレキシティ(Perplexity, PPL)は、言語モデルの性能を測る指標です。テストコーパス $W = w_1, w_2, \ldots, w_N$ に対して、パープレキシティは以下で定義されます。
$$ \text{PPL}(W) = P(w_1, w_2, \ldots, w_N)^{-\frac{1}{N}} $$
連鎖律を用いて展開すると:
$$ \text{PPL}(W) = \left( \prod_{i=1}^{N} P(w_i \mid w_1, \ldots, w_{i-1}) \right)^{-\frac{1}{N}} $$
対数を取って計算すると:
$$ \log \text{PPL}(W) = -\frac{1}{N} \sum_{i=1}^{N} \log P(w_i \mid w_1, \ldots, w_{i-1}) $$
$$ \text{PPL}(W) = \exp\left( -\frac{1}{N} \sum_{i=1}^{N} \log P(w_i \mid w_1, \ldots, w_{i-1}) \right) $$
交差エントロピーとの関係
テストコーパスに対する平均交差エントロピー $H$ は:
$$ H(W) = -\frac{1}{N} \sum_{i=1}^{N} \log_2 P(w_i \mid w_1, \ldots, w_{i-1}) $$
パープレキシティは交差エントロピーの指数関数です:
$$ \text{PPL}(W) = 2^{H(W)} $$
または自然対数を用いた場合:
$$ \text{PPL}(W) = e^{H_e(W)} $$
ここで $H_e$ は自然対数ベースのエントロピーです。
パープレキシティの導出
パープレキシティがなぜこの形になるのか、情報理論の観点から導出しましょう。
真の言語分布を $p$、モデルの分布を $q$ とします。交差エントロピーは:
$$ H(p, q) = -\sum_x p(x) \log q(x) $$
テストコーパスのトークン列 $W = w_1, \ldots, w_N$ について、真の分布は経験分布(各トークンに $1/N$ の重み)と見なせます。このとき:
$$ H(p, q) \approx -\frac{1}{N} \sum_{i=1}^{N} \log q(w_i \mid w_1, \ldots, w_{i-1}) $$
この交差エントロピーを「1トークンあたり何ビットの情報を符号化するのに必要か」と解釈すると、$2^H$ は「モデルが平均的に迷っている選択肢の数」を表します。
直感的理解
パープレキシティは、モデルが次の単語を予測する際に「平均的に何個の選択肢で迷っているか」を表す指標です。
例:
- PPL = 1: 次の単語を完璧に予測できる(確率1で正解)
- PPL = 10: 平均して10個の選択肢で迷っている
- PPL = 100: 平均して100個の選択肢で迷っている
数値例:
語彙サイズが10,000のモデルで: – PPL = 100 は、語彙のうち約1%に絞り込めていることを意味する – PPL = 10,000 は、ランダムに予測しているのと同等
低いパープレキシティ = 良いモデル
パープレキシティが低いほど、モデルはテストコーパスをより正確に予測できています。
均一分布の場合
語彙サイズ $|V|$ の均一分布では、各トークンの確率は $1/|V|$ です。
$$ \text{PPL} = \exp\left( -\frac{1}{N} \sum_{i=1}^{N} \log \frac{1}{|V|} \right) = \exp(\log |V|) = |V| $$
つまり、ランダムに予測するモデルのパープレキシティは語彙サイズに等しくなります。
Pythonでの実装
簡単な言語モデルとパープレキシティ計算
import numpy as np
from collections import defaultdict
class BigramLanguageModel:
"""Bigram言語モデル"""
def __init__(self, smoothing=1e-5):
self.bigram_counts = defaultdict(lambda: defaultdict(int))
self.unigram_counts = defaultdict(int)
self.vocab = set()
self.smoothing = smoothing
def train(self, corpus):
"""
コーパスから学習
Args:
corpus: list of list of str (文のリスト、各文は単語リスト)
"""
for sentence in corpus:
# 文頭・文末記号を追加
tokens = ['<s>'] + sentence + ['</s>']
for i in range(len(tokens)):
self.vocab.add(tokens[i])
self.unigram_counts[tokens[i]] += 1
if i > 0:
self.bigram_counts[tokens[i-1]][tokens[i]] += 1
def get_probability(self, word, context):
"""
P(word | context) を計算(加算スムージング付き)
"""
numerator = self.bigram_counts[context][word] + self.smoothing
denominator = self.unigram_counts[context] + self.smoothing * len(self.vocab)
return numerator / denominator
def sentence_log_probability(self, sentence):
"""
文の対数確率を計算
"""
tokens = ['<s>'] + sentence + ['</s>']
log_prob = 0.0
for i in range(1, len(tokens)):
prob = self.get_probability(tokens[i], tokens[i-1])
log_prob += np.log(prob)
return log_prob
def perplexity(self, test_corpus):
"""
テストコーパスのパープレキシティを計算
"""
total_log_prob = 0.0
total_tokens = 0
for sentence in test_corpus:
tokens = ['<s>'] + sentence + ['</s>']
total_tokens += len(tokens) - 1 # <s>を除く
for i in range(1, len(tokens)):
prob = self.get_probability(tokens[i], tokens[i-1])
total_log_prob += np.log(prob)
avg_log_prob = total_log_prob / total_tokens
ppl = np.exp(-avg_log_prob)
return ppl
# 使用例
train_corpus = [
['I', 'love', 'cats'],
['I', 'love', 'dogs'],
['cats', 'are', 'cute'],
['dogs', 'are', 'cute'],
['I', 'have', 'a', 'cat'],
['I', 'have', 'a', 'dog'],
]
test_corpus = [
['I', 'love', 'cats'],
['dogs', 'are', 'cute'],
]
model = BigramLanguageModel()
model.train(train_corpus)
# パープレキシティの計算
ppl = model.perplexity(test_corpus)
print(f"Test Perplexity: {ppl:.2f}")
# 個々の文の確率
for sentence in test_corpus:
log_prob = model.sentence_log_probability(sentence)
print(f"Log P('{' '.join(sentence)}') = {log_prob:.4f}")
ニューラル言語モデルのパープレキシティ計算
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleLSTMLanguageModel(nn.Module):
"""簡単なLSTM言語モデル"""
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=1):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_dim, vocab_size)
def forward(self, x, hidden=None):
"""
Args:
x: (batch_size, seq_len) - 入力トークンID
Returns:
logits: (batch_size, seq_len, vocab_size)
"""
emb = self.embedding(x) # (batch_size, seq_len, embed_dim)
out, hidden = self.lstm(emb, hidden) # (batch_size, seq_len, hidden_dim)
logits = self.fc(out) # (batch_size, seq_len, vocab_size)
return logits, hidden
def compute_perplexity(model, data_loader, device):
"""
言語モデルのパープレキシティを計算
Args:
model: 言語モデル
data_loader: (input_ids, target_ids) のバッチを生成するDataLoader
device: 計算デバイス
"""
model.eval()
total_loss = 0.0
total_tokens = 0
with torch.no_grad():
for input_ids, target_ids in data_loader:
input_ids = input_ids.to(device)
target_ids = target_ids.to(device)
logits, _ = model(input_ids)
# Cross-entropy loss (reduction='sum')
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)),
target_ids.view(-1),
reduction='sum',
ignore_index=0 # パディングを無視
)
total_loss += loss.item()
total_tokens += (target_ids != 0).sum().item()
avg_loss = total_loss / total_tokens
perplexity = np.exp(avg_loss)
return perplexity
# モデル作成例
vocab_size = 1000
embed_dim = 128
hidden_dim = 256
model = SimpleLSTMLanguageModel(vocab_size, embed_dim, hidden_dim)
print(f"Model parameters: {sum(p.numel() for p in model.parameters()):,}")
パープレキシティの可視化
import matplotlib.pyplot as plt
def visualize_perplexity_vs_entropy():
"""パープレキシティとエントロピーの関係を可視化"""
entropy = np.linspace(0, 10, 100) # ビット
perplexity = 2 ** entropy
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(entropy, perplexity, 'b-', linewidth=2)
ax.set_xlabel('Cross-Entropy (bits)', fontsize=12)
ax.set_ylabel('Perplexity', fontsize=12)
ax.set_title('Relationship between Cross-Entropy and Perplexity', fontsize=14)
ax.grid(True, alpha=0.3)
ax.set_yscale('log')
# 典型的な値をマーク
typical_values = [(2, '4'), (4, '16'), (6, '64'), (8, '256')]
for h, label in typical_values:
ppl = 2 ** h
ax.scatter([h], [ppl], color='red', s=50, zorder=5)
ax.annotate(f'PPL={label}', (h, ppl), textcoords="offset points",
xytext=(10, 10), fontsize=10)
plt.tight_layout()
plt.savefig('perplexity_entropy.png', dpi=150, bbox_inches='tight')
plt.show()
def visualize_prediction_confidence():
"""予測の確信度とパープレキシティの関係を可視化"""
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 3つのシナリオ
scenarios = [
{'name': 'High Confidence\n(PPL ~ 1.5)', 'probs': [0.9, 0.05, 0.03, 0.02]},
{'name': 'Medium Confidence\n(PPL ~ 5)', 'probs': [0.4, 0.3, 0.2, 0.1]},
{'name': 'Low Confidence\n(PPL ~ 10)', 'probs': [0.25, 0.25, 0.25, 0.25]},
]
for ax, scenario in zip(axes, scenarios):
probs = scenario['probs']
words = ['word_1', 'word_2', 'word_3', 'word_4']
colors = plt.cm.Blues(np.array(probs) / max(probs))
bars = ax.bar(words, probs, color=colors, edgecolor='black')
ax.set_ylim(0, 1)
ax.set_ylabel('Probability')
ax.set_title(scenario['name'])
# エントロピーを計算
entropy = -sum(p * np.log2(p) for p in probs if p > 0)
ppl = 2 ** entropy
ax.text(0.5, 0.9, f'H = {entropy:.2f} bits\nPPL = {ppl:.2f}',
transform=ax.transAxes, fontsize=10, ha='center',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
plt.tight_layout()
plt.savefig('prediction_confidence.png', dpi=150, bbox_inches='tight')
plt.show()
# 可視化の実行
visualize_perplexity_vs_entropy()
visualize_prediction_confidence()
パープレキシティの注意点
語彙サイズへの依存
異なる語彙サイズを持つモデル間でパープレキシティを直接比較することはできません。語彙が大きいほど、予測が困難になりパープレキシティは上がる傾向があります。
トークナイゼーションへの依存
サブワードトークナイゼーションを使用する場合、同じテキストでもトークン数が異なります。パープレキシティはトークンあたりの値なので、トークナイゼーション方式が異なるモデル間での比較には注意が必要です。
単語あたりのパープレキシティへの正規化が必要な場合があります:
$$ \text{PPL}_{\text{word}} = \text{PPL}_{\text{token}}^{n_{\text{tokens}} / n_{\text{words}}} $$
Bits Per Character (BPC)
文字レベルでの比較のために、BPC(Bits Per Character)が使われることもあります:
$$ \text{BPC} = \frac{1}{N_{\text{char}}} \sum_{i=1}^{N} -\log_2 P(w_i \mid w_1, \ldots, w_{i-1}) $$
代表的なモデルのパープレキシティ
| モデル | データセット | パープレキシティ |
|---|---|---|
| 5-gram KN | Penn Treebank | ~141 |
| LSTM | Penn Treebank | ~78 |
| GPT-1 | WikiText-103 | ~37 |
| GPT-2 (1.5B) | WikiText-103 | ~17 |
| GPT-3 (175B) | Penn Treebank | ~20 |
注: 数値は参考値であり、評価設定により異なります。
まとめ
本記事では、言語モデルとパープレキシティについて解説しました。
- 言語モデル: トークン列の確率分布をモデル化し、「次の単語は何か」を予測する
- N-gramモデル: 直前のN-1トークンのみを条件とする古典的手法
- ニューラル言語モデル: RNNやTransformerで長距離依存を捉える
- パープレキシティ: 「モデルが平均的に何個の選択肢で迷っているか」を表す評価指標
- 交差エントロピーとの関係: $\text{PPL} = 2^{H}$(Hは交差エントロピー)
パープレキシティは言語モデルの性能を測る標準的な指標ですが、語彙サイズやトークナイゼーションへの依存があるため、比較の際は条件を揃える必要があります。
次のステップとして、以下の記事も参考にしてください。