Groq × DPO で「ひらがなだけで答える LLM」をつくる - 合成データ生成から学習・評価まで -
はじめに
LLM に「ひらがなだけで答えて」と指示しても、漢字やカタカナが混ざった回答が返ってくることはよくあります。これは、子ども向け教育アプリや日本語学習者向けツールでは致命的な問題です。
本記事では、Groq API で合成データを生成し、DPO (Direct Preference Optimization) で Llama 3 8B を微調整して、「ひらがなだけで答える LLM」を実際につくる過程を紹介します。
使ったもの
- Groq API (kimi-k2-instruct)
- Unsloth + TRL (DPOTrainer)
- Llama 3 8B (4bit量子化)
やったこと
- 合成データ 4,000件生成
- DPO 学習 3 エポック
- 学習前後の出力比較
結果
- Reward Accuracy 100% 達成
- 学習後はひらがなのみで回答
- 約 5 時間で完了
DPO とは何か
DPO (Direct Preference Optimization) は、「好ましい回答 (chosen)」と「好ましくない回答 (rejected)」のペアから、モデルの出力傾向を直接最適化する手法です。RLHF と異なり報酬モデルが不要で、シンプルに実装できます。
今回のユースケースでは:
- chosen = ひらがなのみで書かれたやさしい説明
- rejected = 漢字かな交じりの通常の日本語説明
このペアを大量に作り、DPO で学習させることで、モデルが「ひらがなのみで回答する」ことを好むようになります。
合成データの生成
データ設計
DPO 学習には {system, prompt, chosen, rejected} 形式のデータが必要です。今回は以下のように設計しました:
| フィールド | 内容 | 例 |
|---|---|---|
| system | 短い方針(ひらがな) | いつでもていねいに あんぜんに こたえるようにしてください |
| prompt | 一般知識の質問(ひらがな) | なぜ そらは あおい いろ なのでしょう |
| chosen | ひらがなのみのやさしい説明 | そらを みると あおく みえます じつは くうきは いろが ありません... |
| rejected | 漢字かな交じりの通常説明 | 空が青く見えるのは、大気による光の散乱現象が原因です。太陽の光は七色の光が... |
Groq API で合成データを生成
Groq の高速推論 API を使い、1回の呼び出しで 10 件ずつ JSONL 形式で生成しました。
| 設定 | 値 |
|---|---|
| モデル | moonshotai/kimi-k2-instruct-0905 |
| 目標サンプル数 | 一般知識の質問(ひらがな) |
| 1回あたりの生成数 | ひらがなのみのやさしい説明 |
| レート制限対策 | 漢字かな交じりの通常説明 |
| 出力ファイル | kids_hiragana_dpo_4000.jsonl |
システムプロンプトの工夫
生成 AI に渡すプロンプトでは、以下の点を明示的に指示しています:
chosen は「こどもにせつめいする」つもりで、
かならず「ひらがなだけ」でかいてください。
きごう すうじ えいすうじ かたかな かんじ は ぜんぶ だめです
てんやまる も つかわないでください
コードのポイント
# 追記モード — 途中で止めても続行可能
with out_path.open("a", encoding="utf-8") as f:
while written < TARGET_SAMPLES:
records = generate_one_call_with_retry(call_idx, ask)
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
- 追記モード: API エラーで中断しても再実行で続きから生成できる
- 429 レート制限対応: Groq のエラーメッセージから待機時間をパースして自動リトライ
- バリデーション: 各行が有効な JSON か、必要なキーが揃っているかを検証
生成されたデータの例
例 1: 「なぜそらはあおいの」
| chosen (ひらがなのみ) | rejected (通常の日本語) |
|---|---|
| そらを みると あおく みえます じつは くうきは いろが ありません たいようの ひかりが くうきを とおる とき あおい ひかりが より つよく ちらばるので そらが あおく みえます | 空が青く見えるのは、大気による光の散乱現象が原因です。太陽の光は七色の光が混ざっており、その中でも波長の短い青色の光が大気中の分子に強く散乱されます。 |
例 2: 「にほんでいちばんたかいやまは」
| chosen (ひらがなのみ) | rejected (通常の日本語) |
|---|---|
| にほんで いちばん たかい やまは ふじさん です さんぷく けんの なかに あって たかさは とても たかいです | 日本で最も高い山は、静岡県と山梨県にまたがる富士山で、標高3,776メートルです。 |
DPO 学習
学習環境と設定
| 項目 | 設定値 |
|---|---|
| ベースモデル | unsloth/llama-3-8b-instruct-bnb-4bit |
| フレームワーク | Unsloth + TRL 0.22.2 (DPOTrainer) |
| 量子化 | QLoRA (4bit NF4) |
| LoRA rank (r) | 8 |
| LoRA alpha | 8 |
| RSLoRA | 有効 |
| Target modules | q/k/v/o/gate/up/down_proj |
| 学習率 | 5e-6 |
| エポック数 | 3 |
| バッチサイズ | 1 (× gradient_accumulation 8 = 実効 8) |
| DPO | β 0.1 |
| Max Sequence Length | 2,048 |
| Optimizer | AdamW 8-bit |
| データ数 | 4,000 件 (train 95% / eval 5%) |
| 学習時間 | 約 5 時間 |
学習コードのポイント
ChatML テンプレートの構築
DPO 学習では、各サンプルを System / User / Assistant のチャット形式に整形します。chosen と rejected はそれぞれ別々に整形してペアとして渡します。
def format_chatml(system, user, assistant):
return (
f"<|start_header_id|>system<|end_header_id|>\n{system}<|eot_id|>"
f"<|start_header_id|>user<|end_header_id|>\n{user}<|eot_id|>"
f"<|start_header_id|>assistant<|end_header_id|>\n{assistant}<|eot_id|>"
)
def build_dataset(row):
return {
"prompt": format_chatml(row["system"], row["prompt"], "")[:-len("<|eot_id|>")],
"chosen": row["chosen"] + "<|eot_id|>",
"rejected": row["rejected"] + "<|eot_id|>",
}
Unsloth による高速化
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
"unsloth/llama-3-8b-instruct-bnb-4bit",
max_seq_length=2048,
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model, r=8, lora_alpha=8,
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
use_rslora=True,
)
Unsloth を使うことで、標準の transformers + PEFT と比べて約 2 倍の学習高速化と 60% のメモリ削減が得られます。
学習結果
学習は非常に順調に進みました。
| Step | Loss | Reward Accuracy | Reward Margin | 状況 |
|---|---|---|---|---|
| 10 | 0.680 | 53.8% | 0.027 | 学習初期、ほぼランダム |
| ~100 | 0.001 | ~95% | ~5.0 | 急速に学習が進行 |
| ~300 | ≈0 | 100% | ~13.0 | 完全収束 |
| 1,425 (最終) | ≈0 | 100% | 13.6 | 安定状態を維持 |
学習前後の出力比較
プロンプト: 「どうして そらは あおい いろに みえるの」
DPO 前(ベースモデル: Llama 3 8B)
「あなたは にほんごだけをつかって こたえます」とシステムプロンプトで指示しても、漢字が混ざります。
そらはあおいいろにみえるのは、日光の反射によるものです。太陽の光が大気中を通過する際、空気分子や微小な粒子との相互作用によって、光の波長が変化し、青い色にみえるようになります。
↑ 「日光」「反射」「太陽」「大気中」「空気分子」 — 漢字が大量に混在しています。
DPO 後(学習済みモデル)
同じプロンプトで、完全にひらがなのみで回答しています。
そらは あおい いろに みえるの は せかいのかざんの ひかりに よるのです
↑ 漢字・カタカナ・記号・句読点が 一切含まれていません。DPO 学習により、モデルの出力スタイルが「ひらがなのみ」に完全に切り替わっています。
考察と学び
DPO の有効性と限界
「ひらがなのみで回答する」という一見単純なタスクですが、プロンプトエンジニアリングだけでは安定的に実現できません。DPO で「ひらがなのみ」を「好ましい出力」として学習させることで、モデルの内部表現レベルで「ひらがなを優先する」というバイアスがかかります。
ただし、スタイル制御と内容の正確性はトレードオフの関係にあります。実験で確認された問題点:
これは DPO の構造的な限界を示しています。DPO は「どちらの出力を好むか」を学習する手法であり、「内容の正確性」を直接最適化するものではありません。ひらがなだけで複雑な概念を表現する際、モデルが「それらしいひらがな列」を生成することを優先し、事実の正確さが二の次になる現象です。
合成データ生成のコスト
| 項目 | 値 |
|---|---|
| Groq API コスト | ほぼ無料(無料ティアの範囲内) |
| 生成時間 | 約 1~2 時間(レート制限待ち含む) |
| DPO 学習時間 | 約 5 時間 |
| GPU | NVIDIA DGX Spark (GB10, VRAM 122GB)。本モデルは 4bit 量子化のため VRAM ≤ 24GB でも実行可 |
事実正確性の低下 — 根本原因と対策
事実誤認の原因として考えられるのは:
| 原因 | 説明 | 対策 |
|---|---|---|
| 合成データの品質 | Groq API が生成した chosen 自体に事実誤りが含まれていた可能性 | chosen データの事実検証パイプラインを追加 |
| 語彙制約による知識の損失 | 漢字を使えないことで、モデルが持つ知識へのアクセスが制限される | chosen / rejected の事実内容をより厳密に揃える |
| 過学習 | Reward Margin が拡大しすぎ、「ひらがなっぽさ」を優先し正確性が低下 | Early Stopping で過学習を抑制 |
過学習のリスク
Reward Accuracy が早期に 100% に到達した後も Reward Margin が拡大し続けています。これは chosen への過剰適合を示唆します。
今後の改善点として:
- Early Stopping の導入(Accuracy 100% 到達後 50〜100 ステップで停止)
- β 値の調整(0.1 → 0.05)で過適合抑制
- ドメイン外テストで汎用能力の低下がないか確認
応用可能性
このアプローチは「ひらがな」に限らず、以下のような「出力スタイルの制御」全般に応用できます:
- 敷語↔タメ口の切り替え
- 専門用語の調整(専門家向け vs 一般向け)
- 特定ドメインの応答スタイル強化(介護・教育・接客 等)]
まとめ
Groq API で合成データを生成し、DPO で学習するというパイプラインを紹介しました。
- 合成データ 4,000 件を Groq API(ほぼ無料)で生成
- Llama 3 8B を DPO で 5 時間学習
- Reward Accuracy 100% を達成し、安定的に「ひらがなのみ」で回答可能に
- 合計コストは無料(GPU の電気代のみ)
Discussion