コンテンツにスキップ

1. 必要な概念:開発でおなじみのものにたとえて理解する

この章では数学は扱いません。目標は、このあとコードのセルを実行するたびに、画面に出てくる数値が何を意味しているかがわかるようになることです。各概念には日常の開発でおなじみのもの(データベース、Git、Web サービスなど)によるたとえを添えます。たとえは直感をつかむための補助であり、厳密な定義ではありません。

この章の概要

  • この章でやること:「モデルはディスク上のディレクトリである」という話から始めて、トークン、推論、学習と loss、SFT/LoRA/QLoRA、ステップとエポック、7 つのライブラリの役割分担、VRAM を順に説明します
  • 必要なもの:インストールは不要です。Web ページを開けるパソコンが 1 台あれば十分です
  • 得られるもの:このあとノートブックに出てくるパラメータと数値をすべて対応づけられるようになります。エラーが出たときに、どのライブラリを調べればよいかがわかります

8 つの節は「まずモデルがあり、次にそれが文字をどう読むか、どう答えるか、どう学ぶか、どう VRAM を節約するか」という順に並んでいます。各節は前の節を前提にしているので、順番に読んでください。

1.1 ディスク上のモデルは 1 つのディレクトリ

models/qwen3-4b/ を開いてみましょう。物理的には、「モデル」とはこれらのファイルのことです。注意:自分のパソコンで --tokenizer-only を指定してダウンロードしたディレクトリ(2.8 節)には、トークナイザーと設定だけ、約 15 MB しか入っておらず、3 つの重みシャードは含まれていません。シャードは GPU ホストで完全なダウンロード(3.4 節)を行ったあとにはじめて現れます。これは正常な状態で、ダウンロードが途中で止まったわけではありません。

ファイル 何か たとえ
config.json 構造の定義:36 層、隠れ次元 2560、語彙数 151,936 など データベースのテーブル作成文(スキーマ)
model-00001-of-00003.safetensors 〜 model-00003-of-00003.safetensors 重み。約 40 億個の数値で、BF16 で合計 8.0 GB、3 つのシャードに分割。完全なダウンロード後にのみ存在 データベースのデータファイル
model.safetensors.index.json どの重みがどのシャードにあるか シャードのルーティングテーブル
tokenizer.json、vocab.json、merges.txt トークナイザー:テキスト ↔ 整数 ID の変換規則 シリアライズのプロトコル
tokenizer_config.json chat_template を含む:system/user/assistant をどうつなげて 1 つのテキストにするか リクエストのメッセージ形式
generation_config.json デフォルトのサンプリングパラメータ サーバー側のデフォルト設定
model-lock.json このチュートリアルが記録しているバージョン(コミット) package-lock.json

「モデルを読み込む」とは、config.json に従って空の構造を組み立て、safetensors の数値を埋めて VRAM に配置することです。このあと、3 種類のディレクトリに出会います:ベースモデル(上のもの)、アダプター(LoRA の差分だけで約 130 MB、必ずベースモデルと組み合わせて使う)、マージ済みモデル(ベースと差分を合成したあとの完全なディレクトリで、再び 8 GB)。vLLM が読み込むのは、ベースモデルかマージ済みモデルのディレクトリです。

1.2 トークン:モデルから見たテキスト

モデルは文字を読むのではなく、整数を読みます。トークナイザーがテキストを断片(トークン)に切り分け、各断片が語彙の中の ID に対応します。このプロジェクトのトークナイザーで文を切り分けてみると、次のようになります:

"为什么白天亮、晚上黑?"
→ ['为什么', '白天', '亮', '、', '晚上', '黑', '?']
→ [100678, 106772, 99538, 5373, 104030, 56652, 11319]

"The Earth rotates."
→ ['The', ' Earth', ' rotates', '.']
→ [785, 9237, 89059, 13]

(中国語の文は「なぜ昼は明るくて夜は暗いの?」という意味です。)

よく使われる単語は 1 トークンですが、珍しい文字は複数のトークンに分かれることがあります。英語では、スペースは後ろの単語にくっつきます。そのため、トークン数は文字数と同じではありません。中国語ではおおよそ 1 トークン ≈ 1.3〜1.5 文字です。このチュートリアルに出てくる「長さの上限 4096」は、すべてトークン数を指します。

語彙には、どの文字にも対応せず、構造を示す目印として働く特殊トークンもいくつかあります。Qwen の会話フォーマット(chat_template)は、1 回のリクエストを次のように組み立てます:

<|im_start|>system
你是陪小学生探索科学的伙伴。……<|im_end|>
<|im_start|>user
为什么白天亮、晚上黑?<|im_end|>
<|im_start|>assistant
地球在不停转,我们住的地方转向太阳就是白天……<|im_end|>

(system:「あなたは小学生と一緒に科学を探る仲間です……」、user:「なぜ昼は明るくて夜は暗いの?」、assistant:「地球はずっと回っていて、私たちの住んでいる場所が太陽のほうを向くと昼になるんだよ……」)

<|im_end|>(ID 151645)は終了マーカー EOS です。モデルはこれを生成した時点で止まります。学習時には、各回答の最後にこれを出力することをモデルに学ばせなければなりません。そうしないと、デプロイ後に回答が止まらなくなります。

1.3 パラメータと推論:40 億個の係数をもつ関数

モデルの本質は 1 つの関数です。トークン ID の列を入力すると、「次のトークンが語彙の各エントリである確率」を出力します。この関数には約 40 億個の調整可能な係数があり、これをパラメータ(または重み)と呼びます。safetensors に入っている数値がそれです。

推論とは、この関数を繰り返し呼び出すことです。プロンプトを与え、最も確率の高いトークンを選び(貪欲デコーディング、do_sample=False)、それを入力の末尾に付け足して、もう一度呼び出します。これを EOS が出力されるか max_new_tokens に達するまで繰り返します。推論ではパラメータは 1 つも変わりません。

temperature=0 は貪欲デコーディングと同じように振る舞い、毎回最も確率の高いトークンを選ぶので、同じ入力からは同じ出力が得られ、比較に便利です。0 より大きくすると確率に従ってランダムにサンプリングするので、回答のバリエーションは増えますが再現性はなくなります。

1.4 学習:誤差を使ってパラメータを修正する

学習はその逆です。モデルに「文脈 + 参照回答」を見せ、参照回答の各位置で次のトークンを予測させ、予測と参照のずれを 1 つの数値に変換します。これを loss(損失)と呼びます。

loss のおおよその値 意味
約 3 以上 参照回答のどのトークンにもモデルが「驚いて」いる状態。ランダムに当て推量した場合は ln(151936) ≈ 11.9
1〜2 元のモデルによいプロンプトを与えたときの、このプロジェクトの参照回答に対する典型的な出発点
0.5 前後 モデルがすでに参照回答の言い回しにかなり近づいている
0 に近い ほぼ一字一句暗記している状態。同時に検証 loss が上がっていれば過学習です

loss は「モデルが参照回答にどれだけ驚いているか」を表すもので、「科学知識のスコア」ではありません。モデルが学習データの言い回しを身につけつつあるかどうかはわかりますが、回答が正しいかどうかはわかりません。

loss があれば、各パラメータについて「どちらの方向に少し動かせば loss が小さくなるか」を計算できます。これが勾配です(誤差逆伝播法で計算します)。オプティマイザー(AdamW)は勾配に従ってパラメータを更新し、学習率(1e-4)は 1 回の更新の幅を決めます。大きすぎると元のモデルの能力を洗い流してしまい、小さすぎると学習が進みません。「loss を計算 → 勾配を計算 → パラメータを更新」の 1 回分を ステップ(step)と呼びます。

たとえるなら、loss は CI で失敗しているテストの数、勾配は各設定項目をどちらに回せばよいかを教えてくれるもの、学習率は一度にどれだけ変更する勇気があるか、ステップは 1 回のコミットのようなものです。

1.5 SFT、LoRA、QLoRA:3 つの独立した選択

この 3 つの用語は、それぞれ別の問いに答えるもので、独立に組み合わせられます:

SFT(教師ありファインチューニング)は「何で教えるか」に答えます。 モデルに「入力 → 期待される出力」のペアを見せます。つまり第 2 章の会話データです。このプロジェクトでは「参照回答 + EOS」の位置でだけ loss を計算し、文脈部分では計算しません(2.5 節でこのマスクを自分の目で確認します)。

LoRA は「どのパラメータを変えるか」に答えます。 フルファインチューニングでは 40 億個のパラメータをすべて更新する必要があり、それぞれに勾配とオプティマイザーの状態がつくので、GPU 1 枚には収まりません。さらに、実験のバージョンごとに 8 GB の保存領域が必要になります。LoRA では元の重み W にはまったく手を触れず、その横に 2 つの小さな行列 A と B を追加して、それだけを学習します:

实际使用的权重 = W(冻结) + (alpha / r) × B × A

(つまり、実際に使われる重み = W(凍結) + (alpha / r) × B × A です。)

Git にたとえると、ベースモデルはメインブランチ、LoRA はごく小さな diff で、学習で変わるのは diff だけです。adapter/ ディレクトリに保存されているのはまさにその diff です。「マージ」は、diff をメインブランチに適用して新しい完全なファイル一式を作るようなものです。

r(ランク)は小さな行列の幅、つまり diff の容量を決めます。このプロジェクトでは r=16 を使います。7 種類の線形層 × 36 層を合計すると学習可能なパラメータは約 3300 万個で、全体の 0.8%、アダプターファイルは約 130 MB になります。alpha はスケーリング係数で、このプロジェクトでは常に alpha = 2 × r とします。

QLoRA は「凍結したベースモデルをどうやって VRAM に収めるか」に答えます。 凍結した重みはどうせ変わらないので、VRAM 上では BF16(1 つの数値あたり 2 バイト)から 4 bit(1 つの数値あたり 0.5 バイト)に圧縮して保持し、計算時に一時的に BF16 に戻します。4B モデルの重みは 8 GB から約 2.5 GB に縮むので、24 GB のカードに学習の中間結果を置く余裕が生まれます。NF4 は圧縮フォーマットの名前で、double_quant は圧縮に使う補助パラメータもさらに圧縮することを意味します。LoRA の小さな行列自体は BF16 のままで、圧縮されません。

さらに学びたい方へ:LoRA の原論文、QLoRA の原論文。

1.6 サンプル、バッチ、ステップ、エポック

概念 このプロジェクトでの値 意味
サンプル 1150 「文脈 + 1 つの参照回答」の組 1 つ。2 ターンの会話は 2 サンプルに展開されます
マイクロバッチ 1 1 回の順伝播/逆伝播で VRAM に載せるサンプル数
勾配累積 16 16 サンプル分の勾配をためてから、パラメータを 1 回更新します。VRAM には 1 サンプル分しか載せないまま、batch=16 に近い効果が得られます
ステップ 72 オプティマイザーの更新回数 = ⌈1150 ÷ 16⌉。プログレスバーと loss 曲線の横軸です
エポック 1 全サンプルを 1 周すること
チェックポイント 25 ステップごと 学習を再開できるスナップショット:アダプター + オプティマイザーの状態 + 進捗

たとえるなら、サンプルは 1 件のメッセージ、勾配累積は 16 件のメッセージをためてからデータベースにまとめて書き込むこと、ステップは 1 回のデータベース書き込み、エポックはキューを一通り消費することです。

1.7 7 つのライブラリの役割

学習環境には 7 つのライブラリをインストールします。エラーメッセージにはこれらの名前が出てきます:

ライブラリ 担当 どこで目にするか
PyTorch テンソル計算、GPU のスケジューリング、自動微分。ほかのライブラリはすべてこれの上に作られています torch.cuda.*、VRAM の統計
Transformers config.json からモデル構造を組み立てる、重みの読み込み、トークナイザー、チャットテンプレート、generate() AutoModelForCausalLM、AutoTokenizer
PEFT モデルに LoRA 層を追加する、元のパラメータを凍結する、アダプターの保存/読み込み、マージ LoraConfig、get_peft_model、PeftModel
TRL SFT 向けに作られたトレーナー:マスクに従って loss を計算し、ステップ/エポック/チェックポイントを管理します SFTConfig、SFTTrainer
bitsandbytes 4 bit 量子化の保存フォーマットと GPU 計算カーネル BitsAndBytesConfig。NVIDIA の GPU でしか使えません
accelerate モデルを指定したデバイスに配置する、混合精度。TRL が内部で依存しています device_map
datasets トレーナーが要求するデータコンテナ Dataset.from_list

デプロイでは別のライブラリに切り替えます。vLLM は独自の推論エンジンと HTTP サーバーを備えており、必要なのはモデルのディレクトリだけです。上の 7 つのうち PEFT や TRL は不要です。

呼び出しの関係はこうなります:Transformers がモデルを読み込む → bitsandbytes が重みを 4 bit に圧縮する → PEFT が LoRA 層を追加する → datasets がサンプルを格納する → TRL が学習ループを回す → PEFT がアダプターを保存する → PEFT がマージする → vLLM がデプロイする。

1.8 RAM、VRAM、ディスク

リソース 主な用途 よくある誤解
CPU テキスト処理、トークン化、読み込みとスケジューリング コア数が多いからといって、大規模モデルの学習に向いているわけではありません
RAM(ホストメモリ) 重みを読み込むときの中継、CPU 上でのモデルのマージ 100 GB の RAM で 24 GB の VRAM の代わりはできません。学習の計算は GPU 上で行われるので、データは VRAM になければなりません
GPU 大量の行列演算 CUDA を使うメインの流れには NVIDIA が必要です。Mac の MPS は別のルートで、このチュートリアルでは対応していません
VRAM 重み、活性化、勾配、オプティマイザーの状態、推論時の KV キャッシュ モデルファイルが 8 GB だからといって VRAM も 8 GB で済むわけではありません。学習では中間結果が、推論では KV キャッシュが加わります
ディスク ベースモデル、環境、チェックポイント、マージ済みモデル チェックポイントにはそれぞれオプティマイザーの状態が含まれます。マージするたびにさらに 8 GB 増えます

学習時の VRAM の内訳と実際の計測値は 4.3 節に、推論時の VRAM の見積もりは 5.3 節にあります。

確認ポイント: モデルディレクトリの各ファイルが何かを説明できること。loss が 2 から 0.6 に下がることが何を意味するか、そしてそれだけでは回答がよくなったとは言えない理由を説明できること。1150 サンプルがなぜ 72 ステップになるのかを説明できること。7 つのライブラリのうち、LoRA を担当するのはどれで、学習ループを担当するのはどれかを答えられること。