はじめに
ChatGPTをはじめとする大規模言語モデル(LLM)は、「次に来る単語を予測する」 ことで文章を生成しています。
この「予測」は単なる決定論ではなく、確率分布に基づいたサンプリングによって行われます。
「なぜ同じ質問をしても毎回違う答えが返ってくるのか?」
そのカギは Softmax と サンプリング戦略 にあります。
今回は、LLMが次の単語を選ぶ仕組みを 数式・図解・コード で初心者にもわかりやすくシンプルにまとめてみました。
最後に実用的なサンプルコードを置いていますが、長いので折りたたみしているので展開してください。
1. 単語生成の基本:確率分布
例えば、次のような文章を考えます。
私は昨日 ○○ に行った。
「○○」の部分に入る候補として、モデルは内部的に以下のようなスコア(ロジット)を出力するかもしれません。
- 学校 → 2.0
- 会社 → 1.0
- 公園 → 0.5
このスコアをそのまま確率として使うのではなく、Softmax関数 によって確率分布に変換します。
2. Softmaxの役割
Softmaxは、複数のスコアを「確率」として正規化する関数です。
$$
P(w_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}
$$
- $z_i$ … モデルが出力したスコア(logit)
- $P(w_i)$ … 単語 $w_i$ が選ばれる確率
これにより、全ての確率の合計は1になります。
3. サンプリング方法の違い
確率分布が得られた後、実際に「どの単語を選ぶか」を決める方法はいくつかあります。
3.1 Greedy(最大値を選ぶ)
- 常に最も確率の高い単語を選ぶ
- 出力は安定するが、文章が単調でつまらなくなりやすい
3.2 Top-k サンプリング
- 上位k個の単語候補だけを残し、その中から確率的に選択
- 例:k=2なら「学校」「会社」のみを考慮
3.3 Top-p サンプリング(Nucleus)
- 累積確率がpを超えるまで候補を取り、その中から選択
- 動的に候補数が変わるため柔軟
4. Temperatureで「創造性」を調整
Softmaxにはtemperatureというパラメータがあり、確率分布の「鋭さ」を調整できます。
- 低い値(例:0.5) → 確率の高い候補に集中(堅実だが単調)
- 高い値(例:2.0) → 確率が均等化し、珍しい単語も選ばれやすい(創造的だが暴走しやすい)
5. コードで確率の世界を体験
PythonでSoftmaxとサンプリングを実装してみます。
import numpy as np
def softmax(logits, temperature=1.0):
exp = np.exp(np.array(logits) / temperature)
return exp / np.sum(exp)
words = ["学校", "会社", "公園"]
logits = [2.0, 1.0, 0.5] # モデルの出力スコア
# 温度を変えて確率分布を観察
for t in [0.5, 1.0, 2.0]:
probs = softmax(logits, temperature=t)
print(f"Temperature={t}: {dict(zip(words, np.round(probs, 3)))}")
# サンプリング
def sample_word(words, logits, temperature=1.0):
probs = softmax(logits, temperature)
return np.random.choice(words, p=probs)
print("\nサンプリング結果:")
for _ in range(5):
print(sample_word(words, logits, temperature=1.0))
実行例(結果は毎回変わります)
Temperature=0.5: {'学校': 0.79, '会社': 0.16, '公園': 0.05}
Temperature=1.0: {'学校': 0.62, '会社': 0.23, '公園': 0.15}
Temperature=2.0: {'学校': 0.45, '会社': 0.27, '公園': 0.28}
サンプリング結果:
学校
学校
会社
学校
公園
より詳細で実用的な実装(コード)はこちら
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple
def softmax(logits: List[float], temperature: float = 1.0) -> np.ndarray:
"""
Softmax関数でロジットを確率分布に変換
Args:
logits: モデルの出力スコア(ロジット)
temperature: 温度パラメータ(低い=集中、高い=分散)
Returns:
確率分布(合計が1になる)
"""
# temperatureで割って指数関数を適用
exp_logits = np.exp(np.array(logits) / temperature)
# 正規化して確率分布にする
probabilities = exp_logits / np.sum(exp_logits)
return probabilities
def greedy_sampling(words: List[str], logits: List[float]) -> str:
"""
Greedy サンプリング:最も確率の高い単語を選択
"""
probs = softmax(logits)
return words[np.argmax(probs)]
def top_k_sampling(words: List[str], logits: List[float], k: int = 5, temperature: float = 1.0) -> str:
"""
Top-k サンプリング:上位k個の候補から確率的に選択
"""
probs = softmax(logits, temperature)
# 上位k個のインデックスを取得
top_k_indices = np.argsort(probs)[-k:]
top_k_probs = probs[top_k_indices]
# 上位k個の確率を再正規化
top_k_probs = top_k_probs / np.sum(top_k_probs)
# サンプリング
chosen_idx = np.random.choice(top_k_indices, p=top_k_probs)
return words[chosen_idx]
def top_p_sampling(words: List[str], logits: List[float], p: float = 0.9, temperature: float = 1.0) -> str:
"""
Top-p(Nucleus)サンプリング:累積確率がpになるまでの候補から選択
"""
probs = softmax(logits, temperature)
# 確率の降順にソート
sorted_indices = np.argsort(probs)[::-1]
sorted_probs = probs[sorted_indices]
# 累積確率を計算
cumulative_probs = np.cumsum(sorted_probs)
# 累積確率がpを超える最初のインデックスを見つける
cutoff_idx = np.where(cumulative_probs >= p)[0][0] + 1
# 選択された候補の確率を再正規化
selected_indices = sorted_indices[:cutoff_idx]
selected_probs = probs[selected_indices]
selected_probs = selected_probs / np.sum(selected_probs)
# サンプリング
chosen_idx = np.random.choice(selected_indices, p=selected_probs)
return words[chosen_idx]
def demonstrate_sampling():
"""
サンプリング手法のデモンストレーション
"""
# 単語候補とそのロジット(スコア)
words = ["学校", "会社", "公園", "病院", "図書館"]
logits = [2.0, 1.0, 0.5, -0.5, -1.0]
print("=== LLM単語サンプリングのデモ ===\n")
# 1. 確率分布の表示
print("1. 確率分布の計算")
for temp in [0.5, 1.0, 2.0]:
probs = softmax(logits, temperature=temp)
print(f"Temperature={temp}:")
for word, prob in zip(words, probs):
print(f" {word}: {prob:.3f}")
print()
# 2. 各サンプリング手法の比較
print("2. サンプリング手法の比較(10回実行)")
print("-" * 60)
methods = [
("Greedy", lambda: greedy_sampling(words, logits)),
("Top-k (k=3)", lambda: top_k_sampling(words, logits, k=3)),
("Top-p (p=0.8)", lambda: top_p_sampling(words, logits, p=0.8))
]
for method_name, method_func in methods:
print(f"{method_name:15s}: ", end="")
results = [method_func() for _ in range(10)]
print(" | ".join(results))
print("\n" + "=" * 60)
def visualize_temperature_effect():
"""
Temperature効果の可視化
"""
words = ["学校", "会社", "公園"]
logits = [2.0, 1.0, 0.5]
temperatures = [0.1, 0.5, 1.0, 2.0, 5.0]
plt.figure(figsize=(12, 8))
for i, temp in enumerate(temperatures):
probs = softmax(logits, temperature=temp)
plt.subplot(2, 3, i+1)
plt.bar(words, probs, color=['#3498db', '#e74c3c', '#2ecc71'])
plt.title(f'Temperature = {temp}')
plt.ylabel('確率')
plt.ylim(0, 1)
# 確率値を表示
for j, prob in enumerate(probs):
plt.text(j, prob + 0.01, f'{prob:.3f}',
ha='center', va='bottom')
plt.tight_layout()
plt.suptitle('Temperature による確率分布の変化', y=1.02, fontsize=16)
plt.show()
def sample_text_generation():
"""
実際の文章生成をシミュレーション
"""
# 文脈ごとの単語候補とロジット
context_predictions = [
{
"context": "私は昨日",
"words": ["学校", "会社", "公園", "病院"],
"logits": [2.0, 1.5, 0.5, -0.5]
},
{
"context": "私は昨日 学校",
"words": ["に", "で", "から", "まで"],
"logits": [3.0, 1.0, 0.5, -1.0]
},
{
"context": "私は昨日 学校 に",
"words": ["行った", "いた", "向かった", "戻った"],
"logits": [2.5, 1.0, 0.8, 0.2]
}
]
print("=== 文章生成シミュレーション ===")
print("文脈に応じて次の単語を予測し、文章を生成します\n")
for step in context_predictions:
print(f"文脈: '{step['context']}'")
print("候補単語の確率:")
probs = softmax(step["logits"])
for word, prob in zip(step["words"], probs):
print(f" {word}: {prob:.3f}")
# Top-p サンプリングで選択
chosen = top_p_sampling(step["words"], step["logits"], p=0.8)
print(f"選択された単語: '{chosen}'\n")
if __name__ == "__main__":
# NumPyのランダムシードを設定(結果の再現性のため)
np.random.seed(42)
# デモンストレーション実行
demonstrate_sampling()
# 文章生成シミュレーション
sample_text_generation()
# 可視化(コメントアウト解除で実行)
# visualize_temperature_effect()
6. まとめ
- LLMは確率分布に基づいて単語を選んでいる
- Softmaxでスコアを確率に変換し、サンプリング方法(Greedy, Top-k, Top-p)で決定
- Temperatureで創造性を調整できる
- 多様で人間らしい文章は、この確率的仕組みによって生まれる
参考リンク
- The Illustrated Transformer (jalammar.net)
- Top-k & Top-p Sampling in NLP (huggingface blog)
- Softmax関数 — Wikipedia
この仕組みを理解していると、ChatGPTの挙動をコントロールしたり、生成AIを使ったアプリ開発で「どの設定を選ぶべきか」を判断しやすくなります。