Hugging Face 周辺ライブラリを使用して、Fine-tuning をやってみよう
Hugging Face は様々なPythonライブラリを提供しており、本記事では以下のライブラリを使用します。
- transformers
- datasets
- peft
- trl
そもそもなんで Hugging Face 周辺ライブラリを使うの!
Fine-tuning をするためには、
- Fine-tuning 元のモデル
- Fine-tuning に使用するデータセット
- Fine-tuning に必要な様々な処理・機能 (?)
が必要となります。
Fine-tuning 元のモデルは、Hugging Face Hub 上に多く存在するし、
Fine-tuning に使用するデータセットも、Hugging Face Hub 上に多く存在します。
そこらへんのアクセスを簡単にしつつ、Fine-tunig に必要な機能が Python ライブラリとして提供されている Hugging Face 周辺ライブラリを使おうということです。
Fine-tuning ってなにをすればいいの?
Fine-tuningでは、既存のなにかしらのモデルに対し、データセットを使用した追加学習をします。
本記事での Fine-tuning は、SFT (Supervised Fine-Tuning (教師あり学習)) とします。
あなたが Fine-tuning 済みのモデルを手に入れたいのであれば、以下の事前準備が必要です。
- 学習対象のモデルを決める
- 学習に使用するデータセットを決める
- どんな入力に対して、どんな出力ができるようになればOKか決める
そして、どのように Fine-tuning をさせるによって、実装方法が変わってきます。
今回は、LoRAという手法を用いて Fine-tuning 済みのモデルを作成するものとします。
LoRA って??
LoRA (Low-Rank Adaptation)は、モデル全体を再学習させるアプローチとは異なり、ベースモデルに対し小さな行列(LoRA層)を追加することで、出力の調整を可能とする Fine-tuning 手法の1種です。
つまり、ベースモデル + LoRA層 = Fine-tuning 済みモデル ということになります。
ベースモデルとLoRA層をマージして1つのモデルとして出力することも可能です。(というか基本的にそのようにするかも)
そうすると、配布する時などに別途LoRA層を添付する必要がなくなります。
というわけで、Hugging Face ライブラリを使用して、LoRA層を作ってみましょう!
Hugging Face ライブラリを使ってみる
突然「使ってみる」って言われても、、と困りますよね。
本記事の例では、英語対応のモデルに対して日本語のデータセットを与え、日本語で回答してもらえるようにします。
まずは全体像を把握しましょう。
全体像
おおまかに、以下のステップで進めていきます!
前項でも記載したとおり、LoRA層が出力できればOKとします。
- Hugging Face が提供する
transformersライブラリを使用して、トークナイザーを読み込む (トークナイザーってなに?は後で) - 同じく
transformersライブラリを使用して、モデルを読み込む - Hugging Face が提供する
peftライブラリを使用して、LoRAの設定を作成 - Hugging Face が提供する
datasetsライブラリを使用して、データセットを読み込む - Hugging Face が提供する
trlライブラリを使用して、学習設定・トレーナーを作成 -
5で作成したトレーナーを.train()関数で実行し、学習 - LoRA アダプタファイルが完成 🥳
以下で実装例を紹介しますが、ソースコードは簡略化しています。
1. トークナイザーを読み込む
トークナイザーとはなんでしょうか。
Fine-tuning では、データセットを学習するくんに食わせるのですが、データセットは日本語の文章だったりするので、コンピューターは読めません。
そこで、トークナイザーでトークンに分割し、コンピューターが読める形に変換するのです。
各単語(厳密には単語ではない)にidを付与し、ベクトル計算しやすくするみたいなのをイメージすればOKです。
transformersライブラリが提供するAutoTokenizerクラスを使用し、以下のコードでトークナイザーの読み込みが可能です。
# 今回は以下のモデルを使用します
BASE_MODEL = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
# トークナイザー読み込み
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
2. モデルを読み込む
先ほどと似たように、transformersライブラリが提供するAutoModelForCausalLMクラスを使用し、以下のコードでモデルの読み込みが可能です。
# モデル読み込み
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
dtype=torch.float16 # mps想定
)
3. LoRAの設定を記述
peftライブラリが提供するLoraConfigクラスを使用し、以下のコードでLoRAの設定を記述します。パラメータは必要に応じて調整しましょう。
# LoRA設定
peft_config = LoraConfig(
r=16, # LoRAの低ランク行列の次元数
lora_alpha=32, # LoRAのスケーリングパラメータ
lora_dropout=0.05, # LoRA層のドロップアウト率
bias="none", # バイアスを学習しない
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "down_proj", "up_proj"], # LoRAを適用する層の名前リスト
task_type="CAUSAL_LM", # 因果的言語モデル(GPT系)
)
4. データセットを読み込む
datasetsライブラリを使用し、データセットを読み込みます。
データセットの読み込みでは、2つのステップがあります。
4-1. 2つのデータセットを読み込む
今回使用するデータセットは kunishou/databricks-dolly-15k-ja と fujiki/japanese_alpaca_data の2つです。
2つのデータセットを読み込む際、データセットを結合する concatenate_datasets 関数を使用します。
concatenate_datasets 関数を wrap した、load_instruction_like_dataset という独自関数を作成してみました。
def load_instruction_like_dataset(names: List[str], sample_size: int, seed: int) -> Dataset:
"""複数のinstructionデータセットを結合・シャッフル・サンプリング"""
parts = []
for n in names:
ds = load_dataset(n, split="train")
parts.append(ds)
merged = concatenate_datasets(parts)
merged = merged.shuffle(seed=seed)
if sample_size > 0 and sample_size < len(merged):
merged = merged.select(range(sample_size))
return merged
4-2. データセットを、LLMが理解しやすいよう整形する
今回はAlpaca形式と呼ばれる形式で、データセットを整形します。
Alpaca形式は、instruction、input、output の3つに分かれるようなものです。
以下の make_formatting_func という独自関数を作成し、データを整形するものとします。
def make_formatting_func(tokenizer: AutoTokenizer):
"""Instruction形式でデータを整形する関数を返す"""
TEMPLATE = """### Instruction:
{instr}
### Input:
{inp}
### Response:
{out}"""
def formatting_func(example: Dict[str, Any]) -> str:
ins = example.get("instruction", "")
out = example.get("output", example.get("response", ""))
inp = example.get("input", "")
return TEMPLATE.format(instr=ins, inp=inp or "", out=out or "")
return formatting_func
上記2つのステップで、データの結合と整形の準備が完了しました。
以下のコードで、データセットの準備が完了です。
DATASETS = [
"kunishou/databricks-dolly-15k-ja",
"fujiki/japanese_alpaca_data"
]
SAMPLE_SIZE = 4000
# データ準備
dataset = load_instruction_like_dataset(DATASETS, SAMPLE_SIZE, seed=42)
formatting_func = make_formatting_func(tokenizer)
5. 学習設定・トレーナーを作成
trlライブラリが提供するSFTConfigクラスを使用して、教師あり学習におけるFine-tuningの設定が作成できます。
# 学習設定
# 引数に渡している定数は、お好みの値を設定してください。
# エディタ上でSFTConfigクラスをマウスホバーすると、デフォルト値が確認できるはずです。
sft_cfg = SFTConfig(
output_dir=OUT_DIR,
num_train_epochs=EPOCHS,
per_device_train_batch_size=BATCH_SIZE,
gradient_accumulation_steps=GRAD_ACCUM,
learning_rate=LEARNING_RATE,
warmup_ratio=WARMUP_RATIO,
logging_steps=LOGGING_STEPS,
save_steps=SAVE_STEPS,
fp16=(device == "mps"),
bf16=False,
max_length=MAX_LENGTH,
packing=False,
report_to=[],
)
同じくtrlライブラリが提供するSFTTrainerクラスを使用して、トレーニング設定が適用されたトレーナーを作成します。
trainer = SFTTrainer(
model=model,
processing_class=tokenizer,
peft_config=peft_config,
args=sft_cfg,
train_dataset=dataset,
formatting_func=formatting_func,
)
6. トレーナー実行
.train()関数を使用して、トレーナーを実行します。
trainer.train()
# 保存
out_adapter = os.path.join(OUT_DIR, "lora")
trainer.model.save_pretrained(out_adapter)
tokenizer.save_pretrained(OUT_DIR)
最後に、学習結果を保存して完了です。
以下のような構造で保存されます。
LoRAアダプタ
out-tinyllama-ja/lora/
├── adapter_config.json # LoRA設定
├── adapter_model.safetensors # LoRA重み(数百MB)
└── README.md # 説明ファイル
トークナイザー
out-tinyllama-ja/
├── tokenizer.json
├── tokenizer.model
├── tokenizer_config.json
└── special_tokens_map.json
これで、LoRAアダプタの作成が完了しました!🥳
LoRAアダプタの作成が完了しました!🥳と言われて困ったあなたへ
そうですよね、火にかける前の肉みたいな感じで、まだこれだけじゃ食べれないですよね。
上の方で、ベースモデル + LoRA層 = Fine-tuning済みモデルと説明しました。
つまり、今回作成したLoRA層を使用してLLMを動作させるには以下の2通りとなります。
- ベースモデルとLoRA層をマージさせたモデルを作成
- マージ前の状態で、ベースモデルと組み合わせて動かす
今回は、1つめのマージする方法を紹介します。
ベースモデルとLoRA層のマージファイル作成
以下のPythonコードで、マージ済みのモデルを作成します。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LoRA アダプタをベースモデルにマージして、単一モデル(HF形式)を出力します。
使い方:
uv run python merge_lora.py
"""
import os
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# ========================================
# 設定
# ========================================
BASE_MODEL = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
ADAPTER_DIR = "out-tinyllama-ja/lora"
OUT_DIR = "merged-tinyllama-ja"
def main():
os.makedirs(OUT_DIR, exist_ok=True)
print(f"[INFO] Loading base model: {BASE_MODEL}")
base = AutoModelForCausalLM.from_pretrained(BASE_MODEL, device_map="cpu")
print(f"[INFO] Loading LoRA adapter: {ADAPTER_DIR}")
peft_model = PeftModel.from_pretrained(base, ADAPTER_DIR)
print("[INFO] Merging ...")
merged = peft_model.merge_and_unload()
print(f"[INFO] Saving merged model: {OUT_DIR}")
merged.save_pretrained(OUT_DIR)
print(f"[INFO] Saving tokenizer: {OUT_DIR}")
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, use_fast=True)
tokenizer.save_pretrained(OUT_DIR)
print("[INFO] Done.")
if __name__ == "__main__":
main()
LLMとして動作させてみる
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
統合済みモデル(merge後)で簡易推論(日本語)を確認します。
使い方:
uv run python inference_merged.py
"""
import os
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
# ========================================
# 設定
# ========================================
MODEL_DIR = "merged-tinyllama-ja"
MAX_NEW_TOKENS = 256
TEMPERATURE = 0.7
TOP_P = 0.9
def detect_device() -> str:
"""MPS または CPU を返す"""
if torch.backends.mps.is_available():
return "mps"
return "cpu"
def main():
device = detect_device()
print(f"[INFO] device={device}")
print(f"[INFO] Loading model: {MODEL_DIR}")
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, use_fast=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR,
torch_dtype=torch.float16 if device == "mps" else torch.float32
)
model.to(device)
model.eval()
# プロンプト構築関数
def build_prompt(instr: str, inp: str = "") -> str:
return f"""### Instruction:
{instr}
### Input:
{inp}
### Response:
"""
print("\n[INFO] Ready! Type your instruction in Japanese.")
print("[INFO] Press Ctrl+C or Ctrl+D to exit.\n")
while True:
try:
instr = input("[日本語の指示を入力] > ").strip()
except (EOFError, KeyboardInterrupt):
print("\n[INFO] Exiting...")
break
if not instr:
continue
prompt = build_prompt(instr)
inputs = tokenizer(prompt, return_tensors="pt").to(device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=MAX_NEW_TOKENS,
do_sample=True,
temperature=TEMPERATURE,
top_p=TOP_P,
eos_token_id=tokenizer.eos_token_id,
)
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Response部分のみを抽出
if "### Response:" in text:
print("\n--- Response ---")
print(text.split("### Response:")[-1].strip())
else:
print("\n--- Raw ---")
print(text)
print()
if __name__ == "__main__":
# MPS安定化
os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
main()
まとめ
本記事では、以下3つの Pythonファイルを作成しました。
-
train_sft_lora_tinyllama.py- Fine-tuningしてLoRAアダプタを出力
-
merge_lora.py- ベースモデルとLoRA層を組み合わせ、モデルを作成
-
inference_merged.py- ↑で作成したモデルをチャットボットとして実行
なんかちょっと微妙だけど、日本語応答してくれるものができた!

既存のモデルとデータセットのおかげだけど、なんだか愛着が湧くね🥳
Discussion