【LLM全般】LoRA/QLoRAで始めるファインチューニング入門:実践ガイドとよくあるエラー解決

問題の概要:ファインチューニングの実践における壁

大規模言語モデル(LLM)を自社データや特定タスクに適応させる「ファインチューニング」は、生成AI活用の核心技術です。しかし、初心者から中級者のエンジニアが実際に試みると、以下のような課題に直面することが少なくありません。

  • リソース不足: フルファインチューニングには膨大なGPUメモリが必要で、個人開発環境や小規模なクラウドインスタンスでは実行できない。
  • 学習の不安定性: ハイパーパラメータの設定を誤ると、学習が発散したり、モデルの性能が著しく低下したりする。
  • 実装の複雑さ: PEFT(Parameter-Efficient Fine-Tuning)ライブラリやTransformersの使い方に戸惑い、エラーメッセージで行き詰まる。

具体的には、RuntimeError: CUDA out of memory.ValueError: You can't train a model that has been loaded in 8-bit or 4-bit precision. といったエラーが初期のハードルとなります。本記事では、効率的なファインチューニング手法であるLoRAとQLoRAに焦点を当て、実践的な手順とトラブルシューティングを解説します。

原因の解説:なぜフルチューニングは難しいのか?

LLMは数十億から数千億のパラメータを持ちます。これらのすべてを更新するフルファインチューニングでは、モデルパラメータ、オプティマイザの状態、勾配などをすべてGPUメモリに保持する必要があります。例えば、70億パラメータのモデルをFP32(単精度)で学習する場合、パラメータだけでも約28GBのメモリを消費し、実際にはその数倍のメモリが必要になります。

これを解決するのが、LoRA (Low-Rank Adaptation)QLoRA (Quantized LoRA) です。

LoRAの仕組み

モデルの全パラメータを更新する代わりに、特定の層(通常はAttentionのQuery, Key, Value, Outputプロジェクション)に低ランク行列(LoRAアダプター)を追加し、この小さな行列のみを学習します。これにより、更新するパラメータ数が劇的に減少し、メモリ使用量と保存ファイルサイズが大幅に削減されます。

QLoRAのさらなる進化

QLoRAはLoRAに量子化(Quantization)を組み合わせた手法です。ベースモデルを4ビットなどに量子化してメモリにロードし、その上でLoRAアダプターを学習します。これにより、例えば1つの消費電力の高いGPU(例: RTX 4090 24GB)で、70Bパラメータクラスの大規模モデルですらファインチューニング可能にします。

解決方法:LoRA/QLoRAファインチューニング実践ステップ

ここでは、Hugging FaceのTransformersとPEFTライブラリを使用した基本的な手順を紹介します。

ステップ1: 環境構築とライブラリのインストール

必要なパッケージをインストールします。バージョンの互換性に注意が必要です。

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install transformers accelerate peft datasets bitsandbytes scipy

ステップ2: モデルとトークナイザーの読み込み(QLoRAの場合)

4ビット量子化でモデルをロードします。ここでは「elyza/ELYZA-japanese-Llama-2-7b」を例とします。

from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import torch

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True, # 4ビット量子化でロード
    bnb_4bit_use_double_quant=True, # ネストされた量子化(QLoRAの特徴)
    bnb_4bit_quant_type="nf4", # 正規化浮動小数点4ビット
    bnb_4bit_compute_dtype=torch.bfloat16 # 計算時のデータ型
)

model_id = "elyza/ELYZA-japanese-Llama-2-7b"
tokenizer = AutoTokenizer.from_pretrained(model_id)
# トークナイザーのパディングトークン設定(Llama系では必要)
tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto", # モデルを利用可能なGPU/CPUに自動配置
    trust_remote_code=True # 必要に応じて
)

ステップ3: LoRA設定の準備

PEFTライブラリを使用して、LoRAの設定を行います。

from peft import LoraConfig, get_peft_model, TaskType

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, # 因果言語モデリングタスク
    r=8, # LoRAのランク(行列の次元)。小さいほど軽量。
    lora_alpha=32, # スケーリングパラメータ
    lora_dropout=0.1, # ドロップアウト率
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # LoRAを適用するモジュール
    bias="none"
)

# モデルをPeftModelでラップ
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 学習可能パラメータ数を確認
# 出力例: trainable params: 4,194,304 || all params: 7,017,484,288 || trainable%: 0.0598

ステップ4: データセットの準備と前処理

ファインチューニング用のデータをロードし、テキストをプロンプト形式に整形します。

from datasets import load_dataset

# 例: 指示チューニング用データセット
dataset = load_dataset("your_dataset_name", split="train")

def format_instruction(sample):
    # データセットに合わせたプロンプトテンプレートの作成
    prompt = f"""以下は、タスクを説明する指示です。リクエストを適切に完了する応答を書いてください。
### 指示:
{sample['instruction']}

### 入力:
{sample['input']}

### 応答:
{sample['output']}"""
    return {"text": prompt}

formatted_dataset = dataset.map(format_instruction)

# トークナイズ関数
def tokenize_function(examples):
    return tokenizer(
        examples["text"],
        truncation=True,
        padding="max_length",
        max_length=512 # メモリに合わせて調整
    )

tokenized_dataset = formatted_dataset.map(tokenize_function, batched=True)

ステップ5: トレーナーの設定と学習実行

TransformersのTrainer APIを使用して学習を実行します。

from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir="./lora-elyza-finetuned",
    per_device_train_batch_size=4, # GPUメモリに合わせて調整
    gradient_accumulation_steps=4,  # 実効バッチサイズ = per_device_train_batch_size * gradient_accumulation_steps
    num_train_epochs=3,
    learning_rate=2e-4, # LoRAでは比較的高い学習率が使われることが多い
    fp16=True, # 混合精度学習(メモリ節約と高速化)
    logging_steps=10,
    save_steps=100,
    save_total_limit=2,
    remove_unused_columns=False,
    push_to_hub=False, # Hugging Face Hubにプッシュする場合はTrue
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    data_collator=lambda data: {'input_ids': torch.stack([d['input_ids'] for d in data]),
                                'attention_mask': torch.stack([d['attention_mask'] for d in data]),
                                'labels': torch.stack([d['input_ids'] for d in data])} # 因果LMでは入力IDがラベル
)

trainer.train()

ステップ6: モデルの保存と読み込み

学習したLoRAアダプターのみを保存し、推論時にロードします。

# LoRAアダプターのみを保存
model.save_pretrained("./my_lora_adapter")

# 推論時: ベースモデルとアダプターを結合してロード
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(...) # ベースモデルを再度ロード(量子化設定は不要)
model = PeftModel.from_pretrained(base_model, "./my_lora_adapter")

よくあるエラーと解決策

エラー1: CUDA Out of Memory

エラーメッセージ: RuntimeError: CUDA out of memory. Tried to allocate ...

原因と解決策:

  • バッチサイズの削減: per_device_train_batch_sizeを1や2に下げる。
  • 勾配累積の使用: gradient_accumulation_stepsを増やし、実効バッチサイズを維持しながらメモリ使用量を削減。
  • 最大長の短縮: トークナイズ時のmax_lengthを256や128に下げる。
  • グラデーションチェックポインティングの有効化: model.gradient_checkpointing_enable()を呼び出す(計算時間とメモリのトレードオフ)。

エラー2: 量子化モデルの学習に関するエラー

エラーメッセージ: ValueError: You can't train a model that has been loaded in 8-bit or 4-bit precision.

原因と解決策: これは古いバージョンのbitsandbytesやPEFTで発生する可能性があります。QLoRAでは、ベースモデルは量子化された状態で固定され、LoRAアダプターのみが学習されます。ライブラリを最新版に更新し、上記の通りget_peft_modelでラップすることで解決します。

# 解決策:確実にPEFTモデルとしてラップする
model = AutoModelForCausalLM.from_pretrained(..., quantization_config=bnb_config, ...)
model = get_peft_model(model, lora_config) # このステップが必須

エラー3: トークナイザーのパディングエラー

エラーメッセージ: ValueError: Asking to pad but the tokenizer does not have a padding token.

原因と解決策: Llamaや一部のモデルのトークナイザーにはデフォルトのパディングトークンが設定されていません。明示的に設定が必要です。

tokenizer = AutoTokenizer.from_pretrained(model_id)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token # 文末トークンをパディングに流用
    # または tokenizer.add_special_tokens({'pad_token': '[PAD]'})

まとめ・補足情報

LoRAとQLoRAは、限られた計算リソースで強力なLLMをカスタマイズするための革命的な技術です。本ガイドで紹介したステップとトラブルシューティングを参考に、最初のファインチューニングを成功させてください。

次のステップとして:

  • ハイパーパラメータのチューニング: r(ランク)、lora_alpha、学習率を調整して性能を最適化する。
  • 異なるターゲットモジュールの実験: 全結合層など、target_modulesに他の層を追加してみる。
  • マージとエクスポート: 学習したLoRAアダプターをベースモデルにマージし、単一のモデルファイルとしてエクスポートする(merge_and_unload()メソッド)。
  • より高度なライブラリの利用: axolotlやLLaMA-Factoryなど、ファインチューニング専用の高レベルライブラリを利用すると、さらに効率的に実験を進められます。

ファインチューニングは試行錯誤のプロセスです。小規模なデータセットでまずはプロトタイプを作成し、メモリ使用量や学習曲線を確認しながら、段階的に規模を拡大していくことをお勧めします。

🚀 プロンプト技術をさらに磨くなら

プロンプトエンジニアリングの実践には、高性能なAIモデルへのアクセスが不可欠です。

  • ChatGPT Plus — GPT-4o/o1による高度な推論が利用可能
  • Claude Pro — 長文コンテキストと精密な指示理解に強い
この記事は役に立ちましたか?