見出し画像

【ローカルLLMの作り方①】サクッとLlamaをfine-tuningしてみた

こんにちは。IZAIエンジニアリングチームです。
今回は、LLMの学習手法の一つである「fine-tuning」について、ローカル環境のNVIDIA製GPUで手軽に実行する方法を解説します。

今回は3Bパラメータの非常に小さいモデルを使用しますが、同じ原理で7Bや13Bといった主流の小規模言語モデルから、70Bや671Bといった大規模モデルのfine-tuningが行えます。本記事を参考に、各用途に合った高性能なfine-tuningモデルを開発してみてください。

fine-tuningではなく継続事前学習について知りたい方はこちらの記事をご覧管ください。

1. fine-tuningとは


画像
fine-tuningの概要

fine-tuning(ファインチューニング)とは、新たなデータを使って、既に学習済みモデルを追加学習することです。

0からモデルを学習する(=Scratchする)のに比べて、非常に少ないデータセットでモデルの能力を制御できるのが特徴で、広く使用されている手法です。その中でも今回は、LoRA(Low-Rank Adaptation)と呼ばれる広く使われているfine-tuning手法で学習します。

画像
LLM学習手法一覧(筆者作成)。今回はLoRAで学習します

2. さっそくfine-tuningしてみる


それでは実際にfine-tuningをしてみましょう。

本ブログでは、Llama3.2-3B-Instructを使用します。
検証環境は次の通りです。

# 検証環境
Python バージョン:3.9.7
OS : Windows11
CPU:Intel Core i9-13900K
GPU : NVIDIA GeForce RTX 3090(VRAM24GB)

※ 必要VRAMサイズについて
モデルの学習には、推論に必要なモデルサイズのおおよそ4倍程度のVRAMサイズが必要と言われています。
3Bモデルの場合、推論(=LLMを使用する)であれば、VRAM 6GB程度で動作しますが、fine-tuningする場合はVRAMは24GB程度が必要になります。(量子化しない場合)
つまり、家庭用のGPU1枚で学習できるモデルサイズは3B程度がおよそ限界ということです。
手持ちのGPUのVRAMサイズが足りない場合は、Google ColabのA100 40GB を利用するか、学生であればぜひ当社インターンへご応募ください。

3. 学習手順


3-1. GPUとドライバのセットアップ

公式ドキュメントまたは次のページを参考にドライバのセットアップを行ってください。

3-2. 仮想環境構築

今回はpyenvとvirtualenvを用いて仮想環境を用意します。
venvやanacondaを使っても問題ありません。

//作業ディレクトリに移動
pyenv install 3.9.7
pyenv local 3.9.7
virtualenv -p 3.9.7 Llama_FT_env

次のコマンドで仮想環境を起動します。

Llama_FT_env\Scripts\activate

3-3. ライブラリのインストール

次のrequirements.txtを作成してください。

torch==2.5.1+cu121
transformers==4.47.1
datasets==3.2.0
peft==0.9.0
bitsandbytes==0.45.0
wandb==0.19.8

作成したら、必要なライブラリを一括ダウンロードできます。

pip install -r requirements.txt

3-4. MetaへLlamaの使用申請

次のサイトにアクセスして、Llamaの使用申請を行ってください。

  1. Hugging faceにログイン(アカウントがなければ作成してください)

  2. 使用ライセンスの同意

  3. アクセストークンの発行

詳しくは公式サイト、または次のページがわかりやすいです

3-5. データセットの用意

学習に使用するデータセットはshi3z氏によって公開されて日本語訳されたAlpaca_jp を使用します。商用での利用は不可です。

instruction, input, outputがセットになっているデータセットであれば学習可能です。

3-6. wandbの準備

まずはwandbのホームページから、アカウントを作成しましょう。

画像
右上のSIGE UPからアカウント作成

ログインできたら、fine-tuning用のプロジェクトを作成します。Profileページからプロジェクトを作成しましょう。

画像
ProjectsのCreate a projectから新しいプロジェクトを作成

wandbのユーザ名とプロジェクト名は学習実行時に使用するのでメモしておきましょう。

3-7. fine-tuningスクリプトの作成

finetune.pyという名前で学習用のスクリプトを作成します。

import os
import json
import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    DataCollatorForLanguageModeling,
    Trainer,
    TrainingArguments
)
from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_int8_training
)
import argparse          
import wandb             

##パラメータ設定
PER_DEVICE_BATCH_SIZE = 4
GRAD_ACCUMULATION_STEPS = 4
EPOCHS = 3
LEARNING_RATE = 3e-4
LOGGING_STEPS = 20
SAVE_STEPS = 100
SAVE_TOTAL_LIMIT = 1
CUTOFF_LEN = 256

LORA_R = 8 #LORAのR値
LORA_ALPHA = 16 #LORAのα値
TARGET_MODULES = ["q_proj", "up_proj", "o_proj", "k_proj", "down_proj", "gate_proj", "v_proj"]# ターゲットモジュールを設定
LORA_DROPOUT = 0.05 # LORAのDropout率

##モデルとパスの設定
model_name = "meta-llama/Llama-3.2-3B-Instruct"
output_dir = (FT後のモデルを保存するパス)
dataset_path =  (alpaca_cleaned_ja.jsonのパス)

def generate_training_data(data_point, tokenizer):
    
    prompt = f"""\
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
あなたは優秀なアシスタントです。
<|eot_id|><|start_header_id|>user<|end_header_id|>

{data_point["instruction"]}
{data_point["input"]}
<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
    
    user_tokens = tokenizer(prompt, truncation=True, max_length=CUTOFF_LEN + 1, padding="max_length",)
    len_user_prompt_tokens = len(user_tokens["input_ids"]) - 1

    try:
        out_text = data_point["output"]
        
        if out_text is None:
            return {"input_ids": [], "labels": [], "attention_mask": []}
        elif not isinstance(out_text, str):
            out_text = str(out_text)

        full_text = prompt + " " + out_text + "<|eot_id|>"

        full_tokens = tokenizer(
            full_text,
            truncation=True,
            max_length=CUTOFF_LEN + 1,
            padding="max_length"
        )["input_ids"][:-1]

    except Exception as e:
        print("例外が発生したので、このサンプルをスキップ:", e)
        return None
    
    return {
        "input_ids": full_tokens,
        "labels": [-100] * len_user_prompt_tokens + full_tokens[len_user_prompt_tokens:],
        "attention_mask": [1] * (len(full_tokens)),
    }

def is_non_empty(example):
    return len(example["input_ids"]) > 0

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--wandb_user", type=str, required=True, help="wandbのユーザ名")
    parser.add_argument("--wandb_project", type=str, required=True, help="wandbのプロジェクト名")
    args = parser.parse_args()

    os.environ["WANDB_ENTITY"] = args.wandb_user
    os.environ["WANDB_PROJECT"] = args.wandb_project

    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoModelForCausalLM.from_pretrained(
        model_path,
        device_map="auto",
        torch_dtype=torch.float16
    )

    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.pad_token_id = tokenizer.eos_token_id

    model = prepare_model_for_int8_training(model)  # 8bit訓練用に変換

    lora_config = LoraConfig(
        r=LORA_R,
        lora_alpha=LORA_ALPHA,
        lora_dropout=LORA_DROPOUT,
        bias="none",
        task_type="CAUSAL_LM",
        target_modules=TARGET_MODULES
    )

    model = get_peft_model(model, lora_config)

    dataset = load_dataset("json", data_files=dataset_path)["train"]
    dataset = dataset.select(range(1000)) #今回はデモのため、1000サンプルを用いて実行
    dataset = dataset.shuffle().map(lambda ex: generate_training_data(ex, tokenizer))
    dataset = dataset.filter(is_non_empty)

    training_args = TrainingArguments(
        output_dir=output_dir,
        per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
        gradient_accumulation_steps=GRAD_ACCUMULATION_STEPS,
        num_train_epochs=EPOCHS,
        learning_rate=LEARNING_RATE,
        fp16=True,
        logging_steps=LOGGING_STEPS,
        save_steps=SAVE_STEPS,
        save_total_limit=SAVE_TOTAL_LIMIT,
        report_to=["wandb"],
    )

    data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=dataset,
        data_collator=data_collator
    )

    trainer.train()

    model.save_pretrained(output_dir)
    print(f"FT後のモデルを {output_dir} に保存しました。")

if __name__ == "__main__":
    main()

次の2か所をご自身の状況に合わせて書き換えてください。

  • dataset_path = [alpaca_cleaned_ja.jsonのパス]

  • output_dir = [FT後のモデルを保存するパス]

3-8. 実行

次のコマンドで実行します。

 python .\finetune.py --wandb_user=izai --wandb_project=llama_finetune

学習開始すると、下図のようにwandbで学習過程を確認できます。

画像

※ CUDA out of memory” エラーについて
Llama系モデルはパラメータが多く、実行環境によっては “CUDA out of memory” エラーが発生します。今回の例では 3B パラメータと比較的小さめのモデルを扱っていますが、例えば 7B や 13B のモデルを学習をする場合、24GB VRAM でも厳しいでしょう。

4. 推論してみる


fine-tuningの効果を、実際に推論して確認してみましょう。次のinference.pyを作成してください。

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

BASE_MODEL_NAME = "meta-llama/Llama-3.2-3B-Instruct"  #ベースモデル名
FINETUNED_MODEL_PATH = (ローカルに保存されたLoRA差分のパス)

def generate_response(model, tokenizer, prompt, max_new_tokens=128):
    inputs = tokenizer(prompt, return_tensors="pt")
    inputs = {k: v.cuda() for k, v in inputs.items()}  # GPU使用を想定

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            do_sample=True,
            top_k=50,
            top_p=0.9,
            temperature=0.8,
        )
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

def main():
    base_tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME)
    base_model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_NAME,
        device_map="auto",
        torch_dtype=torch.float16,
    )

    base_tokenizer.pad_token = base_tokenizer.eos_token
    base_tokenizer.pad_token_id = base_tokenizer.eos_token_id

    ft_tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME)
    ft_model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_NAME,
        device_map="auto",
        torch_dtype=torch.float16,
    )
    ft_model = PeftModel.from_pretrained(ft_model, FINETUNED_MODEL_PATH)

    ft_tokenizer.pad_token = ft_tokenizer.eos_token
    ft_tokenizer.pad_token_id = ft_tokenizer.eos_token_id

    print("\nプロンプトを入力してください。Ctrl+Cで終了します。")
    while True:
        try:
            user_input = input("\nプロンプト入力: ")
            if not user_input.strip():
                print("有効なプロンプトを入力してください。")
                continue

            print("\n===== プロンプト =====")
            print(user_input)
            print("="*40)

            print("\n[BASE MODEL] 応答生成中...")
            base_response = generate_response(base_model, base_tokenizer, user_input)
            print("----- [BASE MODEL OUTPUT] -----\n", base_response, "\n")

            print("[FINE-TUNED MODEL] 応答生成中...")
            ft_response = generate_response(ft_model, ft_tokenizer, user_input)
            print("----- [FINETUNED MODEL OUTPUT] -----\n", ft_response, "\n")

        except (EOFError, KeyboardInterrupt):
            break

if __name__ == "__main__":
    main()

FINETUNED_MODEL_PATHには、学習スクリプトで指定したoutputディレクトリを指定して実行しましょう。

baseモデルとfine-tuning後のモデルに「色の三原色について教えて。」というプロンプトを与えたところ、それぞれ次の応答を出力しました。

画像
与えたプロンプト
画像
baseモデルの応答
画像
fine-tuningモデルの応答

alpacaデータセットには次のデータセットが含まれており、fine-tuningモデルの方がより学習データに近い応答を含む回答を生成していることが分かります。

画像
alpacan内の学習データセット




5. スクリプト各部の説明


パラメータとパスの設定

PER_DEVICE_BATCH_SIZE = 4
GRAD_ACCUMULATION_STEPS = 4
EPOCHS = 3
LEARNING_RATE = 3e-4
LOGGING_STEPS = 20
SAVE_STEPS = 100
SAVE_TOTAL_LIMIT = 1
CUTOFF_LEN = 256

LORA_R = 8 #LORAのR値
LORA_ALPHA = 16 #LORAのα値
TARGET_MODULES = ["q_proj", "up_proj", "o_proj", "k_proj", "down_proj", "gate_proj", "v_proj"]# ターゲットモジュールを設定
LORA_DROPOUT = 0.05 # LORAのDropout率

##モデルとパスの設定
model_name = "meta-llama/Llama-3.2-3B-Instruct"
output_dir = (FT後のモデルを保存するパス)
dataset_path =  (alpaca_cleaned_ja.jsonのパス)

学習に関するパラメータとパスを設定します。

各パラメータの意味は次の通り

・EPOCHS(重要)
エポック数、すなわち全学習データを何周するかを表す。

・LEARNING_RATE(重要)
学習率。大きいと学習速度が上がるが発散しやすい。小さいと安定する代わりに収束が遅くなる。

・PER_DEVICE_BATCH_SIZE
1回の順伝播で1GPUが処理するサンプル数。大きいほど学習が安定しやすいが、メモリもより使用する。・GRAD_ACCUMULATION_STEPS勾配累積のステップ数。

・CUTOFF_LEN
テキストをトークナイズするときに、1つのサンプルあたりの最大トークン数の設定。

・LORA_R
LoRAのローランク行列の次元を示すパラメータ。大きいほど学習するパラメータが増え、表現力が上がるが学習負荷やメモリが増える。

・LORA_ALPHA
LoRAで学習される重みをスケーリングするときのパラメータ。大きいほど学習で得られる更新量が強調される一方、過学習や発散のリスクも上がる。

・TARGET_MODULES
LoRAを挿入する先のモジュール名リスト。ここで設定した層に対して低ランク行列を学習させる。

・LORA_DROPOUT
LoRAの重みに対して適用するドロップアウト率。過学習を防ぎ、より汎化性能を向上させるために使われるが、高すぎると学習が進まないこともある。

学習データの整形関数

学習データセットの整形には以下のスクリプトを使用しました。

def generate_training_data(data_point, tokenizer):
    
    prompt = f"""\
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
あなたは優秀なアシスタントです。
<|eot_id|><|start_header_id|>user<|end_header_id|>

{data_point["instruction"]}
{data_point["input"]}
<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
    
    user_tokens = tokenizer(prompt, truncation=True, max_length=CUTOFF_LEN + 1, padding="max_length",)
    len_user_prompt_tokens = len(user_tokens["input_ids"]) - 1

    try:
        out_text = data_point["output"]
        
        if out_text is None:
            return {"input_ids": [], "labels": [], "attention_mask": []}
        elif not isinstance(out_text, str):
            out_text = str(out_text)

        full_text = prompt + " " + out_text + "<|eot_id|>"

        full_tokens = tokenizer(
            full_text,
            truncation=True,
            max_length=CUTOFF_LEN + 1,
            padding="max_length"
        )["input_ids"][:-1]

    except Exception as e:
        print("例外が発生したので、このサンプルをスキップ:", e)
        return None
    
    return {
        "input_ids": full_tokens,
        "labels": [-100] * len_user_prompt_tokens + full_tokens[len_user_prompt_tokens:],
        "attention_mask": [1] * (len(full_tokens)),
    }

モデルが学習時に使うときのデータセットを整形し出力しています。出力はそれぞれ次を表します。

・input_ids:ユーザ文 + 回答のトークナイズID
・labels:モデルが予測すべき正解トークン列
・attention_mask:有効なトークン。パディングされた部分は学習対象から除外する。

モデルとトークナイザの読み込み

tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    device_map="auto",
    torch_dtype=torch.float16
)

tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id = tokenizer.eos_token_id

指定したPathからトークナイザとモデルを読み込む。

torch_dtype=torch.float16 (=半精度)でモデルをロードし、VRAM使用量を削減しています。

今回使用したLlama系のモデルはパディング用トークンを持たない場合が多いため、代わりに終了トークンをパディングトークンとして指定しています。

LoRAの設定と適用

lora_config = LoraConfig(
    r=LORA_R,
    lora_alpha=LORA_ALPHA,
    lora_dropout=LORA_DROPOUT,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=TARGET_MODULES
)

model = get_peft_model(model, lora_config)

指定した前述のパラメータを用いてLoRA configを作成する。get_peft_modelによって、元のモデルに対してLoRA層を差し込みます

Trainerの設定と実行

training_args = TrainingArguments(
    output_dir=output_dir,
    per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
    gradient_accumulation_steps=GRAD_ACCUMULATION_STEPS,
    num_train_epochs=EPOCHS,
    learning_rate=LEARNING_RATE,
    fp16=True,
    logging_steps=LOGGING_STEPS,
    save_steps=SAVE_STEPS,
    save_total_limit=SAVE_TOTAL_LIMIT,
    report_to=["wandb"],
)

data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    data_collator=data_collator
)

trainer.train()

指定したパラメータとパスをもとにTrainer confitを作成し、学習を開始する。


6. より賢いモデルを作るには


手法1. 学習データの拡充
今回はデモとして1,000サンプルのデータセットを使用しましたが、より多くのデータセットを利用することで応答品質が向上する可能性があります。
また、金融や科学など、ドメインに特化したデータセットで学習することで領域特化のLLMを開発することもできます。

手法2. モデルのパラメータ数を増やす

今回は非常にパラメータ数の小さいLlamaモデルで fine-tuning しました。Llamaシリーズには、8Bや70Bといったより多いパラメータ数を持つモデルがあります。ただし、パラメータ数が増えるほど必要なVRAM数(モデルサイズに比例)や学習時間(モデルサイズの〜2乗に比例)が増えます。

手法3. ハイパーパラメータの調整

上記2手法以外に、モデル学習時のハイパーパラメータを調整することでも性能改善が期待できます。学習自体の試行回数を増やす必要があり、非常に時間のかかる作業ですが、AI-Scientistなどの自動化手法を使って試してみるのもアリですね。


7. 最後に


株式会社IZAIでは音声認識や音声合成に関する研究開発・サービス開発を行っています。興味のあるエンジニア、学生、企業様などいらっしゃいましたらお気軽にお問い合わせください。

参考になった方はいいね、コメントをいただけると今後の開発の励みになります。それではまた!


参考ページ


  1. https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct

  2. https://github.com/shi3z/alpaca_ja

  3. https://digitaldiy.jp/article/firstdiy/14661/

  4. https://gammasoft.jp/support/how-to-access-models-by-logged-into-hugging-face/

  5. https://wandb.ai/site/ja/

  6. https://qiita.com/bostonchou/items/bf4a34dcbaf45828f886#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB

いいなと思ったら応援しよう!

この記事が参加している募集

ピックアップされています

LLM

  • 345本

コメント

ログイン または 会員登録 するとコメントできます。
「人と機械が自然に共創する社会」を目指す東大発スタートアップ企業「株式会社IZAI」の公式note。 音声AI・生成AIを中心にカスタマサポートやコールセンター、コンテンツ業界、Web業界等で活用できる最新の技術情報を発信しています。https://www.izai.co.jp/
【ローカルLLMの作り方①】サクッとLlamaをfine-tuningしてみた|株式会社IZAI(イザイ) |技術ブログ
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1