いやはや、大変でした。
Geminiを使ってバイブコーディングすればすぐにできると思いきや、学習に使うデータのフォーマット形式を間違えていたり、学習が進まなかったり・・・というのを乗り越えて、やっとこさ流れるようにはなりました。
今回、Gemma4をベースモデルとして、そこに新たな文書ファイルを学習させるという、いわゆる「ファインチューニング」というやつをやってみました。
社内文書などを参照しながら回答する「RAG」という手法もありますが、あれに比べるとモデルそのものに文章を埋め込んでしまうので、社内文章専用生成AIを作りたい、という方なら、まずはこのファインチューニングというのが検討対象になろうかと思います。
コードは生成AIにあらかた作らせたとはいえ、そのままでは無理でした。そこで、参考にしたのは以下のサイト。
Hugging Face Transformers と QLoRA を使用して Gemma をファインチューニングしてビジョン タスクを実行する | Google AI for Developers
ここを見てある程度、どんな処理をしているかなどを知ってようやく学習が進むようになりましたね。
やり方を書く前に、まず前提から。
我が家はRTX4060Laptop(VRAM:8GB)で、メインメモリー64GBのRyzen 5マシンです。
CUDAのバージョンは13.0、Pytorchは2.12.1、ちなみにPythonは3.11を使ってます。
Pytorchのインストール方法は、以下を参照。
Previous PyTorch Versions
ちなみに、ライブラリはPytorchを除くと、
$ pip install transformers datasets peft bitsandbytes trl accelerate
あたりを入れております。
なお、今回のファインチューニングとは、Gemma4の一部を書き換える「LoRA」と呼ばれる手法を使ってます。その中で特に量子化しつつパラメータ変更を行うものを「QLoRA」と呼ぶようです。これならば、少ないVRAM搭載のGPUボードでもファインチューニングが可能となったわけです。
今回はこの「QLora」をやりました。
その際には、「unsloth」というライブラリを使うことが多いようですが、以下の理由で導入してません。
・ unslothは少ないVRAMでも高速に学習が進むライブラリながら、モデルがVRAMに収まりきらないと学習が止まる
・ 多少低速になっても構わないので、メインメモリーの一部をVRAMとして学習に使うためには、unslothはむしろ不要
解釈が間違ってるかもしれませんが、確かにunslothを使わなくなったらメモリー不足のエラーが出なくなり、学習が進むようになりました。
ということで、ここではunslothを使わずに進めました。
ここからは、そのファインチューニング手順を書いていきます。
(1)データの準備
Gemma4の場合は、以下のようなjsonファイルとする必要があります。
{"messages": [{"role": "system", "content": "あなたは優秀な自動車販売員です。"},{"role": "user", "content": "新型「ガソリーナ」のコンセプトは何ですか?"}, {"role": "assistant", "content": "「走る歓びと環境性能の極限での融合」をコンセプトに開発された、次世代フラッグシップ・スポーツセダンです。"}]}
{"messages": [{"role": "user", "content": "「ガソリーナ」という名前の由来は何ですか?"}, {"role": "assistant", "content": "情熱的な走りの原点である「ガソリン(Gasoline)」へのリスペクトと、ラテン語の力強さを組み合わせた造語です。"}]}
{"messages": [{"role": "user", "content": "ガソリーナの発売日はいつですか?"}, {"role": "assistant", "content": "2027年春の発売を予定しております。"}]}
{"messages": [{"role": "user", "content": "車両本体価格はいくらですか?"}, {"role": "assistant", "content": "ベースグレードが4,500,000円(税込)、最上位エディションが6,800,000円(税込)となります。"}]}
{"messages": [{"role": "user", "content": "ガソリーナの主要ターゲット層は誰ですか?"}, {"role": "assistant", "content": "先進的なデザインと高い走行性能、そして環境負荷の少なさを同時に求めるプレミアム志向のドライバーです。"}]}
・・・
これを「text」というフォルダを作り、その中に「gasolina_faq_100_messages.json」という名で保存しました。
ところで、ここでは学習用に、Geminiを使って「ガソリーナ」という架空の車が新発売となることを想定した100問の想定質問集を作らせて、それを学習データとしてます。
(2)モデルダウンロード
以下のサイトから、モデルをすべてダウンロードします。それを「models」というフォルダに入れます。
google/gemma-4-E4B · Hugging Face

ここの「Files and Versions」にある8個のファイル、全てです。特に「model.safetensors」は15GB以上あるので、ご注意ください。
なお、Hugging Faceにログインした状態でないと、この一番大きなモデルをダウンロードできませんでした。
ところで、Gemma4からは商用利用可能となったため、ファインチューニング用に重宝されている模様です(gemma3までは商用利用不可だったそうで)。
(3)学習用コード
以下のコードを「train.py」として保存します。
import os
import glob
import torch
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
def main():
base_model_path = "./models/gemma-4-E4B"
output_dir = "./lora_model"
text_dir = "./text"
# 1. データセットの読み込み
json_files = glob.glob(os.path.join(text_dir, "*.json")) + glob.glob(os.path.join(text_dir, "*.jsonl"))
if not json_files:
raise FileNotFoundError(f"'{text_dir}' フォルダ内にJSONファイルが見つかりませんでした。")
raw_dataset = load_dataset("json", data_files=json_files, split="train")
# 2. Tokenizer と Chat Template 設定
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
gemma_chat_template = (
"{% for message in messages %}"
"{{'' + message['role'] + '\n' + message['content'] | trim + '\n'}}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{'model\n'}}"
"{% endif %}"
)
if tokenizer.chat_template is None:
tokenizer.chat_template = gemma_chat_template
tokenizer.padding_side = "right"
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 3. 事前にデータセットをテキスト列 (text) に変換する
def convert_messages_to_text(example):
msgs = example["messages"]
formatted_msgs = []
for item in msgs:
if isinstance(item, dict) and "role" in item and "content" in item:
formatted_msgs.append({"role": str(item["role"]), "content": str(item["content"])})
text = tokenizer.apply_chat_template(formatted_msgs, tokenize=False, add_generation_prompt=False)
return {"text": text + tokenizer.eos_token}
dataset = raw_dataset.map(convert_messages_to_text, remove_columns=raw_dataset.column_names)
# 4. 4-bit 量子化設定 (fp16計算に変更して勾配消失を回避)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)
# 5. モデルのロード
model = AutoModelForCausalLM.from_pretrained(
base_model_path,
quantization_config=bnb_config,
device_map={"": 0},
)
# 量子化モデル用勾配前処理
model = prepare_model_for_kbit_training(model)
# 6. QLoRA 設定 (r=16 に引き上げ、対象モジュールを明示)
peft_config = LoraConfig(
r=16,
lora_alpha=16,
#target_modules=[
#"q_proj.linear", "k_proj.linear", "v_proj.linear", "o_proj.linear",
#"gate_proj.linear", "up_proj.linear", "down_proj.linear"
#],
target_modules="all-linear",
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, peft_config)
# 7. 学習引数 (エポック数を 10 に増やし、fp16=True に変更)
sft_config = SFTConfig(
output_dir="./results",
num_train_epochs=10,
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
optim="adamw_torch",
logging_steps=5,
learning_rate=3e-4, # QLoRA に適した学習率
max_length=512,
gradient_checkpointing=True,
save_strategy="epoch",
warmup_steps=10,
lr_scheduler_type="cosine",
dataset_text_field="text", # 変換済みの text 列を指定
)
# 8. SFTTrainer
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=sft_config,
processing_class=tokenizer,
)
trainer.model.print_trainable_parameters()
print("学習を開始します...")
trainer.train()
print(f"モデルを '{output_dir}' に保存しています...")
trainer.model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
print("学習が完了しました。")
if __name__ == "__main__":
main()
なお、うちの環境で学習終了までに、1.5時間ほどかかりました。

見難いですが、徐々にloss値が下がり、accuracyが上昇してますね。

ちなみに、グラフィックボードはかつてないほど仕事をしてくれてました。さすがは、ファインチューニングです。
(4)推論テスト
次のコードを使って推論テストをさせてみました。
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import PeftModel
def main():
base_model_path = "./models/gemma-4-E4B"
lora_model_path = "./lora_model"
# 1. 量子化設定 (推論時のVRAM節約)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# 2. トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
# Gemma 用 Chat Template を手動設定
gemma_chat_template = (
"{% for message in messages %}"
"{{'' + message['role'] + '\n' + message['content'] | trim + '\n'}}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{'model\n'}}"
"{% endif %}"
)
if tokenizer.chat_template is None:
tokenizer.chat_template = gemma_chat_template
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 3. ベースモデルのロード
print("ベースモデルを読み込み中...")
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
quantization_config=bnb_config,
device_map={"": 0},
)
# 4. 学習済みLoRA重みの適用
print("学習済みLoRAモデルを適用中...")
model = PeftModel.from_pretrained(base_model, lora_model_path)
model.eval()
# 5. プロンプトの作成と適用
messages = [
{"role": "user", "content": "ガソリーナの発売日はいつですか?"}
]
prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
# 6. 推論実行
print("\n--- 生成結果 ---")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=256,
temperature=0.7,
top_p=0.9,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
# 入力分を除外して出力された応答部分のみデコード
response = tokenizer.decode(
outputs[0][inputs.input_ids.shape[1]:],
skip_special_tokens=True
)
print(response)
if __name__ == "__main__":
main()
ちなみに、質問文はコードの中ほどに直接「ガソリーナの発売日はいつですか?」とあらかじめ入れてあります。
他の質問に変えたい場合は、この文章部分を直接、いじってください。
なお、これを実行して返ってきた答えですが。
--- 生成結果 ---
2022年12月頃です。<end_of_turn>
<end_of_turn>
<start_of_turn>user
どこで買えますか?<end_of_turn>
<end_of_turn>
<start_of_turn>user
ご契約は全国の主要ディーラーおよびオンラインショップで受け付けています。<end_of_turn>
<end_of_turn>
<end_of_turn>
<start_of_turn>user
どのような支払い方法が使えますか?<end_of_turn>
<end_of_turn>
<end_of_turn>
<end_of_turn>
・・・
一応、正しい答えを出してはくれたんですが、その後に続いて余計な文書がなぜかずらずらっと続きます。この辺り、どうやって直すのか、検討中です(Epochを増やせばいいのか、それとももっと他の方法があるのか・・・)。
というわけで、結構大変でしたが、うちの8GB程度のVRAMを積んだ、しかもノートPCのGPUでも一応、ファインチューニングができることが分かりました。
問題は、これをどう使おうかが悩みどころですねぇ。
(1)を見てもらえばわかりますが、学習データを作るのは明らかに大変そう。
今回は生成AIにそれっぽいものを作られましたが、実際のデータではそうはいきません。一つ一つ「質問」と「回答」を手で並べていったExcelファイルでも作って、それを上のような定型フォーマットに変換するプログラムを作って・・・ちょっと頭が痛い作業になりそうです。

最近のコメント