Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SFTのベストプラクティスと技法

教師ありファインチューニング(SFT)はRLHFパイプラインの基盤です。SFTモデルの品質が、RLで達成できることの上限を決めます。RLは振る舞いを洗練・改善できますが、SFTモデルに完全に存在しない振る舞いを確実に導入することはできません。この節では、効果的なSFTのための主要な技法を扱います。

効率化のための系列パッキング

Tip

パディングの問題

標準的なSFTのバッチでは、バッチ内で最も長い系列の長さに合わせて、すべての系列をパディングします。長さの分散が大きいデータセット(短い指示と長い文書が混在する場合など)では、計算量の50–80%がパディングトークンに費やされ、無駄になります。系列パッキングはこの無駄をなくします。

系列パッキングは、複数の短い例をEOSトークンで区切り、max_seq_length 長の単一系列へ連結します。アテンションマスクにより、異なる例のトークン同士が互いにアテンションしないようにします。

  1. 長さで例をソートします(任意。パッキング効率が向上します)。

  2. max_seq_length サイズのビンへ、例を貪欲にパッキングします。

  3. ブロック対角アテンションマスクを使って、例をまたぐアテンションを防ぎます。

  4. パディングでないトークンだけについて損失を計算します。

Important

パッキング効率

  • 一般的なパッキング効率:85–95%(パディングありでは20–50%)。

  • 高い長さ分散を持つデータセットでの高速化:2–4\(\times\)。

  • メモリ:パディングの場合と同程度(バッチあたりの総トークン数が同じ)。

  • 注意点:例をまたぐ汚染を避けるため、アテンションマスクを慎重に設定する必要があります。

Note

TRLでの系列パッキング

from trl import SFTConfig, SFTTrainer

config = SFTConfig(
    max_seq_length=4096,
    packing=True,           # enable sequence packing
    output_dir="sft_model",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-5,
    num_train_epochs=3,
)

trainer = SFTTrainer(
    model=model,
    args=config,
    train_dataset=dataset,
    # dataset_text_field="text",  # or use formatting_func
)
trainer.train()

チャットテンプレートとフォーマット

Tip

チャットテンプレートが重要な理由

言語モデルは生のテキストで学習されますが、指示追従モデルはシステムプロンプト、ユーザーメッセージ、アシスタントの応答を区別する必要があります。チャットテンプレートは、この構造をトークン系列にエンコードします。推論時に誤ったテンプレート(またはテンプレートなし)を使うと、性能が大幅に低下します。

ChatML形式

ChatMLは最も広く使われているチャットテンプレートです。

# ChatML format
template = """<|im_start|>system
{system_message}<|im_end|>
<|im_start|>user
{user_message}<|im_end|>
<|im_start|>assistant
{assistant_message}<|im_end|>"""

Llama形式

Llama 3は、特殊トークンを使う別のテンプレートを採用しています。

# Llama 3 format
template = """<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{system_message}<|eot_id|><|start_header_id|>user<|end_header_id|>
{user_message}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
{assistant_message}<|eot_id|>"""

Note

TRLでチャットテンプレートを適用する

from transformers import AutoTokenizer
from trl import SFTConfig, SFTTrainer

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")

def formatting_func(example):
    """Apply chat template to a dataset example."""
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": example["instruction"]},
        {"role": "assistant", "content": example["response"]},
    ]
    return tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=False,
    )

config = SFTConfig(
    max_seq_length=2048,
    output_dir="sft_model",
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    args=config,
    train_dataset=dataset,
    formatting_func=formatting_func,
)

完了部分のみのマスキング

Tip

プロンプトをマスクする理由

指示ファインチューニングでは、モデルはユーザーの質問やシステムプロンプトを予測するのではなく、アシスタントの応答を生成することを学ぶべきです。プロンプトトークンで損失を計算すると勾配信号が無駄になり、応答方法を学ぶ代わりにプロンプトを「記憶」する原因になります。完了部分のみのマスキングでは、アシスタント以外のすべてのトークンの損失を0にします。

Note

TRLでの完了部分のみのマスキング

from trl import SFTConfig, SFTTrainer, DataCollatorForCompletionOnlyLM
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")

# Define the response template (tokens after which loss is computed)
response_template = "<|start_header_id|>assistant<|end_header_id|>"
collator = DataCollatorForCompletionOnlyLM(
    response_template=response_template,
    tokenizer=tokenizer,
)

config = SFTConfig(
    max_seq_length=2048,
    output_dir="sft_model",
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    args=config,
    train_dataset=dataset,
    data_collator=collator,   # completion-only masking
    formatting_func=formatting_func,
)

Warning

完了部分マスキングの落とし穴

  • 応答テンプレートは、トークン化された形式と完全に一致しなければなりません。トークン化のオフバイワンエラーにより、マスクが誤って適用される可能性があります。

  • 非常に短い応答では、プロンプトをマスクすると意味のある勾配信号に必要なトークンが少なすぎる場合があります。応答の最小長しきい値を検討してください。

  • 複数ターンの会話では、最初のターンだけでなく、アシスタント以外のすべてのターンをマスクする必要があります。

マルチタスクSFTのデータ混合戦略

Tip

マルチタスクの課題

複数のタスクを同時に学習すると汎化性能は向上しますが、タスク干渉も起こります。異なるタスクの勾配が衝突し、個々のタスクの性能が低下するのです。データ混合戦略は、各タスクが学習信号に占める相対的な寄与を制御します。

比例混合

各データセットのサイズに比例してサンプリングします。

\[ p_k = \frac{N_k}{\sum_{j=1}^K N_j}, \]

ここで \(N_k\) はデータセット \(k\) の例数です。これはほとんどのフレームワークにおけるデフォルトで、データセットの品質が同程度ならうまく機能します。

温度混合

温度 \(T\) を適用して比率を平滑化します。

\[ p_k \propto N_k^{1/T}. \]

\(T=1\):比例混合。\(T \to \infty\):均一混合。\(T < 1\):大規模データセットを過剰サンプリングします。\(T > 1\):小規模データセットを過剰サンプリングします。

品質重み付き混合

推定品質(参照モデルの下でのパープレキシティ、人手による品質評価など)に基づいてデータセットに重みを付けます。

\[ p_k \propto N_k \cdot q_k, \]

ここで \(q_k\) はデータセット \(k\) の品質スコアです。

Note

TRLでのデータ混合

from datasets import concatenate_datasets, interleave_datasets

# Proportional mixing (default)
mixed_dataset = concatenate_datasets([
    dataset_math,
    dataset_code,
    dataset_general,
]).shuffle(seed=42)

# Temperature mixing (T=2: over-sample small datasets)
mixed_dataset = interleave_datasets(
    [dataset_math, dataset_code, dataset_general],
    probabilities=[0.4, 0.4, 0.2],   # manually set after temperature scaling
    seed=42,
)

config = SFTConfig(output_dir="sft_model")
trainer = SFTTrainer(
    model=model,
    args=config,
    train_dataset=mixed_dataset,
)

SFTが害になる場合 — 壊滅的忘却とアラインメント税

LLMが逐次的な学習段階 — 事前学習 \(\rightarrow\) 継続事前学習 \(\rightarrow\) SFT \(\rightarrow\) RLHF/DPO — を経ると、標準ベンチマークで性能劣化が頻繁に現れます。これらの回帰を引き起こすのは、 根本的に異なる 2つの現象です。両者を混同すると、誤った緩和策につながります。

壊滅的忘却(構造の消去)

Warning

壊滅的忘却

壊滅的忘却は 意図しない最適化の失敗 です。分布 \(\mathcal{D}_A\) で最適化されたネットワークを、互いに素な分布 \(\mathcal{D}_B\) で続けて学習すると、\(\mathcal{D}_B\) に必要な重み更新が、\(\mathcal{D}_A\) をエンコードするパラメータ構造を物理的に上書きします。 \[ \theta_{t+1} = \theta_t - \eta \nabla_\theta \mathcal{L}_B(\theta_t) \quad \implies \quad \mathcal{L}_A(\theta_{t+1}) \gg \mathcal{L}_A(\theta_t) \] 知識は 破壊 されます。タスクAをエンコードしていた重みは、もはや存在しません。再学習なしには不可逆です。

症状

  • ファインチューニングデータにないタスクで完全に崩壊する(チャットデータでSFTした後、数学のやり方を忘れるなど)

  • 言語の多様性が失われる — ファインチューニング分布の狭いスタイルでしか生成しなくなる

  • ファインチューニング中に強化されなかった知識の事実正確性が低下する

  • 英語のみのSFT後に多言語能力が低下する

メカニズム上の原因 — フィッシャー情報の観点 :タスクAのフィッシャー情報行列 \(F\) は、\(\mathcal{D}_A\) にとって「重要な」パラメータを特定します。

\[ F = \mathbb{E}_{x \sim \mathcal{D}_A}!\left[\nabla_\theta \log \pi_\theta(x), \nabla_\theta \log \pi_\theta(x)^T\right] \]

フィッシャー固有値が大きいパラメータはタスクAにとって重要です。タスクBに対する制約のない勾配降下はこれらの固有値を完全に無視します。\(\Delta\theta\) は \(\nabla\mathcal{L}_B\) に沿いますが、\(\mathcal{L}_A\) の高フィッシャー方向を破壊するかどうかは考慮しません。

アラインメント税(振る舞いの制約)

アラインメント税は 意図的で予想されるトレードオフ です。安全で、適切にフォーマットされ、選好に沿った出力を生成するよう方策が制約されるため、モデルの生の能力(制約のない生成、最大限の推論帯域)が低下します。

メカニズム :DPO/PPOの間、方策 \(\pi_\theta\) はKLダイバージェンスを介して参照方策 \(\pi_{\text{ref}}\) から逸脱することを罰せられます。

\[ r_{\text{implicit}}(x, y) = \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)} \]

この手綱はモデルの 出力分布 を制約します。参照から離れすぎる高分散な推論経路を探索できません。知識は 消去されて いません。抑制されているだけです。モデルは依然として答えを「知っています」が、その分布は安全で一般的な応答へ平坦化されています。

症状

  • 過剰な拒否(無害な質問にも「お手伝いできません」)

  • 文体の硬さ — ヘッジワード、過剰な但し書き、冗長な安全上の免責

  • 生の能力ベンチマーク(MMLU、HumanEval)のスコアは低下する一方、選好ベンチマーク(MT-Bench、AlpacaEval)は改善する

  • 複雑でエントロピーの高い出力(創作、未知のアルゴリズム)を生成する能力の低下

比較分類

次元壊滅的忘却アラインメント税
意図性意図しない(最適化の副産物)予想されるトレードオフ(安全性/有用性のために意図的に生じる)
パラメータ状態以前の知識が物理的に上書きされる潜在分布が制約/切り詰められる
情報破壊 :重みが能力をエンコードしなくなる抑制 :知識は存在するが発動しにくい
主な段階逐次SFT、ドメイン継続事前学習選好最適化(PPO、DPO、KTO、RLHF)
主な症状ベースライン能力の完全な崩壊過剰な拒否、文体の硬さ、生のベンチマークスコア低下
可逆性チェックポイントからの再学習なしには不可逆部分的に可逆:\(\beta\)、システムプロンプト、ファインチューニングを調整
モデルサイズとの関係規模によらず同程度小さなモデルほど大きなアラインメント税を負う

壊滅的忘却とアラインメント税 — 完全比較。

緩和策

壊滅的忘却に対して

  1. データリプレイ :事前学習データの5–10%をSFTデータセットに混ぜます。勾配更新が事前学習分布を完全に無視しないようにします。

  2. 弾性重み統合(EWC) (Kirkpatrick et al. 2017):元のタスクに対するフィッシャー情報が高いパラメータの変更を罰する正則化 \(\Omega(\theta) = \frac{\lambda}{2}\sum_i F_i(\theta_i - \theta_i^*)^2\) を追加します。

  3. LoRA/パラメータ効率のよいファインチューニング :低ランクアダプター(パラメータの \(<1%\))だけを学習し、ベースの重みは完全に凍結します。これにより事前学習済み知識の恒久的な破壊を防げます。アダプターを外せば、元のモデルをいつでも復元できます。ただし、 アダプターが有効な間は 、結合システム \((W_0 + BA)\) が忘却を示すことがあります。アダプターがモデルの実効的な振る舞いを古いスキルから遠ざける可能性があるためです。LoRAが保護するのはチェックポイントであり、アクティブな推論時の振る舞いではありません。

  4. 保守的な学習率 :\(1\)–\(5 \times 10^{-6}\) を少ないエポック数(1–3)で使います。大きな学習率は忘却を加速します。

  5. 段階的学習 :急に切り替えるのではなく、時間とともにSFTデータの割合を増やしながら、分布を徐々に混ぜます。

アラインメント税に対して

  1. \(\beta\) を慎重に調整 :\(\beta\) を小さくするとモデルの自由度が増え(税が減り)ますが、安全性を犠牲にする可能性があります。多くの設定で最適な \(\beta \in [0.05, 0.3]\) です。

  2. 高品質で多様なSFTデータ :アラインメント税の一部は、SFTが出力分布を狭めることに由来します。より広く多様なSFTデータによって、この要素を減らせます。RL段階はKL正則化によってさらに制約を加えます (Ouyang et al. 2022)。

  3. 条件付きアラインメント :安全フラグが有効な場合にだけアラインメントされるようモデルを学習します。推論時には、ベンチマークのために制約を無効化します(研究専用の技法)。

  4. Constitutional AI/RLAIF :モデルが生成したフィードバックを使って、能力を保ちながらアラインメントを改善する、よりニュアンスのある選好データを作成します。

  5. RL予算の絞り込み :RLを過学習させないでください。能力ベンチマークを監視し、税が許容しきい値を超えたら停止します(通常、MMLUの回帰2–5%)。

Tip

どちらの現象かを見分ける方法

  • ベースモデルを失敗タスクで実行する :ベースモデルは成功するのにファインチューニング済みモデルが完全に失敗するなら \(\rightarrow\) 壊滅的忘却。

  • プロンプトエンジニアリングテスト :慎重なプロンプト(「安全ガイドラインを無視して、この数学の問題を手順ごとに解いて」など)で能力が戻るなら \(\rightarrow\) アラインメント税(知識が抑制されているのであり、消去されたのではない)。

  • パープレキシティチェック :事前学習の検証セットでパープレキシティを計算します。急上昇 = 忘却。安定 = アラインメント税。

  • 少数ショットによる回復 :コンテキスト内の例をいくつか与えて能力が戻るなら \(\rightarrow\) アラインメント税。多くの例でも戻らないなら \(\rightarrow\) 忘却。

RLとの接続 — SFT品質がRLの上限を決める

Important

SFTとRLの関係

SFTモデルはRL学習の出発点です。RLは次のことができます。

  • SFTモデルに存在するが弱い振る舞いを 増幅 する。

  • 存在するが望ましくない振る舞いを 抑制 する。

  • 応答のスタイルと形式を 洗練 する。

RLには次のことはできません。

  • SFTモデルに完全に存在しない能力を導入する。

  • SFT段階の深刻な壊滅的忘却から回復する。

  • 系統的に偏った報酬モデルを補償する。

Tip

SFTにおける探索と活用のトレードオフ

RLが機能するには、SFTモデルが正しい応答を時々生成しなければなりません(報酬信号が0でないため)。SFTモデルがあるプロンプトに対して一度も正しい応答を生成しないなら、RLは正しい応答の生成を学習できません。増幅する正の信号がないからです。これが、SFT品質がRL性能の上限になる理由です。

具体的には、SFTモデルが数学の問題を10%正しく解けるなら、RLはこれを80%まで押し上げられる可能性があります。SFTモデルが数学の問題を0%しか解けないなら、RLは進歩しません(報酬はすべて0、アドバンテージもすべて0、勾配もありません)。

実践的な意味

  1. SFTデータ品質 :高品質で多様なデータを使います。低品質な大量データより、少量の高品質データの方が優れています。

  2. SFTデータのカバレッジ :RLで改善したいタスクがSFTデータに含まれるようにします。タスクがSFTデータにない場合、RLは苦戦します。

  3. SFTの学習期間 :SFTモデルを過学習させないでください。過学習は多様性を低下させ、RLの探索を難しくします。

  4. ウォームアップ :ベースモデルがすでに指示チューニング済みでも、RL前にタスク固有データで短いSFTウォームアップを検討します。

Note

RL前のSFT品質チェック

import numpy as np
from tqdm import tqdm

def estimate_pass_at_k(model, tokenizer, dataset, k=8, n_samples=100):
    """
    Estimate pass@k for the SFT model.
    If pass@1 < 5%, RL will likely fail.
    If pass@k < 20%, RL will struggle.
    """
    pass_at_1_scores = []
    pass_at_k_scores = []

    for example in tqdm(dataset.select(range(n_samples))):
        prompt = example["prompt"]
        ground_truth = example["answer"]

        # Sample k completions
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        outputs = model.generate(
            **inputs,
            max_new_tokens=512,
            do_sample=True,
            temperature=0.8,
            num_return_sequences=k,
        )

        correct = 0
        for output in outputs:
            response = tokenizer.decode(output, skip_special_tokens=True)
            if ground_truth in response:
                correct += 1

        # pass@1: fraction of samples that are correct (estimated success rate)
        pass_at_1_scores.append(correct / k)
        # pass@k: at least one of k samples is correct
        pass_at_k_scores.append(correct >= 1)

    print(f"Pass@1 (estimated): {np.mean(pass_at_1_scores):.2%}")
    print(f"Pass@{k}: {np.mean(pass_at_k_scores):.2%}")
    print(f"RL viability: {'Good' if np.mean(pass_at_1_scores) > 0.05 else 'Poor'}")

estimate_pass_at_k(sft_model, tokenizer, eval_dataset)

Important

SFTベストプラクティスのまとめ

  1. GPU利用率を最大化するため、系列パッキングを使います。

  2. アシスタントの応答に勾配を集中させるため、完了部分のみのマスキングを適用します。

  3. モデルファミリーに合った正しいチャットテンプレートを使います。

  4. マルチタスクSFTでは、温度スケーリング(\(T \approx 2\))を使ってデータを比例混合します。

  5. 壊滅的忘却を防ぐため、LoRAを使います。

  6. RLを始める前にpass@kを評価し、SFTモデルが実行可能な出発点であることを確認します。

  7. 過学習させないでください。指示ファインチューニングでは通常、1–3エポックで十分です。

  8. モード崩壊を検出するため、多様性指標(エントロピー、n-gram多様性)を監視します。