コンテンツにスキップ

6. 評価:ファインチューニングは本当にプロンプトに勝てるのか

この章では、エンジニアリング上とても実際的な問いに答えます。GPU 時間をかけてファインチューニングすることは、良いプロンプトを書くだけの場合と比べてどれだけ優れているのでしょうか。この探索的な比較はドラフトデータのままでも実行でき、実験の進め方を練習できます。ただし、結論を外部に報告する前には、先にデータのレビューを済ませてください。

この章の概要

  • やること:3 つのグループのモデル(ベースモデル + シンプルなプロンプト、ベースモデル + 良いプロンプトと例、LoRA)に同じ検証問題へ回答させます。2 つの rank を比較し、ブラインドレビューで採点します。テストセットに触れるのは候補を選んだ後だけです
  • 必要なもの:第 3 章の .venv-train とベースモデル、数時間連続して使える GPU、少なくとも 1 人の評価者(できれば学習を行った本人以外)
  • 得られるもの:生の出力、スコア、不確実性の区間を含み、「ファインチューニングする価値はあるか」に答えられる実験記録
  • いつやるか:第 4~5 章のメインパスが最後まで動いた後です。この章の GPU 時間はおよそ 10~20 時間(複数回の推論と 2 回の学習を含む)で、人手による採点の時間は別にかかります

なぜ loss だけを見てはいけないのでしょうか。loss が測るのは「参考回答にどれだけ似ているか」だけです。参考回答そのものが冗長だったり、誤りや抜けを含んでいたりすることもありますし、ベースモデルに良いプロンプトを与えるだけで十分うまく話せる可能性もあります。ファインチューニングに価値があるかを判断するには、学習に使っていないトピックで「良いプロンプト」と「良いプロンプト + LoRA」にまったく同じ入力へ回答させ、どちらがどちらか分からない状態で人が採点する必要があります。これはバックエンドで A/B テストを行うのと同じことです。変数を制御し、ブラインドでテストし、信頼区間を確認します。

この章は前後半に分かれています。6.1~6.4 節ではデータを準備して 3 グループの回答を生成し、6.5~6.8 節では採点、凍結、結論づけを行います。コマンドはすべて GPU ホストのプロジェクトルートで実行します。

6.1 現在のデータ規模とレビュー要件

すでに 500/50/50 エピソードがあるので、旧版の規模まで再び拡張する必要はありません。各カテゴリは 100/10/10 エピソードで、ファクトカードは合計 60 枚です。「formal」はこの教育用実験のための固定の割り当てであり、LoRA に一般的に求められる最低要件ではありません。

同じトピックのバリエーションはすべて同じ group_id を共有し、ファクトの参照も分割をまたいではいけません。検証セットとテストセットにはそれぞれ独立したトピックが 5 つしかないため、各 50 エピソードあっても、互いに無関係な 50 個の知識テストとして扱うことはできません。

何をもってレビューとするか。 正式なレビューではエピソードを 1 つずつ確認します。出典はファクトを裏付けているか、過度な擬人化はないか、子どもが理解できるか、たとえの限界ははっきりしているか、観察は安全で実行可能か、質問されたくない・やめたいという子どもの意思を尊重しているか。レビューは dataset.json の中で直接行い、履歴はバージョン管理で残します。本当にレビューした項目だけ review.status を approved に変更し、実際の reviewer、reviewer_kind、notes を記入してください。ステータスを一括で変更してレビュー済みに見せかけてはいけません。プログラムで構造はチェックできますが、ほぼ重複した内容、テンプレート的な言い回し、科学的な誤りはチェックできません。10 種類のパスは意図的に同じ教え方を繰り返しており、同じトピック内では回答の一部も再利用されています。50 個の train トピックは 500 個の独立したトピックではありません。val/test は学習トピックから隔離されていますが、光、力、生き物といった基本概念は共有しています。

--allow-draft を付ければ、ドラフトデータでこの章の 6.1~6.6 節の検証セット比較を完了でき、出力される manifest には draft: true が記録されます。候補の凍結とテストセット(6.7 節)には引き続きレビュー済みデータが必要です。ドラフトの結果を正式な結論と取り違えないためです。

6.2 データをさらに改善するには

まずトピックごとに 10 本のパスをすべて読み、子どもからのフィードバックのたびに、それに対応する変化が起きているかを確認します。低学年向けに説明のない専門用語が残っていれば書き直してください。同じたとえを具体的な説明の代わりに何度も使ってはいけませんし、1 つの観察で、それと無関係な結論まですべて証明することもできません。

検証セットで失敗のタイプを見つけたら、それに対応する学習用の手本を追加します。テストの答えをもとにデータを変更することは避けてください。新しいトピックは、まずグループにまとめてから分割に割り当てます。現在の 10 種類のパスは構造化された合成データであり、実際の子どもとのやり取りから得たものではありません。可能であれば、保護者のインフォームドコンセントのもとで匿名のフィードバックを集め、理解度と自発的な探究を別々に評価してください。「会話のラウンドを増やすこと」を唯一の目標にしてはいけません。

6.3 検証と準備

GPU ホストのプロジェクトルートでターミナルを開き、まず第 3 章でインストールした学習環境を有効化します。以下のコマンドには PyTorch と TRL が必要です。

source .venv-train/bin/activate

python -m lab.data \
  --input data/dataset.json --allow-draft --formal

python -m lab.prepare \
  --input data/dataset.json \
  --tokenizer models/qwen3-4b --allow-draft --formal --output data/processed/experiment-v1

python -m lab.audit --data data/processed/experiment-v1 \
  --output runs/formal-token-audit.json

--formal は 500/50/50 エピソード、60 枚のファクトカード、カテゴリごとの 100/10/10 の割り当てを確認し、--allow-draft は未レビューの項目を許可します。この 2 つは独立したチェックです。実際のレビューが終わったら --allow-draft を外せば、正式なデータバージョンが得られます(出力ディレクトリは別のもの、たとえば experiment-v2 を使ってください)。

自動チェックの対象は、割り当て、レビューステータス、出典 ID、完全な会話グループの重なり、同一のユーザー会話、制御文字、空でないターゲット、質問の数です。ほぼ重複した内容、意味的な誤り、出典に裏付けられていない主張などは引き続きレビューが必要です。プログラムは URL だけからファクトの真偽を判断できません。

6.4 3 つのベースラインと 2 つの rank

グループ 重み プロンプトと例
A ベースモデル 基本プロンプト、例なし
B ベースモデル 完全なロールプロンプト + 学習セットから選んだ 3 つの few-shot 例
C ベースモデル + 今回の LoRA B とまったく同じプロンプトと 3 つの few-shot 例

B と C の 3 つの few-shot 例は lab.prepare が選びます。train の先頭付近、中央付近、末尾付近からそれぞれ 1 エピソードずつ(できるだけ 3 つの学年帯をすべてカバーするように)選び、最初の 1 往復の問答だけを取り出して、スナップショットの fewshots.json に書き込みます。few-shot 例は推論時にだけ使います。SFT の学習サンプルには例を含めません。サンプル自身の回答を事前に入力へ貼り付けてしまうのを避けるためです。

3 グループとも、現在の質問に対する同じファクトカード、NF4 精度、貪欲デコーディングを使います。デフォルトでは val を読み込み、参考回答は後のレビューにだけ使います。固定履歴モードでは、前のラウンドの参考回答をコンテキストとして使います。この節の B/C の設定と、ノートブックにある few-shot なしの 3 つの観察用質問は別のプロトコルなので、スコアを混ぜないでください。

まず検証セットで A と B を実行します。

python -m lab.infer --data data/processed/experiment-v1 --variant A \
  --output runs/val-A.jsonl
python -m lab.infer --data data/processed/experiment-v1 --variant B \
  --output runs/val-B.jsonl

r=16、1 エポックで学習します。

python -m lab.train --data data/processed/experiment-v1 \
  --output runs/formal-r16 --allow-draft --epochs 1 --rank 16
python -m lab.infer --data data/processed/experiment-v1 --variant C \
  --adapter runs/formal-r16/adapter --output runs/val-C-r16.jsonl

rank とそれに対応する alpha だけを変え、もう一度ベースモデルから始めます。

python -m lab.train --data data/processed/experiment-v1 \
  --output runs/formal-r8 --allow-draft --epochs 1 --rank 8
python -m lab.infer --data data/processed/experiment-v1 --variant C \
  --adapter runs/formal-r8/adapter --output runs/val-C-r8.jsonl

コードでは常に alpha=2×rank に設定するので、どちらの実行でもスケーリング係数は 2 です。r16 のアダプターから r8 の学習を続けて、それを独立した比較と呼んではいけません。

学習サンプル 1150 件、実効バッチ ≈16 の場合、1 エポックはおよそ 72 回のオプティマイザー更新で、ノートブックのデフォルトと同じ学習量です。グループ B にはすでに良いプロンプトと few-shot 例があるので、グループ C に明確な優位が見られなくてもまったく普通のことです。それ自体が価値ある結論で、このタスクにはプロンプトで十分だということを意味します。

受け入れ基準: 2 回の実行でデータ、プロンプト、生成パラメータ、ベースモデルが同一であること。検証結果、学習時間、VRAM、アダプターのサイズを比較できること。学習 loss だけで「より良い探究パートナー」を選ばないこと。

新しいターミナルを開いたときは、まず source .venv-train/bin/activate を実行し、6.3~6.4 節のデータスナップショットと候補モデルが生成済みであることを確認してください。

6.5 まず固定履歴、次に連続会話

固定履歴(デフォルト、--mode fixed)では、データ内の参考履歴を使います。B/C の各入力のハッシュは同一でなければならないため、特定のラウンドでの言い回しの違いを突き止めるのに向いています。

連続会話では、モデル自身の回答をその後の履歴として使います。

python -m lab.infer --data data/processed/experiment-v1 --variant B \
  --mode rollout --output runs/val-B-rollout.jsonl
python -m lab.infer --data data/processed/experiment-v1 --variant C \
  --adapter runs/formal-r16/adapter --mode rollout --output runs/val-C-rollout.jsonl

両グループは同じユーザースクリプトを使いますが、その後の履歴はそれぞれのモデルが生成するため、後のプロンプトのハッシュは異なることがあります。あらかじめ用意したフォローアップの質問が、生成された回答とかみ合わない場合もあるので、採点時にはそうしたケースを記録してください。実際の人が回答に応じて質問を重ねるのは、別の種類の評価です。

6.6 人手によるブラインドレビュー

プログラム全体:evaluate.py。

python -m lab.evaluate blind --base runs/val-B.jsonl \
  --tuned runs/val-C-r16.jsonl --output runs/blind-val-r16

これにより ratings.jsonl、key.json、およびプロトコルが生成されます。採点時は ratings とファクトカードだけを見て、key は見ないでください。どちらが LoRA かは採点が終わってから明かします。学習に関わっていない人に採点を頼むと、候補に対する作者の偏りを減らせます。

各項目について、winner には X/Y/tie を記入し、reason には具体的な理由を書きます。fact_claims_X/Y と supported_X/Y はそれぞれ別に記入します。確認可能なファクトがない場合は 0 を記入し、それを「100% 正しい」とはみなさないでください。issues フィールドには、事実の誤り、学年帯を超えた中国語や専門用語の羅列、過剰な質問、好みの無視、誤った帰属、子どもの観察を見たふりをする、といった問題を記録します。

観点 0 点 1 点 2 点
ファクト 重要な主張に根拠がない/誤り 一部が不明確、または範囲が広すぎる 確認可能な主張にすべて根拠があり、分からないことは明示している
学年帯への適合 子どものレベルを超え、専門用語を羅列 理解はできるが、フィードバックに応じて説明を調整しない 具体的で簡潔、理解されなければ説明し直す
探究の価値 決まり文句、作り話、質問の押し付け 興味を引く点はあるが、次の一歩があいまい 具体的で安全、選べる観察やフォローアップ
やり取り はっきり示された選択を無視する 基本的には応じるが繰り返しが多い 選択を尊重し、テンポが良い
根拠への意識 推測やたとえを事実として扱う 限定はあるが不十分 観察・説明・たとえを区別し、情報不足を認める

このルーブリックは winner を判断する助けであり、観点ごとのスコアは reason に書いてもかまいません。キーワードによる自動採点ではありません。重大な事実誤りを含む回答は、たとえより「面白い」としても、面白さだけで勝たせるべきではありません。

python -m lab.evaluate score --ratings runs/blind-val-r16/ratings.jsonl \
  --key runs/blind-val-r16/key.json --output runs/val-r16-score.json

勝ちは 1、引き分けは 0.5、負けは 0 として数えます。スクリプトは group_id の科学トピック単位でブートストラップを行い、同じトピックの 10 本のパスや複数ラウンドを独立したサンプルとして扱わないようにします。ラウンド数、会話数、トピック数を報告し、トピックが 2 つ未満の場合は区間を出力しません。検証/テストのトピックはそれぞれ 5 つで依然として非常に少なく、その区間で広い結論を支えることはできません。小さなサンプルでの区間は探索的な結果にすぎず、評価者の偏りを取り除くこともできません。

ファクト裏付け率 = 出典に裏付けられた主張の数 / 確認可能な主張の数です。スクリプトはこれらの主張を自動では検証しません。ファクトレビューの項目がすべて記入されていなければ factual_review_complete=false と記録されるので、部分的な比率を品質についての完全な結論として扱ってはいけません。

6.7 テストセットは凍結した後にだけ使う

この節では、レビュー済みの正式なデータが必要です(6.3 節の prepare を --allow-draft なしで再実行してください)。freeze とテストセットの推論はドラフトデータを拒否します。モデルは検証セットにもとづいて選びます。以下では r16 を例にしていますが、r8 のほうが適していれば、以降のアダプター引数をすべてその候補に変更してください。どちらを選ぶかはこの学習実験の結果であり、チュートリアルがあらかじめ想定しているものではありません。

python -m lab.evaluate freeze --data data/processed/experiment-v1 \
  --adapter runs/formal-r16/adapter --output runs/frozen-candidate.json

python -m lab.infer --data data/processed/experiment-v1 --split test --variant B \
  --freeze runs/frozen-candidate.json --output runs/test-B.jsonl
python -m lab.infer --data data/processed/experiment-v1 --split test --variant C \
  --adapter runs/formal-r16/adapter --freeze runs/frozen-candidate.json \
  --output runs/test-C.jsonl

python -m lab.evaluate blind --base runs/test-B.jsonl \
  --tuned runs/test-C.jsonl --output runs/blind-test

テストセットでの連続会話も同じように、--mode rollout と新しい出力ファイルを使って実行します。テストでは --limit を使って簡単な問題を選り好みすることは禁止です。凍結ファイルには、データのハッシュ、ベースモデル、アダプターの重みと設定を合わせたフィンガープリント、推論コード、デコーディングパラメータが記録されます。

テストファイルは依然としてローカルにあるため、実験者はモデルを凍結するまでテスト問題を見ないようにし、それをもとに学習用の回答を書き足さないようにする必要があります。テスト結果を見た後でさらに調整を続けた場合、次のラウンドには新しい独立したテストセットが必要です。

6.8 結論の出し方

次の順序で 1 ページの実験記録を書きます:学習設定とコスト → ファクトとブラインドレビューにおける B/C の比較 → 代表的な成功例と失敗例 → 不確実性 → 次のステップ。

次のステップは検証結果にもとづいて選びます。差が不確かなら独立した評価を追加し、ある種の振る舞いがうまくいかないならそれに合った学習例を追加し、LoRA の改善が不十分ならプロンプトのベースラインを維持します。結論では、事実の誤りと表現上の好みの両方を考慮する必要があります。

大人の好みやモデルの自己評価では、子どもがより探究したくなることを直接証明できません。本当の目標のためには、子どもが核心となる理由を理解したか、関連する新しい質問を自分から投げかけたか、安全な観察を選ぼうとするかを観察する必要があり、同時に参加を断る意思も尊重しなければなりません。

受け入れ基準: 完全なスコア、生の出力、失敗の分類を保存し、各比較でどの入力を使ったかを他の人が再確認できるようにすること。