0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

継続事前学習(CPT)と事後学習(SFT)を Qwen3-0.6B でやってみた - LLMの知らない知識を足す

0
Posted at

はじめに

素の Qwen3-0.6B-Base に Claude Code の使い方を聞くと、存在しないコマンドが返ってきます。同じ質問を、公式ドキュメントで継続事前学習(CPT) したモデルと、そこへさらに LoRA で SFT を重ねたモデルに聞くと、答えはこう変わりました。

Q: How do you start an isolated Claude Code session in a named Git worktree?
   (名前付きの Git worktree で、独立した Claude Code セッションを開始するには?)

【素のモデル】2 点
  ... navigate to the worktree directory and run the following command:
  `cd <worktree_name> && claudacode`.

【CPT 後】8 点
  ... use the `claude --worktree` flag with the worktree name. For example,
  `claude --worktree my-worktree` starts a session in the `my-worktree` worktree.

【CPT + SFT 後】10 点
  Run `claude -w <worktree-name>`. This creates a new session inside the specified worktree.

点数は、後述する判定モデルによる 10 点満点の採点です。存在しないコマンドが正しいフラグに変わり、SFT を重ねると短縮形の -w を 1 文で答えるようになりました。学習はどちらも RTX 4070 1 枚で、CPT が 1 回約 5 分、SFT が 1 回約 12 秒です。

この記事では、この変化が 1 問だけの偶然ではないかを 70 問 × 12 条件 = 840 回答で測り直しました。どこで効いて、どこで効かなかったのかまで含めて報告します。

なぜモデル本体に知識を入れるのか

これまでこのブログでは、CAG とキャッシュ管理を中心に測ってきました。資料の全文をプロンプトの先頭に固定する CAG を実装し、日本語小説 50 問で、比較対象として定番の RAG も一部試しています。結果は 2.00 点満点で CAG 1.90、RAG 1.58 でした。RAG は検索が外れた瞬間にもっともらしい嘘をつき、CAG は精度と引き換えにプロンプトが 6.4 倍に膨らみます。固定した長い資料を VRAM 12GB で回すために、KV キャッシュを CPU に逃がす運用も測りました。

ただ、RAG も CAG も、学習の終わったモデルにあとから知識を外付けする方法です。精度を出すには、検索の当て方やキャッシュの持たせ方など、外側のチューニングが次々に必要になります。正直なところ、外側の調整を積み重ねて精度を上げていく方向は、エンジニアリングよりもパラメータチューニングの側面が大きく、また求めるものはある用途だけではなく(顧客はドリルではなく穴がほしい、的な)複数のことを考慮する必要があるので、とても難しい作業だと思います。今回は初めて学習側に踏み込み、知識をモデルの重みそのものに入れます。

私が今後ほしいのは、いちばん賢いモデルではなく、自分たちで管理できるサイズのモデルです。重みが手元にあり、社外にデータを出さずに回せて、必要な知識だけを内側に持つ。モデル自身が知識を持っていれば、入力は質問文だけで済みます。そのサイズは、おそらくローカルで動かせる 10B〜30B 程度だと見ています。

ただし、それをいきなり 30B で試すのは高くつきます。先に確かめるべきは「独自知識はそもそもモデルに入るのか」「入れた知識を SFT で答えやすくできるのか」の 2 点で、これはいちばん小さいモデルで測るのが安上がりです。だから 0.6B から始めました。

対象読者は、手元の GPU でドメイン特化モデルを作れるか知りたい人、RAG の運用が重いと感じている人、社外にデータを出せない前提で LLM を使いたい人です。前提知識は、LoRA と RAG の概要が分かる程度で十分です。 ※今回はRAGの技術には触れませんのよ

要点

  • 作ったもの:公式ドキュメント 600 文書を集めて整形し、約 152 万トークンで Qwen3-0.6B-Base を全パラメータ CPT しました(Transformers と Unsloth の 2 実装)。その上に LoRA SFT を 3 世代作り、12 条件 × 70 問 = 840 回答を 2 系統の判定モデルでブラインド採点しています(両者の相関 r = 0.87)。
  • CPT で答えが良くなりました。 全 70 問で +0.80 点(95%信頼区間 [+0.26, +1.36])。SFT に入れていない Claude Code の 20 問では +1.00 点([+0.20, +1.90])で、Transformers 版の CPT では 2 つの判定モデルの両方で有意でした。
  • SFT で回答の形式が揃いました。 平均回答長は CPT 単体の約 270 字から、CPT + SFT で約 110 字になり、最長でも 226 字に収まります。SFT に直接入れた 30 問は +1.77 点([+0.67, +2.93])でした。
  • CPT と SFT を重ねても、SFT に入れていない知識での上乗せは見えませんでした。 未収録 40 問での CPT → CPT + SFT の差は −0.20〜+0.33 点で、4 通りの比較すべてで信頼区間が 0 をまたぎます。
  • Codex の設問では CPT の効果が見えませんでした(±0.00)。学習データを実トークンで数え直すと、19.2% が評価と無関係な OpenAI Platform API のドキュメントで、Codex CLI 本体の 17.5% より多く入っていました。文書数の集計では気づけませんでした。
    • このへんの学習データのクリーニングも重要なファクターであり、学習前にチェックする必要がありました、と反省
  • 知識と回答形式は、別々に測る必要があります。12 条件の総合順位だけを見ると、SFT で直接覚えた分を、CPT と SFT の相乗効果と取り違えます。

CPT・SFT・LoRA はそれぞれ何をするのか

CPT は、ドキュメントの生テキストで「次の単語を当てる」学習を続けてモデルに知識を足します。SFT は、質問と回答のペアで答え方を教えます。LoRA はそのどちらにも使える、元の重みを凍結したまま小さな差分だけを学習する実装方法です。

与えるデータ 狙い 今回の使い方
CPT(継続事前学習) ドキュメントの生テキスト 次トークンの確率分布を書き換え、知識を足す 全パラメータ更新
SFT(教師ありファインチューニング) 指示と回答のペア 答え方・長さ・形式を揃える LoRA で 3 世代
(精度を見て3回実施)
LoRA (学習方法の選択肢) 元の重みは凍結し、小さな差分行列だけを学習 SFT に使用

世間で「ファインチューニングした」と言うとき、多くは SFT を指しています。ファインチューニングは追加学習の総称で、SFT はその代表的な 1 つです。

CPT は名前のとおり事前学習の続きです。損失関数も学習ループも最初の事前学習と同じで、変わるのは食わせるデータが対象ドメインの文書になる点だけです。正解ペアを作らなくて済むのが実務上の利点で、今回もドキュメントを収集して整形しただけで学習に使っています。代わりに、全パラメータを更新すると既存の能力が削れる破滅的忘却のリスクを抱えます。後述の学習率を 1e-5 と低くしたのはそのためです。

0.6B なら全パラメータを更新しても 12GB に収まるので、CPT は制約をかけない全更新にしました。SFT は LoRA にして、同じ設定のアダプタを 3 つの元モデルに付け替えて比べています。

何を作ったか

個人環境で、ドキュメントの収集から採点まで一通り組みました。

規模と、各段階で下した判断です。

段階 規模 判断したこと
収集 600 文書 Claude Code と Codex の公式サイトから収集
整形 466 文書(134 件除外) canonical URL の重複 111 件、800 字未満の断片 18 件、集約ページ 4 件、本文の完全重複 1 件を除外
分割 train 372 / test 94 文書 トークン数で 3:1 に分割し、canonical URL の重複ゼロを確認
CPT 1,524,330 トークン × 2 実装 Transformers と Unsloth で同一データ・同一設定を実行
SFT LoRA アダプタ 9 本 回答形式を変えた 3 世代 × 元モデル 3 種
評価 70 問 全設問に出典 URL・出典本文の SHA-256・根拠抜粋を記録
採点 840 回答 × 2 系統 モデル名を伏せ、候補の並び順を設問ごとに変更

素の Qwen3-0.6B-Base は何を知らなかったのか

素のモデルは、70 問の平均が 2.50 / 10 点で、43% にあたる 30 問が 0〜1 点でした。知らない質問にも「知らない」とは答えず、質問の言い換えか、それらしい識別子の捏造を返します。

Qwen3 の公開は 2025 年 4 月 29 日で、知識カットオフは公表されていません。今回の 70 問は 2026 年 8 月に収集した公式ドキュメントを根拠にしていますが、似た内容が事前学習に含まれていた可能性は否定できません。そこで「知らないはず」と決めつけず、先に素のモデルに解かせて確かめました。ここが曖昧だと、あとで出る改善が「もともと知っていたことを思い出しただけ」なのか区別できないからです。

指標 Qwen3-0.6B-Base
70 問の平均点 2.50 / 10
0〜1 点(実質何も答えられていない) 30 問(43%)
5 点以上 10 問のみ

知らないときの振る舞いは、2 通りに分かれました。

1 つ目は、質問のオウム返しです。 極端な例がこれです。

Q: Which Codex setting controls concurrent subagent threads per session,
   and what is its legacy alias?

A(素のモデル): The Codex setting that controls concurrent subagent threads per session is
               "Subagent Threads per Session." Its legacy alias is "Subagent Threads per Session."

正解: agents.max_concurrent_threads_per_session(旧名は agents.max_threads)

質問文を名詞句に組み替えて返しているだけで、情報はありません。旧名の欄にまで同じ語を置いています。

2 つ目は、それらしい識別子の捏造です。 形式だけを正解に似せてきます。

問い 素のモデルの回答 正解
worktree でセッションを開くコマンド cd <name> && claudacode claude --worktree <name>
ユーザー階層のコマンドルールの保存先 codex_rules テーブル ~/.codex/rules/
codex app-server のプロトコル HTTP/2、既定のトランスポートは tcp JSON-RPC 2.0、既定は stdio://
MCP 経由のセッションのサンドボックスモード sandbox / sandbox-advanced read-only / workspace-write / danger-full-access

裏を返せば、この 70 問は「CPT で足した知識」を測る土俵として使えます。

検証環境と学習設定

- ハードウェア: NVIDIA GeForce RTX 4070 (12,282 MiB / PyTorch から見える実効 11,864 MiB)
- OS / ドライバ: Ubuntu (Linux 7.0.0-28-generic), NVIDIA driver 580.126.20, CUDA 12.8
- Python: 3.12.3
- 主要パッケージ: torch 2.11.0+cu128, transformers 5.15.0, peft 0.20.0,
                  bitsandbytes 0.50.1, unsloth 2026.8.18
- モデル: Qwen/Qwen3-0.6B-Base (revision da87bfb608c14b7cf20ba1ce41287e8de496c0cd)
- 作成支援: Gemini 3.6 Flash, Codex Sol(学習・評価用の質問・評価の仕組みを作成)

学習、評価用の質問、評価の仕組みは、Gemini 3.6 Flash と Codex Sol に作らせました。作られたものは、出典との照合、重複のチェック、SFT データと評価問題の重なり(リーク)の検査を自動で通してから使っています。

CPT の設定

パラメータを fp32 で保持し、計算を bf16、gradient checkpointing 有効で回しました。

項目 値 選んだ理由
sequence_length 1024 12GB に収め、パッキング効率を優先
micro_batch / grad_accum 1 / 16(実効 16) VRAM 制約。ピーク 8,474 MiB に収まる
learning_rate 1e-5、constant、warmup なし 既存能力を壊さない保守側に振る
optimizer adamw_8bit(β=0.9/0.95、wd=0.1) オプティマイザ状態の VRAM を削減
epochs / seed 1 / 20260815 単一 seed(補足を参照)

Transformers と Unsloth で差はあったか

同じデータ・同じ設定で、2 つの実装を 2 回ずつ回しました。

実行 実装 tokens/s peak allocated 学習時間 held-out loss
1 Transformers 4,755 8,474 MiB 321 秒 2.5403 → 2.1455
1 Unsloth 4,762 7,673 MiB 321 秒 2.5402 → 2.1455
2 Transformers 4,981 8,474 MiB 306 秒 2.5403 → 2.1456
2 Unsloth 4,779 7,673 MiB 319 秒 2.5402 → 2.1455

Unsloth は、peak allocated が 2 回とも約 801 MiB 少なくなりました。速度は 1 回目がほぼ同じで、2 回目は Transformers のほうが速く、Unsloth が常に速いという結果にはなっていません。held-out loss は両実装とも 2.54 → 2.15 まで下がり、学習の進み方に差はありませんでした。

SFT の 3 世代

SFT は、回答の形式を変えながら 3 世代作りました。

世代 学習データ(各 200 件) 回答の形式 この世代を作った理由
v1 汎用の技術 Q&A 1 文・100 字以内 まず回答の形式だけを揃える
v2 v1 と同じ題材で、回答を長くしたもの 1〜3 文・300 字以内 v1 は短すぎて、コマンド名や条件が回答から落ちているように見えた
v3 汎用 150 + 曖昧な質問 20 + Claude Code 15 + Codex 15 1〜3 文・300 字以内 ドメイン知識を少量入れ、CPT の知識を引き出せるかを見る

LoRA の設定は全世代で共通です(r=16、alpha=32、dropout=0.05、対象は q/k/v/o/gate/up/down_proj、lr=2e-4、3 epochs、実効バッチ 16、seed 42)。学習するパラメータは 10,092,544 個で、全体の約 1.7% です。v3 の学習は 1 本あたり約 12 秒、ピーク 3,457 MiB で終わります。

v3 に入れた Claude Code 15 件・Codex 15 件のドメイン知識は、次節で説明する評価 70 問のうち sft_seen の 30 問と対応しています。

回答の生成条件

12 条件すべてを、同じ評価プロンプト(v3 の SFT と同じ指示文)、greedy decoding、bf16、max_new_tokens=256 で生成しました。

評価セットはなぜ作り直したのか

最初に使った評価セットは 200 行ありましたが、重複を除くと質問は 83 種類しかありませんでした。30 の重複グループに 147 行が属しており、200 問の独立したベンチマークとしては扱えません。そこで、出典を 1 問ずつ記録した 70 問を作り直しました。

新しい 70 問には、すべてに出典 URL、出典本文の SHA-256、根拠の抜粋を付けています。自動検証では、70 問すべてで「必要なキーワードが 1 つの公式文書の中に揃っている」ことを確認しました。

評価の 70 問は、性格の違う 2 つに分けています。ここを混ぜると、後で結論を読み違えます。

split 問題数 内訳 役割
sft_seen 30 Claude 15 / Codex 15 SFT v3 に直接入れた知識。暗記の対照群
heldout 40 Claude 20 / Codex 20 SFT には入れていない。CPT で入った知識の主評価

採点は 2 系統に分けた

840 回答すべてを、独立した 2 系統の判定モデルで採点しました。1 つは gpt-5.6-terra のサブエージェント 3 体で、split × ドメインで層化して設問を分担させています(以下 Terra)。もう 1 つは gemini-3.5-flash-lite です。どちらにもモデル名を伏せ、候補の並び順を設問ごとに変え、互いのスコアは見せていません。

結果は相関 r = 0.87、有意かどうかの判定は 25 件中 24 件が一致しました。以降の数値は、片方の判定モデルの癖で決まったものではありません。

Terra の 3 体には、平均点で 2.35〜3.34 点という甘辛の差がありました。ただし、同じ設問の 12 条件はすべて同じ採点者が見ているので、同じ設問の中での条件間の差は、甘辛の影響を受けません。以降は、この同一設問内の差だけを使います。

出典を学習データと照合した

70 問の出典 42 URL を、CPT の train 分割と test 分割に照合しました。

出典が train にある : 37 URL
出典が test  にある :  4 URL  ← heldout 4 問の出典。モデルはこの文書を学習していない
学習コーパスに無い  :  1 URL  ← 前処理で除外した集約ページ(sft_seen 2 問の出典)

前処理の時点では、旧評価セットの出典を train 側に固定していました。作り直した 70 問では、heldout のうち 4 問が test 側の文書を出典にしていました。以降の結果は、この 4 問を含む数値と除いた数値の両方を出します。

コーパスに無い 1 URL は、マニュアル全体を 1 ページにまとめた 1,062,687 字の codex-manual.md です。前処理で集約ページとして除外していました。これを出典にした 2 問は sft_seen 側なので、CPT の測定には影響しません。

総合順位表で判断を誤るのはなぜか

12 条件の平均点を並べると、CPT + SFT が首位に来て、相乗効果があるように見えます。しかし上位は 0.1 点差の団子で、その差の多くは SFT に直接入れた 30 問の暗記から来ていました。

順位 条件 平均 / 10(Terra)
1 Unsloth-CPT + SFT v3 3.40
2 Transformers-CPT + SFT v3 3.34
3 Base + SFT v3 3.33
4 Transformers-CPT 3.30
5 Unsloth-CPT 3.29
10 Base 2.50

840 スコアの 57%(476 件)は 2 点以下で、上位群は「多くの設問で当たっていない中での僅差」です。CPT をしていない Base + SFT v3 が 3 位にいるのは、SFT に入れた 30 問を覚えたからです。

点数の中身にも、正直に書いておくべき弱さがあります。Terra の採点基準は、正しさ 4 点・関連性と網羅性 3 点・分かりやすさと簡潔さ 2 点・指示への従い方 1 点の配分で、意味の正しさだけを見ているわけではありません。実際、素のモデルが claudacode と答えた回答や、存在しないサンドボックスモードを挙げた回答にも 2 点が付いています。中身が間違っていても、質問に沿った形で整っていれば点が入るということです。SFT は回答の形を整えるので、総合順位で CPT + SFT が上に来る理由の一部は、知識ではなく形式への加点かもしれません。

機械的な指標は、さらに形に弱くなります。補助指標として、正解キーワードが回答に含まれるかを正規表現で照合する方法も試しましたが、70 問の正解キーワード 214 個のうち 35 個は、質問文にすでに含まれていました。質問をオウム返しするだけで点が入るので、キーワード再現率は素のモデル 30.4%、Transformers-CPT 31.8% とほとんど差が出ません。この指標は主評価に使わず、意味を読んで採点する判定モデルの結果だけを使っています。

そこで、同じ設問どうしで条件を比べる方法に切り替え、ブートストラップ(20,000 回の再標本化)で 95%信頼区間を出しました。設問ごとの難易度の差と採点者の甘辛が相殺され、条件間の差だけが残ります。あわせて、SFT に入れた 30 問と入れていない 40 問を分けて集計しています。

この切り替えで、「効果がない」と「測れなかった」を分けて書けるようになりました。今回の 40 問・単一 seed の規模では、信頼区間の幅が片側 0.6 点前後あり、それより小さい差は区別できません。

CPT で知識は増えたのか

増えました。Transformers 版の CPT では、全 70 問で +0.80 点、SFT に入れていない Claude Code の 20 問で +1.00 点と、2 つの判定モデルの両方で有意です。一方、同じ条件の Codex の 20 問では差が見えませんでした。

冒頭の worktree の質問では、CPT 後の回答にこういう続きがありました。

... `claude --worktree my-worktree` starts a session in the `my-worktree` worktree.
The flag requires Claude Code v2.1.202 or later.

最後の対応バージョンは、出典のどこにも書かれていません。正しい知識のすぐ隣に、創作が混ざります。 小型モデルに知識を入れるときは、この性質を押さえておく価値があります。

素のモデル → CPT の差を、実装と判定モデルごとに並べます。

範囲 CPT の実装 Terra Δ [95%CI] Gemini Δ [95%CI] 判定
全 70 問 Transformers +0.80 [+0.26, +1.36] +0.89 [+0.40, +1.41] 両方で有意
全 70 問 Unsloth +0.79 [+0.23, +1.37] +0.89 [+0.41, +1.41] 両方で有意
未収録 40 問 Transformers +0.50 [−0.10, +1.13] +0.63 [+0.03, +1.25] Gemini のみ有意
未収録 Claude 20 問 Transformers +1.00 [+0.20, +1.90] +1.05 [+0.10, +2.05] 両方で有意
未収録 Claude 20 問 Unsloth +0.80 [−0.05, +1.75] +0.80 [−0.10, +1.80] 同じ向き・有意差なし
未収録 Codex 20 問 Transformers ±0.00 [−0.80, +0.80] +0.20 [−0.35, +0.95] 有意差なし
未収録 Codex 20 問 Unsloth ±0.00 [−0.80, +0.80] +0.25 [−0.30, +1.00] 有意差なし

読み取れることは 3 つです。

Claude Code 側では、CPT による知識の獲得を確認できました。 これが今回の主な成果です。Unsloth 版も同じ向きの +0.80 ですが、信頼区間の下限がわずかに 0 を下回りました。2 実装の差(1.00 対 0.80)は信頼区間の幅よりずっと小さく、実装の優劣としては読めません。

未収録の 40 問をまとめると、判定モデルによって有意かどうかが分かれます。 40 問全体で確実に改善した、とは書けません。

Codex 側は、どの組み合わせでも差が見えません。 同じパイプライン、同じ学習設定、同じ評価で、違うのはドメインだけです。この差の原因は、後の節で切り分けます。

出典が test 側にあった 4 問を除いても、Claude 側の結論は変わりませんでした(Terra、Transformers 版)。

範囲 4 問を含む 4 問を除く
未収録 Claude +1.00 [+0.20, +1.90](20 問) +1.00 [+0.11, +2.00](18 問)
未収録 Codex ±0.00 [−0.80, +0.80](20 問) −0.39 [−1.11, +0.22](18 問)

なお、除いた 4 問は CPT 後に平均 +2.25 点と大きく伸びていました。学習していない文書を出典にした設問で伸びている以上、CPT の効果の一部は、事実の想起ではなく、ドキュメント調の答え方(それらしい形のフラグ名やパス名を出すこと)への適応を含んでいる可能性があります。

SFT は回答の何を変えたのか

SFT は回答の長さと形式を揃え、SFT に直接入れた知識は覚えました。一方、SFT に入れていない知識については、CPT の後に SFT を重ねても点数の上乗せは見えませんでした。

回答は短く、形式が揃った

条件 平均文字数 中央値 95 パーセンタイル
Base 211 160 736
Transformers-CPT 272 217 940
Unsloth-CPT 270 224 913
Base + SFT v3 114 112 176
Transformers-CPT + SFT v3 111 104 189
Unsloth-CPT + SFT v3 110 106 181

SFT v3 の 3 条件は、210 回答の最長でも 226 字で、SFT で指定した 300 字以内を全件が守りました。冒頭の worktree の質問で、CPT + SFT が claude -w <worktree-name> と 1 文で答えたのがこの効果です。

SFT に入れた知識は覚えた

SFT v3 に直接入れた 30 問では、素のモデル → Base + SFT v3 が Terra で +1.77 点([+0.67, +2.93])、Gemini で +1.87 点([+0.90, +2.90])でした。これは CPT の効果ではなく、SFT データで答えを直接学んだ結果です。

SFT に入れていない知識への上乗せは見えなかった

当初の仮説は「CPT で知識を入れ、SFT で答え方を整えれば、SFT に入れていない質問にもいちばんよく答える」でした。未収録 40 問で、CPT → CPT + SFT v3 の差を測ると、次のとおりです。

CPT の実装 Terra Δ [95%CI] Gemini Δ [95%CI]
Transformers −0.20 [−0.83, +0.43] +0.28 [−0.35, +0.90]
Unsloth −0.13 [−0.75, +0.50] +0.33 [−0.33, +0.98]

4 通りすべてで信頼区間が 0 をまたぎ、判定モデルによって符号も分かれます。今回の規模では、この仮説を支持する結果は得られませんでした。悪化したとも言えず、「測れる大きさの上乗せはなかった」というのが正確なところです。

短く整った回答が、名前を崩すことがある

SFT 後に、CPT の段階では出せていた名前が崩れた例です。

Q: Which in-session command makes the current Claude Code conversation remotely controllable?
正解: /remote-control(短縮形は /rc)

【CPT】5 点
  The in-session command that makes the current Claude Code conversation remotely
  controllable is `claude remote-control`. ...

【CPT + SFT v3】1 点
  `claude remote-contest`. This allows you to inspect and control remote Claude Code sessions.

CPT の段階では名前がほぼ合っていたのに、SFT 後は綴りの似た別の単語に変わっています。

CPT をしていない Base + SFT v3 では、存在しない名前が、整った 1 文のまま出てきます。

問い Base + SFT v3 の回答 正解
worktree でセッションを開くコマンド git worktree worktree-name && claude --model <model> claude --worktree <name>(-w)
同時サブエージェントスレッド数の設定 subagent_threads(既定は 1) agents.max_concurrent_threads_per_session
codex app-server のプロトコル HTTP/2 JSON-RPC 2.0(既定は stdio://)

答え方が整うほど、間違いも読みやすく、自信ありげに見えます。知識と回答形式は、別々に測る必要があります。

Codex で CPT の効果が見えなかったのはなぜか

出現回数・重複・評価問題の出典の 3 つを疑って順に確かめましたが、どれも決め手になりませんでした。最後に学習データを実トークンで数え直すと、Codex 側として集めた文書の約半分が、評価と無関係な OpenAI Platform API のドキュメントでした。

重要な単語の出現回数は関係なかった

未収録 40 問について、正解キーワードが学習コーパスに何回出現するかを数え、CPT 後のスコアと突き合わせました。

相関(log 最小出現回数, CPT スコア) = -0.139
相関(log 総出現回数,   CPT スコア) = +0.065
最小出現回数 問題数 素のモデル平均 CPT 平均 差
0 回(未出現) 4 1.25 3.50 +2.25
1〜4 回 18 3.33 3.78 +0.44
5〜19 回 7 2.71 2.43 −0.29
20 回以上 11 2.55 3.00 +0.45

相関はほぼゼロでした。個別に見ると、もっとはっきりします。

  • ~/.codex/rules … コーパスに 1 回しか出ないのに、CPT 後に保存先として答えられた(0 → 4 点)
  • SKILL.md … 132 回出現しても答えられないまま(1 → 1 点)
  • --profile … 18 回出現していて、素のモデルの 4 点から CPT で 0 点に低下

同じ文字列を何度も見せることは、この規模では効いていませんでした。

重複もむしろ少なかった

同じ内容が混ざっていて、実質的な情報量が少ないのではないかも疑いました。

指標 Claude Code Codex 側(API ドキュメント含む)
近似重複の文書ペア(8-gram Jaccard ≥ 0.3) 0 件 0 件
ユニークな 8-gram の割合 97.7% 97.9%
20 字以上の行のユニーク率 85.5% 88.5%

重複はほぼ差がなく、むしろ Codex 側のほうがきれいでした。

ここで 1 つ引っかかりました。単純に行単位で数えると、5 回以上繰り返される行の割合は Codex 側 35.5%、Claude 側 24.7% で、Codex 側の重複が多く見えていたのです。中身を確認すると、``` や { } でした。Codex 側のドキュメントは、コード例と JSON の比率が高いだけです。20 字以上の行に絞ると、評価が逆転します。集計単位を変えると結論が反転するという経験が、次の一手につながりました。

評価問題の出典を除いても変わらなかった

前述のとおり、出典が test 側にあった 4 問を除いても、Codex 側は −0.39 点([−1.11, +0.22])で、改善は見えないままでした。評価の精度は上がりましたが、Claude と Codex の差を説明するものではありません。

実トークンで数え直すと、19.2% が無関係だった

最後に、学習データを文書数ではなく実トークン数で、評価対象ごとに数え直しました。

分類 文書数 トークン 全体比 70 問の評価対象か
Claude Code のドキュメント 160 964,056 63.2% ○(Claude 側の設問)
Codex CLI のドキュメント 111 267,107 17.5% ○(Codex 側の設問)
OpenAI Platform API のドキュメント 101 293,167 19.2% × 無関係

収集時はドキュメントサイト単位で「Claude 側」「Codex 側」と分けていました。Codex 側として集めた 212 文書のうち 101 文書は、同じサイトの /api/docs/ 配下にある OpenAI Platform API のドキュメントでした。中身はこういうものです。

structured-outputs / function-calling / embeddings / text-to-speech
video-generation / images-vision / moderation / realtime-translation
supervised-fine-tuning / prompt-caching / token-counting / retrieval ...

Codex CLI の使い方とは関係がありません。Codex CLI 本体(267k トークン)より、無関係な文書(293k トークン)のほうが多い状態でした。

評価対象だけで比べると、比率はこうなります。

Claude Code : Codex CLI = 964,056 : 267,107 = 3.61 : 1

サイト単位で見えていた 1.7 倍の差ではなく、実質 3.6 倍の差です。しかも文書数だけは Codex 側のほうが多い(212 対 160)ため、集計表を眺めている限り「Codex 側も十分ある」ように見えていました。

設問あたりに直しても、差は揃います。

対象トークン 未収録の設問数 1 問あたり 出典文書の長さ(中央値)
Claude Code 964,056 20 48,203 32,150 字
Codex CLI 267,107 20 13,355 11,023 字

総量・設問あたり・出典文書の長さの 3 つとも、Claude 側が約 3 倍多く、Claude +1.00 / Codex ±0.00 という差と向きが揃います。ただし比べたドメインは 2 つだけで、トピックの難しさや文書の書き方の違いも残ります。有力な候補ではありますが、原因を特定したわけではありません。 確かめるには、API ドキュメントを除いて再学習するのが次の一手です。

やってみて分かったこと

独自知識をモデルに入れること自体は、個人環境でも成立しました。 RTX 4070 1 枚・5 分の CPT で、存在しないコマンドを作っていた 0.6B モデルが、Claude Code の正しいフラグを答えるようになりました。ローカルで完結するので、社内ドキュメントを外に出さずに同じことができます。

同時に、0.6B の天井も見えました。 正しいフラグを答えた直後に、出典に無いバージョン番号を作る。~/.codex/rules/ は出せても、ポリシーを定義する prefix_rule() は出てこない。知識が「うっすら入った」状態で、その薄いところをモデルが創作で埋めます。業務で使う回答としては採用できません。本命を 10B〜30B に置いているのはそのためで、今回はそこへ進む前に、いちばん安いサイズで手順とデータの作り方を確かめた、という位置づけです。

実際に手を動かして得た教訓は 4 つです。

1. 知識と回答形式は別々に測る。 SFT で短く整った回答でも、固有名詞は崩れることがあります。回答の長さが揃ったことと、中身が正しいことは別の指標です。

2. 総合順位ではなく、SFT に入れた問題と入れていない問題を分けて比べる。 混ぜたまま平均すると、SFT で直接覚えた分を相乗効果と取り違えます。同じ設問どうしの比較で信頼区間を出したことで、「効果がなかった」と「測れなかった」を分けて書けました。

3. 学習データは、文書数ではなく実トークンで、評価対象ごとに数える。 文書数では Codex 側のほうが多く見えていました。19.2% の無関係な文書は、実トークンで数えるまで表面化しませんでした。

4. 評価問題の出典を、学習データの train / test と照合する。 出典 URL を突き合わせるだけで、数分で終わります。今回はこれで、学習していない文書を出典にした 4 問を見つけられました。

もう 1 つ、同じ文字列を繰り返し見せても効きませんでした(相関 −0.14)。自分の用途にモデルを作り替えるなら、同じ文を増やすより、同じ事実を違う文脈で見せる方向に投資すべきだと考えています。これは今回の結果からの仮説で、次の実験で確かめたいところです。

補足:この実験で言えないこと

  • 単一 seed・1 epoch です。 学習の分散を評価していないため、条件間の小さな差は seed 由来かもしれません。
  • 比べたドメインは 2 つだけです。 トピックの難しさ、文書の書き方、素のモデルの事前知識(未収録 20 問での素点は Claude 3.20 / Codex 2.40)といった交絡が残ります。Codex 側の結果に対する実トークンの説明は、有力な候補であって、証明された原因ではありません。
  • Unsloth 版の CPT では、未収録 Claude 20 問の信頼区間がわずかに 0 をまたぎます(+0.80 [−0.05, +1.75])。本文の「両方で有意」は Transformers 版の結果です。
  • 採点の絶対点は低く(840 スコアの 57% が 2 点以下)、上位群の 0.1 点差には意味がありません。本文で使ったのは、同じ設問どうしの差だけです。
  • 70 問・単一ルーブリックのため、片側 0.6 点前後より小さい効果は検出できません。
  • SFT v1 / v2 は学習時と異なるプロンプトで評価しています。 12 条件の比較を揃えるため、生成はすべて v3 の指示文で行いました。本文で SFT の効果を論じたのは v3 だけです。
  • CPT と SFT の相乗効果が見えなかった理由は分離できていません。 0.6B という容量、個人規模のデータ量、最小限のクリーニングのどれが効いているのかは、今回の設計では区別できません。
  • 電力・コスト・プロンプト長は測っていません。 冒頭に書いた「管理できるサイズのモデル」の利点は動機であり、本記事の実測結果ではありません。
  • 検証したのは 0.6B だけです。 10B〜30B についての見立ては、0.6B で見えた創作の混ざり方からの推測です。

検証データを公開しています

記事中の数値は、すべて再生成できる形で置いてあります。

リポジトリ: ai-systems-notes/CPT_SFT

入っているものは次のとおりです。

  • 70 問の評価セット — 各設問に出典 URL、出典本文の SHA-256、根拠の抜粋
  • 840 回答 — 12 条件 × 70 問、すべて同一プロンプト・同一生成設定
  • 2 系統の判定モデルの生スコア — 集計後ではなく、設問ごとの素点
  • 解析結果 — 同一設問での効果量、判定モデル間の一致、回答長、コーパス構成、重複、出現回数と正答の相関、出典の照合結果
  • 収集から学習・評価までの全スクリプト

効果量・信頼区間・判定モデル間の一致・回答長は、採点結果だけから再生成できます。ブートストラップは seed を固定しているので、信頼区間まで一致します。

git clone https://github.com/ai-systems-notes/CPT_SFT.git
cd CPT_SFT
python3 cpt_training/scripts/analyze_blog_claims.py

公式ドキュメントの本文そのものは置いていません。 スクレイピングしたページは各社のものなので、再配布しない判断をしました。コーパス構成・重複・出現回数・出典照合の節は、手元でコーパスを再生成したときだけ出力されます。

./ai_coding_agent_cpt_data/setup_env.sh
./cpt_training/setup_transformers_env.sh
(cd ai_coding_agent_cpt_data && ../.venv-data/bin/python scripts/scrape_docs_spider.py)
./cpt_training/run_preprocess.sh
HF_HOME=.cache/huggingface .venv-cpt/bin/python cpt_training/scripts/analyze_blog_claims.py

公式ドキュメントは更新され続けるので、今から収集すると本文が変わっている可能性があります。全設問が出典 URL と本文の SHA-256 を持っているので、取得したページが実験時と同じかどうかは照合できます。

おわりに

フロンティアモデルを追いかけ続ける以外に、自分たちで管理できるサイズのモデルを、自分たちの用途に作り替えるという道があります。今回はその一番手前で、「独自知識は本当にモデルに入るのか」「入れた知識を SFT で答えやすくできるのか」を、いちばん安いサイズで測りました。

答えは、知識は入る、SFT で形も整う、ただし重ねても未収録の知識で上乗せは見えなかった、です。そしてデータの数え方を誤ると、入れたつもりで学習予算の約 2 割を無関係な文書に使っていました。

次は、混入していた API ドキュメント 101 文書(293k トークン)を除いての再学習です。フィルタを 1 つ足すだけで、新しいデータ収集なしに実行できます。そのうえで本題の 10B〜30B に上げ、RAG や CAG と比べて入力トークンがどれだけ減るのか、1 回答あたりの消費電力がどう変わるのか、社外に何も送らない構成で業務に耐える精度が出るのかを実測する予定です。

同じことを考えている方、社内で検討している方がいれば、ぜひコメントで教えてください。 どのサイズのモデルを選んだか、データをどう作ったか、SFT で知識が崩れないようにどう工夫したか、そのあたりの実例がいちばん知りたいところです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?