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には、多様なタスクを含むデータセットが必要です。 統一されたフォーマットを使用することで、学習効率が向上します。 または: 指示部分を損失計算から除外する実装です。 Instruction Tuningの効果はモデルサイズとデータ量に依存します。 経験的に、以下の関係が観察されています。 $$
\text{Performance} \propto \log(\text{Model Size})
$$ 大きなモデルほど、少ないデータでも指示に従う能力を獲得しやすい傾向があります。 データ量 $N$ と性能の関係: $$
\text{Performance} \approx a \cdot N^{-b} + c
$$ 初期は急速に改善し、徐々に飽和します。 タスクの多様性が高いほど、汎化性能が向上します。FLAN-T5の研究では、1,800以上のタスクを使用することで、未知のタスクへの転移能力が大幅に向上しました。 少量でも高品質なデータが、大量の低品質データより効果的です。 LIMA(Less Is More for Alignment)の研究では、わずか1,000件の高品質データでも優れた性能を達成しました。 LLM自身を使って指示データを生成する手法です。 本記事では、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)
スケーリング則
モデルサイズの影響
データ量の影響
タスク多様性
代表的なデータセット
データセット
サイズ
特徴
FLAN
62タスク
多様なNLPタスク
Super-Natural Instructions
1,600+タスク
大規模・多言語
Alpaca
52K
GPT-3.5で生成
Dolly
15K
人手で作成
OpenAssistant
160K
対話形式
品質向上のテクニック
データ品質の重要性
データ拡張
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
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
まとめ