ChatGPTが「有害な回答を避ける」「ユーザーの意図に沿った回答を生成する」ことができるのは、事前学習の後にアライメント(alignment)と呼ばれる工程を経ているからです。最も広く知られたアライメント手法はRLHF(Reinforcement Learning from Human Feedback)ですが、RLHFには報酬モデルの学習、PPO(Proximal Policy Optimization)による強化学習、参照モデルの保持など、複雑で不安定な要素が多く存在します。
2023年にRafailov et al.が提案したDPO(Direct Preference Optimization)は、この複雑さを劇的に解消しました。RLHFの目的関数を数学的に変形することで、報酬モデルもPPOも不要にし、人間の選好データから直接LLMを最適化するシンプルな手法です。DPOの損失関数は分類問題(binary cross-entropy)と同じ形をしており、通常のファインチューニングと同じ手順で実装できます。
DPOを理解することは、以下のような場面で直接役立ちます。
- LLMの選好調整: 自社データに合わせてLLMの回答スタイルや安全性を調整する際の標準手法です
- RLHFの理論的理解: DPOの導出を追うことで、RLHF全体の数理的構造が明確になります
- 最新手法の基盤: IPO、KTO、ORPOなどのDPO派生手法を理解するための前提知識になります
本記事の内容
- RLHFの概要と問題点
- Bradley-Terryモデルによる選好のモデル化
- RLHFの目的関数からDPO損失関数への導出(省略なし)
- DPO損失関数の直感的理解
- 勾配の分析: DPOが何を最適化しているか
- Pythonでの実装とシミュレーション
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
RLHFの概要と問題点
RLHFの3ステップ
RLHF(Reinforcement Learning from Human Feedback)は、以下の3ステップで構成されます。
ステップ1: SFT(Supervised Fine-Tuning): 人手で作成した高品質な(プロンプト, 回答)ペアで教師あり学習を行い、基本的な指示追従能力を獲得します。このモデルを $\pi_{\text{SFT}}$ とします。
ステップ2: 報酬モデルの学習: 同じプロンプトに対して複数の回答を生成し、人間が「どちらが良いか」を判定した選好データ $(x, y_w, y_l)$($x$ はプロンプト、$y_w$ は選ばれた回答、$y_l$ は棄却された回答)を収集します。このデータから報酬モデル $r_\phi(x, y)$ を学習します。
ステップ3: PPOによる最適化: 報酬モデルのスコアを報酬として、PPOアルゴリズムでLLMのパラメータを更新します。KLダイバージェンスによる正則化で、SFTモデルから大きく逸脱しないように制約します。
RLHFの問題点
RLHFは効果的ですが、以下の実用上の問題があります。
1. 報酬モデルの学習: 選好データからスカラー報酬を正確に学習すること自体が難しい。報酬ハッキング(reward hacking)が発生しやすい
2. PPOの不安定性: PPOは多くのハイパーパラメータ(クリッピング比率、GAEのλ、価値関数の係数など)を持ち、チューニングが困難。学習が不安定になりやすい
3. 計算コスト: 学習中に4つのモデル(現在のポリシー、参照ポリシー、報酬モデル、価値関数)をGPUメモリに載せる必要がある
4. 実装の複雑さ: 強化学習のインフラストラクチャ(ロールアウト、アドバンテージ推定など)が必要
DPOはステップ2と3を1つのシンプルなステップに置き換え、これらの問題を解決します。では、その数学的な導出を見ていきましょう。
選好のモデル化: Bradley-Terryモデル
選好データの形式
選好データは三つ組 $(x, y_w, y_l)$ の集合です。$x$ はプロンプト、$y_w$ は人間が選んだ回答(winner)、$y_l$ は棄却された回答(loser)です。
この選好を確率的にモデル化するために、Bradley-Terryモデルを使います。人間が回答 $y_1$ を $y_2$ より好む確率は:
$$ P(y_1 \succ y_2 \mid x) = \sigma(r^*(x, y_1) – r^*(x, y_2)) $$
ここで $\sigma(z) = 1/(1+e^{-z})$ はシグモイド関数、$r^*(x, y)$ は人間の選好を反映した潜在的な報酬関数です。
この式の直感は明快です。報酬の差 $r^*(x, y_1) – r^*(x, y_2)$ が大きいほど、$y_1$ が選ばれる確率がシグモイド関数を通じて1に近づきます。差が0なら50:50です。
報酬モデルの学習(RLHF)
RLHFでは、この報酬関数をパラメトリックモデル $r_\phi(x, y)$ で近似し、選好データの尤度を最大化します:
$$ \mathcal{L}_R(\phi) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}}\left[\log \sigma(r_\phi(x, y_w) – r_\phi(x, y_l))\right] $$
DPOの鍵となる洞察は、この報酬モデルを明示的に学習する必要がないということです。
RLHFの目的関数
KL制約付き報酬最大化
RLHFのステップ3の目的関数は、報酬を最大化しつつ、参照モデル $\pi_{\text{ref}}$(通常はSFTモデル)からのKLダイバージェンスを制約する次の式です:
$$ \max_{\pi_\theta} \mathbb{E}_{x \sim \mathcal{D}, y \sim \pi_\theta(\cdot|x)}\left[r(x, y)\right] – \beta \, D_{\text{KL}}\left[\pi_\theta(\cdot|x) \| \pi_{\text{ref}}(\cdot|x)\right] $$
$\beta > 0$ は正則化の強さを制御するハイパーパラメータです。$\beta$ が大きいほど、最適化されたモデルは参照モデルに近い振る舞いを保ちます。
KLダイバージェンスを展開すると:
$$ D_{\text{KL}}[\pi_\theta \| \pi_{\text{ref}}] = \mathbb{E}_{y \sim \pi_\theta}\left[\log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}\right] $$
目的関数全体を書き下すと:
$$ \max_{\pi_\theta} \mathbb{E}_{x, y \sim \pi_\theta}\left[r(x, y) – \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}\right] $$
最適方策の閉じた形
この最適化問題には閉じた形の解が存在します。これがDPOの数学的な基盤です。
目的関数の $y$ に関する部分を書き出します。各 $x$ について:
$$ \max_{\pi_\theta(\cdot|x)} \sum_{y} \pi_\theta(y|x) \left[r(x, y) – \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}\right] $$
$\sum_y \pi_\theta(y|x) = 1$ の制約の下でラグランジュ乗数法を適用します。ラグランジアンは:
$$ \mathcal{L} = \sum_{y} \pi_\theta(y|x) \left[r(x, y) – \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}\right] + \lambda\left(1 – \sum_y \pi_\theta(y|x)\right) $$
$\pi_\theta(y|x)$ で微分して0とおきます:
$$ r(x, y) – \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)} – \beta – \lambda = 0 $$
$\pi_\theta(y|x)$ について解くと:
$$ \pi_\theta(y|x) = \pi_{\text{ref}}(y|x) \cdot \exp\left(\frac{r(x, y) – \beta – \lambda}{\beta}\right) $$
正規化条件 $\sum_y \pi_\theta(y|x) = 1$ を適用すると、$\exp(-(\beta + \lambda)/\beta)$ は分配関数 $Z(x)$ の逆数に対応します:
$$ \pi^*(y|x) = \frac{1}{Z(x)} \pi_{\text{ref}}(y|x) \exp\left(\frac{r(x, y)}{\beta}\right) $$
ここで $Z(x) = \sum_y \pi_{\text{ref}}(y|x) \exp(r(x, y)/\beta)$ は正規化定数です。
この結果の解釈は明快です。最適方策は、参照モデルの確率に $\exp(r(x,y)/\beta)$ の重みを掛けたものです。報酬が高い回答の確率が指数的に増幅され、報酬が低い回答の確率が減衰します。$\beta$ が小さいほど、報酬の差に対する反応が鋭くなります。
DPO損失関数の導出
報酬関数の抽出
DPOの核心的なステップは、上で導出した最適方策の式を逆向きに解くことです。つまり、方策 $\pi_\theta$ が与えられたとき、それに対応する報酬関数を抽出します。
最適方策の式を $r(x, y)$ について解きます。まず両辺の対数を取ります:
$$ \log \pi^*(y|x) = \log \pi_{\text{ref}}(y|x) + \frac{r(x, y)}{\beta} – \log Z(x) $$
$r(x, y)$ について整理すると:
$$ r(x, y) = \beta \log \frac{\pi^*(y|x)}{\pi_{\text{ref}}(y|x)} + \beta \log Z(x) $$
この式は、任意の方策 $\pi_\theta$ に対して「暗黙の報酬関数」を定義します:
$$ r_\theta(x, y) = \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)} + \beta \log Z(x) $$
Bradley-Terryモデルへの代入
この暗黙の報酬をBradley-Terryモデルに代入します。選好データの尤度は:
$$ P(y_w \succ y_l \mid x) = \sigma(r_\theta(x, y_w) – r_\theta(x, y_l)) $$
報酬の差を計算すると、$\beta \log Z(x)$ の項がキャンセルします:
$$ r_\theta(x, y_w) – r_\theta(x, y_l) = \beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} – \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)} $$
$Z(x)$ が消えるのは非常に重要です。$Z(x)$ は全ての回答にわたる和を含んでおり、計算が困難(intractable)だからです。
DPO損失関数
以上をまとめると、DPO損失関数は:
$$ \mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}}\left[\log \sigma\left(\beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} – \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\right)\right] $$
この式の各項を整理すると、DPOが最適化しているのは:
$$ \mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}\left[\log \sigma\left(\beta \left(\underbrace{\log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)}}_{\text{winnerの対数確率比}} – \underbrace{\log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}}_{\text{loserの対数確率比}}\right)\right)\right] $$
直感的理解
DPO損失関数は、「winnerの確率を(参照モデルに対して相対的に)上げ、loserの確率を下げる」ことを促します。
- $\log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)}$ が大きい → winnerが参照モデルよりも高い確率を持つ
- $\log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}$ が小さい → loserが参照モデルよりも低い確率を持つ
この差が大きいほどシグモイド関数の入力が大きくなり、損失は0に近づきます。逆に、winnerとloserの確率比が区別できない場合、損失が大きくなって学習が進みます。
$\beta$ はこの「差」のスケールを制御します。$\beta$ が大きいと、小さな確率比の差でも損失が急速に減少するため、参照モデルからの逸脱が小さくなります。
勾配の分析
DPO勾配の構造
DPO損失関数のパラメータ $\theta$ に関する勾配を計算しましょう。
$$ \nabla_\theta \mathcal{L}_{\text{DPO}} = -\beta \, \mathbb{E}\left[\underbrace{\sigma(-\hat{r}_\theta)}_{\text{重み}} \left(\underbrace{\nabla_\theta \log \pi_\theta(y_w|x)}_{\text{winnerの勾配}} – \underbrace{\nabla_\theta \log \pi_\theta(y_l|x)}_{\text{loserの勾配}}\right)\right] $$
ここで $\hat{r}_\theta = \beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} – \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}$ は暗黙の報酬の差です。
この勾配の構造を読み解きましょう。
重み $\sigma(-\hat{r}_\theta)$: 現在のモデルがwinnerとloserをうまく区別できていない($\hat{r}_\theta$ が小さい)場合、重みが大きくなり、学習が強く進みます。すでに十分区別できている場合は重みが小さくなり、過学習を防ぎます。これは暗黙的な報酬に基づく適応的な重み付けです。
winnerの勾配 $\nabla_\theta \log \pi_\theta(y_w|x)$: この項はwinnerの対数尤度を最大化する方向を示します。通常のSFT(教師あり学習)と同じ勾配です。
loserの勾配 $-\nabla_\theta \log \pi_\theta(y_l|x)$: 負符号により、loserの対数尤度を最小化する方向に更新します。これにより、望ましくない回答の生成確率が下がります。
つまりDPOの勾配は、「区別が難しいサンプルに重点を置きつつ、winnerの確率を上げ、loserの確率を下げる」という、直感的に理解しやすい更新を行っています。
次に、この理論をPythonで実装して動作を確認しましょう。
Pythonによる実装
DPO損失関数の実装
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
def log_softmax(logits):
"""数値安定なlog-softmax。"""
max_logit = np.max(logits)
return logits - max_logit - np.log(np.sum(np.exp(logits - max_logit)))
def compute_log_probs(policy_logits, token_ids):
"""トークン列の対数確率を計算する。"""
total_log_prob = 0.0
for t, token_id in enumerate(token_ids):
log_probs = log_softmax(policy_logits[t])
total_log_prob += log_probs[token_id]
return total_log_prob
def dpo_loss(policy_logits_w, policy_logits_l,
ref_logits_w, ref_logits_l,
tokens_w, tokens_l, beta=0.1):
"""DPO損失関数を計算する。
Parameters
----------
policy_logits_w, policy_logits_l : np.ndarray
現在のポリシーのlogits(winner, loser)
ref_logits_w, ref_logits_l : np.ndarray
参照モデルのlogits(winner, loser)
tokens_w, tokens_l : list
winner, loserのトークンID列
beta : float
KL正則化の強さ
Returns
-------
loss : float
DPO損失
"""
# 対数確率の計算
log_pi_w = compute_log_probs(policy_logits_w, tokens_w)
log_pi_l = compute_log_probs(policy_logits_l, tokens_l)
log_ref_w = compute_log_probs(ref_logits_w, tokens_w)
log_ref_l = compute_log_probs(ref_logits_l, tokens_l)
# 対数確率比
log_ratio_w = log_pi_w - log_ref_w
log_ratio_l = log_pi_l - log_ref_l
# DPO損失
logit = beta * (log_ratio_w - log_ratio_l)
loss = -np.log(1 / (1 + np.exp(-logit))) # -log sigmoid(logit)
return loss, logit
# シミュレーション: 簡略化された設定
vocab_size = 100
seq_len = 10
# 参照モデルのlogits(ランダム初期化)
ref_logits = np.random.randn(seq_len, vocab_size) * 0.5
# winnerとloserのトークン列
tokens_w = np.random.randint(0, vocab_size, seq_len)
tokens_l = np.random.randint(0, vocab_size, seq_len)
# 学習シミュレーション
# ポリシーモデルのlogitsを更新して損失を最小化
policy_logits_w = ref_logits.copy() + np.random.randn(seq_len, vocab_size) * 0.01
policy_logits_l = ref_logits.copy() + np.random.randn(seq_len, vocab_size) * 0.01
losses = []
log_ratio_diffs = []
lr = 0.05
beta = 0.1
for step in range(200):
loss, logit = dpo_loss(policy_logits_w, policy_logits_l,
ref_logits, ref_logits,
tokens_w, tokens_l, beta)
losses.append(loss)
log_ratio_diffs.append(logit / beta)
# 勾配の近似更新(winnerの確率を上げ、loserの確率を下げる)
weight = 1 / (1 + np.exp(logit)) # σ(-r_hat)
for t in range(seq_len):
# winnerトークンの確率を上げる
grad_w = np.zeros(vocab_size)
probs_w = np.exp(log_softmax(policy_logits_w[t]))
grad_w -= probs_w
grad_w[tokens_w[t]] += 1.0
policy_logits_w[t] += lr * beta * weight * grad_w
# loserトークンの確率を下げる
grad_l = np.zeros(vocab_size)
probs_l = np.exp(log_softmax(policy_logits_l[t]))
grad_l -= probs_l
grad_l[tokens_l[t]] += 1.0
policy_logits_l[t] -= lr * beta * weight * grad_l
# 可視化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# 左: 損失の推移
axes[0].plot(losses, color='#00d4ff')
axes[0].set_xlabel('Training Step')
axes[0].set_ylabel('DPO Loss')
axes[0].set_title('DPO Training Loss')
axes[0].grid(True, alpha=0.3)
# 中央: 対数確率比の差の推移
axes[1].plot(log_ratio_diffs, color='#ffa726')
axes[1].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
axes[1].set_xlabel('Training Step')
axes[1].set_ylabel('log π(y_w)/π_ref(y_w) - log π(y_l)/π_ref(y_l)')
axes[1].set_title('Implicit Reward Difference')
axes[1].grid(True, alpha=0.3)
# 右: βの効果
betas = [0.01, 0.05, 0.1, 0.5, 1.0]
x_range = np.linspace(-5, 5, 200)
for b in betas:
y = -np.log(1 / (1 + np.exp(-b * x_range)))
axes[2].plot(x_range, y, label=f'β = {b}')
axes[2].set_xlabel('Log Ratio Difference')
axes[2].set_ylabel('DPO Loss')
axes[2].set_title('Effect of β on DPO Loss Shape')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
axes[2].set_ylim(0, 5)
plt.tight_layout()
plt.savefig('dpo_training.png', dpi=150, bbox_inches='tight')
plt.show()
左のグラフでは、DPO損失が学習ステップとともに減少していく様子が確認できます。これは、モデルがwinnerとloserを徐々に区別できるようになっていることを意味します。
中央のグラフでは、暗黙の報酬差(対数確率比の差)が正の方向に増加しています。これは、winnerの確率が参照モデルに対して相対的に増加し、loserの確率が相対的に減少していることを示しています。初期状態では差がほぼ0(参照モデルと同じ)ですが、学習が進むにつれて明確な差が生じます。
右のグラフは、$\beta$ が損失関数の形状に与える影響を示しています。$\beta$ が小さいほど、対数確率比の差に対する損失の変化が緩やかになり、大きな確率比の変化を許容します。$\beta$ が大きいほど、小さな確率比の差でも損失が急速に減少し、参照モデルからの逸脱が抑制されます。
DPOの派生手法
IPO(Identity Preference Optimization)
DPOはBradley-Terryモデルを仮定していますが、これが成り立たない場合に問題が生じます。IPO(Azar et al., 2023)はBradley-Terryモデルの仮定を外し、直接的にペアワイズの選好を最適化します。
KTO(Kahneman-Tversky Optimization)
KTO(Ethayarajh et al., 2024)は、ペアデータ $(y_w, y_l)$ が不要で、単一の回答に対する良い/悪いのラベルだけで学習できます。ペアデータの収集はコストが高いため、KTOは実用上の大きな利点があります。
ORPO(Odds Ratio Preference Optimization)
ORPO(Hong et al., 2024)は、SFTとDPOを1つの損失関数に統合します。SFTの交差エントロピー損失にオッズ比ベースの選好項を追加することで、2段階の学習を1段階に削減します。
まとめ
本記事では、DPOの理論を導出から実装まで解説しました。
- RLHFのKL制約付き報酬最大化問題の最適方策の閉じた形を導出し、そこから報酬関数を逆解きすることでDPO損失関数を導いた
- DPO損失は $-\log \sigma(\beta(\log \frac{\pi_\theta(y_w)}{\pi_{\text{ref}}(y_w)} – \log \frac{\pi_\theta(y_l)}{\pi_{\text{ref}}(y_l)}))$ というシンプルなbinary cross-entropyの形を取る
- 勾配分析から、DPOは「区別困難なサンプルに適応的な重みを置き、winnerの確率を上げloserの確率を下げる」更新を行うことがわかった
- $\beta$ は参照モデルからの逸脱を制御し、大きいほど保守的な最適化になる
- 報酬モデルもPPOも不要で、通常のファインチューニングと同じ手順で実装できる
DPOはLLaMA、Mistral、Gemmaなど主要なオープンソースLLMのアライメントに広く使われており、LLMの選好調整のデファクト手法です。
次のステップとして、以下の記事も参考にしてください。