👨‍👦

PythonでLLMのSFT(教師ありファインチューニング)を実践するための最短ガイド

2024/11/24に公開

概要

  • 大規模言語モデルを扱うのに慣れていない初心者でも「教師ありファインチューニング (SFT : Supervised Fine-Tuning)」にチャレンジできる手順をまとめました!
  • 学習目的でとりあえずSFTに触れてみたい!という人向けに、Google Colabと公開データを使って最短距離で再現する方法を紹介してみます。

これを読むとできるようになること

  • 教師ありファインチューニング (SFT : Supervised Fine-Tuning)をやる前後でモデルの回答品質を比べてみる。
  • LLMを自分で作った学習データでファインチューニングできる。

下準備

今回使用するサービスを紹介します。全て基本的に無料で利用できます。

  • Hugging Face:LLM用のGitHubみたいな自然言語処理(NLP)に特化したプラットフォームです。事前学習済みモデル、データセット、コミュニティツールなどが提供されています。MetaのオープンソースモデルMeta-Llama-3-8Bなどもここで公開されています。
  • WandB(Weights&Biases):機械学習モデルの管理用プラットフォームです。実験のパラメータ、メトリクス、コードのバージョンなどを記録し、比較や分析が簡単にできます。
  • Google Colab:Googleが提供するクラウドベースのコード実行環境です。無料でGPUを使えるので、LLM開発に便利です。環境構築が不要で気軽にブラウザからアクセスできます。
    • 現在、Google ColabではいくつかのGPUが無料で提供されています。設定しておきましょう。
      • 「ランタイム>ランタイムのタイプを変更」でGPUを選択して保存すれば、処理実行時に自動で割り当てられるようになっています。「T4 GPU」(Tesla T4)がおすすめです。

なお、今回Hugging FaceとWandBはAPIアクセスして利用するのでアカウントを作成しておきましょう。また、今回はGoogle Colab環境での実行を前提に解説していきます!

教師ありファインチューニング(SFT)の手順

早速、手順を紹介します。以下のコードは全てGoogle Colabで実行することを前提にしています。以下の二段階に分けて解説します!

  • モデルの学習
  • モデルの推論

1. モデルの学習

ライブラリのインストール

必要なライブラリをインストールします。

!pip install --upgrade --no-cache-dir "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git" # Unslothを最新版にアップデート
!pip install --upgrade torch # Pytorchを最新版にアップデート
!pip install --upgrade xformers # xformersを最新版にアップデート

# GPUのCUDA Compute Capabilityが8以上の場合のみFlashAttention 2ライブラリをインストール
import torch
if torch.cuda.get_device_capability()[0] >= 8:
    !pip install --no-deps packaging ninja einops "flash-attn>=2.6.3"

メジャーな機械学習ライブラリPytorchやファインチューニングのライブラリUnslothなどをインストールしています。

モデルの定義

今回は大規模言語モデル研究開発センター(LLMC)で日本語に強いGPT-3級の大規模言語モデルとして開発されたLLM-jp-3-13bをベースモデルとして利用させていただきます。

# llm-jp/llm-jp-3-13bを4bit量子化のqLoRA設定でロードする

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from unsloth import FastLanguageModel
import torch
max_seq_length = 512 # コンテクスト長。unslothでRoPEをサポートしているので可変。
dtype = None # Noneにしておけば自動で設定される。
load_in_4bit = True # 4bit量子化を使うかどうか。メモリ使用量を大幅に削減できるが精度が若干低下することがある

model_id = "llm-jp/llm-jp-3-13b"
new_model_id = "llm-jp-3-13b-sft-1.0" # 新しいモデルの名前
# FastLanguageModelでHugging Faceからモデルをロードし、インスタンスを作成
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name=model_id,
    dtype=dtype,
    load_in_4bit=load_in_4bit,
    trust_remote_code=True,
)

# SFT用のモデルを用意
model = FastLanguageModel.get_peft_model(
    model,
    r = 32, # LoRAランク。小さい値ほどパラメータ数が減りメモリ効率が上がるが、精度も低下する可能性がある
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 32, # 低ランク行列のスケーリング係数。値が大きいほど、追加されるパラメータの影響が大きくなる
    lora_dropout = 0.05, # LoRAの低ランク行列に適用するドロップアウト率
    bias = "none", # バイアスの処理方法。
    use_gradient_checkpointing = "unsloth", # Unslothによる勾配計算のメモリ効率化
    random_state = 3407, # 乱数のシード値
    use_rslora = False, # rslora(別のLoRA手法)を使うかどうか。ここでは使用しない。
    loftq_config = None, # LoRAと量子化の組み合わせ設定。ここでは使用しない。
    max_seq_length = max_seq_length, # 最大シーケンス長。
)

UnslothのモジュールでHugginf Faceからロードしています。

モデルのアップロード設定

今回開発したモデルはHugging Faceにアップロードします。サインアップし、APIのアクセストークンを取得して入力してください。
参考:Hugging Faceにサインアップ & アクセストークンを取得する

# Hugging Faceで取得したAccess Token(取得したものを入力してください)
HF_TOKEN = "~~~"

学習用データセットのロード

ファインチューニングに使用するデータセットをロードします。

今回は理化学研究所革新知能統合研究センター言語情報アクセス技術チームが公開している以下のデータを使わせていただきます。幅広い日本語のQ&Aがリスト化されたデータになっています。
https://liat-aip.sakura.ne.jp/wp/llmのための日本語インストラクションデータ作成/llmのための日本語インストラクションデータ-公開/
関根聡, 安藤まや, 後藤美知子, 鈴木久美, 河原大輔, 井之上直也, 乾健太郎.
ichikara-instruction: LLMのための日本語インストラクションデータの構築. 言語処理学会第30回年次大会(2024)

リンクからデータをダウンロード・展開してGoogle Colabのファイルブラウザにドラッグ&ドロップでアップロードしてください。ここではichikara-instruction-003-001-1.jsonを使った例を示します。

アップロードしたデータのパスを下記のdata_filesの変数に書き込みます。

from datasets import load_dataset

dataset = load_dataset("json", data_files="/content/ichikara-instruction-003-001-1.json")
dataset

dataset が学習に用いるデータセットにあたります。

Google Colab環境のディレクトリは画面左のサイドバーからGUIで確認できます。
初期表示がルートディレクトリではなくcontent/配下になっているのでファイルパスに注意しましょう。ケバブメニュー(縦に三点並んだマーク)の「パスをコピー」から行えば早くて安心です。

学習時のプロンプトフォーマットを定義

生データには質問と回答の文章のみが入っているため、プロンプトの質問と回答として整形します。

prompt = """### 指示
{}
### 回答
{}"""

EOS_TOKEN = tokenizer.eos_token # トークナイザーのEOSトークン(文末トークン)
def formatting_prompts_func(examples):
    input = examples["text"] # 入力データ
    output = examples["output"] # 出力データ
    text = prompt.format(input, output) + EOS_TOKEN # プロンプトの作成
    return { "formatted_text" : text, } # 新しいフィールド "formatted_text" を返す

全データにフォーマットを適用して処理用のデータセットを作成

上記で定義したフォーマットを全ての学習データに適用します。

dataset = dataset.map(
    formatting_prompts_func,
    num_proc= 4, # 並列処理数
)

dataset

学習用のハイパーパラメータを定義

学習時のハイパーパラメータを設定します。学習の精度や効率に大きく影響するところですので慎重に確認していきましょう。ハイパーパラメータとは、学習率などのチューニングの過程に関わる変数のことです。
参考:機械学習におけるハイパーパラメータとは?概要やチューニング方法を解説

from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset=dataset["train"],
    max_seq_length = max_seq_length,
    dataset_text_field="formatted_text",
    packing = False,
    args = TrainingArguments(
        per_device_train_batch_size = 2, # デバイスごとの訓練バッチサイズ
        gradient_accumulation_steps = 4, # 勾配の修正一回ごとの試行ステップ数
        num_train_epochs = 1, # エポック数
        eval_steps=0.2, # 評価を行うstep数の間隔
        logging_steps = 10, # ログを出力するステップ数の間隔
        warmup_steps = 10, # 学習率のウォームアップのステップ数(徐々に学習率を上げる)
        save_steps=100, # モデルを保存するステップ数
        save_total_limit=2, # 保存するcheckpoint数の制限
        max_steps=-1, # 訓練の最大ステップ数
        learning_rate = 2e-4, # 学習率
        fp16 = not is_bfloat16_supported(), # 16bit浮動小数点の設定
        bf16 = is_bfloat16_supported(), # BFloat16の設定
        group_by_length=True, # バッチをグループ化する入力シーケンスの長さ
        seed = 3407, # 乱数のシード値
        output_dir = "outputs", # 新しいモデルの保存先
    ),
)

学習の実行

いよいよ、学習を実行します。時間が最も長くかかる工程になるので、コーヒーでも飲みながら気長に待ちましょう。
GPUにもよりますが、学習データが多いほど処理時間は長くなります。私の場合ファインチューニング用の質問データ2000件で約1時間かかりました。

trainer_stats = trainer.train()

Colabの環境で行う場合、ブラウザを閉じたりPCがオフラインになったりするとランタイムが切断されて最初からやり直しになってしまうので注意しましょう

SFT済みモデルをHugging Faceにアップロード

学習済みのモデルをHugging Faceにアップロードします。

model.push_to_hub_merged(
    new_model_id,
    tokenizer=tokenizer,
    token=HF_TOKEN,
    private=True # モデルの公開・非公開を選択する(アップロード後に変更も可)
)

以上で、ファインチューニングによるモデルの学習は完了です!

2. モデルの推論

次は、ファインチューニングを行ったモデルで推論を実行してみましょう。

ライブラリのインストール

!pip install -U bitsandbytes
!pip install -U transformers
!pip install -U accelerate
!pip install -U datasets
!pip install ipywidgets --upgrade

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)
import torch
from tqdm import tqdm
import json

量子化のためのライブラリbitsandbytesやPytorchを便利に使えるライブラリAccelarateなどをインストールしています。

読み込むモデルの指定

この後に読み込むLLMのモデルを指定します。
先ほどHuggingFaceにアップロードした、自分のリポジトリ名を指定します。
「"{ユーザー名}/{上述の学習セクションで入力したモデル名}"」になるはずですが、過去に作ったモデルや他人がHugging Faceに公開しているモデルを使いたい場合は右辺に指定してください。

model_name = ""

QLoraの設定とモデルのロード

推論に使用するモデルの量子化とロードを行います。

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=False,
)

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
    token = HF_TOKEN
)

tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True, token = HF_TOKEN)

タスク用データセットの読み込み

今回はELYZA-tasks-100と呼ばれるベンチマーク用の質問集を使ってみたいと思います。
こちらも学習済みモデルと同様、Hugging Faceにて公開されているCSVファイルを読み込んで使用しています。

import pandas as pd
df = pd.read_csv("hf://datasets/elyza/ELYZA-tasks-100/test.csv")

処理用データセットの作成

タスク用のデータセットは生データでは質問と回答の羅列になっているため、回答を求める質問の形式に整形します。

inputs = df["input"].tolist()

results = []
for input_text in tqdm(inputs):  # inputs リストを反復処理
    prompt = f"""### 指示
{input_text}
### 回答:
"""

推論の回答を生成

用意したデータセットをもとに、全ての質問に対する回答を推論し、生成します。

    tokenized_input = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt").to(model.device)
    with torch.no_grad():
        outputs = model.generate(
            tokenized_input,
            max_new_tokens=100,
            do_sample=False,
            repetition_penalty=1.2
        )[0]
    output = tokenizer.decode(outputs[tokenized_input.size(1):], skip_special_tokens=True)

    results.append({"input": input_text, "output": output}) # 生成結果をリストに追加

tqdmでコーンソール上にプログレスバーを表示しているので、進捗状況がビジュアル化されるはずです。

JSONL形式で回答をエクスポートする

ColabのフォルダにアウトプットのJSONLファイルを生成します。
こちらに生成されているのが今回SFT(教師ありファインチューニング)を施したLLMの出力結果です!

import json
import re

model_name_simple = re.sub(".*/", "", model_name)  # モデル名からパスを除去

with open(f"./{model_name_simple}-outputs.jsonl", 'w', encoding='utf-8') as f:
    for result in results:
        json.dump(result, f, ensure_ascii=False)
        f.write('\n')

ランタイムを終了するとこちらのファイルは削除されてしまうので、忘れずローカルにダウンロードしておきましょう。

まとめ

以上、ファインチューニングによる学習と推論を解説しました!
どうでしたか?簡単なSFTでも、出力結果が変わることがお分かりいただけたのではないかと思います。

今回解説しませんでしたがプロンプトやパラメータを調整したり学習データ変えたりすれば、もっとブラッシュアップできる余地がありますね

自分でもモデルを育ててみたい!と興味が湧いてきた方もいるのではないでしょうか?

もし、間違いのご指摘やご意見などあれば記事にコメントしていただけると幸いです。最後までお読みいただき、ありがとうございました!🙇

※補足:Google ColabのGPU無料使用枠について

  • 無料枠だと月間のGPUの使用時間が限られており、今回のような学習を数回行うと制限がかかってしまいます。
    • 月額1,179円課金してColab Proに入ると月100コンピューティングユニット(T4なら約50時間分)が使えるようになります。
    • コスパは良いので、今後もGPUを使う可能性がある方は入っておいて損はないでしょう。

Discussion