1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LoRAで小規模データセットから日本語LLMを追加学習する

1
Last updated at Posted at 2026-10-20

社内用語や製品固有の回答など、汎用LLMが知らない知識を日本語で答えさせたい場合があります。しかし、モデル全体を追加学習するフルファインチューニングは、GPUメモリと計算時間の負担が大きくなります。

そこで今回は、LoRA(Low-Rank Adaptation)を使って、小規模な日本語データセットからLLMを追加学習します。私が実際に試した構成では、ベースモデルの重みを固定し、少数の追加パラメータだけを学習することで、比較的扱いやすい手順にできました。

重要なのは、データを増やすことよりも「回答形式を揃えること」と「過学習を確認すること」です。

1. 使用するモデルと環境

今回は日本語を含む多言語対応モデルとして、Qwen/Qwen2.5-1.5B-Instructを使います。1.5Bクラスなので、より大きなモデルよりも検証しやすい構成です。

必要なライブラリをインストールします。

pip install torch transformers datasets peft accelerate

GPUを使う場合は、CUDAに対応したPyTorchを先にインストールしてください。学習前に、GPUが認識されているか確認します。

python -c "import torch; print(torch.cuda.is_available())"

LoRAではベースモデルの全パラメータを更新しないため、通常の追加学習よりも必要メモリを抑えられます。ただし、モデルサイズや系列長によって必要なメモリは変わるため、最初は小さなバッチサイズで試すのがおすすめです。

2. 小規模データセットを用意する

学習データはJSONL形式にしました。1行を1サンプルとして、ユーザーの質問と期待する回答を保存します。

train.jsonlの例です。

{"instruction":"LoRAとは何ですか?","output":"LoRAは、元のモデル重みを固定したまま、低ランク行列を追加して効率的に追加学習する手法です。"}
{"instruction":"社内申請の受付時間を教えてください。","output":"社内申請の受付時間は、平日の午前9時から午後5時までです。"}
{"instruction":"製品Aの特徴を3つ説明してください。","output":"製品Aの特徴は、導入が簡単であること、管理画面が日本語に対応していること、既存システムと連携しやすいことです。"}

実際のデータでは、回答の文体や敬語をできるだけ統一します。質問によって「です・ます調」と「だ・である調」が混在すると、学習後の出力にも揺れが出やすくなります。

また、同じ質問に対して回答が複数ある場合は、正解を一つに決めておきます。小規模データでは、重複データや矛盾した回答の影響が特に大きくなります。

3. LoRAで追加学習する

小規模データセットの準備から学習後の評価までの流れは次の通りです。

以下が学習スクリプトです。回答部分だけを損失計算の対象にするため、ユーザー入力部分のラベルを-100でマスクしています。

import json
import torch
from datasets import Dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    Trainer,
    TrainingArguments,
)
from peft import LoraConfig, get_peft_model

MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"
OUTPUT_DIR = "./lora-japanese"

with open("train.jsonl", "r", encoding="utf-8") as f:
    records = [json.loads(line) for line in f]

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

def tokenize_record(record):
    messages = [
        {"role": "user", "content": record["instruction"]},
        {"role": "assistant", "content": record["output"]},
    ]
    prompt_messages = [
        {"role": "user", "content": record["instruction"]},
    ]

    prompt_text = tokenizer.apply_chat_template(
        prompt_messages,
        tokenize=False,
        add_generation_prompt=True,
    )
    full_text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=False,
    )

    prompt_ids = tokenizer(
        prompt_text,
        add_special_tokens=False,
    )["input_ids"]
    full_ids = tokenizer(
        full_text,
        add_special_tokens=False,
        truncation=True,
        max_length=512,
    )["input_ids"]

    prompt_length = min(len(prompt_ids), len(full_ids))
    labels = [-100] * prompt_length + full_ids[prompt_length:]

    return {
        "input_ids": full_ids,
        "attention_mask": [1] * len(full_ids),
        "labels": labels,
    }

dataset = Dataset.from_list(records)
tokenized_dataset = dataset.map(
    tokenize_record,
    remove_columns=dataset.column_names,
)

def collate_fn(features):
    max_length = max(len(x["input_ids"]) for x in features)

    input_ids = []
    attention_mask = []
    labels = []

    for feature in features:
        padding = max_length - len(feature["input_ids"])
        input_ids.append(
            feature["input_ids"] + [tokenizer.pad_token_id] * padding
        )
        attention_mask.append(
            feature["attention_mask"] + [0] * padding
        )
        labels.append(
            feature["labels"] + [-100] * padding
        )

    return {
        "input_ids": torch.tensor(input_ids, dtype=torch.long),
        "attention_mask": torch.tensor(attention_mask, dtype=torch.long),
        "labels": torch.tensor(labels, dtype=torch.long),
    }

use_bf16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported()
use_fp16 = torch.cuda.is_available() and not use_bf16

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16 if use_bf16 else (
        torch.float16 if use_fp16 else torch.float32
    ),
)

lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    num_train_epochs=3,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    logging_steps=10,
    save_strategy="epoch",
    report_to="none",
    fp16=use_fp16,
    bf16=use_bf16,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    data_collator=collate_fn,
)

trainer.train()
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)

rはLoRAの低ランクの大きさです。小規模データでまず試すなら、r=8程度から始めると調整しやすいです。学習率を高くしすぎると、数件の回答をそのまま暗記するような過学習が起きやすいため、学習後の生成結果を必ず確認します。

4. 学習済みLoRAを使って推論する

保存したLoRAアダプターをベースモデルに読み込み、質問を入力します。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

BASE_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
ADAPTER_DIR = "./lora-japanese"

tokenizer = AutoTokenizer.from_pretrained(ADAPTER_DIR)
base_model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL,
    torch_dtype="auto",
    device_map="auto",
)
model = PeftModel.from_pretrained(base_model, ADAPTER_DIR)

messages = [
    {
        "role": "user",
        "content": "社内申請の受付時間を教えてください。",
    }
]

inputs = tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True,
    return_tensors="pt",
).to(model.device)

with torch.no_grad():
    outputs = model.generate(
        inputs,
        max_new_tokens=128,
        do_sample=False,
    )

answer = tokenizer.decode(
    outputs[0][inputs.shape[-1]:],
    skip_special_tokens=True,
)

print(answer)

評価では、学習に使った質問だけでなく、似ているが少し表現の異なる質問も用意します。学習データと完全に同じ質問に正答できても、言い換えに対応できなければ、実用上は不十分です。

私が確認するときは、次の3種類に分けています。

  • 学習データと同じ質問
  • 表現を変えた類似質問
  • 学習データに存在しない一般的な質問

最後の質問で急に不自然な回答になったり、特定の回答を繰り返したりする場合は、エポック数を減らす、データを増やす、または学習率を下げると改善することがあります。

5. LoRA学習で注意したこと

小規模データセットでは、LoRAを使っても万能に知識を追加できるわけではありません。特に数件のデータだけで広い分野の知識を覚えさせようとすると、元モデルの能力を損なう可能性があります。

今回のような用途では、モデルに新しい知識を大量に記憶させるというより、次のような目的に向いています。

  • 特定の回答形式に揃える
  • 社内用語に合わせた返答をさせる
  • FAQのような限定された質問に対応する
  • 文体や敬語を統一する

事実情報を頻繁に更新する必要がある場合は、LoRAだけで解決しようとせず、検索結果をプロンプトに渡すRAGと組み合わせる方が運用しやすいケースもあります。

まとめ

LoRAを使うと、モデル全体を更新せずに、日本語の小規模データセットから特定の応答形式や専門用語を追加学習できます。

実装上のポイントは、以下の通りです。

  • JSONLで質問と回答の形式を統一する
  • 回答部分だけを損失計算の対象にする
  • 最初は小さなLoRA設定と少ないエポック数で試す
  • 学習済みデータ以外の言い換え質問でも評価する
  • 頻繁に変わる知識はRAGとの役割分担を考える

まずは数十件程度のきれいなデータで学習し、出力の変化を確認してからデータ量やモデルサイズを増やすのが、失敗しにくい進め方です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?