Chuyển đến nội dung chính

レッスン 12: PEFT — LoRA、QLoRA、およびアダプター メソッド

LoRA および QLoRA を使用したパラメーター効率の良い微調整 (PEFT) を学習します。これは、コンシューマー GPU だけで数十億のパラメーターを使用して LLM を微調整できる技術です。 LoRA 数学、4 ビット BitsAndBytes 構成をマスターし、メソッド間の効率を比較します。

🧠 AI と ML — レッスン 11 レッスン 12: PEFT — LoRA、QLoRA、アダプター メソッド

AI と LLM: 基本から高度まで

パート 3: LLM のトレーニングと微調整

xdev.asia

レッスン 12: PEFT — LoRA、QLoRA、およびアダプター メソッド

1. フルファインチューニングの問題

大規模な LLM のパラメータ全体を微調整するには、大きな課題が生じます。

必要なメモリ

モデルパラメータの数VRAM (FP32)VRAM (BF16)VRAM + アダム (BF16)
GPT-2117M0.5GB0.25GB~1 GB
LLaMA-7B7B28GB14GB~56 GB
LLaMA-13B13B52GB26GB~104 GB
LLaMA-70B70B280GB140GB~560 GB

Adam オプティマイザーは、各パラメーターの 運動量 と 分散 を保存します (モデルの重みメモリの 3 倍)。 LLaMA-7B を完全に微調整するには、4× A100 80GB が必要です。

その他の問題

  • 壊滅的な忘却: モデルはトレーニング前の知識を「忘れる」
  • ストレージ: 微調整された各バージョン = フルモデルのコピー (7B の場合は 14GB)
  • 費用: 1 回のトレーニング実行で数千米ドル

2. パラメーター効率の良い微調整 (PEFT) の概要

PEFT は、パラメータの小さなサブセットのみを更新し、残りは変更しないままにする一連の手法です。

Full Fine-Tuning:  cập nhật 100% params  → VRAM cao, chậm, tốn kém
PEFT:              cập nhật 0.1% - 5%    → VRAM thấp, nhanh, tiết kiệm

主なPEFT手法:

  1. アダプターレイヤー: トランスレイヤーの間に小さなレイヤーを追加します。
  2. プレフィックス調整: シーケンスの先頭にトレーニング可能なトークンを追加します。
  3. プロンプト チューニング: ソフト プロンプトの埋め込みのみを微調整します
  4. LoRA: 重み更新行列を 2 つの低ランク行列の積に分解します。

3. LoRA: 低ランクの適応

LoRA の数学

基本的なアイデア: 微調整における重みの更新は、ランクが低いことがよくあります。

直接学習する代わりに:

W_new = W_original + ΔW   (ΔW có kích thước d×d, rất lớn)

LoRA は ΔW を次のように因数分解します。

ΔW = B × A

その中で:

  • W_original ∈ ℝ^(d×d): 凍結、更新されない
  • A ∈ ℝ^(r×d): トレーニング可能なランダムなガウス初期化
  • B ∈ ℝ^(d×r): トレーニング可能、0 に初期化されます (最初は ΔW = 0 になります)
  • r ランク (通常 4-64)、r << d
Forward pass: h = W_original × x + (B × A) × x × (alpha/r)

パラメータの利点:

d = 4096 (LLaMA-7B hidden dim)

Full ΔW:   4096 × 4096 = 16,777,216 tham số
LoRA r=16: (4096×16) + (16×4096) = 131,072 tham số
→ Giảm 128 lần!

ランクとアルファ

lora_config = LoraConfig(
    r=16,          # Rank: càng cao → càng nhiều params nhưng expressive hơn
    lora_alpha=32, # Scaling factor: alpha/r = scale của LoRA
    # Thường đặt alpha = 2*r hoặc alpha = r
    lora_dropout=0.05,  # Regularization
)

ランク選択の手順:

ランク(r)使用例トレーニング可能なパラメータの数 (7B モデル)
4単純なタスク、少量のデータ~400 万 (~0.06%)
8バランスが良い~800 万 (~0.12%)
16より複雑なタスク~1,700 万 (~0.24%)
64大規模なドメインの適応~6,700 万 (~0.95%)

4. ターゲットモジュールの選択

LoRA は、アテンション メカニズムの 線形レイヤー に適用されます。

# Với LLaMA/Mistral architecture:
target_modules = [
    "q_proj",   # Query projection
    "v_proj",   # Value projection
    "k_proj",   # Key projection
    "o_proj",   # Output projection
    # Tùy chọn thêm:
    "gate_proj", # MLP gate
    "up_proj",   # MLP up
    "down_proj", # MLP down
]

戦略:

# Minimal (nhanh, ít VRAM):
target_modules = ["q_proj", "v_proj"]

# Standard (cân bằng tốt):
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"]

# Full attention + MLP (tốt nhất, nhưng chậm hơn):
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                  "gate_proj", "up_proj", "down_proj"]

5. QLoRA: 4 ビット量子化 + LoRA

QLoRA (Dettmers et al., 2023) は、次の 2 つの技術を組み合わせています。

  1. NF4 (NormalFloat 4 ビット): モデルの重みを 4 ビットまで量子化します。
  2. 二重量子化: 両方の量子化定数を量子化します。
  3. ページ オプティマイザー: CPU オフロードを使用したオプティマイザー メモリ管理

NF4 が INT4 よりも優れているのはなぜですか?

NF4 は、モデルの重みの 正規分布 向けに設計されています。

  • LLM 重みは正規分布します (ガウス分布)。
  • NF4 は高密度領域により多くのビンを割り当てます
  • INT4と比較して量子化誤差が大幅に減少
FP16 → NF4: giảm 75% bộ nhớ, chất lượng gần như giữ nguyên

VRAM 比較 (LLaMA-7B)

方法VRAM トレーニング品質
フルFT(FP16)~56 GBベースライン
LoRA (FP16)~28 GB~98% ベースライン
QLoRA (NF4)~10 GB~97% ベースライン

6. BitsAndBytes: 4 ビット モデルのロード

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

# Cấu hình 4-bit quantization
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,              # Bật 4-bit loading
    bnb_4bit_quant_type="nf4",      # Dùng NF4 (tốt hơn fp4)
    bnb_4bit_compute_dtype=torch.bfloat16,  # Dtype cho compute
    bnb_4bit_use_double_quant=True, # Double quantization (tiết kiệm thêm ~0.4 bit/param)
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=bnb_config,
    device_map="auto",  # Tự động phân bổ layers lên GPU/CPU
)

# Kiểm tra bộ nhớ
print(f"Model footprint: {model.get_memory_footprint() / 1e9:.2f} GB")
# → ~4.5 GB thay vì ~16 GB (BF16)

7. プレフィックスチューニングとプロンプトチューニング

迅速なチューニング

ソフト プロンプトの埋め込みを微調整するだけです (仮想埋め込みトークンが入力に追加されます)。

from peft import PromptTuningConfig, TaskType, get_peft_model

config = PromptTuningConfig(
    task_type=TaskType.CAUSAL_LM,
    num_virtual_tokens=20,  # Số soft prompt tokens
    tokenizer_name_or_path="gpt2",
)
model = get_peft_model(model, config)
# trainable params: 15,360 / 124,475,648 → 0.01%!

プレフィックスチューニング

各アテンション レイヤーの キーと値にトレーニング可能な「プレフィックス」を追加します。

from peft import PrefixTuningConfig

config = PrefixTuningConfig(
    task_type=TaskType.CAUSAL_LM,
    num_virtual_tokens=30,
    encoder_hidden_size=512,  # MLP để tạo prefix
)

プレフィックス チューニングはプロンプト チューニングよりも優れていますが、多くのタスクにおいて LoRA よりは劣ります。トレーニング可能なパラメータがほとんど必要ない場合に使用します。


8. コード全体: PEFT + TRL を使用した QLoRA の LLaMA 微調整

import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)
from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_kbit_training,
    TaskType,
)
from trl import SFTTrainer, SFTConfig

# ============================================================
# 1. Cấu hình
# ============================================================
MODEL_ID = "meta-llama/Meta-Llama-3-8B"
OUTPUT_DIR = "./llama3-8b-qlora-vi"
MAX_SEQ_LENGTH = 2048

# ============================================================
# 2. Load Tokenizer
# ============================================================
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

# ============================================================
# 3. Load Model với 4-bit Quantization
# ============================================================
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=bnb_config,
    device_map="auto",
    attn_implementation="flash_attention_2",  # Faster attention
)

# Chuẩn bị model cho k-bit training
model = prepare_model_for_kbit_training(model)

# ============================================================
# 4. Cấu hình LoRA
# ============================================================
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM,
)

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

# ============================================================
# 5. Dataset: Vietnamese instruction data
# ============================================================
dataset = load_dataset("tatsu-lab/alpaca", split="train[:10000]")

def format_chat(example):
    messages = [
        {"role": "system", "content": "Bạn là trợ lý AI hữu ích, trả lời bằng tiếng Việt."},
        {"role": "user", "content": example["instruction"]
                                    + (f"\n\n{example['input']}" if example["input"] else "")},
        {"role": "assistant", "content": example["output"]},
    ]
    text = tokenizer.apply_chat_template(messages, tokenize=False)
    return {"text": text}

dataset = dataset.map(format_chat, remove_columns=dataset.column_names)

# ============================================================
# 6. Training Configuration
# ============================================================
training_args = SFTConfig(
    output_dir=OUTPUT_DIR,
    num_train_epochs=2,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,      # Effective batch = 16
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.05,
    bf16=True,
    gradient_checkpointing=True,
    gradient_checkpointing_kwargs={"use_reentrant": False},
    max_seq_length=MAX_SEQ_LENGTH,
    dataset_text_field="text",
    logging_steps=10,
    save_strategy="epoch",
    optim="paged_adamw_8bit",           # Paged optimizer cho QLoRA
    report_to="tensorboard",
)

# ============================================================
# 7. Train
# ============================================================
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
)

trainer.train()

# ============================================================
# 8. Lưu LoRA weights (chỉ ~80MB, không phải toàn bộ model!)
# ============================================================
trainer.save_model(OUTPUT_DIR + "/lora-weights")
tokenizer.save_pretrained(OUTPUT_DIR + "/lora-weights")

# ============================================================
# 9. Merge LoRA vào base model (cho inference)
# ============================================================
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
merged_model = PeftModel.from_pretrained(base_model, OUTPUT_DIR + "/lora-weights")
merged_model = merged_model.merge_and_unload()
merged_model.save_pretrained(OUTPUT_DIR + "/merged")
print("Done! Merged model saved.")

9. 比較: フル FT vs LoRA vs QLoRA

基準完全な微調整LoRA(BF16)QLoRA (NF4)
VRAM(7Bモデル)~56 GB~28 GB~10 GB
必要な GPU4× A1002×A1001× RTX 3090
トレーニング速度最速速い~30% 遅くなる
品質100% (ベースライン)~98-99%~96-98%
チェックポイントのサイズ14GB~80MB~80MB
テストが簡単難しい簡単最も簡単
壊滅的な物忘れ曹操低い低い

いつ何を使用するか?

  • フル FT: 多くの GPU、大規模なデータセットがあり、最高の品質が必要
  • LoRA: A100/H100 GPU、品質とリソースのバランスが良い
  • QLoRA: コンシューマ GPU (RTX 3090/4090)、個人研究、ラピッド プロトタイプ

概要

  • 完全な微調整は、通常の GPU のモデル 7B+ では現実的ではありません
  • LoRA は重みの更新を因数分解して、 B × A ランクが低い場合 — トレーニング可能なパラメータを 99% 以上削減します
  • QLoRA は、NF4 量子化と LoRA を組み合わせます — 2x RTX 3090 で 70B モデルを微調整します
  • target_modules 重要: から始める q_proj, v_proj、必要に応じて展開します
  • トレーニング後、LoRA を基本モデルにマージしてオーバーヘッドなしで推論します

次の記事では、RLHF とアライメント について説明します。これは、指示に従うだけでなく、安全で人間の価値観と一致するようにモデルをトレーニングするプロセスです。