小型LLMをQLoRAで「教師ありファインチューニング (SFT)」してみた
東京大学の松尾研究所LLMコミュニティで
小型LLMを
教師ありファインチューニング (SFT :Supervised Fine-Tuning)しようというハンズオン講座がありました。
非常に勉強になったので、
実装方法などをご紹介していきたいと思います。
なぜ、LLMを「教師ありファインチューニング」する必要があるのか?
実装に入る前に、そもそも、なぜ、教師ありファインチューニングを行う必要があるのか整理しておきます。
膨大なテキストデータを用いて事前学習された
大規模言語モデル(LLM)は、そのままでは、
ChatGPTのような人間の意図を汲んだ振る舞いはできません。
例えば、「東京の観光名所を具体的に3つ挙げてください」
といった要求には答えられません。
なぜなら、
事前学習では「次に来る単語の予測」が目的となっているからです。
事前学習済みのLLMは豊富な知識を持っていますが、
人間の指示に従って応答するよう最適化されていないのです。
そこで、教師ありファインチューニングが必要になってきます。
人間の「指示」と「理想的な応答」のペアから成る
データセットを用いてモデルを再学習させることで、
LLMに人間の指示に従って応答させることができるようになるのです。
ちなみに、こうした教師ありファインチューニングを
指示チューニングと呼びます。
(以降、指示チューニングといいます)
指示チューニングによって、
LLMが人間の意図や指示を理解し、それに応じた適切な応答を生成できるようになるのです。
実装の流れ
ここから実装に入りますが、
最初に、全体の流れを確認します。
大きく分けて、
❶準備フェーズ
❷学習フェーズ
❸推論フェーズ
に分かれます。
今回は、学習を効率的に進めるために、
QLoRAという技術を使います。(後述)
講座と異なる点
❶チューニングするモデル
講座では、Qwen2.5のinstructionモデル(指示チューニング済みモデル)を使っていましたが、
この記事では、大規模言語モデル研究開発センター(LLMC)のllm-3-1.8B(指示チューニング前)を使っています。
❷学習データセット
講座では、独自のデータセットを使っていましたが、
この記事では、Databricksが公開したDatabricks-dolly-15k-jaという指示チューニング用のデーターセットを使います。
1️⃣準備フェーズ
ライブラリー準備・基本設定
🌀ライブラリーのインストール
!pip install peft
!pip install transformers
!pip install datasets
!pip install accelerate bitsandbytes evaluate trl
🌀ライブラリーのインポート
import torch
from torch import cuda, bfloat16
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
BitsAndBytesConfig,
HfArgumentParser,
TrainingArguments,
pipeline,
logging
)
from datasets import load_dataset
from peft import LoraConfig, PeftModel
from trl import SFTTrainer
🌀基本パラメータの設定
基本パラメータとして、
使用するモデル、
チューニング後のモデル名、
outputディレクトリー名を設定します。
model_id = "llm-jp/llm-jp-3-1.8b"
peft_name = "llm-jp-3-1.8B-inst_L4_3ep"
output_dir = "output_neftune"
学習用データセットの準備
🌀データセットのダウンロードと刈り込み
# データダウンロード
dataset = load_dataset("llm-jp/databricks-dolly-15k-ja")
print(dataset)
print出力すると、15,011データあることがわかります。
# 出力結果
DatasetDict({
train: Dataset({
features: ['instruction', 'context', 'response', 'category'],
num_rows: 15011
})
})
学習に時間がかかるため、
学習に使うのは最初の1,000データだけにします。
import pandas as pd
df = dataset["train"].to_pandas()[:1000]
# JSONファイルに保存
df.to_json(
"/content/dataset.json",
orient = "records",
force_ascii = False,
indent =4)
dataset = load_dataset("json",data_files="/content/dataset.json")
print(dataset)
print出力すると、1,000データになっていることがわかります。
# 出力結果
DatasetDict({
train: Dataset({
features: ['instruction', 'context', 'response', 'category'],
num_rows: 1000
})
})
🌀プロンプトテンプレートの設定
指示・回答の対話型データをLLMの入力形式に変換するためには、
対話をまとめて一つのテキストに変換する必要があります。
datasetから、「instruction」と「response」を抽出して、
各データをプロンプトに合わせた形式にします。
# 学習時のプロンプトフォーマットの定義
prompt = """
あなたは優秀なアシスタントです。指示に対して適切な回答を行なってください。
### 指示
{}
### 回答
{}"""
EOS_TOKEN = tokenizer.eos_token # トークナイザーのEOSトークン(文末トークン)
def formatting_prompts_func(examples):
input = examples["instruction"] # 入力データ
output = examples["response"] # 出力データ
text = prompt.format(input, output) + EOS_TOKEN # プロンプトの作成
return { "text" : text, } # 新しいフィールド "formatted_text" を返す
pass
# 各データにフォーマットを適用
dataset = dataset.map(
formatting_prompts_func,
num_proc= 4, # 並列処理数を指定
)
dataset
学習用モデルの設定
‼️QLoRAによる学習
小型LLMといっても、
今のモデルはパラメータ数が多いため、
普通に学習すると大量のメモリが必要となり、
学習に多大な時間がかかってしまいます。
そこで、学習を効率的に進めるため、
QLoRAを用います。
QLoRAとは、
量子化(Quantization)とLoRAという二つの手法を
組み合わせてファインチューニングさせる手法です。
ひとつずつ解説していきます。
🌀量子化の設定
モデルのパラメータは数値になっていて、
一般的に、32ビットや16ビットの浮動小数点を使って
表現されることが多いです。
この数値表現が、
メモリの使用量に大きな影響を与えます。
ビット数が大きいほど高精度ではありますが、
その代わりに計算コストが大きくなってしまいます。
そこで、
量子化という技術を使って、
数値表現を8ビット整数や4ビット整数に変換し、
精度は下がるけれども、メモリの使用量を削減します。
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
🌀モデルとトークナイザーの設定
LoRAに入る前に、
ここで、transformersからモデルとトークナイザーをダウンロードして、
必要な設定を行います。
なお、トークナイザーは、テキストをモデルが処理できる形式(トークン)に変換したり、その逆を行うツールです。
# モデルの設定
model = AutoModelForCausalLM.from_pretrained(
model_id,
trust_remote_code=True,
quantization_config=bnb_config, # 量子化
device_map='auto',
torch_dtype=torch.bfloat16,
)
# トークナイザーの設定
tokenizer = AutoTokenizer.from_pretrained(
model_id,
padding_side="right",
add_eos_token=True # 入力テキストの最後に EOSトークン を自動的に追加します
)
if tokenizer.pad_token_id is None:
tokenizer.pad_token_id = tokenizer.eos_token_id
🌀LoRAの設定
LoRAとは、
事前学習済みのモデルが持っているパラメータは固定したままで、
そこからの差分を、
パラメータ数の少ない二つの行列の積として学習することで
メモリ使用量を抑える技術です。
モデルは学習によって、パラメータが更新されます。
これを式で表すと、
更新後のパラメータ=元のパラメータ+差分となりますよね
この差分に着目し、
また、差分を小さな行列(低ランク行列)で表すことで
計算コストを抑えることができるのです。
LoRAの詳細は、以下のサイトが割と分かりやすいです。
LoRAは線形層に適用されるので、
まずは、モデルの線型層を取得します。
import bitsandbytes as bnb
def find_all_linear_names(model):
target_class = bnb.nn.Linear4bit
linear_layer_names = set()
for name_list, module in model.named_modules():
if isinstance(module, target_class):
names = name_list.split('.')
layer_name = names[-1] if len(names) > 1 else names[0]
linear_layer_names.add(layer_name)
if 'lm_head' in linear_layer_names:
linear_layer_names.remove('lm_head')
return list(linear_layer_names)
# 線形層の名前を取得
target_modules = find_all_linear_names(model)
print(target_modules)
量子化もLoRAもconfigなんですよね。
peft_config = LoraConfig(
r=8, # 差分行列のランク
lora_alpha=16, # LoRA層の出力のスケールを調整するハイパラ
lora_dropout=0.05,
target_modules = target_modules,
bias="none",
task_type="CAUSAL_LM",
modules_to_save=["embed_tokens"],
)
🌀学習パラメータの設定
eval_steps = 20
save_steps = 20
logging_steps = 20
training_arguments = TrainingArguments(
bf16=True,
per_device_train_batch_size=4,
gradient_accumulation_steps=16,
num_train_epochs=3,
optim="adamw_torch_fused",
learning_rate=2e-4,
lr_scheduler_type="cosine",
weight_decay=0.01,
warmup_steps=100,
group_by_length=True,
# report_to="wandb"
report_to="none",
logging_steps=logging_steps,
eval_steps=eval_steps,
save_steps=save_steps,
output_dir=output_dir,
save_total_limit=3,
push_to_hub=False,
auto_find_batch_size=True # GPUメモリがオーバーフローさせないため
)
2️⃣学習フェーズ
🌀SFTrainerの設定
SFTTrainerは、教師ありファインチューニング(Supervised Fine-tuning)で学習するためのライブラリです。
このライブラリーでtrainerをセットします。
NEFTuneで学習の質を上げています。
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
dataset_text_field="text",
peft_config=peft_config,
args=training_arguments,
max_seq_length=1024,
packing=False,
neftune_noise_alpha=5,
)
🌀学習の実行
%%time
model.config.use_cache = False
trainer.train()
model.config.use_cache = True
# QLoRAモデルの保存
trainer.model.save_pretrained(peft_name)
3️⃣推論フェーズ
ここからは指示チューニングの効果を確認していきます。
モデルに指示を入れて、テキスト生成させましょう。
まずは、テキスト生成を実行する前に、
指示チューニングで使用していたGPUメモリをリセットしています。
import torch
torch.cuda.empty_cache() # GPUメモリをリセット
🌀モデルとトークナイザーのダウンロード・設定
%%time
# 量子化設定
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# モデルの設定
model = AutoModelForCausalLM.from_pretrained(
model_id,
trust_remote_code=True,
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.bfloat16,
)
# tokenizerの設定
tokenizer = AutoTokenizer.from_pretrained(
model_id,
padding_side="right",
add_eos_token=True
)
# eos_token_id の設定
if tokenizer.eos_token_id is None:
tokenizer.add_special_tokens({"eos_token": "<|endoftext|>"}) # 必要ならトークン追加
tokenizer.eos_token_id = tokenizer.convert_tokens_to_ids("<|endoftext|>")
# pad_token_id の設定
if tokenizer.pad_token_id is None:
tokenizer.pad_token_id = tokenizer.eos_token_id
🌀指示チューニングモデルの作成
チューニング前のモデルにチューニング済みのQLoRAのパラメータを足し合わせます。
from peft import PeftModel
ft_model = PeftModel.from_pretrained(model, peft_name)
🌀指示チューニング後のモデルでテキスト生成する!
「時間管理能力をあげるには?」というプロンプトを入れます。
input_text = "時間管理能力をあげるには?"
prompt = f"""
### 指示
{input_text}
### 回答:
"""
# Tokenizerの呼び出しでattention_maskを生成
tokenized_input = tokenizer(prompt, add_special_tokens=False, return_tensors="pt", padding=True).to(model.device)
# 終端トークンを設定
eos_token_id = tokenizer.eos_token_id
# テキスト生成
with torch.no_grad():
outputs = ft_model.generate(
input_ids=tokenized_input["input_ids"],
attention_mask=tokenized_input["attention_mask"],
max_new_tokens=100,
eos_token_id=eos_token_id,
pad_token_id=tokenizer.pad_token_id,
do_sample=False,
repetition_penalty=1.2
)[0]
# 出力のデコード
output = tokenizer.decode(outputs[tokenized_input["input_ids"].size(1):], skip_special_tokens=True)
print(output)
出力結果
1.スケジュール帳やカレンダーアプリを使って、重要なイベントの日付と時刻を記録する。
2.タスクリストを作る。
3.優先順位をつける。
4️⃣HuggingFaceへのモデルのアップロード
作ったモデルはHuggingFaceへアップロードしておくと、
何かと便利です。
from huggingface_hub import login
from google.colab import userdata
# HuggingFaceログイン
login(userdata.get('HF_TOKEN'))
repo_name = "******/llm-jp-3-1.8B-inst_L4_3ep_2"
#トークナイザーをアップロード
tokenizer.push_to_hub(repo_name)
#モデルをアップロード
ft_model.push_to_hub(repo_name)
ここまでお読みいただきありがとうございました。
さいごに:小型LLMのファインチューニングを学ぶ意義など
OpenAIのCEO サム・アルトマンは、
「巨大AIモデルを用いる時代は終った」と語っているそうです。
その真意は分かりかねますが、
確かに最近は、巨大AIモデルについて、
コストの高さやセキュリティ問題など
さまざまな課題が指摘されていますよね。
そんな中、
注目を集めているのがオープンソースの小型LLMです。
代表的なモデルとして、
メタが開発したLlamaやアリババが開発したQwenがあります。
小型LLMは、大規模言語モデルの能力を維持しつつ、
効率性と実用性を重視した設計が特徴です。
オープンソースの小型LLMはコストが安く、
また、
クラウドを使わずローカル環境でデータ処理できるので、
セキュリティの観点でも優れています。
そして、何より、
オープンソースの小型LLMであれば、
今回ご紹介するように、
自由に「カスタマイズ」することができます。
これからの時代、
ChatGPTのような万能LLMに加え、
小型LLMによる自社専用・自分専用の特化型LLMも
流行っていく予感がします。
こうした点で、
小型LLMをファインチューニングする方法を
学ぶ意義は大変大きいと感じています。