Instruction Tuningの理論と実装

ChatGPTやClaudeのようなLLMが自然な指示に従えるのは、Instruction Tuning(指示チューニング)と呼ばれるファインチューニング手法のおかげです。

本記事では、事前学習済みモデルを指示に従うモデルに変換するInstruction Tuningの仕組みを、数学的な観点から解説します。

本記事の内容

  • Instruction Tuningの基本概念
  • 学習の数学的定式化
  • データセット設計と品質管理
  • Pythonでの実装例

前提知識

この記事を読む前に、以下の概念を理解しておくと役立ちます。

  • Transformerアーキテクチャの基礎
  • 言語モデルの事前学習
  • 交差エントロピー損失

事前学習済みLLMの限界

事前学習モデルの振る舞い

GPTのような事前学習済みモデルは、次のトークンを予測するよう訓練されています。

$$ P_\theta(x_t | x_1, x_2, \ldots, x_{t-1}) $$

これは「テキストの続きを書く」能力であり、「質問に答える」「要約する」といった指示に従う能力ではありません。

例えば、「日本の首都は?」という入力に対して: – 事前学習モデル: 「という問いに対する答えは…」(続きを書く) – 指示チューニング済みモデル: 「東京です。」(質問に答える)

指示に従うための追加学習

Instruction Tuningは、事前学習モデルに「指示 → 適切な応答」のパターンを学習させることで、指示に従う能力を獲得させます。

Instruction Tuningの数学的定式化

学習データの形式

学習データは以下の形式のペアで構成されます。

$$ \mathcal{D} = \{(\text{instruction}_i, \text{response}_i)\}_{i=1}^{N} $$

指示と応答を連結した系列:

$$ x = [\text{instruction}; \text{response}] $$

損失関数

応答部分に対する負の対数尤度を最小化します。

$$ \mathcal{L}(\theta) = -\sum_{i=1}^{N} \sum_{t=1}^{T_i} \log P_\theta(r_t^{(i)} | \text{instruction}^{(i)}, r_1^{(i)}, \ldots, r_{t-1}^{(i)}) $$

ここで $r_t^{(i)}$ は $i$ 番目のサンプルの応答の $t$ 番目のトークンです。

指示部分のマスキング

重要なポイントとして、指示部分は予測対象から除外します。これは、モデルに「指示を生成する」のではなく「指示に従って応答する」ことを学習させるためです。

損失の計算:

$$ \mathcal{L} = -\sum_{t \in \text{response}} \log P_\theta(x_t | x_{

データセット設計

多様なタスクの包含

高品質なInstruction Tuningには、多様なタスクを含むデータセットが必要です。

タスクカテゴリ
質問応答 「フランスの首都は?」→「パリ」
要約 「以下の文章を要約: …」→「要約文」
翻訳 「以下を英語に翻訳: …」→「英訳」
分類 「以下の感情を判定: …」→「ポジティブ」
創作 「俳句を書いて」→「古池や…」
推論 「AがBより大きく、BがCより大きい時…」→「AがCより大きい」

指示のフォーマット

統一されたフォーマットを使用することで、学習効率が向上します。

### 指示:
{instruction}

### 入力:
{input}

### 応答:
{response}

または:

<s>[INST] {instruction} [/INST] {response}</s>

Pythonでの実装

データセットの準備

from datasets import Dataset

def create_instruction_dataset():
    """指示チューニング用データセットの作成"""
    data = [
        {
            "instruction": "以下の文章を要約してください。",
            "input": "人工知能(AI)は、機械が人間のような知能を持つ技術です。機械学習はAIの一分野で、データから学習するアルゴリズムを研究します。深層学習は、多層のニューラルネットワークを使った機械学習の手法です。",
            "output": "AIは機械に知能を持たせる技術で、機械学習はデータから学習し、深層学習は多層ニューラルネットワークを使用します。"
        },
        {
            "instruction": "以下の質問に答えてください。",
            "input": "日本で最も高い山は何ですか?",
            "output": "富士山です。標高は3,776メートルです。"
        },
        {
            "instruction": "以下の英文を日本語に翻訳してください。",
            "input": "Machine learning is a subset of artificial intelligence.",
            "output": "機械学習は人工知能の一分野です。"
        },
        {
            "instruction": "以下の文章の感情を「ポジティブ」「ネガティブ」「中立」で分類してください。",
            "input": "今日は素晴らしい天気で、とても気分がいいです。",
            "output": "ポジティブ"
        },
    ]

    return Dataset.from_list(data)

def format_instruction(sample):
    """サンプルをプロンプト形式に変換"""
    if sample["input"]:
        prompt = f"""### 指示:
{sample["instruction"]}

### 入力:
{sample["input"]}

### 応答:
{sample["output"]}"""
    else:
        prompt = f"""### 指示:
{sample["instruction"]}

### 応答:
{sample["output"]}"""

    return {"text": prompt}

# データセット作成
dataset = create_instruction_dataset()
dataset = dataset.map(format_instruction)

print("Sample data:")
print(dataset[0]["text"])

ファインチューニングの実装

import torch
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling
)

def prepare_model_and_tokenizer(model_name="gpt2"):
    """モデルとトークナイザーを準備"""
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name)

    # パディングトークンの設定
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
        model.config.pad_token_id = tokenizer.eos_token_id

    return model, tokenizer

def tokenize_function(examples, tokenizer, max_length=512):
    """テキストをトークン化"""
    return tokenizer(
        examples["text"],
        truncation=True,
        max_length=max_length,
        padding="max_length"
    )

def train_instruction_model(dataset, model, tokenizer, output_dir="./instruction_model"):
    """Instruction Tuningを実行"""
    # トークン化
    tokenized_dataset = dataset.map(
        lambda x: tokenize_function(x, tokenizer),
        remove_columns=dataset.column_names
    )

    # データコレーター
    data_collator = DataCollatorForLanguageModeling(
        tokenizer=tokenizer,
        mlm=False  # Causal LMなのでFalse
    )

    # 学習設定
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=3,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,
        learning_rate=2e-5,
        warmup_steps=100,
        logging_steps=10,
        save_steps=500,
        fp16=torch.cuda.is_available(),
    )

    # トレーナー
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=data_collator,
    )

    # 学習実行
    trainer.train()

    return model

# 使用例(実行にはGPUが必要)
# model, tokenizer = prepare_model_and_tokenizer("gpt2")
# trained_model = train_instruction_model(dataset, model, tokenizer)

推論の実装

def generate_response(model, tokenizer, instruction, input_text="", max_length=256):
    """学習済みモデルで応答を生成"""
    if input_text:
        prompt = f"""### 指示:
{instruction}

### 入力:
{input_text}

### 応答:
"""
    else:
        prompt = f"""### 指示:
{instruction}

### 応答:
"""

    inputs = tokenizer(prompt, return_tensors="pt")

    with torch.no_grad():
        outputs = model.generate(
            inputs["input_ids"],
            max_length=max_length,
            num_return_sequences=1,
            temperature=0.7,
            do_sample=True,
            pad_token_id=tokenizer.pad_token_id
        )

    response = tokenizer.decode(outputs[0], skip_special_tokens=True)

    # 応答部分のみを抽出
    if "### 応答:" in response:
        response = response.split("### 応答:")[-1].strip()

    return response

# 使用例
# response = generate_response(trained_model, tokenizer, "猫について説明してください")
# print(response)

応答部分のみの損失計算

指示部分を損失計算から除外する実装です。

import torch
import torch.nn as nn

class InstructionTuningLoss(nn.Module):
    def __init__(self, ignore_index=-100):
        super().__init__()
        self.ignore_index = ignore_index
        self.loss_fn = nn.CrossEntropyLoss(ignore_index=ignore_index)

    def create_labels(self, input_ids, instruction_lengths):
        """応答部分のみをラベルとするマスクを作成"""
        labels = input_ids.clone()

        # 指示部分をマスク(ignore_indexに設定)
        for i, inst_len in enumerate(instruction_lengths):
            labels[i, :inst_len] = self.ignore_index

        return labels

    def forward(self, logits, input_ids, instruction_lengths):
        """損失を計算"""
        # ラベルを作成
        labels = self.create_labels(input_ids, instruction_lengths)

        # シフトして損失計算(次トークン予測)
        shift_logits = logits[..., :-1, :].contiguous()
        shift_labels = labels[..., 1:].contiguous()

        # 損失計算
        loss = self.loss_fn(
            shift_logits.view(-1, shift_logits.size(-1)),
            shift_labels.view(-1)
        )

        return loss

# 使用例
# loss_fn = InstructionTuningLoss()
# loss = loss_fn(model_output.logits, input_ids, instruction_lengths)

スケーリング則

Instruction Tuningの効果はモデルサイズとデータ量に依存します。

モデルサイズの影響

経験的に、以下の関係が観察されています。

$$ \text{Performance} \propto \log(\text{Model Size}) $$

大きなモデルほど、少ないデータでも指示に従う能力を獲得しやすい傾向があります。

データ量の影響

データ量 $N$ と性能の関係:

$$ \text{Performance} \approx a \cdot N^{-b} + c $$

初期は急速に改善し、徐々に飽和します。

タスク多様性

タスクの多様性が高いほど、汎化性能が向上します。FLAN-T5の研究では、1,800以上のタスクを使用することで、未知のタスクへの転移能力が大幅に向上しました。

代表的なデータセット

データセット サイズ 特徴
FLAN 62タスク 多様なNLPタスク
Super-Natural Instructions 1,600+タスク 大規模・多言語
Alpaca 52K GPT-3.5で生成
Dolly 15K 人手で作成
OpenAssistant 160K 対話形式

品質向上のテクニック

データ品質の重要性

少量でも高品質なデータが、大量の低品質データより効果的です。

LIMA(Less Is More for Alignment)の研究では、わずか1,000件の高品質データでも優れた性能を達成しました。

データ拡張

def augment_instruction(instruction, input_text, output):
    """指示の言い換えによるデータ拡張"""
    variations = [
        f"次の指示に従ってください: {instruction}",
        f"以下のタスクを実行: {instruction}",
        f"あなたの仕事: {instruction}",
        instruction,  # オリジナル
    ]

    augmented = []
    for var in variations:
        augmented.append({
            "instruction": var,
            "input": input_text,
            "output": output
        })

    return augmented

Self-Instruct

LLM自身を使って指示データを生成する手法です。

def generate_instruction_data(llm_client, seed_tasks, n_generate=10):
    """Self-Instructによるデータ生成(疑似コード)"""
    prompt = f"""以下は指示と応答のペアの例です:

{format_seed_tasks(seed_tasks)}

同様の形式で、新しい指示と応答のペアを{n_generate}個生成してください:"""

    response = llm_client.generate(prompt)
    new_tasks = parse_generated_tasks(response)

    return new_tasks

まとめ

本記事では、Instruction Tuningの仕組みと実装方法を解説しました。

  • 目的: 事前学習モデルを指示に従うモデルに変換
  • 損失関数: 応答部分のみに対する交差エントロピー損失
  • データ設計: 多様なタスクと統一されたフォーマットが重要
  • スケーリング: モデルサイズとデータ品質が性能を左右

次のステップとして、以下の記事も参考にしてください。