🔧

QLoRAを使って文法修正モデルをファインチューニングしてみた

に公開

※これはU-ZERO Advent Calendar 2025の5日目の記事です。前日の記事はこちら

はじめに

株式会社U-ZEROで働いているエンジニアです。私たちの組織では、k-meansなどの機械学習を実装したり、LLMなどのAI技術を活用したりしています。

今回は、QLoRA(Quantized Low-Rank Adaptation)を使用して、Gemma 2Bモデルを文法修正タスクにファインチューニングし、その効果を検証してみました。

QLoRAはLoRAに4bit量子化を組み合わせることで、さらにメモリ効率を高め、低リソース環境でもファインチューニングが可能になる手法です。


LoRA / QLoRAとは

概要

LoRA(Low-Rank Adaptation)は、大規模言語モデル(LLM)を効率的にファインチューニングする手法です。2021年にMicrosoftの研究者によって提案されました。

QLoRAはこれに量子化を組み合わせた手法で、GPUメモリ使用量をさらに削減できます。

従来のファインチューニングの課題

従来のファインチューニングでは、モデルの全パラメータを更新する必要がありました。例えば、GPT-3のような175Bパラメータのモデルをファインチューニングするには:

  • 膨大なGPUメモリが必要
  • 学習に非常に長い時間がかかる
  • タスクごとにモデル全体のコピーを保存する必要がある

LoRAのアプローチ

LoRAは、事前学習済みモデルの重みは固定したまま、低ランク行列を追加で学習します。

W' = W + ΔW = W + BA
  • W: 元の重み行列(固定)
  • B, A: 低ランク行列(学習対象)
  • r: ランク(ハイパーパラメータ)

例えば、元の重み行列が d × k の場合:

  • A: r × k 行列
  • B: d × r 行列

ランク r を小さく設定することで、学習パラメータ数を大幅に削減できます。

LoRA / QLoRAのメリット

メリット 説明
メモリ効率 学習パラメータが大幅に削減(本実験では全体の0.12%のみ)
学習速度 更新するパラメータが少ないため高速
保存効率 LoRAアダプターのみ保存すればよい(数MB程度)
切り替え容易 同じベースモデルに対して複数のLoRAアダプターを切り替え可能
低リソースでの学習 4bit量子化により、GPUメモリを大幅に節約

実験設定

使用モデル

  • ベースモデル: google/gemma-2-2b-it
  • 量子化: 4bit(QLoRA)

タスク

英語の文法修正タスク。特に「Although ... but ...」という二重接続詞の誤りを修正するタスクを対象としました。

誤った文: "Although it is expensive, but I bought it anyway."
正しい文: "Although it is expensive, I bought it anyway."

("Although"と"but"は両方とも接続詞なので、片方を削除する必要がある)


実装

1. 依存関係のインストール

!pip install transformers accelerate peft datasets sentencepiece bitsandbytes sacrebleu language-tool-python -q

2. 学習データの準備

from datasets import Dataset
from huggingface_hub import login

login()  # HuggingFace トークン入力

train_data = {
    "input_text": [
        # 短文パターン
        "Although it was cold, but we went swimming.",
        "Although she was tired, but she continued working.",
        "Although the movie was long, but I enjoyed it.",
        # ... 中文・長文パターンも含め76件
    ],
    "target_text": [
        "Although it was cold, we went swimming.",
        "Although she was tired, she continued working.",
        "Although the movie was long, I enjoyed it.",
        # ...
    ]
}

dataset = Dataset.from_dict(train_data)

3. モデル読み込み(Gemma 2B + QLoRA)

from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model
import torch

model_name = "google/gemma-2-2b-it"

tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    load_in_4bit=True,      # 4bit量子化(QLoRA)
    device_map="auto",       # GPUに自動配置
    torch_dtype=torch.float16
)

# LoRA設定
lora_config = LoraConfig(
    r=16,                          # ランク
    lora_alpha=32,                 # スケーリング係数
    target_modules=["q_proj", "v_proj"],  # 適用するモジュール
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

出力:

trainable params: 3,194,880 || all params: 2,617,536,768 || trainable%: 0.1221

全パラメータの 0.12% のみを学習することがわかります。

4. 学習データの変換とトークナイズ

def format_example(example):
    prompt = f"Correct this sentence:\n{example['input_text']}\nCorrected:"
    example["text"] = prompt + example["target_text"]
    return example

dataset = dataset.map(format_example)

def tokenize(batch):
    return tokenizer(batch["text"], padding=True, truncation=True, max_length=256)

tokenized = dataset.map(tokenize, batched=True)

# GPU対応のテンソル形式に変換(重要!)
tokenized.set_format(type="torch", columns=["input_ids", "attention_mask"])

5. 学習の実行

from transformers import TrainingArguments, Trainer, DataCollatorForLanguageModeling
import os
os.environ["WANDB_DISABLED"] = "true"

training_args = TrainingArguments(
    output_dir="./results",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,
    logging_steps=1,
    num_train_epochs=30,        # 30エポックで十分に収束
    fp16=True,
    learning_rate=2e-4,
    optim="paged_adamw_32bit",
    report_to=None,
    save_strategy="epoch"
)

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=False,
    return_tensors="pt"
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized,
    data_collator=data_collator,
    tokenizer=tokenizer
)

trainer.train()

学習結果:

TrainOutput(global_step=570, training_loss=0.1628377268123522, ...)

6. 推論関数

def correct(model, tokenizer, text, max_new_tokens=300):
    prompt = f"Correct this sentence:\n{text}\nCorrected:"
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)  # GPUへ転送

    with torch.no_grad():
        output = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            do_sample=False
        )

    return tokenizer.decode(output[0], skip_special_tokens=True)

7. 評価

import sacrebleu
import language_tool_python

tool = language_tool_python.LanguageTool('en-US')
references = [[t] for t in test_data["target_text"]]

# BLEUスコア
bleu_base = sacrebleu.corpus_bleu(pred_base, references)
bleu_lora = sacrebleu.corpus_bleu(pred_lora, references)

# 文法エラー数
err_base = sum(len(tool.check(p)) for p in pred_base)
err_lora = sum(len(tool.check(p)) for p in pred_lora)

結果

定量評価

指標 ベースモデル QLoRAモデル 改善
BLEUスコア 11.68 35.98 +24.30 (+208%)
文法エラー数 87 82 -5 (-5.7%)

定性評価(出力例)

入力: "Although it is expensive, but I bought it anyway."

ベースモデル出力:

Correct this sentence:
Although it is expensive, but I bought it anyway.
Corrected: **Although it is expensive, I bought it anyway.**

Here's why the original sentence is incorrect and how the correction works:

* **"Although it is expensive, but"**  This structure is grammatically incorrect.
  The "but" should be replaced with a conjunction like "and" or "so" to create
  a logical connection.
* **"I bought it anyway"** This part is correct. It expresses a decision to
  purchase the item despite its high cost.

Let me know if you have any other sentences you'd like help with!

QLoRAモデル出力:

Correct this sentence:
Although it is expensive, but I bought it anyway.
Corrected:Although it is expensive, I bought it anyway.

正解: "Although it is expensive, I bought it anyway."

出力の違い

観点 ベースモデル QLoRAモデル
出力形式 冗長な説明付き シンプルで簡潔
正確性 正しいが余計な装飾あり 正解と完全一致
実用性 後処理が必要 そのまま使用可能

考察

1. BLEUスコアの大幅改善

BLEUスコアが11.68から35.98へと約3倍に向上しました。これは:

  • 出力形式の学習: QLoRAモデルが「修正文のみを出力する」というフォーマットを学習した
  • タスク特化: 特定の文法パターン(Although...but...)の修正に特化した

2. 文法エラー数の改善が限定的な理由

文法エラー数の改善は5件(5.7%減)にとどまりました。考えられる理由:

  • ベースモデルの出力に含まれる「説明文」が文法チェッカーに検出されている
  • 実際の文法修正能力自体は両モデルで大差ない可能性
  • 評価対象のテストデータが25件と少ない

3. QLoRAの効果

QLoRAにより、モデルは以下を学習したと考えられます:

  1. 出力フォーマット: 説明なしで修正文のみを出力
  2. タスク理解: 「Correct this sentence」というプロンプトの意図を正確に理解
  3. パターン認識: Although...but...の二重接続詞パターンの修正方法

気づき・学び

1. 学習データ量の重要性

最初は少量のデータ(10件程度)で実験しましたが、QLoRAの効果がほとんど見られませんでした。76件まで増やすことで明確な改善が確認できました。

QLoRAは効率的ですが、タスクを学習するための十分なデータは依然として必要です。

2. GPU活用による高速化

当初CPUで処理していた部分をGPUで処理するよう変更したところ、処理速度が大幅に向上しました。

ポイント:

# GPU対応のテンソル形式に変換
tokenized.set_format(type="torch", columns=["input_ids", "attention_mask"])

# 推論時にGPUへ転送
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

device_map="auto" により、利用可能なGPUに自動的にモデルが配置されます。

3. 重み公開モデルの必要性

LoRA/QLoRAを使用するには、モデルの重みが公開されている必要があります。APIのみ提供されているモデル(例:GPT-4、Claude)ではLoRAによるファインチューニングはできません。

今回使用したGemma 2Bは、Googleが重みを公開しているため、QLoRAの適用が可能でした。

4. エポック数とロス値

エポック数を30に設定することで、ロス値を十分に下げることができました。

少ないエポック数(5〜10)では、モデルが十分に収束せず、ベースモデルとの差が小さくなる傾向がありました。


まとめ

項目 内容
手法 QLoRA(量子化LoRA)
ベースモデル Gemma 2B (4bit量子化)
学習パラメータ 全体の0.12% (約320万パラメータ)
学習データ 76件
エポック数 30
BLEUスコア改善 +208% (11.68 → 35.98)

QLoRAは、低リソース環境でも効率的にLLMをファインチューニングできる強力な手法です。特に、特定のタスクや出力形式に特化させたい場合に有効であることが確認できました。


参考文献

Discussion