Skip to content
Published on

LLMファインチューニング実践 — LoRA、QLoRA、PEFTで自分だけのモデルを作る

Authors
  • Name
    Twitter
LLM Fine-tuning with LoRA

はじめに

7B、13B、70BパラメータのLLMをゼロから学習するには、数十〜数百のGPUと数百万ドルが必要です。しかし、ファインチューニングを活用すれば、コンシューマー向けGPU1枚でも自分だけの特化モデルを作ることができます。

この記事では、LoRAQLoRAPEFTライブラリを活用した実践的なファインチューニング方法を解説します。

Full Fine-tuning vs Parameter-Efficient Fine-tuning

Full Fine-tuningの問題点

7BモデルをFull Fine-tuningするには:

  • モデルパラメータ: 7B × 4バイト (FP32) = 28GB
  • Optimizer状態: Adamはパラメータの2倍 = 56GB
  • 勾配: パラメータと同じ = 28GB
  • 合計VRAM: 約112GB以上が必要

A100 80GB 1枚でも不足します。

PEFTの登場

Parameter-Efficient Fine-tuning(PEFT)は全パラメータの0.1〜1%のみを学習します:

方法学習パラメータ比率VRAM(7B基準)
Full Fine-tuning100%約112GB
LoRA約0.1-1%約16GB
QLoRA約0.1-1%約6GB

LoRA: Low-Rank Adaptation

数学的原理

LoRAの核心的アイデア:重み更新行列ΔWは低ランク(low-rank)である。

元の線形変換:

y=Wxy = Wx

LoRA適用後:

y=Wx+αrBAxy = Wx + \frac{\alpha}{r} \cdot BAx

ここで:

  • WRd×dW \in \mathbb{R}^{d \times d}: 元の重み(凍結)
  • BRd×rB \in \mathbb{R}^{d \times r}: 低ランク行列(学習対象)
  • ARr×dA \in \mathbb{R}^{r \times d}: 低ランク行列(学習対象)
  • rr: ランク(通常4〜64、元の次元に比べて非常に小さい)
  • α\alpha: スケーリングファクター
元のW (4096 × 4096) = 16Mパラメータ [凍結]

LoRA:
A (r × 4096) + B (4096 × r) = r × 8192パラメータ
r=8の場合: 65,536パラメータ (0.4%)

コードで実装する

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

# 1. ベースモデルのロード
model_name = "meta-llama/Llama-3.1-8B"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 2. LoRA設定
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,                          # ランク
    lora_alpha=32,                 # スケーリング(通常rの2倍)
    lora_dropout=0.05,             # ドロップアウト
    target_modules=[               # LoRAを適用するモジュール
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    bias="none"
)

# 3. PEFTモデルの作成
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 41,943,040 || all params: 8,072,204,288 || trainable%: 0.5194

target_modules選択ガイド

# モデルの全Linearレイヤーを確認
for name, module in model.named_modules():
    if isinstance(module, torch.nn.Linear):
        print(name, module.in_features, module.out_features)

# 一般的な選択肢:
# - Attentionのみ: ["q_proj", "v_proj"] — 最小VRAM
# - Attention全体: ["q_proj", "k_proj", "v_proj", "o_proj"] — 推奨
# - MLP含む: 上記 + ["gate_proj", "up_proj", "down_proj"] — 最大性能

QLoRA: 4-bit量子化 + LoRA

QLoRAが特別な理由

QLoRAは3つのイノベーションにより、コンシューマーGPUでの大規模モデルファインチューニングを可能にします:

  1. 4-bit NormalFloat(NF4): 正規分布の重みに最適化された量子化
  2. Double Quantization: 量子化定数自体も量子化してメモリをさらに節約
  3. Paged Optimizers: GPUメモリ不足時にCPUへ自動ページング

実装

from transformers import BitsAndBytesConfig
import torch

# 4-bit量子化設定
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NormalFloat4
    bnb_4bit_compute_dtype=torch.bfloat16, # 演算はbf16で
    bnb_4bit_use_double_quant=True,        # Double Quantization
)

# 量子化モデルのロード
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto"
)

# LoRA適用 (QLoRA = 4bit量子化モデル + LoRA)
model = get_peft_model(model, lora_config)

VRAM使用量の比較

モデルFull FP16LoRA FP16QLoRA 4-bit
7B約28GB約16GB約6GB
13B約52GB約30GB約10GB
70B約280GB約160GB約48GB

QLoRAを使えば、**RTX 3090/4090(24GB)**1枚で13Bモデルまでファインチューニングできます。

データ準備と学習

データセットフォーマット

Instructionファインチューニング用のデータフォーマット:

from datasets import load_dataset

# Alpacaスタイルデータセット
dataset = load_dataset("json", data_files="train_data.json")

# データ例
# {
#   "instruction": "次のテキストを要約してください。",
#   "input": "Kubernetesはコンテナ化されたワークロードとサービスを...",
#   "output": "Kubernetesはコンテナオーケストレーションプラットフォームです。"
# }

# プロンプトテンプレートの適用
def format_instruction(sample):
    if sample["input"]:
        text = f"""### Instruction:
{sample["instruction"]}

### Input:
{sample["input"]}

### Response:
{sample["output"]}"""
    else:
        text = f"""### Instruction:
{sample["instruction"]}

### Response:
{sample["output"]}"""
    return {"text": text}

dataset = dataset.map(format_instruction)

SFTTrainerによる学習

from trl import SFTTrainer, SFTConfig

training_args = SFTConfig(
    output_dir="./output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,    # 実効バッチ = 4 × 4 = 16
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    max_seq_length=2048,
    bf16=True,
    logging_steps=10,
    save_strategy="epoch",
    optim="paged_adamw_8bit",         # QLoRA用ページングオプティマイザー
    gradient_checkpointing=True,      # VRAMのさらなる節約
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset["train"],
    args=training_args,
)

trainer.train()

学習後のモデル保存とマージ

# LoRAアダプターのみ保存(数十MB)
model.save_pretrained("./lora-adapter")

# 後でアダプターをロード
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
model = PeftModel.from_pretrained(base_model, "./lora-adapter")

# ベースモデルとアダプターのマージ(デプロイ用)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged-model")

ハイパーパラメータチューニングガイド

LoRAランク(r)の選択

# ランク別の特性
# r=4:   最少パラメータ、シンプルなドメイン適応に適する
# r=8:   一般的な出発点
# r=16:  良いバランス(推奨)
# r=32:  複雑なタスク、より多くのVRAMが必要
# r=64+: Full Fine-tuningに近い性能、それだけ非効率

# 経験的に、r=16 + alpha=32がほとんどのケースで良好に動作

学習率

# LoRA/QLoRAの学習率はFull FTより高く設定
# Full FT: 1e-5 ~ 5e-5
# LoRA:    1e-4 ~ 3e-4
# QLoRA:   2e-4(一般的)

高度なテクニック

DoRA: Weight-Decomposed Low-Rank Adaptation

LoRAの発展形で、重みを大きさ(magnitude)と方向(direction)に分解します:

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    use_dora=True,   # DoRAを有効化
)

複数のLoRAアダプターの組み合わせ

from peft import PeftModel

# ベースモデルに複数のアダプターをロード
model = PeftModel.from_pretrained(base_model, "./adapter-korean")
model.load_adapter("./adapter-code", adapter_name="code")

# アダプターの切り替え
model.set_adapter("code")

# またはアダプターの重みを組み合わせ
model.add_weighted_adapter(
    adapters=["default", "code"],
    weights=[0.7, 0.3],
    adapter_name="merged"
)

まとめ

LoRAとQLoRAは、LLMファインチューニングのアクセシビリティを革命的に変えました。コンシューマーGPU1枚で数十億パラメータのモデルをカスタマイズできることは、AI民主化の核心です。

要点まとめ:

  • LoRA: 低ランク分解で学習パラメータを0.1〜1%に削減
  • QLoRA: 4-bit量子化でVRAMをさらに約4倍節約
  • PEFT: Hugging Faceライブラリで数行のコードで適用可能

ぜひご自身でデータを準備し、学習し、デプロイしてみてください。思っている以上に簡単です。

クイズ

Q1: LoRAにおけるランク(r)が意味するものは? 重み更新行列ΔWを分解する際の低次元サイズです。rが小さいほど学習パラメータが少なくVRAM消費が減りますが、モデルの表現力も制限されます。

Q2: 7BモデルをFull Fine-tuningするのに必要なVRAMはおよそどのくらい? 約112GB以上。モデルパラメータ(28GB)+ Optimizer状態(56GB)+ 勾配(28GB)が必要です。

Q3: QLoRAの3つの核心的イノベーションは?
  1. 4-bit NormalFloat(NF4)量子化、2) Double Quantization(量子化定数の量子化)、3) Paged Optimizers(GPUからCPUへの自動ページング)

Q4: LoRAのlora_alphaパラメータの役割は? LoRA更新のスケーリングファクターです。実際のスケールはalpha/rで計算され、通常rの2倍に設定します(r=16ならalpha=32)。

Q5: QLoRAで使用されるNF4量子化が通常のINT4より優れている理由は? ニューラルネットワークの重みはおおよそ正規分布に従うため、正規分布に最適化されたNF4量子化は、均一分布を仮定するINT4よりも情報損失が少なくなります。

Q6: LoRAアダプターをベースモデルとマージする理由は? 推論時の追加的な計算オーバーヘッドをなくすためです。マージすると元のモデルと同じ構造になり、アダプターを分離した状態よりも推論速度が速くなります。

Q7: gradient_checkpointing=Trueの効果は? 順伝播の中間活性値をメモリに保存せず、逆伝播で再計算します。VRAMを節約しますが、学習時間は約20〜30%増加します。

Q8: LoRAのtarget_modulesの選択が性能に与える影響は? より多くのモジュールにLoRAを適用するほど性能は向上しますが、VRAMと学習時間が増加します。Attentionのq_proj、v_projのみの適用が最小構成で、MLPまで含めると最大の性能が得られます。