はじめに
Trial 19で「対数確率評価0% vs 実際生成100%」という矛盾を発見しました。この矛盾はTrial 13(3Bモデル)でも、Trial 19(13Bモデル)でも再現され、モデルサイズに関わらず発生する根本的な問題であることが判明しました。 Trial 20では、この問題の根本原因を特定し、解決策を実装しました。既存の実装を詳細分析
まず、verify_trial19.pyの実装を詳しく調べました。驚きの発見:既にプロンプト除外が実装されていた
verify_trial19.pyは既にプロンプト除外が実装されていました。 それなのに0%成功率... つまり、プロンプト除外だけでは不十分ということです。verify_trial19.py より(30-64行目)
def compute_log_prob(model, prompt, response): """修正版: 応答部分のみの対数確率を計算(プロンプト除外)"""# プロンプトのトークン数を取得 prompt_tokens = tokenizer(prompt, return_tensors="pt").to(device) prompt_length = prompt_tokens["input_ids"].shape[1]
# 応答部分のみのトークンの対数確率を取得 labels = tokens["input_ids"][:, prompt_length:].unsqueeze(-1) selected_log_probs = torch.gather( log_probs[:, prompt_length-1:-1, :], dim=-1, index=labels ).squeeze(-1)
根本原因:応答長の違い
DPOデータセットの構造
Chosenは詳しく長い、Rejectedは短い これがDPO訓練のデータセット構造です。Chosen(好まれる応答): 「サイズ展開は、XXS、XS、S、M、L、XL、XXLの6サイズです。 サイズ表は以下のページに掲載していますので、ご確認ください。」 → 約100トークン
Rejected(好まれない応答): 「S、M、Lサイズがあります。」 → 約20トークン
対数確率の計算
各トークンの対数確率は負の値です(例: -0.5, -1.2, -3.0)。 Total log probabilityは、これらの合計です。 実際の計算例:Chosen(100トークン): -0.8 + -0.7 + -0.9 + ... (100回繰り返し) = -80.0Rejected(20トークン): -1.5 + -2.0 + -1.8 + ... (20回繰り返し) = -35.0
比較: Chosen: -80.0 Rejected: -35.0 結果: Chosen < Rejected ← Rejectedが勝つ!❌
問題の本質
負の値をたくさん足すほど、合計値が小さくなる これは数学的に当然のことです。- Chosen: 100個の負の値を足す → 大きな負の数
- Rejected: 20個の負の値を足す → 小さな負の数
解決策:Per-token Average Log Probability
応答長で正規化
def compute_log_prob_normalized(model, tokenizer, prompt, response): # 応答部分のみの対数確率を計算(プロンプト除外) selected_log_probs = ... # 応答部分のみ# 応答部分のトークン数を取得 response_length = mask.sum(dim=-1).item()
# Per-token averageを返す if response_length > 0: return selected_log_probs.sum(dim=-1).item() / response_length else: return 0.0
効果
Per-token averageでは、トークン単位の品質を比較できるため、応答長の影響を受けません。Chosen(100トークン): Total: -80.0 Per-token average: -80.0 / 100 = -0.80Rejected(20トークン): Total: -35.0 Per-token average: -35.0 / 20 = -1.75
比較: Chosen: -0.80 per token Rejected: -1.75 per token 結果: Chosen > Rejected ← Chosenが勝つ!✅
実装
verify_trial20_normalized.py
両方の評価方法を実装しました:1. Total log probability(従来の方法、プロンプト除外済み) 2. Per-token average log probability(新しい方法)
Total log probability
chosen_logp_total = compute_log_prob_total(model, prompt, chosen)
rejected_logp_total = compute_log_prob_total(model, prompt, rejected)
Normalized log probability
chosen_logp_norm = compute_log_prob_normalized(model, prompt, chosen)
rejected_logp_norm = compute_log_prob_normalized(model, prompt, rejected)
両方を同時に計算して比較できます。
理論的予測
従来の方法(Total log prob)
- 予測成功率: 0-10%
- 理由: Chosenが長いため、負の値をたくさん足して不利
新しい方法(Per-token average log prob)
- 予測成功率: 80-100%
- 理由: トークン単位の品質を正しく評価
実際の生成品質
- Trial 19での確認: 100%成功(DPOモデルが全10ペアでChosen風の応答を生成)
実証実験結果
Lambda GPU検証完了(2025-10-31)
理論を証明するため、Lambda GPUで実際に検証しました: 実行環境:- GPU: gpu_1x_a10 ($0.75/時間)
- 所要時間: 8.5分
- コスト: 約$0.11
- モデル: cyberagent/open-calm-3b
- データセット: 50ペア
| 評価方法 | 成功率 | 予測 | 結果 | |---------|-------|------|------| | Total log prob(従来) | 0.0% (0/50) | 0-10% | ✅ 予測通り | | Per-token average log prob(新) | 96.0% (48/50) | 80-100% | ✅ 予測通り | | 実際の生成(Trial 19確認済) | 100% | - | ✅ 評価と一致 |
詳細データ
従来の方法(Total log probability):- 平均 Chosen logp: -114.82
- 平均 Rejected logp: -16.06
- 差分: -98.77(Rejectedの方が大きい = 失敗)
- 成功率: 0.0% ❌
- 平均 Chosen logp: -1.6845
- 平均 Rejected logp: -3.6900
- 差分: +2.0055(Chosenの方が大きい = 成功)
- 成功率: 96.0% ✅
まとめ
発見
1. プロンプト除外だけでは不十分 - verify_trial19.pyは既にプロンプト除外実装済み - それでも0%成功率
2. 根本原因は応答長の違い - Chosen(長い)vs Rejected(短い) - 負の値を足すほど合計が小さくなる数学的性質
3. 解決策はPer-token average - 応答長で正規化することで公平な比較 - トークン単位の品質を評価
技術的意義
この発見は、DPO訓練の評価方法に関する重要な洞察を提供します:- ❌ 間違った評価方法: Total log probability(応答長の影響を受ける)
- ✅ 正しい評価方法: Per-token average log probability(応答長に依存しない)
今後の展開
1. ✅ Lambda GPUで実証実験 → 96%成功達成 2. ✅ 0%→96%の改善を確認 → 理論が証明された 3. 標準評価方法として確立 → 今後のTrialで使用 4. Trial 19でも検証予定(13Bモデルでも同じ効果が出るか確認)
関連記事
--- 実験データ:/Users/masaka/dpo-rlhf-demo/TRIAL20_ANALYSIS.md
検証スクリプト: verify_trial20_normalized.py
