「学習させれば知識が増える」
増えません。数百件のデータで動く重みはわずかで、事実は定着しません。定着したように見えても、更新のたびに学習し直しになります。事実はプロンプトか検索で渡し、重みには作法を入れてください。この分担を間違えると、学習の手間をかけて存在しない列名を書くモデルができます。
DBC Tech Academy / AI / 中級
プロンプトに規約を書き連ねても、10 回に 1 回は守らない。そんなモデルの癖を直すのがファインチューニングです。この講座では、自然文から社内規約どおりの SQL を書くモデルを題材に、データ作りから学習・評価・配布までを通しで行います。Mac(Apple Silicon)を主軸に、NVIDIA GPU での差分も置きます。
規約で決めている 4 点は、暗黙結合を書かない・別名は AS・日付は半開区間で名前付きプレースホルダ・列名はスキーマの実名です。学習後は、聞き方を変えてもこの 4 点がそろいます。
What you will have
この講座は、考え方ではなく動くものを持ち帰るための手順書です。最初に、到達点をそのまま示します。
テーブル定義はシステムプロンプトで渡し、書き方の規約だけを重みに入れたモデルです。定義が変わってもプロンプトを差し替えるだけで済み、学習し直しは要りません。この分担が、この講座で一番持ち帰ってほしい判断です。
統合メモリ 32 GB 以上で 7B、16 GB なら 3B。mlx-lm を使います。この講座の主軸です。
VRAM 16 GB 以上で 7B の QLoRA。Unsloth を使います。設定の対応表を用意しました。
データ作りに 1 日から数日、学習そのものは 7B・400 件で 1 時間ほど。時間の大半はデータの点検に使います。
How it works
7B のモデルを丸ごと学習し直すと、重みそのもの(fp16 で約 14 GB)に加えて勾配と最適化器の状態が要り、100 GB を超えます。手元のマシンでは回りません。LoRA は、この前提を変えます。
LoRA(Low-Rank Adaptation)は、元の重み行列 W を凍結したまま、その横に小さな 2 つの行列 A・B を置き、出力を W x + B A x と計算します。学習で動くのは A と B だけです。
W が 4096×4096 なら 1,600 万個の値ですが、ランク 16 の A(16×4096)と B(4096×16)は合わせて 13 万個で、およそ 128 分の 1 です。学習対象が小さいので勾配も最適化器の状態も小さく、必要なメモリはほぼ「元の重みを載せる分」で済みます。
学習結果(アダプタ)は数十 MB のファイルになり、元のモデルと分けて配れます。規約を変えて学習し直したときに、配布するのはこの数十 MB だけです。
| つまみ | 意味 | 上げると |
|---|---|---|
| ランク r | 差分行列の幅。学べる癖の細かさ | 細かい作法まで学べるが、覚え込み(過学習)しやすくなる |
| 学習率 | 1 回の更新で重みをどれだけ動かすか | 早く下がるが、行き過ぎて発散する |
| 反復回数 | データ全体を何周するか | 訓練データに寄る。検証損失が上がり始めたら止め時 |
この 3 つが実際にどう効くかは、止め時を決める節で動かして確かめられます。
元の重みを 4 bit に量子化して載せ、その上で LoRA を学習する方法を QLoRA と呼びます。量子化は「重みの値を粗い目盛りに丸めて保存する」ことで、精度をわずかに落として容量を 4 分の 1 にします。
7B の重みが約 4 GB になり、12〜16 GB の GPU でも学習できます。Mac の MLX でも同じ考え方で、4 bit のモデルの上に LoRA を載せます。この講座の手順は、どちらも 4 bit を前提にしています。
確かめ方。学習後にアダプタのファイルサイズを見てください。7B・ランク 16・全層で 40〜80 MB になります。この小ささが、元の重みを凍結していることの直接の証拠です。
Before you start
ここを飛ばすと、学習が終わったあとに「良くなった気がする」以上のことが言えなくなります。順に決めていきます。
最初の判断です。重みに入れるのは「どう書くか」であり、「何を知っているか」ではありません。
「社内のテーブル定義を覚えさせたい」「最新の製品情報を答えさせたい」という望みは、ファインチューニングでは満たせません。数百件のデータで重みをわずかに動かしても事実は定着せず、定着したように見えても更新のたびに学習し直しになります。事実はプロンプトに入れるか、検索して渡すのが正しい置き場所です。
一方で、「暗黙結合を書かない」「日付は半開区間」「プレースホルダは :name 形式」のような作法は、プロンプトに書いても守られないことがあります。作法は毎回同じ形で現れるので、重みに入れると安定します。これがファインチューニングの仕事です。
確かめ方。分担が正しければ、学習後にテーブル定義を差し替えても、モデルは新しい列名で規約どおりの SQL を書きます。学習後に列名を間違え始めたら、分担が崩れています(症例 01)。
学習は、プロンプトの代わりではありません。学習したモデルにも、テーブル定義はプロンプトで渡し続けます。学習が減らすのは、規約の記述と、それでも守らない回のほうです。
LoRA はファインチューニングの一種で、動かす重みを差分行列だけに絞った方式です。動かす範囲が狭いぶん手元のマシンで回り、失敗してもアダプタを捨てれば元に戻せます。その代わり、学べるものにも幅があります。どこまでを LoRA に任せるかは、動かしたいものの大きさで決めます。
| 動かしたいもの | 手段 | 必要なデータ | 規模感(7B) |
|---|---|---|---|
| 出力の形、作法、分類のラベル | LoRA(この講座) | 数百〜数千件の対話 | 手元のマシン、数時間 |
| 作法をより強く、幅広い入力に対して | フルファインチューニング | 数万件以上 | GPU 8 枚、数時間〜1 日 |
| 語彙、文体、分野の知識の分布 | 継続事前学習 | 数十億トークン以上の生テキスト | GPU 数十枚以上、数週間 |
LoRA で足りないと判断するのは、評価が頭打ちになったときだけです。データを 500 件まで増やし、ランクを 64 まで上げても規約違反が減らなくなったら、動かしたいものが作法の範囲を超えています。その手前で判断すると、データ不足をモデルの限界と取り違えます。逆に、扱う文章そのものがベースモデルの得意分野から遠い場合(独自の記法、古い業務文書の言い回し、独自形式のログ)は、最初から継続事前学習の領域で、LoRA では届きません。
| 規約 | 判定方法 |
|---|---|
暗黙結合(FROM a, b)を書かない | 正規表現で FROM 句の中のカンマを検出する |
別名は AS を付ける | FROM orders o の形を検出する |
日付範囲は >= と < の半開区間で、値は :name 形式 | リテラル日付と BETWEEN を検出する |
SELECT * を書かない | 文字列一致 |
| 列名・テーブル名がスキーマに存在する | EXPLAIN を実行して通るかを見る |
「読みやすい」「適切な」のように判定できない語は、規約に入れないでください。判定できない規約は、学習データの点検にも評価にも使えず、書いた人以外には守れているかどうかが分かりません。
評価セットの質問を、学習データに混ぜないでください。混ざった時点で、評価は「覚えたものを答えられるか」の測定になり、意味を失います。評価セットは別ディレクトリに置き、学習用の JSONL を作るスクリプトから物理的に届かない場所にしてください。
Building the dataset
Mac の mlx-lm も NVIDIA の Unsloth も、チャット形式の JSONL を受け取ります。1 行が 1 会話です。
system にスキーマを入れているのは、分担のためです。推論時に渡すものは、学習時も同じ形で渡します。学習時だけスキーマを抜くと、モデルは「スキーマを見ずに列名を推測する」癖を学び、本番で存在しない列名を書き始めます。
作法を固定する目的なら、この範囲で効果が出ます。1,000 件を超えて増やしても作法の学習では伸びが止まり、代わりに質の低い例が混ざる害のほうが増えます。
件数を増やす労力は、全件を人が読んで規約違反をゼロにするほうに使ってください。学習データに違反があれば、モデルはその違反を学びます。
Will it fit
始める前に見積もります。重みは固定費、活性化(途中計算の保存)は系列長とバッチに比例する変動費です。この 2 つの性質の違いが分かると、落ちたときにどのつまみを触るかが決まります。 値を動かして、自分のマシンで回るかを確かめてみましょう。
Mac では統合メモリ、NVIDIA では VRAM の量です。OS と他のプロセスが使う分があるため、実際に学習へ回せるのは全体の 8 割ほどです。
この図が再現しているのは、各項目が何に比例して増えるかです。重みはパラメータ数と精度で、活性化は系列長とバッチで決まります。絶対値はモデルの構造(隠れ次元・層数・注意ヘッド数)とフレームワークの実装で 2 割前後ずれます。実際に回す前に、系列長を短くした 10 反復だけ走らせて実測を取り、その値で目盛りを合わせてください。Mac では mlx-lm が学習ログに Peak mem を出し、NVIDIA では watch -n 1 nvidia-smi で使用 VRAM が見られます。
Training on Mac
Apple Silicon の Mac で mlx-lm を使います。統合メモリ 32 GB 以上を想定します。16 GB のマシンでも、モデルを 3B にすれば同じ手順がそのまま通ります。
SQL が題材なので、コード系の指示追従モデルを選びます。ここは自由に差し替えてかまいませんが、変えたら評価をやり直してください。
mlx-lm はこのディレクトリ名とファイル名を固定で読みます。評価セットはここに置かず、別の場所に隔離してください。
| 引数 | 意味 | 決め方 |
|---|---|---|
--num-layers 16 | LoRA を付ける層の数。後ろから数えます | 全層にすると学べる幅が広がる代わりにメモリが増えます。まず 16 で始めます |
--iters 600 | 反復回数 | データ 400 件・バッチ 4 なら 1 周が 100 反復。多めに指定して、曲線で止め時を見ます |
--steps-per-eval 50 | 50 反復ごとに検証損失を出します | 曲線を読むために必ず付けます。これがないと止め時が分かりません |
--mask-prompt | 損失を assistant の応答部分だけで計算します | 質問文やスキーマの再現は学ばせません。付けないとスキーマを暗記します |
--grad-checkpoint | 活性化を捨てて再計算します | メモリが足りているなら外して速度を取ります |
300 反復で検証損失が底を打ち、450 以降は上がっています。訓練損失だけが下がり続けているのは、モデルが規約を学ぶ段階を過ぎて訓練データを覚え込みに入った合図です。読み方は次の次の節で扱います。mlx-lm は --save-every の間隔で途中のアダプタを番号付きで保存するので、底の時点のファイルを選べます。
--adapter-path を外すと学習前の出力が出るので、同じ質問で並べて比べられます。
--adapter-path に既存のディレクトリを指定すると、その中のアダプタは上書きされます。設定を変えて試行を重ねるときは、ディレクトリ名を adapters-r16、adapters-r32 のように分けてください。上書きしてしまうと、底の時点のアダプタごと失われます。
Training on NVIDIA
Linux + NVIDIA GPU では Unsloth を使います。VRAM 16 GB で 7B の QLoRA が回ります。手順の構造は Mac と同じで、違うのは「量子化して載せる」を読み込み時に行うことと、設定を Python で書くことです。
| Mac / mlx-lm | NVIDIA / Unsloth | 意味 |
|---|---|---|
--num-layers | target_modules | LoRA を付ける場所。mlx-lm は「後ろから何層か」、Unsloth は「各層のどの行列か」で指定する |
| (既定のまま) | lora_alpha | 差分の効き具合の倍率。ランクとの比で決める |
--iters | num_train_epochs | どれだけ回すか |
--learning-rate 1e-5 | learning_rate=2e-4 | 学習率。既定値の桁が違う |
--mask-prompt | train_on_responses_only | 応答部分だけで損失を取る |
--grad-checkpoint | use_gradient_checkpointing | 活性化を再計算してメモリを減らす |
差分 B A x には alpha / r の倍率が掛かります。alpha を上げると差分が強く効き、ランクを上げると同じ alpha でも 1 つあたりの効きが薄まります。alpha = r か alpha = 2r のどちらかで始めて、以後はランクを変えたら alpha も同じ比で動かします。比を固定しておけば、ランクを変えたときに学習率を取り直さずに済みます。
上のコードは r = 16、alpha = 16 で比が 1 です。学習が遅いと感じたら学習率ではなく alpha を 2 倍にするほうが、発散しにくく戻しやすい手です。
1 つの層には、注意機構の 4 つの行列(q_proj k_proj v_proj o_proj)と、MLP の 3 つ(gate_proj up_proj down_proj)があります。注意機構だけに付けると、アダプタは小さく、学べるのは「どこを見るか」の癖まで。MLP まで付けると、アダプタは 2〜3 倍になり、「何を出すか」の癖まで学べます。
この講座は 7 つすべてに付けています。SQL の作法は出力の形そのものなので、MLP まで要るからです。分類のラベル付けのような「見る場所」で決まる仕事なら、注意機構だけで足りることがあります。どちらにするかは、両方で評価セットを流して決めてください。
学習率の桁が違うのは、既定値の出どころが違うからです。mlx-lm は量子化された重みに対する LoRA の既定として小さめの値を採り、Unsloth と trl は fp16 の LoRA での慣行値を既定にしています。どちらも「既定から始めて、曲線で決める」が正しい使い方で、片方の値をもう片方に持ち込むと発散するか、ほとんど学習しないかのどちらかになります。
output_dir にチェックポイントが 25 ステップごとに保存されます。1 つが数十 MB から数百 MB になるため、長く回すとディスクを使い切ります。上のコードで save_total_limit を付けているのはこのためです。
When to stop
学習の成否は、最終的な損失の値ではなく、訓練損失と検証損失という 2 本の曲線の関係で判断します。 データ件数・学習率・ランクを動かして、止め時がどこへ移るかを確かめてみましょう。
バッチサイズは 4 で固定しています。1 周(エポック)にかかる反復回数は、件数 ÷ 4 です。
この図が再現しているのは、2 本の曲線がどちらへ動くかと、底の位置が要因でどう移るかです。損失の絶対値と反復数の目盛りは、モデル・データ・トークン数で変わり、実物とは一致しません。実際の学習では --steps-per-eval の間隔で出る検証損失をそのまま並べ、底を過ぎて 2 回連続で上がったら、底の時点のアダプタを採ってください。
訓練損失は、学習に使ったデータでの誤差です。反復を重ねれば必ず下がります。下がったこと自体は、何の証拠にもなりません。
検証損失は、学習に使っていないデータでの誤差です。途中まで下がり、あるところから上がり始めます。この上がり始めた点が、モデルが「規約を学ぶ」から「訓練データを丸暗記する」に切り替わった点です。
丸暗記に入ると何が起きるか。学習データにある質問には完璧に答え、少しでも言い回しの違う質問には崩れた SQL を返すようになります。評価セットでは数字が落ち、本番では「たまに変な答えを返す」形で現れます。
--mask-prompt の対象がずれてシステムプロンプトの再現を学んでいます。まず学習率を 3 倍にして 50 反復だけ試します。Does it actually work
損失は「訓練データに似た出力を出せるか」の指標であって、「規約を守るか」「SQL が動くか」を直接は測っていません。先に作った評価セットで測ります。
EXPLAIN し、通るかを数えます。列名・テーブル名が実在するかがここで分かります。| 指標(50 件中) | 学習前 | 学習後 |
|---|---|---|
| 規約違反 | 17 | 1 |
| EXPLAIN が通る | 46 | 47 |
| 結果が一致 | 39 | 41 |
規約違反が減り、実行成功率が下がっていなければ合格です。規約違反は減ったのに EXPLAIN の失敗が増えたなら、モデルが列名を推測する癖を学んでいます。学習データのシステムプロンプトにスキーマが入っているかを確かめてください。
評価は temperature 0 で行います。温度を上げると出力が揺れ、規約違反の件数が実行のたびに変わって、学習前後の比較ができなくなります。
本番で温度を上げて使う予定があるとしても、評価は 0 で固定してください。比較したいのはモデルの変化であって、サンプリングの揺れではありません。
評価用の PostgreSQL は、本番と切り離した固定データのものを使ってください。モデルの出力には DELETE や UPDATE が混ざることがあり、そのまま実行すると消えます。実行前に文頭が SELECT であることを判定に入れてください。
Ship it
学習したアダプタを、日常使いの Ollama で動かします。経路は 2 つあり、配布の軽さが違います。
Unsloth なら同じことが 1 行です。
元のモデルを Ollama に既に持っているなら、アダプタだけを変換して被せられます。
アダプタは数十 MB なので、規約を変えて学習し直したときの配布が軽くなります。ベースの量子化と学習時の量子化が違うと出力が少しずれるので、評価を Ollama 上でもう一度流してください。
チャットテンプレートが一致しているかを確かめてください。学習時のテンプレート(<|im_start|> などの区切り)と Ollama が使うテンプレートが違うと、学習した作法が出ません。ollama show sql-writer --template で確認し、違えば Modelfile の TEMPLATE に学習時と同じものを書きます。
ADAPTER 行を替えるだけで sql-writer と reply-writer を同じベースから作れ、ディスクはベース 1 つ分で済みます。1 つに混ぜると、片方の規約を直したときにもう片方の評価が動き、原因が追えなくなります。ollama create は同じ名前の既存モデルを置き換えます。以前の版を残すなら sql-writer:v2 のようにタグを分けてください。置き換えてしまうと、評価が悪化したときに戻す先がありません。mlx_lm.fuse --dequantize が書き出す fp16 の重みは、7B で約 15 GB になります。GGUF への変換が終わったら消してください。試行を重ねるとディスクを使い切ります。SYSTEM を学習時と変えないでください。変えると、学習で固定した作法の出方が変わります。変えたなら評価を流し直します。Read the symptom
ファインチューニングでよく持ち込まれる 3 つの症状です。それぞれ、どこに原因があるのかを考えてみましょう。
Myth vs. reality
経験のある人ほど、この 3 つで判断を誤ります。
増えません。数百件のデータで動く重みはわずかで、事実は定着しません。定着したように見えても、更新のたびに学習し直しになります。事実はプロンプトか検索で渡し、重みには作法を入れてください。この分担を間違えると、学習の手間をかけて存在しない列名を書くモデルができます。
作法の固定では 200〜500 件で頭打ちになり、それ以上は質の低い例が混ざるリスクだけが増えます。件数を集める労力は、全件を人が読んで規約違反をゼロにするほうに使ってください。学習データに違反が 1 割あれば、モデルはその違反も学びます。
訓練損失は回せば必ず下がります。下がったことは、規約を学んだ証拠にも、丸暗記した証拠にもなります。見るべきは検証損失の底で、合否を決めるのは規約違反の件数と実行成功率です。損失の値そのものは、モデルの良し悪しを表しません。
Knowledge check
答えを当てるだけでなく、なぜそう判断できるのかを説明できれば合格です。
選択肢を1つ選んでください。
The whole thing in one line
目次