社内FAQや製品サポートの回答を、毎回プロンプトに大量に詰め込む方法には限界があります。回答形式や用語の使い方が安定しない場合は、既存の日本語チャットモデルにFAQの応答例を追加学習させる方法が有効です。
この記事では、Qwen2.5-1.5B-InstructにLoRAを適用し、日本語FAQ対応モデルを作成します。私が実際に試した構成では、ベースモデル本体を書き換えず、数十〜数百件程度のFAQデータから小さなアダプターを作成できました。
データセットを作る
FAQデータの準備から学習後の評価までの流れを図にすると次の通りです。
まず、FAQをJSONL形式で用意します。1行が1会話です。質問文の言い換えや、「わからない場合」の例も含めるのがポイントです。
{"messages":[{"role":"system","content":"あなたは製品サポート担当です。FAQに基づいて簡潔に回答してください。情報がない場合は、推測せず担当者への確認を案内してください。"},{"role":"user","content":"パスワードを忘れました。"},{"role":"assistant","content":"ログイン画面の「パスワードを忘れた場合」から、登録済みメールアドレスを入力して再設定してください。メールが届かない場合は迷惑メールフォルダーも確認してください。"}]}
{"messages":[{"role":"system","content":"あなたは製品サポート担当です。FAQに基づいて簡潔に回答してください。情報がない場合は、推測せず担当者への確認を案内してください。"},{"role":"user","content":"請求書はいつ発行されますか?"},{"role":"assistant","content":"請求書は毎月末締めで、翌月5営業日以内に登録済みメールアドレスへ送付されます。"}]}
実データでは、氏名、メールアドレス、顧客番号などの個人情報を除去します。また、質問と回答の対応が曖昧なデータを混ぜると、モデルが誤った関連付けを学習します。
私は最初から完璧なFAQを作ろうとせず、カテゴリごとに代表的な質問を集めました。質問の表記揺れを追加すると、実際の利用者の入力に対しても安定しやすくなります。
環境を準備する
今回はPython 3.11、CUDAが利用できるGPU、Hugging Face Transformers、Datasets、PEFTを使います。
python -m venv .venv
source .venv/bin/activate
pip install torch transformers datasets peft accelerate sentencepiece
Windowsの場合は、仮想環境の有効化を次のようにします。
.venv\Scripts\activate
チャットモデルは、モデルごとに会話の区切り方が異なります。そのため、単純に質問:や回答:という文字列を連結するのではなく、Tokenizerのチャットテンプレートを使います。apply_chat_templateは、モデルに合った形式へメッセージを変換するためのAPIです。
詳しくはTransformersのチャットテンプレート documentationと、LoRAの設定項目をまとめたPEFTのドキュメントを確認できます。
LoRAで追加学習する
以下が学習用スクリプトです。faq.jsonlを読み込み、8対2に分割して検証用データも確保します。
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
)
from peft import LoraConfig, get_peft_model, TaskType
BASE_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
raw = load_dataset("json", data_files="faq.jsonl")["train"]
raw = raw.train_test_split(test_size=0.2, seed=42)
def make_text(example):
text = tokenizer.apply_chat_template(
example["messages"],
tokenize=False,
add_generation_prompt=False,
)
return {"text": text}
formatted = raw.map(make_text)
def tokenize(batch):
return tokenizer(
batch["text"],
truncation=True,
max_length=1024,
)
tokenized = formatted.map(
tokenize,
batched=True,
remove_columns=formatted["train"].column_names,
)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype="auto",
)
model.config.use_cache = False
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
args = TrainingArguments(
output_dir="./faq-lora",
per_device_train_batch_size=1,
per_device_eval_batch_size=1,
gradient_accumulation_steps=8,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=10,
eval_strategy="steps",
eval_steps=50,
save_steps=50,
save_total_limit=2,
bf16=True,
report_to="none",
)
collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False,
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized["train"],
eval_dataset=tokenized["test"],
data_collator=collator,
)
trainer.train()
trainer.save_model("./faq-lora/adapter")
tokenizer.save_pretrained("./faq-lora/adapter")
学習を開始します。
python train.py
bf16=Trueは対応GPUが必要です。対応していない場合は、GPU環境に応じてfp16=Trueへ変更します。メモリが足りない場合は、max_length、バッチサイズ、ベースモデルのサイズを小さくします。
LoRAでは、ベースモデルの全パラメーターではなく、指定した層に低ランクの差分を学習させます。学習後に保存されるのは主にアダプターなので、モデル全体を保存するより扱いやすいのが利点です。
推論して回答を確認する
学習したアダプターを読み込んで回答を生成します。
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
BASE_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
ADAPTER = "./faq-lora/adapter"
tokenizer = AutoTokenizer.from_pretrained(ADAPTER)
base = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype="auto",
device_map="auto",
)
model = PeftModel.from_pretrained(base, ADAPTER)
model.eval()
messages = [
{
"role": "system",
"content": "あなたは製品サポート担当です。FAQに基づいて簡潔に回答してください。",
},
{"role": "user", "content": "請求書はいつ届きますか?"},
]
input_ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
).to(model.device)
with torch.no_grad():
output = model.generate(
input_ids,
max_new_tokens=200,
do_sample=False,
)
answer = output[0][input_ids.shape[1]:]
print(tokenizer.decode(answer, skip_special_tokens=True))
ここで重要なのは、学習時と推論時でsystemメッセージやチャットテンプレートを大きく変えないことです。形式が変わると、学習した応答パターンがうまく再現されない場合があります。
評価で確認すること
検証用データのlossだけでは、FAQとして正しい回答かどうかまでは判断できません。私は次の3種類で確認しました。
まず、学習に使っていない質問を入力し、回答がFAQの内容と一致するかを確認します。次に、同じ質問を少し言い換えて、回答できるかを確認します。最後に、FAQに存在しない質問を入力し、推測で断定せず確認を促せるかを確認します。
簡単なキーワード評価は次のように書けます。
tests = [
{
"question": "パスワードを再設定したいです",
"must_include": ["パスワード", "再設定"],
},
{
"question": "登録内容を変更できますか?",
"must_include": ["確認"],
},
]
def contains_required(text, words):
return all(word in text for word in words)
for case in tests:
# 実際には先ほどの生成処理を関数化して呼び出す
answer = ask_model(case["question"])
ok = contains_required(answer, case["must_include"])
print(ok, case["question"], answer)
キーワード一致率は補助指標です。最終的には、正確性、不要な断定、個人情報の漏えい、回答の長さ、担当者への引き継ぎ表現を人手で確認します。
まとめ
LoRAを使うと、日本語FAQへの回答傾向を小さなアダプターとして追加学習できます。重要なのは、学習手法よりもデータの品質と評価設計です。
特に、個人情報の除去、質問と回答の正しい対応付け、未知の質問への応答例、学習に使っていないデータでの検証を忘れないようにします。まずは小規模なFAQで試し、ベースモデルとの差分を確認してからデータを増やす進め方がおすすめです。