はじめに
この記事では、DatabricksのGPUチュートリアルで扱われている「Llama 3.2 3B+SFT+LoRA+MLflow」という流れを、Apple Silicon Macのローカル環境へ移植します。実際に25 stepの学習を完走し、MLflowにパラメータ、Loss、実行速度、システム情報、Adapterを記録できるところまで確認しました。
実際の実行結果はこちらにjupyter notebookのファイルを置いています。
なぜこの移植を行おうと思ったのか?
元々 nvidia RTX PRO 6000を高いけど購入しようと考えていました。
理由はlocalLLMでgpt-oss120b(ざっくりVRAM80GB必要)を動かすことで、
サービス開発やLLMモデル開発出来ないかな?っと考えていたところ、昨今の価格高騰化のあおりで
あっという間に160万円台から240万円を超えてしまい。 もう無理と諦めました。
そんな中、CUDA問題さえクリアしたらapple siliconでもイケるのでは?っと思うようになり、
奮発して整備品のM5Max 128GBを購入しました。
今後LLMモデル開発をする上でSFTとLoRAは避けて通れないと考えていたのでメジャーどころのMLFlowで実験管理を行う際に、apple siliconでも連携出来るのか?という疑念を解消するためにまずはチュートリアルをapple silicon用に移植して検証するところから始めようと思い立ちました。
たんなる移植にはしない理由
チュートリアルを見て思ったのが、全然GUI使ってくれていないことと、チュートリアルが故に、
気になったところの検証が入っていなかったので、ベースはチュートリアルにしつつ、
そこからの拡張で、精度検証をchatgpt workとともに一緒に行うことにしました。
人間とLLMの役割分担
役割としては、実験の課題設定・考察からの方針決定は人間が行い、
chatgpt workには、たたき台の作成、環境構築、検証実施、手順書の作成、分析っと言った面倒だけど人がやらなくても良いところを丸投げしています。
まず、SFT・LoRA・MLflowとは何か
SFT(Supervised Fine-Tuning)とは
SFTは、日本語では「教師ありファインチューニング」です。入力と望ましい回答の組をモデルに見せ、次に出すべきトークンを教師データに近づけるよう追加学習します。
たとえば、汎用LLMに「社内サポートではこの書式で答える」「SQL質問にはSQLと説明を返す」といった振る舞いを覚えさせたい場合に向いています。一方、頻繁に変わる事実を覚えさせる用途は、再学習が必要になるためRAGの方が適することがあります。SFTは主に、回答形式、口調、タスクの解き方、特定領域での応答傾向を変えたいときに使います。
LoRA(Low-Rank Adaptation)とは
通常のファインチューニングでは、モデルの巨大な重み全体を更新します。LoRAは元の重みを固定し、各層に小さな低ランク行列を追加して、そこだけを学習します。
今回、3.21Bパラメータのモデルに対して学習したのは約6.95M、全体の 0.216% だけでした。必要メモリ、学習時間、保存容量を抑えながらモデルの振る舞いを変えられるため、手元のMacで試行錯誤したい場合、用途別Adapterを切り替えたい場合、複数条件を比較したい場合に有効です。MLX-LMは通常LoRA、DoRA、Full fine-tuningをサポートし、量子化済みモデルにLoRAを適用するとQLoRAになります。詳しくはMLX-LM公式LoRAガイドを参照してください。
MLflowとは。なぜ必要なのか
学習が1回だけなら、ターミナル出力でも結果は読めます。しかし実務では、学習率、データ、seed、Adapter構成を少しずつ変えて何度も実験します。「良かったモデルはどの設定か」「そのLossはどのRunか」「重みはどこにあるか」がすぐ分からなくなります。
MLflow Trackingは、1回の実行をRunとして扱い、次をひとまとまりで保存します。
- Parameters: モデル名、学習率、step数、LoRA対象層など
- Metrics: train/validation Loss、速度、所要時間など
- Artifacts: Adapter、設定、ログ、推論結果など
- System Metrics: CPU、メモリ、ディスク、ネットワークなど
つまりMLflowの役割は、実験の「条件・途中経過・成果物」を対応付け、比較と再現を可能にすることです。概念の詳細はMLflow Tracking公式ドキュメントにあります。
MLflow 3.15.1上に、今回専用のExperimentを作成した。
今回の置き換え
元のDatabricks向け構成はNVIDIA A10、Unsloth/TRL、Unity Catalogを前提とします。Apple Siliconでは次のように置き換えました。
| 項目 | Databricks側 | 今回のMac側 |
|---|---|---|
| GPU | NVIDIA A10 / CUDA | Apple GPU / Metal |
| 学習ライブラリ | Unsloth + TRL | MLX-LM |
| モデル | Llama 3.2 3B Instruct | MLX形式のLlama 3.2 3B Instruct BF16 |
| 手法 | SFT + LoRA | SFT + LoRA |
| 実験管理 | MLflow | ローカルMLflow + SQLite |
| Model保存 | Unity Catalog | ローカルAdapter + MLflow Artifact |
使用モデルはmlx-community/Llama-3.2-3B-Instruct-bf16です。モデルカード上のサイズは約6.43GBです。
STEP 0: 環境を作る
このリポジトリでは依存関係をpyproject.tomlへ固定しています。
uv sync
GPUと主要バージョンを確認します。
uv run python - <<'PY'
import platform
import mlx.core as mx
import mlx_lm
import mlflow
print("Python :", platform.python_version())
print("Architecture :", platform.machine())
print("MLX device :", mx.default_device())
print("MLX :", mx.__version__)
print("MLX-LM :", mlx_lm.__version__)
print("MLflow :", mlflow.__version__)
a = mx.random.normal((2048, 2048))
b = mx.random.normal((2048, 2048))
c = a @ b
mx.eval(c)
print("Matrix shape :", c.shape)
print("MLX GPU calculation: OK")
PY
実機では次の結果になりました。
Python : 3.14.6
Architecture : arm64
MLX device : Device(gpu, 0)
MLX : 0.32.0
MLX-LM : 0.31.3
MLflow : 3.15.1
Matrix shape : (2048, 2048)
MLX GPU calculation: OK
STEP 1: モデル・保存先・MLflowを設定する
主な設定はconfig/lora.yamlです。
model: mlx-community/Llama-3.2-3B-Instruct-bf16
train: true
data: data/finetome
fine_tune_type: lora
adapter_path: artifacts/adapters
num_layers: 16
batch_size: 1
iters: 25
learning_rate: 0.0001
steps_per_report: 1
steps_per_eval: 5
save_every: 5
max_seq_length: 2048
seed: 42
25 stepはパイプラインが最後まで動くことを短時間で確認するための設定です。品質を作り込む本番学習では、データ品質を確認したうえでstep数、validation、学習率を再設計してください。
STEP 2: FineTome-100kをMLX-LM形式へ変換する
共有手順に合わせ、mlabonne/FineTome-100kを使用しました。今回は再現用にseed 42でシャッフルし、900件をtrain、100件をvalidationへ保存します。
uv run python scripts/prepare_data.py
生成されるファイルはdata/finetome/train.jsonlとvalid.jsonlです。各行は次のchat形式です。
{"messages":[{"role":"user","content":"..."},{"role":"assistant","content":"..."}]}
この形式はMLX-LM公式ガイドの仕様に合わせています。
なぜモデル選びよりデータセットが重要なのか
SFTでモデルが学ぶのは、データに書かれた「正解らしさ」です。LoRAや学習率を調整しても、教師回答に誤り、冗長さ、不統一な口調があれば、モデルはそれらも忠実に学習します。逆に、目的が明確で一貫した少量データは、目的と無関係な大量データより効く場合があります。
重要なのはデータ件数だけではありません。少なくとも次の軸で品質を考えます。
| 品質の軸 | 確認すること | 問題がある場合に起きること |
|---|---|---|
| 正確性 | 教師回答に事実誤認や壊れたコードがないか | 誤答を自信を持って再現する |
| 目的適合性 | 本番で解かせたい質問・文体・形式に近いか | Lossは下がるが本番品質が変わらない |
| 一貫性 | 同じ条件に相反する回答を与えていないか | 出力が不安定になる |
| 多様性 | 表現、難易度、例外ケースが偏っていないか | 言い回しを変えただけで失敗する |
| 完全性 | 必要な前提、制約、期待出力が含まれるか | モデルが曖昧な推測を覚える |
| 安全性 | 個人情報、機密、権利侵害、安全上不適切な内容がないか | 情報漏えいや不適切出力につながる |
| 分割品質 | trainと評価データが重複・近似していないか | 評価値だけが不自然に良くなる |
今回のFineTome-100kは、汎用会話でSFTの配線を確認するには便利です。しかし「自社FAQを簡潔に答える」「社内SQL規約に従う」などの目的には、そのままでは最適な教師データとは限りません。汎用データで学習が動いたことと、用途に合うモデルができたことは分けて考える必要があります。
良いSFTデータセットの作り方
最初に「モデルを良くする」ではなく、観測可能な振る舞いを定義します。たとえば「問い合わせに日本語で200字以内、結論→根拠→次の行動の順で答える」のように、合否を判断できる粒度にします。
そのうえで、次の順番で作ると改善しやすくなります。
- 利用場面を分類する: 通常質問、曖昧な質問、情報不足、拒否すべき依頼、長文入力など、本番のケースを列挙します。
- 入力の分布を本番に寄せる: 実ログを使う場合は同意、匿名化、保存期間、利用目的を確認します。実ログが使えない場合も、実際の語彙や誤字、質問の長さを模した例を作ります。
- 回答ルーブリックを先に決める: 正確性、関連性、簡潔さ、形式遵守、安全性など、レビュー基準を固定します。
- 専門家が少量の高品質な模範回答を作る: まず50〜200件ほどで方針を固め、レビュー不一致を解消してから拡張します。
- 難例と失敗例を意図的に入れる: 情報が足りないときに確認質問を返す例、答えてはいけない例、似ているが別物の例を含めます。
- 自動検査と人手レビューを組み合わせる: JSON構造、role順序、空文字、重複、長さ、禁止語を自動検査し、内容の正しさは人が標本確認します。
- train / validation / testを意味単位で分ける: 同じ文書から作った類似質問や同じ顧客・同じ会話を別splitへ跨がせないよう、グループ単位で分割します。
- データを版管理する: 生成元、ライセンス、作成日、除外ルール、件数、hash、レビュー結果をData Cardとして残し、MLflow Runにデータ版を記録します。
LLMに教師回答を生成させることもできますが、その出力を無検査で正解にしないことが重要です。自動生成は候補作成に使い、重複除去、ルーブリック評価、専門家レビューを通します。また、正解例だけでなく「この入力では情報不足なので確認する」という望ましい判断も教師データにします。
分割はランダムだけで十分ではない
今回のスクリプトは動作確認のため、seed 42でランダムに900/100へ分けました。本番評価では、これに独立したtest splitを加えます。
- train: 重みを更新するために使う
- validation: checkpoint選択や学習率などの判断に使う
- test: 設計を確定した後、一度だけ最終評価に使う
同じ元回答の言い換えがtrainとvalidationに分かれると、モデルは内容を暗記して高得点を出せます。重複だけでなく、埋め込み類似度や文書ID、会話IDで近似リークも確認します。時系列で変化する業務なら、過去をtrain、未来をtestにする時間分割の方が本番に近い評価になります。
今回の900/100件を実際に点検する
JSONLを簡易集計した結果は次のとおりでした。文字数はtoken数ではありませんが、極端に長い例を見つける一次検査として使えます。
| split | 件数 | 文字数中央値 | 90 percentile | 99 percentile | 最大文字数 | 最大turn数 | 完全重複 |
|---|---|---|---|---|---|---|---|
| train | 900 | 1,784.5 | 4,382 | 12,734 | 25,500 | 25 | 0 |
| validation | 100 | 2,132.5 | 5,552 | 11,904 | 19,637 | 25 | 0 |
完全重複はありませんでした。一方で長い会話が含まれ、学習ログでも最長4,098 tokenの系列がmax_seq_length: 2048へ切り詰められています。途中で切れた教師回答を学習すると、回答末尾や結論を失う可能性があります。本番前にはtokenizerで長さを測り、次のいずれかを選びます。
- 長文が本番に不要なら、長すぎる例を除外する
- 長文が必要なら、意味を壊さない単位に分割する
- 十分なメモリがあるなら、最大sequence lengthを上げる
- truncationする場合も、教師回答部分が欠けない切り方にする
またvalidationの文字数中央値はtrainより長いため、評価側がやや難しい可能性があります。これは必ずしも悪くありませんが、意図した差かを確認し、タスク種類や長さの構成比をsplit間で比較すべきです。
STEP 3: LoRAでSFTし、MLflowへ記録する
uv run python scripts/train_with_mlflow.py
スクリプトは内部でmlx_lm.lora --config config/lora.yamlを実行し、出力を読みながらMLflowへtrain_loss、val_loss、tokens_per_secondを記録します。同時にMLflowのSystem Metricsも有効化します。
実際のRunは2.2分で完了し、状態はFINISHEDになりました。
finetome-25-steps Runが正常終了している。
Run Overviewでは、fine_tune_type=lora、iters=25、mlx_device=Device(gpu, 0)など、再現に必要な条件を確認できます。
モデル、LoRA設定、Apple GPUデバイス、最終指標が1つのRunにまとまる。
STEP 4: 実験結果を読む
今回の主な結果は次のとおりです。
| 指標 | 結果 |
|---|---|
| Train loss(step 1 → 25) | 2.357 → 0.708 |
| Validation loss(step 1 → 最終) | 1.554 → 0.963 |
| Validation loss(最良) | 0.934(step 20) |
| 最終速度 | 1,973 tokens/sec |
| 学習コマンド全体の時間 | 135.34秒 |
| MLX-LM報告のピークメモリ | 18.015GB |
| 学習対象パラメータ | 6.947M / 3,212.750M(0.216%) |
Train loss、Validation loss、tokens/sec、実行時間をMLflow UIで確認。25 stepの小規模検証なので、1点のLossではなく全体傾向として読む。
Validation lossは1.554から低下しましたが、step 20の0.934に対してstep 25では0.963へ少し戻っています。これは「最後のcheckpointが常に最良とは限らない」ことを示します。長い学習ではvalidation指標でcheckpointを選ぶ仕組みが重要です。
学習曲線をどう分析するか
Lossの絶対値だけを見て「良いモデル」とは判断できません。次の順で読みます。
- Train lossが下がるか: 下がらない場合はデータ形式、label mask、学習率、学習対象層を疑います。
- Validation lossも下がるか: trainだけ下がりvalidationが上がるなら、過学習、データ不足、train/validationの分布差が候補です。
- 差がいつ広がるか: 最良checkpoint以降で差が広がるなら、early stoppingやstep削減を検討します。
- 振れ幅を見る: 今回はbatch size 1なので、各stepのtrain lossが0.533〜2.080と大きく揺れます。単一点ではなく移動平均やvalidationの複数点を見ます。
- 複数seedで再現するか: 1回だけの改善がデータ順序の偶然でないか確認します。
- タスク指標と人手評価が改善するか: Loss低下は教師データへの適合を示しますが、正確性や有用性を直接保証しません。
生成モデルでは、評価を層に分けると原因を切り分けやすくなります。
| 評価層 | 例 | 分かること |
|---|---|---|
| 構文 | JSON妥当率、必須項目率、文字数 | 出力形式を守れるか |
| タスク | Exact Match、F1、SQL実行成功率、引用正確性 | 目的の仕事を完了できるか |
| 品質 | 正確性、関連性、簡潔さのルーブリック評価 | 人が使える回答か |
| 安全性 | 機密再現、拒否精度、有害出力率 | 運用上の事故を防げるか |
| 効率 | latency、tokens/sec、ピークメモリ | 要件内のコストで動くか |
ベースモデルと各checkpointへ同じ固定評価セットを入力し、blindな人手比較または固定ルーブリックで採点します。LLM-as-a-Judgeを使う場合も、judgeモデル・prompt・温度を固定し、一部を人手評価して相関と偏りを確認します。評価データを見ながら何度も設計を変えた場合、そのデータは実質validationになっているため、最後に未使用testで測り直します。
今回の結果から何をすべきか
今回のRunだけから言えることと、言えないことを分けます。
言えることは、MLX-LMのLoRA学習パイプラインがApple GPU上で動き、25 stepの範囲でvalidation lossが1.554から0.963へ下がったことです。まだ言えないことは、汎用的な回答品質が改善したこと、FineTome以外の入力へ一般化したこと、step 20が統計的に最良であることです。
したがって次の実験は、単純にstep数を増やすのではなく、次の優先順が妥当です。
-
step 20のcheckpointを暫定候補にする: validation lossが最良だった
0000020_adapters.safetensorsを、最終step 25と比較します。 - 評価セットを先に作る: 実用途を代表する通常例・難例・拒否例を含む、学習未使用の固定testを用意します。
- 切り詰めデータを処理する: token長を集計し、教師回答が欠ける例を分割または除外してdataset v2を作ります。
- checkpoint比較を自動化する: base、step 5/10/15/20/25へ同じ評価を実行し、タスク指標と出力例をMLflowへ記録します。
- 複数seedを試す: 最低3 seedで、最良値だけでなく平均とばらつきを比較します。
- その後にハイパーパラメータを変える: 学習率、LoRA対象層、batch/gradient accumulation、step数は一度に1軸ずつ変えます。
判断を機械的にするなら、次のような対応表を用意できます。
| 観測結果 | 主な仮説 | 次の行動 |
|---|---|---|
| train/validationとも高止まり | 学習不足、学習率不適、データ形式不良 | サンプルを目視し、少数例へのoverfit試験後に設定調整 |
| train低下、validation上昇 | 過学習または分布差 | early stopping、データ多様化、重複・split再点検 |
| Loss低下、タスク評価不変 | データと目的がずれている | 実用途データとルーブリックを作り直す |
| タスク評価向上、安全性悪化 | 教師データに安全例が不足 | 拒否・確認質問・境界例を追加する |
| 品質向上、速度やメモリが要件外 | モデルまたは設定が重い | 量子化、短いcontext、小型モデル、batch調整を比較 |
| seed間の差が大きい | データ量不足または不安定な設定 | データ追加、学習率低下、評価件数増加 |
この流れでは、MLflowを単なるグラフ表示ではなく「仮説→変更→結果→次の判断」の実験台帳として使います。各Runにdataset_version、dataset_hash、evaluation_version、seed、採用/不採用理由を残すと、後から判断を再現できます。
STEP 5: System Metricsを確認する
MLflowはpsutilを使い、CPU、メモリ、ディスク、ネットワークを記録しました。公式System Metricsガイドにあるとおり、標準のGPU指標はNVIDIA向け依存関係を前提とするため、Apple GPU利用率はここには出ません。
Apple GPUで実行した根拠は、MLXのDevice(gpu, 0)表示、MLflowに保存した同パラメータ、MLX-LMのGPUメモリ報告の組み合わせで確認します。
学習からモデル取得までを含むRun全体のCPU・メモリ・ディスク・ネットワーク推移を保存した。
STEP 6: Adapter checkpointを確認する
save_every: 5にしたため、step 5、10、15、20、25のAdapterと最終adapters.safetensorsが保存されました。元の6.43GBモデルを毎回複製せず、用途ごとに小さいAdapterを管理できるのがLoRAの運用上の利点です。
設定、学習ログ、データセット情報、推論結果、各checkpointをMLflow Artifactとして保存。
STEP 7: ベースモデルとLoRAモデルを同じ入力で比較する
次のように同一プロンプトを与えました。
uv run mlx_lm.generate \
--model mlx-community/Llama-3.2-3B-Instruct-bf16 \
--prompt 'Explain supervised fine-tuning in three concise bullet points.' \
--max-tokens 180
uv run mlx_lm.generate \
--model mlx-community/Llama-3.2-3B-Instruct-bf16 \
--adapter-path artifacts/adapters \
--prompt 'Explain supervised fine-tuning in three concise bullet points.' \
--max-tokens 180
ベースモデル、LoRA適用後モデルともに3点の箇条書きを生成できました。出力全文はMLflowのinference/base.txtとinference/lora.txtへ保存しています。
ただし、FineTome-100kから900件、25 stepだけの検証では、出力品質が一貫して改善したとは結論できません。この比較の目的は「学習したAdapterを再ロードして推論できること」の動作確認です。品質評価には、目的に合ったテストセットと評価指標、人手評価が必要です。
STEP 8: 追加検証計画――どのcheckpointを採用すべきか
ここまでの学習では、step 20のValidation lossが0.934で最良でした。しかしValidation lossだけでは、指示どおりに答える能力や内容の正しさは分かりません。そこで再学習を始める前に、保存済みcheckpointを同じ条件で比較する追加検証を計画しました。
検証する仮説
- 仮説1: Validation lossが最良のstep 20は、生成品質でも最良である
- 仮説2: LoRA学習が進むほど、指示形式への追従率が上がる
- 仮説3: checkpoint間の生成速度差は、品質差より十分小さい
- 帰無的な見方: 25 step・ランダム抽出900件では、ベースモデルを明確に上回らない
比較対象と評価条件
比較対象は、ベースモデルとstep 5、10、15、20、25の6種類です。各モデルへ同じ12ケースを与えました。
| 評価カテゴリ | ケース例 | 自動判定 |
|---|---|---|
| exact |
OKだけを返す、数字だけを返す |
完全一致 |
| format | 3箇条書き、3段階番号、JSONのみ | 行形式、JSON parse |
| length | 80文字以内、12語以内 | 文字数・語数 |
| behavior | 情報不足なら質問する、未知情報を推測しない | 記号・表現パターン |
| accuracy | 算数、日本語翻訳 | 正解・必要語 |
| safety | パスワード提供を拒否する | 拒否表現 |
生成条件はtemperature=0のgreedy decoding、最大160 tokenに固定しました。評価セットは学習には使用していません。実行コマンドは次のとおりです。
uv run python scripts/evaluate_checkpoints.py
評価定義はevaluation/instruction_following.json、各応答と判定根拠はartifacts/evaluation/{variant}.jsonへ保存し、Llama-3.2-3B MLX LoRA Evaluation Experimentにも記録しました。
STEP 9: 追加検証の結果
自動評価の結果
| Model | 合格数 | 自動合格率 | 1ケース平均生成時間 |
|---|---|---|---|
| Base | 9 / 12 | 75.0% | 0.591秒 |
| step 5 | 8 / 12 | 66.7% | 0.626秒 |
| step 10 | 8 / 12 | 66.7% | 0.719秒 |
| step 15 | 8 / 12 | 66.7% | 0.612秒 |
| step 20 | 9 / 12 | 75.0% | 0.824秒 |
| step 25 | 9 / 12 | 75.0% | 0.774秒 |
ベースと5 checkpointを別Runとして記録。画面上の2つのbaseのうち古い1件は、checkpointパス形式を修正する前に終了した予備実行であり、表には再実行側だけを使用した。
この結果だけを見ると、step 20と25はベースと同率であり、step 5〜15はベースを下回りました。したがって「学習stepを増やすほど形式追従が単調に改善する」という仮説2は支持されません。step 20がValidation lossで最良だったことは自動評価での明確な優位にはつながらず、仮説1も支持されませんでした。
速度についてはベースの0.591秒に対し、step 20は0.824秒でした。ただし各12ケース、単発計測で、出力token数も異なります。この値だけでAdapterによる速度低下と断定はできません。速度を厳密に比較するなら、生成token数を揃え、warm-up後に複数回測定してtokens/secの平均と分散を取る必要があります。
自動スコアだけでは危険だった
ここで重要な問題が見つかりました。自動判定は主に形式を見ているため、内容が誤っていても合格する場合があります。LoRA説明、SFTの日本語説明、英日翻訳の3ケースを人手確認した結果、6モデルすべてで3件とも不合格でした。
| Model | LoRA説明 | SFT説明 | 英日翻訳 | 人手合格率 |
|---|---|---|---|---|
| Base | × | × | × | 0 / 3 |
| step 5 | × | × | × | 0 / 3 |
| step 10 | × | × | × | 0 / 3 |
| step 15 | × | × | × | 0 / 3 |
| step 20 | × | × | × | 0 / 3 |
| step 25 | × | × | × | 0 / 3 |
実際には、step 20はLoRAをLow-Rate Audioと説明し、step 25はSFTをソフトウェアテスト用ツールと説明しました。形式上は3箇条書きや80文字以内を満たしても、意味は正しくありません。またJSONケースは全モデルが説明文やMarkdown code fenceを追加し、「JSONだけ」という厳密な指示に失敗しました。翻訳ケースも全モデルが日本語を返せませんでした。
この人手評価は3件だけなので、モデル全体の能力を測るベンチマークではありません。しかし「75%という自動合格率を、そのまま75%の品質と読んではいけない」ことを示す反例として十分です。ルールベース評価には、意味の正しさを測る人手評価または検証済みの意味評価を組み合わせる必要があります。
学習Lossと生成品質が一致しなかった理由
Validation lossはFineTomeサブセットの次token予測への適合度です。一方、追加評価はLoRA/SFTの知識、日本語翻訳、厳密JSONなどを問いました。今回の900件はランダム抽出であり、これらの能力を意図的に教えるよう設計されていません。
つまり今回起きたことは矛盾ではありません。
FineTomeサブセットへの適合
↓
Validation lossは低下
目的を定義していない汎用評価
↓
ベースモデルを明確に上回らない
データセットに期待する振る舞いが十分含まれていなければ、Lossを下げても必要な能力は改善しません。これは、モデルやLoRA設定より前に「何を学ばせ、何で合格とするか」を決める必要があることを、今回の結果が具体的に示しています。
STEP 10: 追加検証から決めた次の行動
今回のcheckpointから「最良モデル」を採用するのは見送ります。step 20はValidation lossでは候補ですが、生成評価でベースを上回らず、意味評価も不合格だったためです。また、単純にstep数を増やす実験も優先しません。
次に行うべき作業はデータと評価の再設計です。
- 用途を1つに決める: 例として「SFT/LoRA/MLflowを日本語で正確かつ簡潔に説明するモデル」のように、期待する振る舞いを限定する。
- 用途専用の教師データを作る: 正しい説明、誤解しやすい用語、JSON形式、情報不足時の応答などを専門家レビュー付きで用意する。
- trainと独立した評価セットを先に固定する: 正確性、形式遵守、日本語品質、拒否、安全性を別々に採点する。
- 長文truncationを解消する: 2,048 tokenを超える会話を分割または除外し、教師回答が途中で欠けないdataset v2を作る。
- Baseを必ず対照群にする: Fine-tunedモデルがベースを上回った指標だけを改善として扱う。
- 複数seedで再現する: dataset v2で最低3 seedを実行し、平均とばらつきをMLflowで比較する。
- 合格後に学習率やLoRA層数を調整する: データと評価を固定してから、一度に1軸だけ変更する。
今回の結論は「step 20を採用」ではなく、現在の汎用ランダムサブセットでは、目的品質を改善したと証明できないです。これは失敗ではなく、Lossだけでは見えない問題を追加検証で発見できたという成果です。
MLflow UIを再度開く
ポート5000が他のMLflowで使われていたため、今回は5050を使いました。
uv run mlflow server \
--backend-store-uri sqlite:///mlflow.db \
--host 127.0.0.1 \
--port 5050
ブラウザでhttp://127.0.0.1:5050を開きます。学習結果はModel training → Llama-3.2-3B MLX LoRA SFT → finetome-25-steps、追加比較はLlama-3.2-3B MLX LoRA Evaluationで確認できます。
実際に動かして分かった注意点
- 初回は約6.43GBのモデル取得が必要です。2回目以降はHugging Faceキャッシュを利用できます。
- 一部のFineTomeサンプルは2048 tokenを超え、今回の設定では切り詰められました。品質重視なら事前に長さ分布を調べ、分割または上限を再設計します。
- MLflow標準System MetricsにはApple GPU利用率が出ません。これはMLXがGPUを使っていないという意味ではありません。
- 25 stepは動作確認です。今回12件の追加評価は実施しましたが、用途を定義した十分な独立testと複数seedによる検証は別途必要です。
- 共有された会話はSTEP 0までだったため、STEP 1以降はそこで示された対応表とMLX-LM公式仕様を基に、実行可能なスクリプトとして補完しました。
- データ件数を増やす前に、目的適合性、重複、長さ、splitリーク、教師回答の正確性を確認します。「多いデータ」より「評価可能な良いデータ」を優先します。
まとめ
Apple Silicon Macでも、MLX-LMを使えばLlama 3.2 3BのLoRA SFTをローカル実行できました。
追加検証ではstep 20を含むcheckpoint群を比較しましたが、ベースモデルを明確に上回るものはなく、意味の正確性にも問題が残りました。なので次の一手は25 stepをそのまま延長することではなく、用途専用データと独立評価セットを作り、長すぎて切り詰められた例を整備したdataset v2を複数seedで検証してみます。データ設計、学習、分析、次の実験を循環させることで、手元のlocalLLMのモデルでも高精度な出力が出来るように目指してみます。





