25
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

確率の世界 — LLMが次の単語を選ぶ仕組み

Posted at

はじめに

ChatGPTをはじめとする大規模言語モデル(LLM)は、「次に来る単語を予測する」 ことで文章を生成しています。

この「予測」は単なる決定論ではなく、確率分布に基づいたサンプリングによって行われます。

「なぜ同じ質問をしても毎回違う答えが返ってくるのか?」

そのカギは Softmaxサンプリング戦略 にあります。

今回は、LLMが次の単語を選ぶ仕組みを 数式・図解・コード で初心者にもわかりやすくシンプルにまとめてみました。

最後に実用的なサンプルコードを置いていますが、長いので折りたたみしているので展開してください。

1. 単語生成の基本:確率分布

例えば、次のような文章を考えます。

私は昨日 ○○ に行った。

「○○」の部分に入る候補として、モデルは内部的に以下のようなスコア(ロジット)を出力するかもしれません。

  • 学校 → 2.0
  • 会社 → 1.0
  • 公園 → 0.5

このスコアをそのまま確率として使うのではなく、Softmax関数 によって確率分布に変換します。

2. Softmaxの役割

a001 (1).png

Softmaxは、複数のスコアを「確率」として正規化する関数です。

$$
P(w_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}
$$

  • $z_i$ … モデルが出力したスコア(logit)
  • $P(w_i)$ … 単語 $w_i$ が選ばれる確率

これにより、全ての確率の合計は1になります。

3. サンプリング方法の違い

a002 (1).png

確率分布が得られた後、実際に「どの単語を選ぶか」を決める方法はいくつかあります。

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とサンプリングを実装してみます。

python
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}

サンプリング結果:
学校
学校
会社
学校
公園
より詳細で実用的な実装(コード)はこちら
python
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で創造性を調整できる
  • 多様で人間らしい文章は、この確率的仕組みによって生まれる

参考リンク

この仕組みを理解していると、ChatGPTの挙動をコントロールしたり、生成AIを使ったアプリ開発で「どの設定を選ぶべきか」を判断しやすくなります。

25
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?