DPO — 直接選好最適化
動機
PPOは、4つのモデル(方策、参照モデル、報酬モデル、価値ヘッド)をメモリ上に必要とし、複雑なRLインフラを要するうえ、悪名高いほど不安定です。DPO (Rafailov et al. 2023) は、次の問いを投げかけます。RLを省略し、選好から直接学習できないか?
重要な洞察 :RLHFの目的関数(報酬最大化+KLペナルティ)のもとでの最適方策には、 閉形式の解 があります。この同じ目的関数を暗黙に最適化する教師あり損失を導出できます。
数学的導出
ステップ1 :RLHFの目的関数:\(\max_\pi \mathbb{E}_{x,y\sim\pi}[r(x,y)] - \beta D_\text{KL}[\pi\\vert \pi_\text{ref}]\)
ステップ2 :最適解は次のとおりです:\(\pi^*(y\vert x) = \frac{1}{Z(x)} \pi_\text{ref}(y\vert x) \exp\left(\frac{r(x,y)}{\beta}\right)\)
ステップ3 :報酬を方策で表すように整理します:\(r(x,y) = \beta \log \frac{\pi^*(y\vert x)}{\pi_\text{ref}(y\vert x)} + \beta \log Z(x)\)
ステップ4 :Bradley-Terry選好モデル \(P(y_w \succ y_l) = \sigma(r(y_w) - r(y_l))\) に代入します。\(Z(x)\) は相殺されます。
\[ \boxed{\mathcal{L}_\text{DPO}(\theta) = -\mathbb{E}_{(x, y_w, y_l)}\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]} \]
Tip
DPOが実際に行っていること
暗黙の報酬 を \(\hat{r}(x,y) = \beta\log\frac{\pi_\theta(y\vert x)}{\pi_\text{ref}(y\vert x)}\) と定義します。
DPOは、「選ばれた応答の暗黙の報酬は、棄却された応答より高い」という「ラベル」を使った交差エントロピー損失を最小化します。マージンは \(\beta\) によって制御されます。
大きな \(\beta\):大きなマージンが必要 \(\rightarrow\) 方策が積極的に移動 \(\rightarrow\) 忘却のリスク
小さな \(\beta\):小さなマージンで十分 \(\rightarrow\) 方策が参照モデルの近くにとどまる \(\rightarrow\) 保守的
参照モデルは正則化器として機能します。方策は、選好への整合を示すことで、参照モデルからのあらゆる逸脱を「正当化」しなければなりません。
勾配の分析
DPOの勾配は次のように分解できます。
\[ \nabla_\theta \mathcal{L} = -\beta \cdot \underbrace{\sigma(-\hat{r}_w + \hat{r}_l)}_{\text{weight: higher when model is wrong}} \cdot \left[\nabla_\theta \log\pi_\theta(y_w|x) - \nabla_\theta \log\pi_\theta(y_l|x)\right] \]
解釈 :勾配は、選ばれた応答の確率を上げ、棄却された応答の確率を下げます。モデルが現在、誤った答えを好んでいるときに重みが最大になり、「混乱を招く」ペアに学習を集中させます。
Note
DPOの具体例
プロンプト :「10歳の子どもに量子もつれを説明してください。」
選択された応答 (\(y_w\)):「2枚の魔法のコインを持っていると想像してください。片方を投げて表が出ると、どれだけ離れていても、もう片方は瞬時に裏になります!」
\(\log\pi_\theta(y_w\vert x) = -15.3\)、\(\log\pi_\text{ref}(y_w\vert x) = -16.1\)棄却された応答 (\(y_l\)):「量子もつれとは、2つの粒子が相関した状態になり、一方の粒子の量子状態を独立には記述できなくなる現象です。」
\(\log\pi_\theta(y_l\vert x) = -12.8\)、\(\log\pi_\text{ref}(y_l\vert x) = -12.5\)暗黙の報酬 :\(\hat{r}_w = 0.1 \times ((-15.3) - (-16.1)) = 0.08\)、\(\hat{r}_l = 0.1 \times ((-12.8) - (-12.5)) = -0.03\)
損失への入力 :\(\sigma(0.08 - (-0.03)) = \sigma(0.11) = 0.527\)
損失 :\(-\log(0.527) = 0.64\) — モデルは選択された応答をわずかに好んでいるだけです。勾配は強く押し上げます。
学習後:マージンが \(1/(2\beta)\) 付近で安定するまで、選択された応答の確率が増加し、棄却された応答の確率が減少します。
TRLによる実装
以下は、HuggingFace TRLを使った最小限の動作例です。
from trl import DPOConfig, DPOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig
from datasets import load_dataset
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B-Instruct",
torch_dtype=torch.bfloat16, attn_implementation="flash_attention_2")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
# Dataset format: {"prompt": str, "chosen": str, "rejected": str}
dataset = load_dataset("argilla/ultrafeedback-binarized-preferences")
lora_config = LoraConfig(r=64, lora_alpha=16, lora_dropout=0.05,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"])
dpo_config = DPOConfig(
output_dir="./dpo_output",
beta=0.1, # KL regularization strength
learning_rate=5e-7, # Very low LR for stability
loss_type="sigmoid", # Standard DPO loss
max_length=2048, # Max sequence length
max_prompt_length=1024, # Truncation for prompts
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # Effective batch = 16
gradient_checkpointing=True,
bf16=True,
num_train_epochs=1, # DPO overfits fast - 1 epoch!
warmup_ratio=0.1,
logging_steps=10,
eval_strategy="steps",
eval_steps=200,
save_strategy="steps",
save_steps=500,
)
trainer = DPOTrainer(
model=model,
ref_model=None, # With LoRA, ref = base model (no copy needed!)
args=dpo_config,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
tokenizer=tokenizer,
peft_config=lora_config,
)
trainer.train()
# Key metrics to monitor: train/rewards/chosen, train/rewards/rejected, train/rewards/margins
DPOの仕組み:完全なメカニクス
この節では、DPOの計算の詳細、つまり学習中にトークンレベルで何が起きるのかを完全に説明します。
系列レベルの対数確率
DPOにおける重要な量は、 完全な系列 \(y = (y_1, y_2, \ldots, y_T)\) の、プロンプト \(x\) が与えられたときの対数確率です。これは、 トークンごとの対数確率の合計 として計算します。
\[ \boxed{\log \pi_\theta(y|x) = \sum_{t=1}^{T} \log \pi_\theta(y_t \mid x, y_{<t})} \]
各項 \(\log \pi_\theta(y_t \vert x, y_{<t})\) は、位置 \(t\) における、系列内の実際のトークン \(y_t\) のlog-softmax出力です。これは標準的な言語モデリングで使う交差エントロピー損失と同じですが、ここでは平均ではなく 合計 します。
重要な詳細 :\(y_w\) と \(y_l\) の両方で、勾配は すべてのトークン位置 を通って流れます。中間トークンのマスクはありません。すべてのトークンが系列レベルの対数確率に寄与します。
DPO損失の分解
損失から始めます。
\[ \mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}}!\left[\log \sigma!\left(\beta \cdot h_\theta(x, y_w, y_l)\right)\right] \]
ここで「暗黙の報酬マージン」\(h_\theta\) は次のとおりです。
\[ h_\theta(x, y_w, y_l) = \underbrace{\log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)}}_{\text{chosen reward proxy}} - \underbrace{\log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}}_{\text{rejected reward proxy}} \]
トークンレベルの項に展開すると、次のようになります。
\[ \boxed{h_\theta = \sum_{t=1}^{|y_w|}!\left[\log\pi_\theta(y_w^t | x, y_w^{<t}) - \log\pi_{\text{ref}}(y_w^t | x, y_w^{<t})\right] - \sum_{t=1}^{|y_l|}!\left[\log\pi_\theta(y_l^t | x, y_l^{<t}) - \log\pi_{\text{ref}}(y_l^t | x, y_l^{<t})\right]} \]
フォワードパス:ステップごとの処理
1つの学習例 \((x, y_w, y_l)\) に対して、次を行います。
-
連結 :2つの系列 \([x; y_w]\) と \([x; y_l]\) を作ります。バッチ内で長さがそろうようにパディングします。
-
フォワードパス(方策 \(\pi_\theta\)) :両方の系列をモデルに通します。各応答位置のロジットを収集します。
-
対数確率の抽出 :応答の各位置 \(t\) で、\(\log\text{softmax}(\text{logits}_t)[y_t]\)、つまり実際のトークンの対数確率を取得します。
-
トークン上で合計 :
\[ \begin{aligned} \text{logp_chosen} &= \sum_{t \in \text{response positions}} \log\pi_\theta(y_w^t | x, y_w^{<t}) \\ \text{logp_rejected} &= \sum_{t \in \text{response positions}} \log\pi_\theta(y_l^t | x, y_l^{<t}) \end{aligned} \]
- 参照モデルを引く (事前計算済み、または2回目のフォワードパスから取得):
\[ \begin{aligned} \text{ratio_w} &= \text{logp_chosen} - \text{ref_logp_chosen} \\ \text{ratio_l} &= \text{logp_rejected} - \text{ref_logp_rejected} \end{aligned} \]
-
損失を計算 :\(\mathcal{L} = -\log\sigma(\beta \cdot (\text{ratio_w} - \text{ratio_l}))\)
-
バックワードパス :勾配がステップ5 \(\rightarrow\) 4 \(\rightarrow\) 3 \(\rightarrow\) 2 を逆向きに流れ、\(\theta\) を更新します。
トークンレベルの勾配分析
すべてのトークンに勾配が付くのか? はい。選択された系列の位置 \(t\) におけるロジットに関する勾配は次のとおりです。
\[ \frac{\partial \mathcal{L}}{\partial \text{logits}_t^{(w)}} = -\underbrace{\sigma(-\beta \cdot h_\theta)}_{\text{scaling factor}} \cdot \beta \cdot \frac{\partial \log\pi_\theta(y_w^t | \cdot)}{\partial \text{logits}_t^{(w)}} \]
重要な洞察 :\(\sigma(-\beta \cdot h_\theta)\) のスケーリング係数は、両方の系列のすべてのトークンで 共有 されます。これは適応的な学習率として機能します。
-
\(h_\theta\) が小さい場合(モデルが選択された応答と棄却された応答を区別できない):スケーリングは \(\approx 0.5\)。勾配が強く、積極的に学習します。
-
\(h_\theta\) が大きい場合(モデルがすでに選択された応答を好んでいる):スケーリングは \(\approx 0\)。勾配は無視できるほど小さくなり、過適合を防ぎます。
選択されたトークンへの効果 :確率が増加します(対数確率が押し上げられます)。
棄却されたトークンへの効果 :確率が減少します(対数確率が押し下げられます)。
参照モデルとの相対関係 :\(\pi_{\text{ref}}\) との差だけが重要です。モデルがすでに選択された応答に高い確率を割り当てている場合(参照モデルと一致している場合)、勾配はほとんどありません。
トークン単位と系列単位:長さの正規化
微妙な問題があります。長い系列は、各項を合計するため自然に対数確率が低くなります(各項は \(\leq 0\))。\(\vert y_w\vert \gg \vert y_l\vert\) の場合、損失が短い応答を好む方向に偏る可能性があります。
解決策 :
-
長さを正規化したDPO :\(\log\pi_\theta(y\vert x)\) を \(\frac{1}{\vert y\vert }\sum_t \log\pi_\theta(y_t\vert \cdot)\) に置き換えます。一部の実装で使われています(SimPOが採用)。
-
標準DPO :生の合計(正規化なし)を使います。これは暗黙に冗長さを罰します。モデルは、選択された応答のすべてのトークンに高い確率を割り当てなければなりません。
-
実際の影響 :ベンチマークでは、長さを正規化したDPOは長さの悪用を減らしますが、指示追従品質を損なうことがあります。標準(非正規化)DPOの方がプロダクションでは一般的です。
ラベルマスキング:どのトークンに勾配が付くか
Important
DPOで勾配を受け取るトークン
プロンプトトークン (\(x\)): 勾配なし 。損失は応答位置だけで計算されます。プロンプトトークンはコンテキストを提供しますが、そのロジットは \(\log\pi(y\vert x)\) に寄与しません。
選択された応答トークン (\(y_w\)): すべてのトークンに勾配が付きます 。各 \(y_w^t\) が合計に寄与し、その確率を上げる方向に勾配が働きます。
棄却された応答トークン (\(y_l\)): すべてのトークンに勾配が付きます 。各 \(y_l^t\) が合計に寄与し、その確率を下げる方向に勾配が働きます。
パディングトークン : 勾配なし 。アテンションマスクで除外されます。
疑似コード:DPO学習ステップ
Note
DPOのフォワード+バックワード(PyTorch形式)
def dpo_loss(model, ref_model, batch, beta=0.1): """One DPO training step.""" # batch contains: input_ids_chosen, input_ids_rejected, # labels_chosen, labels_rejected (prompt masked to -100) # 1. Forward pass: get per-token log-probs logps_chosen = get_sequence_logprob(model, batch["chosen"]) logps_rejected = get_sequence_logprob(model, batch["rejected"]) # 2. Reference log-probs (pre-computed or computed here) with torch.no_grad(): ref_logps_chosen = get_sequence_logprob(ref_model, batch["chosen"]) ref_logps_rejected = get_sequence_logprob(ref_model, batch["rejected"]) # 3. Compute implicit reward margins chosen_rewards = beta * (logps_chosen - ref_logps_chosen) rejected_rewards = beta * (logps_rejected - ref_logps_rejected) # 4. DPO loss = -log(sigmoid(chosen_reward - rejected_reward)) loss = -F.logsigmoid(chosen_rewards - rejected_rewards).mean() return loss def get_sequence_logprob(model, sequences): """Sum of log-probs over response tokens only.""" outputs = model(sequences["input_ids"], attention_mask=sequences["mask"]) logits = outputs.logits[:, :-1, :] # Shift for next-token prediction # Gather log-prob of actual tokens labels = sequences["labels"][:, 1:] # Shifted labels log_probs = F.log_softmax(logits, dim=-1) token_logps = log_probs.gather(-1, labels.unsqueeze(-1)).squeeze(-1) # Mask: only sum over response tokens (labels != -100) mask = (labels != -100).float() return (token_logps * mask).sum(dim=-1) # Shape: [batch_size]
よくある落とし穴
Warning
DPO実装の落とし穴
プロンプトのマスクを忘れる :プロンプトトークンを対数確率の合計に含めると、モデルはプロンプトの尤度(役に立たないもの)を最適化し、実効的な \(\beta\) が誤ります。
合計ではなく平均を使う :\(\frac{1}{T}\sum_t \log\pi\) と \(\sum_t \log\pi\) では、暗黙の長さペナルティが異なります。\(\pi_\theta\) と \(\pi_{\text{ref}}\) の間で一貫させなければなりません。
参照モデルが古い :\(\pi_{\text{ref}}\) が \(\pi_\theta\) から遠すぎる場合(例:ベースモデル対ファインチューニング済みモデル)、KL項が支配的になり勾配が消えます。解決策は、ベースモデルではなくSFTチェックポイントを参照モデルとして使うことです。
\(\beta\) が大きすぎる :対数確率の差を増幅します \(\rightarrow\) sigmoidが飽和します \(\rightarrow\) 勾配がゼロになります。\(\beta = 0.1\) から始め、\([0.05, 0.5]\) の範囲で調整します。
\(\beta\) が小さすぎる :理論上は参照モデルからの自由度が増えます(KL制約が弱くなる)が、勾配 \(\propto \beta \cdot \sigma(-\beta h)\) が非常に小さくなります \(\rightarrow\) 損失地形が平坦になります \(\rightarrow\) 収束が極端に遅くなります。モデルには遠くへ移動する「許可」がある一方、どこへ移動すべきかを示すシグナルはほとんどありません。
DPOのバリアントとそれぞれが失敗する場合
Warning
DPOが失敗する場合
1. 分布シフト :古いモデルから得た選好データを使います。現在の方策は異なるテキストを生成します \(\rightarrow\) 損失が無関係な例を最適化することになります。
2. 探索がない :データセットにない振る舞いを発見できません。局所最適解にとどまります。
3. 参照モデルの崩壊 :参照モデルが強すぎると、方策が動けません。弱すぎると、正則化がありません。
4. データ品質 :ノイズのあるラベルが学習を汚染します。多くのサンプルで平均を取るPPOとは異なり、DPOは個々のペアを記憶します。
5. 選好データの多様性 :選択/棄却ペアが、品質差の全範囲をカバーするようにします(単に良い対ひどいだけにしない)。品質だけでなくアプローチが異なるペアは、より豊かな方策の区別を教えます。
\(\beta\) の選択ガイド
| \(\beta\) | 領域 | 使用する場合 |
|---|---|---|
| 0.01 | 非常に積極的 | データが極めてクリーンで、大きな分布シフトが必要な場合のみ |
| 0.05 | 積極的 | 良いデータがあり、SFTからの目立った改善を望む場合 |
| 0.1 | 標準 | デフォルトの開始点。品質と安定性のバランスが良い |
| 0.2 | 保守的 | データにノイズがある場合、またはモデルが望ましい振る舞いにすでに近い場合 |
| 0.5 | 非常に保守的 | 能力を壊してはいけない安全性ファインチューニング |
DPOのバッチサイズ設定とスケーリング
単一系列のトークン予測を行う標準的なSFTとは異なり、DPOは、好ましい系列と好ましくない系列を比較する ペア単位の損失 を利用します。これにより、メモリ使用量と最適化の安定性が根本的に変わります。
グローバルバッチサイズの目標
DPO実装全体の経験的な証拠から、最適なグローバルバッチサイズの範囲が確立されています。
\[ \boxed{B_{\text{global}} \in [32, 128]} \]
-
\(B_{\text{global}} < 32\):暗黙の報酬推定における勾配ノイズが深刻になります \(\rightarrow\) 方策が、(有用性と安全性という)アラインメント目標の間で破壊的に振動します。
-
\(B_{\text{global}} > 128\):収束速度に対する限界効果が小さくなり、分散計算間の通信オーバーヘッドが大きくなります。
数学的分解
DPOは2つのモデルコピー(アクティブな方策 \(\pi_\theta\)+凍結した参照モデル \(\pi_{\text{ref}}\))を同時に読み込むため、系列あたりのメモリは2倍になります。グローバルバッチサイズは次のように分解されます。
\[ \boxed{B_{\text{global}} = B_{\text{micro}} \times N_{\text{GPUs}} \times K_{\text{accum}}} \]
-
\(B_{\text{micro}}\):デバイスごとのマイクロバッチサイズ(1回のフォワードパスにおける選好ペア数)。
-
\(N_{\text{GPUs}}\):並列データ処理デバイスの数。
-
\(K_{\text{accum}}\):重みを更新する前の勾配蓄積ステップ数。
ペアリングの倍率 :1つのDPOデータインスタンスには、プロンプト(\(x\))、選択された応答(\(y_w\))、棄却された応答(\(y_l\))が含まれます。マイクロバッチあたりの実際のテンソル負荷は次のとおりです。
\[ T_{\text{sequences}} = 2 \times B_{\text{micro}} \]
パラメータが\(>\)7Bのモデルを80GB GPU上で、コンテキスト長4096〜8192トークンで動かす場合、物理的な上限によって \(B_{\text{micro}} \in [1, 2]\) に厳しく制限されます。
分散スケーリングの設定
| 設定 | 単一GPU | 8-GPUノード |
|---|---|---|
| \(B_{\text{global}}\) | 64 | 64 |
| \(B_{\text{micro}}\) | 2(4系列) | 2(4系列) |
| \(N_{\text{GPUs}}\) | 1 | 8 |
| \(K_{\text{accum}}\) | 32ステップ | 4ステップ |
| スループット | 逐次/低速 | 高い並列スループット |
DPO学習の分散スケーリングプロファイル(\(B_{\text{global}} = 64\) を目標)。
VRAM最適化:参照モデルの対数確率を事前計算する
DPO損失は次のとおりです。
\[ \mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}_{(x, y_w, y_l)}!\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] \]
\(\pi_{\text{ref}}\) は学習中 完全に静的 なので、その出力を事前計算できます。
Important
参照モデルを退避する戦略
学習開始前に、データセット \(\mathcal{D}\) 上で \(\pi_{\text{ref}}\) だけを使ってフォワードパスを実行します。
スカラー \(\log \pi_{\text{ref}}(y_w\vert x)\) と \(\log \pi_{\text{ref}}(y_l\vert x)\) をディスクにキャッシュします。
\(\pi_{\text{ref}}\) をGPUメモリから完全に退避させます。
結果 :利用可能なGPUメモリが2倍になります \(\rightarrow\) \(B_{\text{micro}}\) を1〜2から4〜8に増やせるため、ハードウェア利用率と学習スループットを最大化できます。
実装:TRLでは、DPOConfig に
precompute_ref_log_probs=Trueを設定します。70Bモデルでは、クラスタ全体で\(\sim\)140GBのGPUメモリを節約できます。
DPOの拡張とバリアント
直接選好最適化(DPO)は、報酬関数と最適方策の間の閉形式の対応関係を導出することで、RLHFを教師あり学習問題として再定式化します。標準DPOの損失は次のとおりです。
\[ \mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}_{(q,y_w,y_l)}!\left[ \log \sigma!\left( \beta \log \frac{\pi_\theta(y_w|q)}{\pi_{\text{ref}}(y_w|q)} - \beta \log \frac{\pi_\theta(y_l|q)}{\pi_{\text{ref}}(y_l|q)} \right) \right], \]
ここで \(y_w\) は好ましい(勝った)応答、\(y_l\) は好ましくない(負けた)応答、\(\beta\) はKLペナルティの強さを制御します。以下の小節では、最も重要な拡張とバリアントを扱います。
f-DPO — 一般化されたfダイバージェンスDPO
Tip
逆方向KLを超えて
標準DPOは、方策と参照モデルの間の正則化器として逆方向KLダイバージェンスを使います。逆方向KLはモードを探す性質があり、確率質量を少数の高報酬応答に集中させることを好みます。順方向KLはモードをカバーする性質があり、もっともらしい応答をすべてカバーするよう確率質量を広げます。f-DPO (J. Wang et al. 2024) は任意のfダイバージェンスへ一般化し、実務者がこれらの振る舞いをトレードオフできるようにします。
f-DPOの損失は、対数比をfダイバージェンス生成関数の導関数に置き換えます。
\[ \mathcal{L}_{f\text{-DPO}} = -\mathbb{E}!\left[ f'!\left(\frac{\pi_\theta(y_w|q)}{\pi_{\text{ref}}(y_w|q)}\right) - f'!\left(\frac{\pi_\theta(y_l|q)}{\pi_{\text{ref}}(y_l|q)}\right) \right], \]
ここで \(f'\) はfダイバージェンス生成関数の導関数です。
Important
TRLにおけるfダイバージェンスの選択肢
reverse_kl :\(f'(u) = \log u\)。標準DPO。モードを探します。
forward_kl :\(f'(u) = -1/u\)。モードをカバーします。多様性が向上します。
js_divergence :\(f'(u) = \log(2u/(u+1))\)。モード探索とモードカバーのバランスを取ります。
alpha_divergence :\(f'(u) = u^{\alpha-1}\)。順方向KLと逆方向KLの間を補間します。
Note
TRLでのf-DPO
from trl import DPOConfig, DPOTrainer # Jensen-Shannon divergence (balanced) config = DPOConfig( f_divergence_type="js_divergence", beta=0.1, ) # Alpha divergence (alpha=0: forward KL, alpha=1: reverse KL) config_alpha = DPOConfig( f_divergence_type="alpha_divergence", f_alpha_divergence_coef=0.5, # alpha parameter beta=0.1, ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
Important
f-DPOを使う場合
多様性と品質のバランスが必要な場合は、 JSダイバージェンス を使います。
多様性が最優先の創造的なタスクには、 順方向KL を使います。
正解が1つのタスクには、 逆方向KL (標準DPO)を使います。
alphaダイバージェンス を使うと、連続的に補間してトレードオフを調整できます。
Robust DPO — ロバストDPO
Tip
選好データのノイズのあるラベル
人間による選好のアノテーションにはノイズがあります。アノテーターは意見が一致しなかったり、間違えたり、選好/非選好のラベルを逆にしたりすることがあります。標準DPOはすべてのラベルを正解として扱うため、モデルがノイズに過適合する可能性があります。Robust DPO (Chowdhury et al. 2024) は、既知のノイズモデルのもとで損失を解析的にバイアス補正します。
各ラベルが確率 \(\epsilon\)(ノイズ率)で反転すると仮定します。バイアス補正後の損失は次のとおりです。
\[ \boxed{ \mathcal{L}_{\text{robust}} = \frac{(1-\epsilon),\mathcal{L}_{\text{DPO}}(y_w, y_l) - \epsilon,\mathcal{L}_{\text{DPO}}(y_l, y_w)}{1 - 2\epsilon}, } \]
ここで \(\mathcal{L}_{\text{DPO}}(y_w, y_l)\) は \(y_w\) を選好された応答として扱う標準DPO損失、\(\mathcal{L}_{\text{DPO}}(y_l, y_w)\) はラベルを反転した場合の損失です。この補正によって、ラベルノイズが導入するバイアスを取り除きます。
Important
ロバストDPOの直感
この式は、反転したラベルの寄与を「差し引く」線形結合です。\(\epsilon=0\) では標準DPOに戻ります。\(\epsilon=0.5\) では分母が0に近づきます。ラベルが完全なノイズになり、学習は不可能です。実際には、\(\epsilon \in [0.05, 0.2]\) で現実のアノテーションノイズの大部分をカバーできます。
Note
TRLでのロバストDPO
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="robust", label_smoothing=0.1, # corresponds to epsilon = 0.1 beta=0.1, ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
TR-DPO — 信頼領域DPO
Tip
古くなった参照モデルの問題
標準DPOは学習中ずっと固定された参照モデル \(\pi_{\text{ref}}\) を使います。方策 \(\pi_\theta\) が改善すると、KLペナルティ \(\beta \log(\pi_\theta/\pi_{\text{ref}})\) が大きくなり、やがて損失を支配して、それ以上の改善を妨げます。TR-DPO (Gorbatenko 2024) は、現在の方策を追跡するよう参照モデルを定期的に更新します。
TR-DPOは指数移動平均(EMA)を使って参照モデルを更新します。
\[ \pi_{\text{ref}}^{(t+1)} \leftarrow \alpha \cdot \pi_\theta^{(t)} + (1-\alpha) \cdot \pi_{\text{ref}}^{(t)}, \]
ここで \(\alpha \in (0,1)\) は混合係数です。これは \(T_{\text{sync}}\) 回の勾配ステップごとに適用します。
Note
TRLでのTR-DPO
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="sigmoid", # standard DPO loss sync_ref_model=True, # enable TR-DPO ref_model_mixup_alpha=0.6, # alpha: how much of current policy to mix in ref_model_sync_steps=512, # T_sync: sync every 512 steps beta=0.1, ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
Important
TR-DPOを使う場合
方策が初期参照モデルから大きく離れる長時間の学習。
KLペナルティの支配によってDPO損失が早期に頭打ちになる場合。
現在の方策から新しい選好データを収集する反復的なDPOパイプライン。
\(\alpha\) を1に近づけると参照モデルを速く更新し、0に近づけるとゆっくり更新します。
EXO — 厳密な最適化
Tip
DPOにおけるKLの方向の問題
DPOは逆方向KL制約のもとで最適方策を解くことから導出されます。しかし、結果として得られる損失は実際には報酬空間で順方向KL目的関数を最適化しており、方向が間違っています。EXO (Ji et al. 2024) は、アラインメントに理論的に正しい目的関数である、逆方向KLの確率マッチングを使ってこれを修正します。
EXOは、モデル分布と目標(報酬最適)分布の間の逆方向KLを最小化します。
\[ \mathcal{L}_{\text{EXO}} = \mathbb{E}_{y \sim \pi_\theta}!\left[ \log \frac{\pi_\theta(y|q)}{p^*(y|q)} \right], \]
ここで \(p^*(y\vert q) \propto \pi_{\text{ref}}(y\vert q) \exp(r(y,q)/\beta)\) は最適方策です。実際には、EXOは利用可能な選好ペアを使って次のように近似します。
\[ \mathcal{L}_{\text{EXO}} \approx -\mathbb{E}!\left[ \log \sigma!\left( \beta \log \frac{\pi_{\text{ref}}(y_w|q)}{\pi_\theta(y_w|q)} - \beta \log \frac{\pi_{\text{ref}}(y_l|q)}{\pi_\theta(y_l|q)} \right) \right]. \]
DPOと比べて、\(\pi_\theta\) と \(\pi_{\text{ref}}\) の役割が入れ替わっていることに注目してください。
Note
TRLでのEXO
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="exo_pair", beta=0.1, ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
NCA — ノイズ対照アラインメント
Tip
DPOにおける尤度崩壊
DPOの既知の失敗モードは尤度崩壊です。モデルは負けた応答の確率を下げることを学びますが、損失が気にするのは差だけなので、勝った応答の確率も下げてしまいます。NCA (H. Chen et al. 2024) は、これを防ぐために絶対尤度項を追加します。
NCAはアラインメントをノイズ対照推定として捉え直します。損失は3つの項を持ちます。
\[ \boxed{ \mathcal{L}_{\text{NCA}} = -\log \sigma(r_w) - \tfrac{1}{2}\log \sigma(-r_w) - \tfrac{1}{2}\log \sigma(-r_l), } \]
ここで \(r_y = \beta \log(\pi_\theta(y\vert q)/\pi_{\text{ref}}(y\vert q))\) は暗黙の報酬です。第1項は \(y_w\) に高い報酬を与えるよう促し、第2項と第3項は \(y_w\) と \(y_l\) の両方に高い報酬を与えることを罰します(崩壊を防ぎます)。
Note
TRLでのNCA
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="nca_pair", beta=0.01, # small beta: absolute likelihood term dominates ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
Important
NCAを使う場合
DPO学習中に勝った応答の確率が下がっている場合。
相対的な順位だけでなく、応答の絶対的な品質が重要なタスク。
絶対尤度項により大きな重みを与えるため、小さな \(\beta\)(例:0.01)を使います。
SLiC-HF — 系列尤度キャリブレーション
Tip
より単純な代替手段としてのヒンジ損失
DPOのlog-sigmoid損失は滑らかですが、マージンが大きい場合には収束が遅くなることがあります。SLiC-HF (Yao Zhao et al. 2023) はヒンジ損失を使います。これはマージンが閾値を超えると0になり、それ以外では線形になります。単純で高速であり、驚くほど競争力があります。
SLiC-HFの損失は次のとおりです。
\[ \mathcal{L}_{\text{SLiC}} = \max!\left(0,; \delta - \beta\log\frac{\pi_\theta(y_w|q)}{\pi_{\text{ref}}(y_w|q)} + \beta\log\frac{\pi_\theta(y_l|q)}{\pi_{\text{ref}}(y_l|q)} \right), \]
ここで \(\delta\) はマージンの閾値です。モデルが勝った応答と負けた応答の間にすでに \(\delta\) のマージンを割り当てている場合、損失は0になります。
Note
TRLでのSLiC-HF
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="hinge", beta=0.1, ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
Iterative RPO — 推論選好最適化
Tip
DPOは生成方法を忘れる
標準DPOは、勝った応答と負けた応答を識別するようモデルを学習します。しかし、推論タスクでは、モデルは正しい推論トレースを生成する必要もあります。識別できても生成できないモデルは、推論時には役に立ちません。RPOは勝った応答にNLL(負の対数尤度)項を追加し、モデルがそれを生成することを学ぶようにします。
RPOの損失は、DPOとSFTを組み合わせたものです。
\[ \mathcal{L}_{\text{RPO}} = \lambda_1 \mathcal{L}_{\text{DPO}}(y_w, y_l) + \lambda_2 \mathcal{L}_{\text{NLL}}(y_w), \]
ここで \(\mathcal{L}_{\text{NLL}}(y_w) = -\log \pi_\theta(y_w\vert q)\) は、勝った応答に対する標準的な言語モデリング損失です。
Note
TRLでのIterative RPO
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="sigmoid", # Standard DPO loss rpo_alpha=1.0, # NLL regularisation weight (RPO) beta=0.1, ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=config, train_dataset=dataset, )
Important
RPOを使う場合
モデルがステップごとの解を生成しなければならない推論タスク(数学、コード、論理)。
DPO学習によって、モデルの流暢さや生成品質が失われる場合。
反復パイプライン:ロールアウトを生成し、ラベルを付け、RPOで学習し、繰り返します。
NLL項が正則化器として機能し、方策の崩壊を防ぎます。
SimPO — 単純な選好最適化
Tip
参照モデルなしの選好学習
DPOは暗黙の報酬を計算するために参照モデルを必要とします。これによりメモリ使用量が2倍になり、複雑さも増します。SimPO (Meng et al. 2024) は、応答の平均対数確率を暗黙の報酬として使い、モデルが短い応答を好むことを防ぐ長さの正規化項を加えることで、参照モデルを取り除きます。
SimPOは暗黙の報酬を次のように定義します。
\[ r_{\text{SimPO}}(y|q) = \frac{\beta}{|y|} \log \pi_\theta(y|q), \]
損失は次のとおりです。
\[ \boxed{ \mathcal{L}_{\text{SimPO}} = -\mathbb{E}!\left[ \log \sigma!\left( \frac{\beta}{|y_w|}\log\pi_\theta(y_w|q) - \frac{\beta}{|y_l|}\log\pi_\theta(y_l|q) - \gamma \right) \right], } \]
ここで \(\gamma > 0\) は目標報酬マージンです。これにより、勝った応答の報酬が負けた応答より少なくとも \(\gamma\) だけ厳密に高くなることを保証します。
Important
SimPOとDPOとORPOの比較
DPO :参照モデルを使う。比に基づく暗黙の報酬。
ORPO :参照モデルなし。SFT損失にオッズ比項を追加。
SimPO :参照モデルなし。長さを正規化した対数確率の報酬+マージン。
SimPOはDPOより単純(参照モデルが不要)で、ORPOより原理に忠実です。
SimPOの長さの正規化は重要です。これがないと、モデルは長い応答を好みます。
Note
TRLでのSimPO
from trl import DPOConfig, DPOTrainer config = DPOConfig( loss_type="simpo", simpo_gamma=0.5, # target reward margin gamma beta=2.5, # length normalisation coefficient # No ref_model needed! ) trainer = DPOTrainer( model=model, ref_model=None, # SimPO is reference-free args=config, train_dataset=dataset, )