目次を表示する

LLM完全解説:スクラッチから理解する大規模言語モデル

RLHF とアラインメント ── 「役に立つ AI」への調整

RLHF とアラインメント ── 「役に立つ AI」への調整

LLM との関係:事前学習だけの LLM は「次の単語を予測する」だけ。人間の指示に従い、有用で安全な応答をするように調整するのが RLHF。ChatGPT の「対話が自然に感じる」理由がここにある。


RLHFとアラインメント — SFT・Reward Model・PPO・DPO

この章で何ができるようになるか:事前学習済みモデルがなぜそのままでは「使いにくい」のかを説明でき、RLHF の3ステップ(SFT → Reward Model → PPO)を理解できる。


なぜ事前学習だけでは不十分か

プロンプト: 「フランスの首都は?」

事前学習のみの GPT の出力(ありがちな例):
  「フランスの首都は? イタリアの首都は? ドイツの首都は?」
  → 次の単語として「質問を続ける」のが確率的に自然だと学習した

  または:
  「フランスの首都はパリです。パリはセーヌ川のほとりにあり、
   エッフェル塔は1889年に...(延々と続く)」
  → 「止め方」を知らない

RLHF 後の出力:
  「パリです。」
  → 簡潔に質問に答え、適切なところで止まる

事前学習モデルは「テキストの続き」を生成するが、「ユーザーの質問に答える」ことは直接学習していない。


RLHF の3ステップ

Step 1: SFT (Supervised Fine-Tuning)
  → 人間が書いた「良い応答」の例で教師ありファインチューニング

Step 2: Reward Model の訓練
  → 「どちらの応答が良いか」の人間の判断を学習するモデルを作る

Step 3: PPO (Proximal Policy Optimization)
  → Reward Model のフィードバックで LLM を強化学習的に最適化

Step 1: SFT(Supervised Fine-Tuning)

# SFT 用の訓練データの形式
sft_data = [
    {
        "prompt": "フランスの首都は?",
        "response": "フランスの首都はパリです。"
    },
    {
        "prompt": "PythonでフィボナッチのXX番目を求める関数を書いて",
        "response": "```python\ndef fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)\n```"
    },
    # ... 数万〜数十万件
]

# 訓練は通常の CLM と同じ(プロンプト部分の損失は無視)
def compute_sft_loss(model, prompt_ids, response_ids):
    full_ids = torch.cat([prompt_ids, response_ids], dim=1)
    logits = model(full_ids)

    # プロンプト部分のラベルは -100(損失計算で無視)
    labels = full_ids.clone()
    labels[:, :prompt_ids.size(1)] = -100

    return nn.CrossEntropyLoss(ignore_index=-100)(
        logits[:, :-1].reshape(-1, logits.size(-1)),
        labels[:, 1:].reshape(-1)
    )

Step 2: Reward Model の訓練

人間が2つの応答を比較し、「どちらが良いか」を判定する。

# 比較データの形式
comparison_data = [
    {
        "prompt": "量子コンピュータとは?",
        "chosen": "量子コンピュータは量子力学の原理を利用した...",  # 人間が選んだ良い応答
        "rejected": "量子コンピュータは超すごいコンピュータで...", # 人間が選ばなかった応答
    },
]

class RewardModel(nn.Module):
    def __init__(self, base_model):
        super().__init__()
        self.base = base_model  # 事前学習済み LLM
        self.reward_head = nn.Linear(base_model.config.hidden_size, 1)

    def forward(self, input_ids):
        hidden = self.base(input_ids).last_hidden_state
        # 最後のトークンの隠れ状態からスカラー報酬を出力
        reward = self.reward_head(hidden[:, -1, :])
        return reward.squeeze(-1)

def reward_model_loss(model, chosen_ids, rejected_ids):
    """
    Bradley-Terry モデル: chosen のスコアが rejected より高くなるように訓練
    """
    r_chosen = model(chosen_ids)
    r_rejected = model(rejected_ids)
    # r_chosen > r_rejected となる確率を最大化
    return -torch.log(torch.sigmoid(r_chosen - r_rejected)).mean()

Step 3: PPO(Proximal Policy Optimization)

Reward Model のスコアを使って LLM を最適化する。

def rlhf_training_step(policy_model, ref_model, reward_model,
                        prompt_ids, optimizer):
    """PPO の1ステップ(簡略化)"""
    # 1. 現在のポリシー(LLM)で応答を生成
    with torch.no_grad():
        response_ids = policy_model.generate(prompt_ids, max_length=256)

    # 2. Reward Model でスコアを取得
    full_ids = torch.cat([prompt_ids, response_ids], dim=1)
    reward = reward_model(full_ids)

    # 3. KL ペナルティ(元のモデルから離れすぎないように)
    with torch.no_grad():
        ref_logprobs = compute_logprobs(ref_model, full_ids)
    policy_logprobs = compute_logprobs(policy_model, full_ids)
    kl_penalty = (policy_logprobs - ref_logprobs).sum(dim=1)

    # 4. 報酬 = Reward Model のスコア - β × KL ペナルティ
    beta = 0.1  # KL ペナルティの強さ
    total_reward = reward - beta * kl_penalty

    # 5. PPO の目的関数で更新
    loss = -total_reward.mean()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

KL ペナルティがなぜ必要か:Reward Model を「ハック」してしまうのを防ぐ。KL ペナルティなしだと、モデルは Reward Model のスコアだけを最大化しようとし、意味不明だが高スコアな出力を生成する(Reward Hacking)。


DPO(Direct Preference Optimization)

RLHF の Step 2, 3 を1つに統合した効率的な手法(2023年、Stanford)。

def dpo_loss(policy_model, ref_model, chosen_ids, rejected_ids, beta=0.1):
    """
    Reward Model を明示的に訓練せず、
    人間の選好データから直接 LLM を最適化する
    """
    # 各応答の対数確率を計算
    pi_chosen = compute_logprobs(policy_model, chosen_ids).sum(dim=1)
    pi_rejected = compute_logprobs(policy_model, rejected_ids).sum(dim=1)

    with torch.no_grad():
        ref_chosen = compute_logprobs(ref_model, chosen_ids).sum(dim=1)
        ref_rejected = compute_logprobs(ref_model, rejected_ids).sum(dim=1)

    # 暗黙的な報酬の差
    logits = beta * ((pi_chosen - ref_chosen) - (pi_rejected - ref_rejected))

    return -torch.nn.functional.logsigmoid(logits).mean()

DPO の利点:Reward Model の訓練が不要。PPO の複雑さ(クリッピング・GAE・バリューネットワーク)が不要。実装がシンプルで安定。


Constitutional AI(CAI)

Anthropic(Claude の開発元)が提唱。人間のフィードバックをAIのフィードバックで一部代替する。

CAI のプロセス:
  1. LLM に「この応答は有害か?」と自己評価させる
  2. 「有害でない版に書き直して」と自己修正させる
  3. 元の応答 vs 修正版 の比較データを生成
  4. このデータで RLHF を実行

利点:
  - 人間のアノテーターの負担を軽減
  - 一貫した評価基準(人間の評価者によるばらつきを減らせる)
  - スケーラブル(AI が AI を評価する)

まとめ

手法何をするかコスト
SFT良い応答例で教師あり学習中(データ作成が主)
RLHF (PPO)Reward Model + 強化学習高(3段階の訓練)
DPO選好データから直接最適化中(RM不要)
CAIAI による自己評価・自己修正低(人間の評価を削減)

次章では、訓練後のモデルを実際にユーザーにサーブするための推論の高速化──KV Cache、量子化、投機的デコーディングを見ていく。