IDCF テックブログ

IDCF テックブログ

クラウドサービスを提供するIDCフロンティアの公式テックブログ

ClaudeCode × Unsloth × Gemma4でファインチューニングして上司をAI化した件。

こんにちは!IDCフロンティアでAIインフラや生成AIの検証を担当している中嶋です。
以前ローカルLLMの構築についての記事を公開しましたが、今回はその続編となります!

blog.idcf.jp

今回は、導入したDGX Sparkを使用して、私の上司の発言をデータセットにファインチューニングを行いました。
ローカルLLM導入後の活用事例として、またファインチューニングに興味がある方々へ参考になれば幸いです。

登場人物紹介

【運用システム部】
森田さん...........部長。発言に癖があるため今回ファインチューニングされる。
趙さん...........主にファインチューニングを実施。天才なのでclaude codeのアカウントを部内で一人だけ払い出されている。
中嶋(私)...........森田さんでファインチューニングしてみたいと言い出した人。

序章

ファインチューニングの前提知識

  • ファインチューニングとは何か?
    ファインチューニングとは、GPTやQwenやGemmaなど、ローカルLLMのモデルが元々持っている事前学習(Pre-training)で獲得した汎用的な表現能力を維持しつつ、特定のタスクやドメインのデータセットを用いて追加学習させるという技術です。
    外部知識をそのまま注入するRAGとは違い、モデルの内部パラメータをいじって最適化する必要があります。

  • ファインチューニングを行うメリット
    挙動・フォーマットの完全固定
    JSON形式や特定の口調(トンマナ)を、プロンプトで縛るよりも確実に固定できます。
    コスト(トークン)の大幅削減
    長文の指示や例題(Few-shot)が不要になり、入力プロンプトを削ってAPI費用を抑えられます。
    回答スピードの高速化
    プロンプトの短縮とモデルの最適化により、レスポンスが圧倒的に速くなります。

 

第一部 沈黙のAI

1.文明#001は質の悪いデータセットによって滅亡しました。

ある日のこと
森田さんに借りた『Pythonでまなぶ ローカルLLMの訓練と使いこなし (著)クジラ飛行机』という書籍を読んでファインチューニングのすべてを理解した気になった私は、早速天才エンジニア・趙さんに相談します。

私:「森田さんのslack発言をデータセットにして、ファインチューニングをやりたいです。」
趙さん:「うーんまあ私も詳しくないけど、一回試しでやってみましょうか。」

というわけでまずはGeminiに聞き、LLaMA-Factoryというツールを使ってみることにしました。
LLaMA-Factoryとは、ファインチューニングの一連の面倒な工程をすべてパッケージ化し、Web画面(GUI)や数行のコマンドだけで完結できるようにしたツールです。
参照:
GitHub - hiyouga/LlamaFactory: Unified Efficient Fine-Tuning of 100+ LLMs & VLMs (ACL 2024) · GitHub

ここで、ファインチューニングの流れを簡単に説明します。

1. 教師データ(Dataset)の準備とクレンジング
AIに真似させたい「特定の振る舞い」のデータを集めて整形します。
データ形式:基本的には(指示)]と(理想の回答)がペアになった、JSONL形式のデータ(指示学習用データ)を数百〜数千件以上用意します。
クレンジング:表記揺れの統一や、ノイズ(ゴミデータ)の削除を行います。

2. ベースとなる「学習済みモデル」の選定
土台(基礎知識)となるモデルを選びます。オープンソースのローカルLLM(Llama 3、Mistral、Qwenなど)や、OpenAI等の商用APIモデルが対象です。
選定基準:ベースの日本語能力の高さ、タスクの複雑さ、そして自社インフラ(GPUのVRAM容量)に収まるパラメータサイズ(8B、70Bなど)を考慮して決定します。

3. ハイパーパラメータの設定と「学習」の実行
GPUサーバー(DGXなど)を回して学習を開始します。
全ての重みを書き換えるとコストもマシンパワーも足りないため、通常は「LoRA*1」などの手法を使い、特定の小さな差分パラメータだけを効率的に学習させます。

4. 評価(Evaluation)とハイパーパラメータの微調整
学習が終わったら、損失関数*2のグラフをチェックしつつ、実際にテスト用のプロンプトを投げて出力を確認します。

5. 推論環境へのデプロイ(実戦投入)
納得のいく精度が出たら、ベースモデルに学習済みの差分(LoRAの重み*3)をマージ(統合)するか、あるいは動的にロードする形で推論サーバーを立ち上げ、アプリケーション(APIやチャットUI)に組み込みます。

早速、一回目のチャレンジが開始されました。
まずデータセットの用意に関して、私がランダムに森田さんのslackから100件分発言を抽出し、スプレッドシートにコピペしました。
それを特にデータクレンジングもしないまま、Geminiを使って、instructions形式というJSONL形式に書き換えます。
※outputは森田さんの発言で、inputの部分はGeminiに考えてもらいました。

例:
[
{"instruction": "森田さん、質問です。",
"input": "今度のイベントで登壇ネタありませんか?",
"output": "悪いが人柱となってくれ。 私は事例から学ぶシステム開発で話すようなネタは特に無い。"
},
{"instruction": "森田さん、質問です。",
"input": "UIのモックどんな感じですか?",
"output": "やばい、またFF15のおにぎりみたいな物を作ろうとしている気がしてきた。"
}
]
...

次にDGX Spark上で環境構築を行います。

1.仮想環境を作成
python3 -m venv .venv

2.LlamaFactoryのリポジトリをクローン
git clone --depth 1 https://github.com/hiyouga/LlamaFactory.git

3.仮想環境を有効化
source .venv/bin/activate

4.ファインチューニング用のツール(LLaMA-Factoryなど)をシステムにインストール。オプション機能を追加して、カレントディレクトリをPythonに登録
cd LlamaFactory
pip install -e ".[torch,metrics,bitsandbytes]""
sudo apt update
sudo apt install -y python3-dev build-essential

5.Llama Board(GUI)の起動
CUDA_VISIBLE_DEVICES=0 llamafactory-cli webui

そして、作成したデータセットをdataフォルダ配下に置きます。

LLaMA-FactoryのUI画面が開いたら、モデルを選定してからパラメータを入力し、データセットを選択し、開始ボタンを押して実行していきます。
一回目は下記のモデルとパラメータで実行しました。

モデル:
Qwen3-4B-Instruct-2507
パラメーター:
--learning_rate :1e-4
【学習率】AIが一歩ずつ知識を吸収するスピード。大きすぎると記憶が壊れ、小さすぎるといつまでも覚えない。LoRAでは定石の初期値
--num_train_epochs :10.0
【エポック数】用意した教科書(データ)を何回読み直すか。森田さんのクセを染み込ませるために多めの10回で設定
--lora_rank : 16
【LoRAランク】新しく覚える知識を書き留める容量。メモ帳のサイズ。大きいほど複雑な表現を覚えられるが、GPUのメモリを消費する
--lora_alpha : 32
【LoRAアルファ】新しく覚えた知識をどれくらい「強め」に回答に活かすか。反映の強さ。基本はメモ帳サイズ(16)の2倍にするのがお約束
--lora_target : all
【LoRAターゲット】モデルのどこをチューニングするか。改造する範囲。「all(全部)」にすることで口調やニュアンスを漏れなく学習させる

LLaMA-FactoryのUI画面↓

学習が進行中...

メモリの使用率チェック↓

ファインチューニングが完了したら、モデルをローカルにロードします。
その後、chatの部分からモデルを読み込むを押下して、質問を投げてテストします。
結果は...

...森田さんのような回答を返した!?

と喜び、GGUF変換してOpenWebUIにインストールしてみました。
すると、なぜだか森田さんではなくただの生成AIっぽい回答になってしまいました。

失敗...。
今回の敗因をGeminiとともに考察すると、

①100件のデータでは少なすぎた
Instructモデルは、世の中のありとあらゆる標準的な文章を膨大に叩き込まれて作られています。
100件という極小のデータはLLMにとって砂漠に落ちた水一滴のようなものでしかなく、森田さんの個性を引き出すまでもなく元のモデルの普通のAIとしての慣性に押し負けてしまったのだと思われます。

②データクレンジングしなかったせいで汎用的な言葉も大量に含まれていた
「お疲れ様です!」「承知いたしました!」といった全人類が使う特徴のない定型文を排除せずに学習させたことで、元のモデルが持っている普通のAIの癖がただ強化されてしまった可能性が考えられます。

というわけで、またのログインをお待ちしております。

2.文明#002はモデル選定ミスによって滅亡しました。

二回目のチャレンジは、データセットの質を向上させるべく、slackAPIでまず森田さんの過去の発言約6000件を全て取得してきました。
データの一次加工として、まずは空行、メンション、スタンプ、URLだけの不要なデータをPythonスクリプトで一掃します。
例)

それをGeminiで下記のようなプロンプトでさらに二次加工します。

例)
添付したログファイルは、エンジニア森田さんのSlack発言集です。これをファインチューニング用のデータセットに変換してください。

【変換ルール】
選別:
整形ルール(学習に適した形に加工)
- 口調の維持: 上司独特の語尾、比喩、口癖は削除せず、そのまま残してください。
- 一般的な定型句の削除: 「お疲れ様です」「承知いたしました」「よろしくお願いします」といった、どの社会人でも使うような汎用的・定型的な挨拶や返答はノイズになるため削除してください。

構成:
instruction: "森田さん、質問です。"(固定)
input: その発言(output)を引き出すための、具体的で自然な質問文をあなたが考えて作成してください。
output: 中身をそのまま出力してください。
フォーマット: JSONL形式で出力してください。

一気に数千件はNotebookLMでも処理しきれなかったため、100件くらいずつGeminiに処理させます。
何回かやらせると出力の質が悪くなってくるので都度チャットを新しくしつつ進めました。

質のいいデータセットが700件ほど集まったところで、再度ファインチューニングしていくことにしました。

二回目はQwen3.5-4B-Thinkingモデルで実行しました。パラメータは前回と同じです。
データセットのボリュームが大きくなったため、前回は30分ほどで終わりましたが今回は時間がかかりそうです。
趙さんにバックグラウンド実行しておいてもらうことにしました。

後日...

中嶋:「結果はどうでしょうか。」
趙さん:「ダメでした。全然賢くない笑」

というわけで、失敗です。

今回の敗因としては、趙さん曰くモデルに問題がありました。
我々はまだ、Thinkingモデルというものを使いこなせていなかったのです。
こちらについては次の章で説明していきます。

またのログインをお待ちしております。

第二部 三点

3.文明#003は未来の文明のために新たな進路を定めました。

三回目のチャレンジです。
趙さんはGeminiに相談し、我々のファインチューニングがうまくいかなかった原因と対策を三点導き出しました。 この提案をもとにClaude Codeに実行させました。

①データセットにThinkingを混ぜる必要がある。
②Gemma 4を使え。
③Unslothを使え。

①に関しては、Instructionモデルが「問いに対する直接的な答え」を学習するのに対し、Thinkingモデルは「答えに至るまでの思考プロセス(Chain of Thought: CoT)」をセットで学習します。

そのため、データセットにこのような、「なぜそのセリフ(出力)に至ったのか」の脳内プロセスを言語化して仕込むのがThinkingモデル用データセットの基本です。

{
"conversations": [
{
"from": "system",
"value": "あなたはIDCFクラウド(IDCフロンティア)のクラウド本部部長kmoritaです。AI利活用・クラウドインフラ・チームマネジメントを担当するIT企業のマネージャーで、Slackやチャットでの日常的なやり取りが多い。フランクで率直な言葉遣い、時にユーモアや自虐を交えながら話す。釣り(特にチヌの落とし込み)・園芸・アクアリウム・SF小説・80〜90年代アニメが趣味。投資はインデックスのドルコスト平均法派。"
},
{
"from": "human",
"value": "最近のフルリモート推奨の風潮についてどう思いますか?"
},
{
"from": "gpt",
"value": "<thinking>\nフルリモートについての本音を聞かれてる。あまり声高に言えないけど正直に言うと、コロナや家族の事情など仕方ない理由でFワークする人はともかく、ただ通勤がだるいというだけで出社しないのはちょっとなぁと思う。でもこれ言うと考えが古いと思われるかな。集まって仕事することで生まれる情報共有や関係性の価値は確かにある。正直に言いつつ、自分の考えが古いかもという留保もつけよう。\n</thinking>\n\n実はあまり言えないですが、同じ観点でコロナやご家族の事情等で一時的にリモートワークしないといけない社員はともかくとして、通勤がだるいという理由でFワークするのもなんだかなぁ〜と思うのですが、みんなで集まって仕事しないとと思うのは考え方が古いんですかね"
}
]
},

このように、thinkタグを含めたものをデータセットに混ぜるといいとのことでした。
(unslothの公式ページには75%と書いてあるのですがなぜか1%でうまくいきました。 参照: Gemma 4 Fine-tuning Guide | Unsloth Documentation

②のGemma4モデルに関しては、思考モードのON/OFFの制御の柔軟性と、最高峰の推論能力を備えているため、小型の思考モデルよりバグや崩壊を起こしにくいとのことです。
また31bというサイズはQLoRAを使えばVRAM22GB程度でファインチューニングできる、最高にコスパがよく賢いサイズ感になっています。
参照:
unsloth Gemma4ガイド:https://unsloth.ai/docs/models/gemma-4/train
GoogleAI開発者ドキュメント:https://ai.google.dev/gemma/docs/capabilities/thinking?hl=ja

③のUnslothとは、LoRAやQLoRA*4などの手法と組み合わせて超高速かつ省メモリでファインチューニングを可能にする世界標準のオープンソースツールです。Gemma4モデルを使用するにあたって、TransformerのバージョンがLlamaFactoryだと合わなかったのでunslothで実行することにしました。
また、UnslothはQwen3.5やGemma4の特殊なカーネルを動かすための専用パッチを自前で実装しており、メモリ不足エラーで落ちにくいと定評があります。
今回はPythonコードベースで実行しましたが、ブラウザからノーコードで学習できるツールも提供されています。
参照: Unsloth公式リポジトリ:https://github.com/unslothai/unsloth

これらの要件を提案した後、Claude codeは必要なDockerファイルやPythonファイルを全て生成してくれました。

この時のモデルとパラメータは以下になります。

モデル:
google/gemma-4-31b-it
パラメーター:
--learning_rate :2e-4
--num_train_epochs :10.0
--lora_rank : 16
--lora_alpha : 32
--lora_target : q/k/v/o_proj, gate/up/down_proj (all attention + MLP)

7時間後...

ついに学習が完了した。
趙さんの指は、そのボタンから二センチメートルのところにあった。
全人類の命運が、この細い二本の指にかかっている。
趙さんはチャットにお決まりのメッセージである「Who are you?」を打ち込み、ためらうことなく送信ボタンを押した。

(というのは嘘で、本当はclaude codeが作った質問を自動で投げてテストしました汗)

第三部 AIの落日

4.この世界はあなたがたのメッセージを受けとった。

claude codeの投げた10の質問に、一つずつ、着実に応答が出力されていきました。
以下はその時の実際のやりとりです。

...いかにも森田さんっぽい回答が!

監視員としての退屈な時間が終わりを迎え、遂にAI森田からの応答を観測しました。
早速これをGGUF*5化してもらい、OpenWebUIで色々と会話してみました。

本物の森田さんとは僅かに違いが見られますが、口調の模倣具合/回答内容としてはかなりいい感じに仕上がっていました!

(ちなみに本物の森田さんはこう↓)

まとめ

経緯

今回私たちは部長である森田さんの発言を学習させてAI化するという近未来的な試みをしてみた訳ですが、開発テーマを決める会議では他にも様々なアイデアが出ました。
しかしせっかくDGX Sparkという高級なおもちゃ高性能GPUサーバを手に入れたので、この巨大なマシンパワーを解き放つにはファインチューニングしかないのだと私は思いました。(ファインチューニングは数B(数十億)〜数十Bパラメータのモデルの重みを書き換えるために、膨大な行列計算を発生させます。)
また、RAG技術については社内でも導入が進んでいるため、単純に誰もやったことがないファインチューニングというものを試してみて可能性を探りたかったというのもあります。
上司である森田さんをAI化することで、上司の目に入る前に文書等を検閲してくれる事前検閲環境としての役割や、暗黙知の組織への還元といった役割をゆくゆくはAI森田さんに担わせられると考えました。 
そういった理由からこのテーマに決めました。

所感

まず驚いたのは、LLama factoryなど便利なGUIツールの存在です。てっきりPythonでコードを書く必要があると思っていたので拍子抜けでした。主導していただいた趙さんは最初にGeminiにやり方を一から聞いていたので、さすが生成AIを使いこなしてるなと思いました。
実行前はパラメータの値の調整に苦戦すると予想していましたが、実際にやってみて一番苦戦した点はデータセットの準備です。引っ張ってきた大量のデータをどうやって加工するかについて頭を悩ませた時間が一番長かったです。
学習に関して、それぞれのパラメータの値が何を意味しているのか、根本的に理解しようと思うと高度な機械学習や数学的な知識が問われると思いますが、正直私にはまだ理解しきれてはいません。
しかしめげずに教えてくれるGeminiの偉大な力を借りつつ、なるべく私のような初心者にも読みやすいようにこの記事を書き上げました。

最後までお読みいただき、ありがとうございました!

*1:LoRAは、元のモデルの重みを固定したまま、微調整の差分だけを小さな低ランク行列に分解して追加学習させる効率的なファインチューニング手法です。計算コストや必要なVRAM容量を劇的に削減しつつ、元の汎用知識の崩壊を抑制できます。

*2:損失関数は、モデルの予測出力とデータセットの正解値との間にある誤差を算出し、定量化するための数理関数です。ファインチューニングのプロセスでは、この関数の出力値(ロス)が最小化されるようにアルゴリズムが動作し、モデルの出力精度を最適化します。

*3:重みとは、ニューラルネットワークの階層間で情報を伝達する際の結合強度を表す数値パラメータであり、LLMの持つ「知識」の正体です。ファインチューニングとは、特定のデータセットに適合するように、これら膨大な重みの数値を再調整して更新するプロセスを指します。

*4:QLoRAとは、巨大なAIモデルのデータを4ビット(約4分の1の軽さ)に超軽量化(量子化)し、さらにごく一部のパーツだけを追加学習(LoRA)させる画期的な手法です。

*5:GGUF(GPT-Generated Unified Format)は、巨大なLLMを量子化(軽量化)し、普通のパソコンのCPUやGPUで超高速かつ省メモリでローカル実行するためのファイル形式。

Copyright © IDC Frontier Inc.