コンテンツにスキップ

2. 学習データ:モデルは何から、何を学ぶのか

ファインチューニングの結果はほぼ完全にデータで決まります。モデルが最終的にどんな話し方をするかは、あなたが見せたお手本そのものです。だからこそ、お金を払って GPU を借りる前に、まずデータをしっかり理解しておきましょう。

この章のまとめ

  • この章でやること:事実カード 1 枚と会話 1 本をフィールドごとに読む。1 本の会話がどのように学習サンプルに分割されるかを見る。モデルが実際に「何を見て、何を学ぶのか」をはっきりさせる
  • 必要なもの:ブラウザかエディタだけ。自分でチェックを実行したい場合は、2.8 節で自分のパソコンに軽量な環境を入れる方法を説明しています(GPU は不要)
  • 得られるもの:1150 という数字がどこから来るのか説明できる。1 つのサンプルのうちどのトークンが loss に関わるか指摘できる。データを変更したときにそれを反映させる方法がわかる

データの流れ全体は、まず一文で覚えておけます。1 つの JSON ファイル → 「コンテキスト + 参照回答 1 つ」のサンプルに分割 → チャットテンプレートでトークンに変換 → 参照回答の位置だけで loss を計算。 以下の節はこの順番で説明していきます。

2.1 管理するのは dataset.json 1 つだけ

data/dataset.json を開いてください。参照資料のすべて、500 本の学習用会話、評価用の会話は、すべてこの 1 ファイルに入っています。トップレベルには同じ階層の配列が 4 つあります。facts は参照用の事実カード、train・val・test は完全な会話の 3 つのセットです。実際に学習に使われるのは train だけで、事実カードはコンテキストとしてのみ使われ、学習対象ではありません。

3 つのセットは科学トピック(group_id)単位で分けられています。1 つのトピックは 1 つのセットにしか現れないため、学習中に見たトピックにモデルが val/test で再び出会うことはありません。

セット 科学トピック 完全な会話 回答ターゲット 用途
train 50 500 1150 LoRA パラメータを更新する
val 5 50 115 設定を比較し、候補を選ぶ
test 5 50 115 候補を凍結した後に評価する

そもそも val セットはなぜ必要なのでしょうか? train と test だけではだめなのですか?

1 回学習して結果をそのまま使うだけなら、セットは 2 つで足ります。問題は test の結果をもとに選択をする ときに起こります。r=16 で学習して test を見る。満足できないので r=8 に変えてまた test を見る。さらに学習率を調整し、プロンプトを変え……最終的に test で一番スコアの高いバージョンを選ぶ。こうなると、test はもう「見たことのないデータ」とは言えません。モデルのパラメータは一度も test に触れていませんが、あなたの選択はすべて test を参考にしています。test を使って答えを選んだのと同じことなので、最終的に得られるスコアは水増しされています。

そこで、「選択に使うデータ」と「最終成績を出すデータ」を分けます。バックエンドのリリースにたとえると、train は開発環境で、モデルはここで何度もパラメータを更新します。val はステージング環境で、比較や選択のために何度見てもかまいません。test は本番の実トラフィックで、候補が決まった後に一度だけ見て、最終レポートを作ります。

このプロジェクトでは、val が次の選択を支えています。学習中に 12 ステップごとに検証 loss を計算して過学習していないか判断する(4.5 節)。r16 と r8 を比較する(6.4 節)。「ベースモデル + よいプロンプト」の B 群と LoRA の C 群を比較する(6.5 節、6.6 節)。そして最終的に候補を選ぶ(6.7 節)。選んだら evaluate freeze で凍結します。そうして初めてコードは test での推論を許可し、--limit で一部の問題だけを選り好みすることも許しません。test を見た後でモデルやデータを変更しに戻った場合、次のラウンドでは新しいテストセットが必要です。

よそで「学習セットとテストセットだけでよい」と書かれているのを見かけたら、たいていは 1 回だけ学習して比較を一切しないケースです。あるいは交差検証を使っていて、train の中から検証用の部分を順番に切り出している(val が train の中に隠れている)ケースか、単に test を val として使っているケースです。最後のケースでは、報告される test スコアは楽観的になります。

学習データの学年帯の内訳:1–2 年生が 167 本、3–4 年生が 167 本、5–6 年生が 166 本です。各トピックには 10 通りの会話パスがあり、それぞれ 2~3 ターンです。同じ知識を異なる学年帯向けに説明するものもあれば、子どもの反応の違いによって変わる追加質問もあります。

data/dataset.json
├── schema_version / description   文件版本与说明
├── facts[60]                      参考事实卡,只存一份,被对话按 id 引用
├── train[500]                     训练对话:唯一参与参数更新的部分
├── val[50]                        验证对话:比较配置、选候选、算 eval loss
└── test[50]                       测试对话:候选冻结后才使用

train / val / test 里的每段对话结构相同:
├── id / group_id                  对话编号 / 科学主题分组
├── grade_band                     1-2 / 3-4 / 5-6
├── fact_ids                       引用 facts 中的 id
└── turns[]                        user / assistant / behavior

(日本語訳:schema_version / description はファイルのバージョンと説明です。facts[60] は参照用の事実カードで、1 か所にだけ保存され、会話から id で参照されます。train[500] は学習用の会話で、パラメータ更新に使われる唯一の部分です。val[50] は検証用の会話で、設定の比較、候補の選択、eval loss の計算に使います。test[50] はテスト用の会話で、候補を凍結した後にだけ使います。train / val / test のどの会話も構造は同じで、id / group_id は会話の ID と科学トピックのグループ、fact_ids は facts 内の id を参照します。)

会話がどのセットに属するかは、どの配列に入っているかだけで決まります。会話には split フィールドをもう持たせていません。これにより「val に入っているのに train と書いてある」といった自己矛盾を防いでいます。プログラムがファイルを読み込むとき(lab.data.read_dataset、ノートブックでは tag_splits)、メモリ上で各会話に split を付け加え、以降の絞り込みやセットをまたぐチェックはすべてそれを使います。元のファイルは変更されません。会話を別のセットに移したいときは、会話をまるごと別の配列に切り取って移し、同じ group_id の他の会話と、その会話が参照する事実カードも一緒に移してください。

JSON は // コメントをサポートしていません。フィールドの説明はここで行い、ノートブックでは実際に使われている箇所を D1~D7 で示しています。JSON ファイルをまるごとプロンプトに詰め込んだり、評価用の会話をトレーナーに渡したりしないでください。

2.2 1 枚目の事実カード

{
  "id": "day-night",
  "category": "space",
  "topic": "昼夜",
  "text": "自转使地球表面各处交替朝向和背向太阳,形成主要的昼夜变化。太阳没有按昼夜关闭发光。地球还绕太阳公转;假定取消自转但保留公转,光照位置仍会随公转缓慢变化。",
  "source_url": "https://spaceplace.nasa.gov/seasons/en/",
  "checked_at": "2026-09-29",
  "source_review": "reference_read_claim_review_pending",
  "scope": "昼夜的入门解释;模型和比喻不能替代真实测量。",
  "kind": "fact",
  "reuse": "科学事实摘要和教学对话为原创中文表达;来源仅供事实核对,不复制来源的教材段落、题目或图像,不把网页正文纳入训练。链接不表示来源机构认可本数据或授予网页内容的训练许可。"
}

(大意:トピックは「昼と夜」です。text:「自転によって地表の各地点は太陽の方を向いたり背を向けたりを交互に繰り返し、これが主な昼夜の変化を生む。太陽は夜に光を消しているわけではない。地球は太陽の周りを公転もしている。仮に自転が止まって公転だけが続いた場合でも、光が当たる位置は公転に伴ってゆっくり移動する。」scope:「昼と夜の入門的な説明。模型やたとえは実際の測定の代わりにはならない。」reuse:要約と対話はオリジナルの中国語で書かれたもので、出典は事実確認のためだけに使っており、リンクがあっても出典元がこのデータを承認したり学習への利用を許諾したりしたことを意味しない、という内容です。)

フィールド 意味と用途
id 主キー。会話の fact_ids から参照される
category/topic 分類とトピック。検索、統計、レビュー用
text 回答の根拠にできる科学資料。コンテキストに入る
kind/scope 資料の種類と適用範囲。コンテキストに入る
source_url、checked_at、source_review、reuse 出典とレビューの記録。人間専用で、モデルには入らない

プロンプトに組み込まれるのは id、text、kind、scope の 4 フィールドだけです。それ以外のフィールドはレビュー担当者向けなので、最初に読むときは飛ばしてかまいません。

事実カードはなぜ必要か:よくある 3 つの疑問

ファインチューニングが教えるのは「どう言うか」です(0.1 節)。ここまで読んだ初心者は、たいてい次の 3 つの疑問を立て続けに抱きます。

Q1:小学校の理科は常識で、ベースモデルはもう知っています。なぜわざわざ事実カードを用意するのですか?

たしかに、こうした常識のほとんどはベースモデルも知っています。「知識をどこから得るか」だけが問題なら、事実カードは必須ではありません。それでもカードを使うのは、知識を補うためではありません。

  1. 学習で「どう言うか」だけを学ばせるため。 loss は参照回答に対して計算され、参照回答には言い回しと事実内容の両方が含まれています。入力に資料がないと、モデルは「太陽の方を向く」「光の当たらない側」といった内容を重みに丸暗記するしかなく、学習は「言い回し」と「知識」を混ぜて学ぶことになります。資料が入力の中にあれば、内容は資料に沿って言うだけで正しく予測できるので、LoRA の限られた容量は主に「短い文、まず誤解を正す、安全な小さな観察を 1 つ提案する」に使われます。仕組みは Q3 で説明します。
  2. ベースモデルの知識は確認も修正もできませんが、事実カードならできます。 4B モデルがどの問題で間違えるか(たとえば季節の原因を太陽との距離にしてしまうなど)は事前にはわかりませんし、間違えたとしても重みを開いて直すことはできません。事実カードは目に見えるテキストで、出典もレビュー状態もあります。間違っていればカードを 1 枚直すだけで、次のリクエストから反映されます。バックエンドでたとえるなら、ビジネスルールはコンパイル済みバイナリの中に散らばせるのではなく、設定やデータベースに置くのと同じです。
  3. カードには、ベースモデルが自分からは守らない境界が書かれています。 たとえば day-night の text には「仮に自転が止まって公転だけが続いた場合でも、光が当たる位置は公転に伴ってゆっくり移動する」と明記されており、参照会話の 2 ターン目の追加質問はこれを根拠にしています。scope には「模型やたとえは実際の測定の代わりにはならない」と書かれています。kind は fact、legend、current を区別するので、伝説を事実として語ってはいけません。
  4. これが実際のプロダクトの形であり、評価の根拠でもあります。 実際のビジネスでは、モデルが説明しなければならないのは、そもそもモデルが知らないことであるケースがよくあります。製品仕様、特定の学校の教科書、今日のデータなどです。よくあるやり方は、呼び出し側が資料を調べてプロンプトに入れ、モデルがそれをわかりやすく説明するというもので、一般に RAG(検索拡張生成)と呼ばれます。このプロジェクトでは事実カードを使ってこのパターンを一通り練習します。ブラインド評価でも、採点者は事実カードと照らし合わせて各主張に根拠があるかを判断します(6.6 節)。

Q2:事実カードも学習サンプルに入っているなら、学習後のモデルはこの知識を覚えているのですか?

その考え方は正しくありません。事実カードは学習サンプルの 入力 であって、学習対象 ではありません。2.5 節の 408 トークンのサンプルでは、事実カードは mask=0 の 386 トークンの中にあります。モデルはそれを見ることはできますが、「覚えていない」ことでペナルティを受けることはありません。loss が問うのは 1 つだけです。この資料を見たうえで、回答が参照回答にどれだけ近いか、です。

学習後に身につくか 理由
資料の読み方、資料を使った回答の組み立て方 身につく loss が学ばせようとしているのはまさにこれだから
資料そのものの内容と境界 当てにならない 学習中、資料は常に目の前にあったので、覚える必要がなかった

1 つ正直に言っておくと、参照回答自体にも事実内容が含まれており、その部分は mask=1 の領域にあるので、モデルはよくある言い回しをついでに少しは覚えます。しかし 1 エポックだけ、各サンプルを一度見るだけでは、その記憶は浅く、カードの完全な内容や境界までは覚えられません。しかも、何を覚えて何を間違えて覚えたのかを知る方法はありません。だからこそデプロイ時には 毎回必ず事実カードを渡す 必要があり、5.5 節の呼び出し例でもまず read_dataset で facts を読み込んでいるのです。

自分で確かめるには:学習後に同じ質問を 2 回してみてください。1 回は事実カード付きで、もう 1 回は CURRENT REFERENCE FACTS の部分を取り除いて、回答の正確さと境界を比べます。差が大きいほど、モデルが暗記した知識ではなく、その場で与えられた資料に頼っていることになります。

Q3:知識の暗記に頼らないのなら、なぜ学習時にも入れるのですか? デプロイ時に渡すだけではだめなのですか?

だめです。学習の目的はモデルに事実カードを 覚えさせる ことではなく、使い方 を身につけさせることです。そして使い方を身につけるには、学習中にカードを見ている必要があります。新入社員の研修にたとえてみましょう。

方法 A:Q&A だけ 方法 B:Q&A の横に製品マニュアルを添える(このプロジェクト)
研修資料 顧客が X と質問 → 標準回答 Y 顧客が X と質問 + マニュアルの該当ページ → 標準回答 Y
身につく習慣 質問を聞いたら、記憶から答えを暗唱する まずマニュアルを確認し、それから標準の口調で説明する
現場でマニュアルを渡される マニュアルを読む練習をしていないので、たぶん古い答えを暗唱し続ける まさに練習してきた手順そのもの
マニュアルの 1 項目が更新される 古い答えを言い続ける 新しいマニュアルに従う
研修で扱わなかった質問を受ける あいまいな記憶でごまかし、でっち上げがち マニュアルに沿って説明し、マニュアルにないことはでっち上げない

このプロジェクトの同じサンプルで見てみましょう。ターゲットの回答はまったく同じで、入力だけが違います。

不带事实卡:
  输入:为什么白天亮、晚上黑?
  目标:地球在不停转,我们住的地方转向太阳就是白天……

带事实卡:
  输入:[事实卡:自转使地球表面交替朝向和背向太阳……] 为什么白天亮、晚上黑?
  目标:地球在不停转,我们住的地方转向太阳就是白天……

(日本語訳:「事実カードなし ― 入力:どうして昼は明るくて、夜は暗いの? ターゲット:地球はずっと回っていて、ぼくたちの住んでいる場所が太陽の方を向くと昼になるんだ……」、「事実カードあり ― 入力:[事実カード:自転によって地表は太陽の方を向いたり背を向けたりを交互に繰り返し……]どうして昼は明るくて、夜は暗いの? ターゲット:同じ」。)

カードがないと、入力には根拠がまったくないのに、ターゲットは確定した科学的結論です。そのため 1150 個のサンプルが繰り返し強化するのは「質問を聞いたら、何もないところから確定した結論を出す」ことになります。カードがあれば、ターゲットの内容はすべて入力の中に見つかるので、強化されるのは「与えられた資料から内容を取り出し、子どもにわかる言葉に言い換える」ことです。

学習時にカードを入れず、デプロイ時にだけ追加すると、4 つの問題が起こります。

  1. モデルが資料を読む練習をしていない。 デプロイ時に大きな CURRENT REFERENCE FACTS ブロックが追加で現れますが、モデルにとっては見慣れない入力なので無視されるかもしれず、これまでどおり記憶から答えてしまいます。
  2. 資料を編集しても効果がない。 カードの誤りに気づいたり、あるカードの scope に適用範囲の境界を追加したくなったりしても、方法 A のモデルはそれに合わせて変わりません。学習データを修正して学習し直すしかありません。
  3. 新しいトピックででっち上げやすくなる。 方法 A で身につくのは「何もないところから結論を出す」習慣なので、学習データにないトピックでも自信満々に結論を出してしまいます。
  4. 入力形式が一致しない。 学習時のプロンプトには事実カードがなく、デプロイ時にはある。モデルが身につけた話し方は、学習時に見た入力の形に依存しています。5.5 節で「プロンプトは学習時と同じでなければならない」と強調しているのもこのためです。

コードの言葉で言えば、学習とは関数 answer = f(question, facts, grade_band, history) を学ぶことです。学習時に question しか与えなければ、学ぶのは f(question) です。デプロイ時に一度も使ったことのない引数 facts を追加で渡しても、モデルがそれを使い始めることはありません。

事実カードなしでも学習はできますし、回答もできます。その代償が上に挙げたことすべてです。直せない間違い、過信しやすい傾向、そしてベースモデルが知らない分野に移るたびにやり直しになることです。「事実」と「言い回し」を分けるのはこのプロジェクトの設計上のトレードオフであって、唯一のやり方ではありません。その効果は、最終的には第 6 章の比較で検証する必要があります。

事実カードが使われる場所

なぜ別に保存するのか。 60 枚のカードは 600 本の会話から fact_ids で参照されます(1 本の会話が複数のカードを参照することもあります)。これはデータベースの外部キーによく似ています。各事実を編集するのは 1 か所だけで、それを参照するすべての会話は、次に読み込まれたときに新しい内容を使います。

なぜセットごとにも分離するのか。 1 枚のカードは、1 つのセット(train/val/test のいずれか)の会話からしか参照できません。そうしないと、検証やテストの問題の根拠となる資料をモデルが学習中にすでに見ていることになり、評価結果が楽観的になってしまいます。

段階 コードの場所 事実カードの使われ方
読み込みと検証(D1) data.py validate id が一意。全フィールドがそろっている。source_url が https。kind は fact/legend/current のいずれか。会話の fact_ids の各要素が存在する。同じカードを train/val/test で共有できない
プロンプトの組み立て(D3) common.py build_messages fact_ids でカードを取得し、id/text/kind/scope だけを残して、system プロンプトの CURRENT REFERENCE FACTS: の後に JSON として書き込む。出典やレビューのフィールドはモデルに入らない
ノートブックの学習・検証サンプル ノートブックのステップ 2、prepare-data セル train の 1150 サンプルと val の 115 サンプルはすべて build_messages を通り、それぞれの事実カードを持つ
ノートブックの観察用の質問 ノートブックのステップ 4、cases セル 3 つの観察用の質問はそれぞれ fact_id を指定し、学習前後で同じカードを使って回答を比べる
CLI スナップショット prepare.py スナップショットの facts.jsonl にそのまま書き込まれる。few-shot の例は自身の fact_ids を記録する
A/B/C 比較推論 infer.py スナップショットから facts.jsonl を読み込む。3 群とも現在の質問に対して同じ事実カードを受け取る(6.4 節)
ブラインド評価 evaluate.py blind 採点表に fact_ids が含まれ、採点者は事実カードと照らし合わせて fact_claims/supported を記入する(6.6 節)
デプロイ後の呼び出し 5.5 節の呼び出し例 read_dataset で facts を読み込み、messages_for(fact_id, ...) で学習時と同じ system プロンプトを組み立てる

出典とレビューのフィールド

source_url は人間が出典を確認しに戻るための入り口で、プログラムが自動でオンラインにアクセスして読むことはありません。checked_at は参照先を読んだことを示すだけです。source_review が reference_read_claim_review_pending であれば、個々の主張がまだレビュー待ちであることを意味します。reuse は出典の明記とオリジナルの書き直しであることの注記であり、出典元の組織がこのデータを承認していることを意味しません。NASA、NOAA、USGS などの公式な科学普及コンテンツや OpenStax の教科書が参照先です。本文はオリジナルの中国語で書かれており、たとえ、子どもの反応、観察の設計は合成したお手本です。URL があるからといって、すべての文が出典に裏付けられているわけではありません。

2.3 1 本目の完全な会話

{
  "id": "day-night-01",
  "group_id": "day-night",
  "category": "space",
  "topic": "昼夜",
  "grade_band": "1-2",
  "scenario": "repair_misconception",
  "fact_ids": [
    "day-night"
  ],
  "provenance": {
    "type": "synthetic",
    "author": "Codex",
    "date": "2026-09-29",
    "method": "按主题逐项撰写知识、误解、追问、比喻边界和安全观察,再组合为10种教学对话路径;不是儿童真实对话,部分主题内回答复用。"
  },
  "review": {
    "status": "draft",
    "reviewer": "",
    "reviewer_kind": "",
    "notes": "待逐段核查科学事实、来源支持、适读学段、观察可行性、近重复与上下文衔接;成人审阅不代表已验证儿童探索兴趣。"
  },
  "turns": [
    {
      "user": "为什么白天亮、晚上黑?",
      "assistant": "地球在不停转,我们住的地方转向太阳就是白天,转到背光的一面就是晚上。",
      "behavior": "explain_at_grade"
    },
    {
      "user": "太阳晚上关灯睡觉了吧?",
      "assistant": "太阳没有在晚上关灯。是我们所在的地面转到了背向太阳的一侧。还可以接着想:如果地球不转,就没有黑夜了吗?",
      "behavior": "repair_then_offer_specific_question"
    }
  ]
}

(2 ターンの日本語訳:子どもが「どうして昼は明るくて、夜は暗いの?」と聞き、アシスタントが「地球はずっと回っているんだ。ぼくたちの住んでいる場所が太陽の方を向くと昼、光の当たらない側に回ると夜になるよ。」と答えます。続いて子どもが「太陽は夜になると電気を消して寝ちゃうんでしょ?」と聞き、アシスタントが「太陽は夜に電気を消したりしないよ。ぼくたちのいる地面が、太陽に背を向けた側に回っただけなんだ。次にこんなことを考えてみよう:もし地球が回らなかったら、夜はまったくなくなるのかな?」と答えます。provenance.method には、対話はトピックごとに書かれて 10 通りの教育用パスに組み合わされたこと、実際の子どもとの会話ではないこと、トピック内で一部の回答を再利用していることが書かれています。review.notes には、まだ人間による確認が必要な項目が並んでいます。)

子どもはまず昼と夜について質問し、次に「太陽が寝る」という推測を口にします。参照回答はまず現象を説明し、続いて具体的な誤解を正し、さらに考え続けたければ選べる質問を 1 つ残します。「你真棒」(「すごいね!」)のような決まり文句だけを覚えさせないようにしましょう。

フィールド モデルに入るか、どこで使われるか
id day-night-01:0 のようなサンプル ID を生成し、追跡に使う
group_id 1 つの科学トピックをまとめる。すべての学年帯、パス、言い換えは同じ split に入っていなければならない
(所属する配列) この会話は train 配列にある。読み込み時に split="train" が付けられ、D2 で train だけに絞り込まれる。プロンプトには書き込まれない
grade_band D3 で「小学学段」(小学校の学年帯)として system プロンプトに書き込まれる。見える入力である
fact_ids D3 がこのファイルから事実カードを選び、id/text/kind/scope だけを渡す
turns[].user 子どもの現在の質問。それより前の質問は history に入る
turns[].assistant 現在の回答が学習ターゲット。それより前の回答はコンテキストとしてのみ使われる
turns[].behavior このターンがどんな振る舞いのお手本なのかを人間が確認するためのもの。モデルには渡されない
category/topic/scenario 統計とレビュー用のラベル。「子どもがどこを間違えたか」を入力にこっそり忍ばせることはしない
provenance、review 合成方法とレビュー状態。人間専用。現在すべての会話は draft

この表から 1 つだけ覚えるなら、これです。モデルが見るのは system プロンプト(役割 + 学年帯 + 事実カード)、history、現在の user ターンだけで、学ぶのは現在の assistant ターンだけ。 2.5 節では、この一文を実際のトークンとマスクにして見せます。

2.4 会話を回答ごとに展開する

day-night-01 は 2 ターンあるので、2 つのサンプルに展開されます。

サンプル コンテキスト loss に関わるターゲット
day-night-01:0 役割 + 学年帯 + 昼夜の資料 +「どうして昼は明るくて夜は暗いの」 1 番目の参照回答 + EOS
day-night-01:1 同じ役割、学年帯、資料 + 1 ターン目の Q&A +「太陽が寝る」という推測 2 番目の参照回答 + EOS

1 つ目のサンプルからは 2 ターン目が見えません。2 つ目のサンプルでは、history にある前の回答はこのターンの loss に関わりません。train の 500 本の会話のうち、350 本が 2 ターン、150 本が 3 ターンなので、1150 サンプル になります。1150 はトークン数でもオプティマイザのステップ数でもありません。

2 ターンの会話は「2 回学習する」ということですか? 同じ内容を 2 回学習するのではなく、その会話が独立したサンプルを 2 つ提供し、それぞれが 1 エポックにつき 1 回学習されます。1150 サンプルはトレーナーに渡された後シャッフルされ、16 サンプルごとにパラメータが更新されるので、day-night-01:0 と day-night-01:1 は通常、別々のステップに入ります。1 番目の回答が loss の対象になるのは 1 つ目のサンプルの中だけで、2 つ目のサンプルではただの history です。

ターン数が増えても同じルールです。 セッションの完全なチャットログと同じように、k 番目のサンプルはそれまでの k−1 ターンの Q&A をすべて history として扱い、k 番目の回答だけを学びます。これはデプロイ時の呼び出し方と一致しています。サーバーは状態を持たず、クライアントが毎回完全な履歴を送ります(5.5 節)。知っておくべき違いが 2 つあります。

  • 学習時の history に入っているのは 参照回答 ですが、デプロイ時には モデル自身が言ったこと が入り、参照回答とは異なる場合があります。6.5 節のマルチターンモードは、この違いを専用にチェックします。
  • 後ろのサンプルほど長くなり、history はサンプルごとに毎回計算し直されます。このプロジェクトの検証は 2~4 ターンの会話しか受け付けず、4096 トークンを超えるものは黙って切り詰められるのではなく、即座にエラーになります。

2.5 マスク:モデルが実際に学ぶのはどのトークンか

以下は day-night-01:0 をこのプロジェクトのトークナイザーでレンダリングしたものです(実際の出力です。system プロンプトの途中にある役割ルールを数行省略しています)。サンプル全体は 408 トークンで、loss に関わるのは最後の 22 トークンだけです。

mask │ 渲染后的文本
─────┼──────────────────────────────────────────────────────────
  0  │ <|im_start|>system
  0  │ 你是陪小学生探索科学的伙伴。用中文,先回应孩子真正的问题,再按需要解释。
  0  │ ……(角色规则共 10 行)
  0  │ 小学学段:1-2 年级。
  0  │ CURRENT REFERENCE FACTS:
  0  │ [{"id": "day-night", "text": "自转使地球表面……", "kind": "fact",
  0  │   "scope": "昼夜的入门解释;……"}]<|im_end|>
  0  │ <|im_start|>user
  0  │ 为什么白天亮、晚上黑?<|im_end|>
  0  │ <|im_start|>assistant
  1  │ 地球在不停转,我们住的地方转向太阳就是白天,转到背光的一面就是晚上。<|im_end|>

(見出しは「mask │ レンダリング後のテキスト」です。system プロンプトは「あなたは小学生と一緒に科学を探検する仲間です。中国語を使い、まず子どもの本当の質問に応え、それから必要に応じて説明します。」で始まり、続いて「(役割ルールは全 10 行)」と「小学校の学年帯:1-2 年生。」が来ます。)

mask が 0 の 386 トークンはモデルから見えますが、ここでは loss を計算しません。学習対象は mask が 1 の最後の 22 トークンだけです。事実カードはこの 386 トークンの中にあります。モデルはそれを暗記はしませんが、それを見ながら答えることを学ぶ必要があるので、学習時にも含めなければなりません(2.2 節の Q2 と Q3)。

最後の行をトークンに分けると、地球 在 不停 转 , 我们 住 的地方 转向 太阳 就是 白天 , 转 到 背 光 的一面 就是 晚上 。 <|im_end|> となります。学習中、モデルはこの 22 か所それぞれで「次のトークン」を 1 回予測し、その外れ具合が loss になります。その前の 386 か所はコンテキストを提供するだけです。

なぜ system や user の部分もモデルに学ばせないのでしょうか? デプロイ後、その内容は常に呼び出し側が提供するからです。モデルが学ぶ必要があるのは「こういう入力が来たら、どう答えるべきか」だけです。user ターンを loss に含めてしまうと、モデルは子どもの質問をまねることに力を使い、さらには次の質問を自分ででっち上げることまで覚えてしまうかもしれません。

rows には確認しやすいように case_id/prompt/completion/input_ids/completion_mask が残してあります。実際の学習で使うのは最後の 2 列だけです。ノートブックは 1 行目の完全なプロンプト、ターゲット、実際のトークン長を表示します。古いシナリオの長さの数字を使い回さないでください。

completion_mask=0 はコンテキストを、=1 は現在の回答と EOS を示します。TRL はコンテキストとパディング位置のラベルを -100 に設定するので、それらの位置では loss が直接計算されませんが、モデルは回答を予測するときにコンテキストを見ることができます。

EOS は <|im_end|> です。各ターゲットにちょうど 1 回だけ現れ、モデルが止まることを学べるようにします。PAD は EOS とは別物です。4096 トークンを超えるとエラーになり、サンプルを書き直す必要があります。回答が黙って切り詰められることはありません。

2.6 振り返り:1 つの JSON ファイルからパラメータ更新までの 7 ステップ

ここまでの 5 つの節では、「データはどんな形か → どう分割されるか → モデルは何を見て何を学ぶか」の順にデータを説明しました。これらをつなげると、次の 7 ステップになります。ノートブックのコードコメントには、どの行がどのステップを担当しているかが D1~D7 で示されています。学習中に照らし合わせながら見れば、どの行がこの章のどの部分を行っているのかがわかります。

ステップ 何をするか この章の対応箇所
D1 読み込み dataset.json を読み、各会話に所属セットを付け、事実カードの参照とセット間の分離をチェックする 2.1 節、2.2 節
D2 分割 train だけを取り出し、各会話を回答ごとにサンプルに分割する。history にはそれより前のターンだけを含める 2.4 節
D3 入力の組み立て 役割ルール + 学年帯 + 事実カード + history + 現在の質問を、1 つの messages リストにまとめる 2.2 節、2.3 節
D4 ターゲットの指定 チャットテンプレートを適用してトークン化し、現在の回答と EOS を mask=1 にする 2.5 節
D5 トレーナーへ渡す input_ids と completion_mask の 2 列だけを TRL に渡す 2.5 節
D6 再確認 トレーナーが実際に生成したラベルを確認する。コンテキストとパディングの位置はすべて -100(loss なし) 2.5 節
D7 記録を残す この実行のデータのコピーとそのフィンガープリントを保存する。その後で初めて trainer.train() が実際にパラメータを更新する 第 4 章

2.7 データを変更する、または自分のデータに切り替える

演習のためにデータを変更する必要はありません。この節は、編集を始めたくなったときに読んでください。

既存データの編集。 言い回しを変えるには assistant を編集します。新しい反応パスを追加するときは、前後のターンも調整し、モデルが実際には言っていないことを子どもが引用することがないようにしてください。事実カードを変更した場合は、それを参照しているすべての会話を確認し直してください。編集が終わったら、まず構造チェックを実行します。

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

{'train': 500, 'val': 50, 'test': 50} と表示されるはずです。これがチェックするのは構造だけです。全フィールドがそろっていること、参照している事実が存在すること、セットをまたぐトピックがないこと、各回答の疑問符が最大 1 つであること。科学的に正しいかどうかはチェックしません。

変更を反映させる。 すでに学習済みのアダプタは、JSON を編集しても変わりません。ノートブックで RUN_DIR を新しいディレクトリ(たとえば runs/science-002)に設定し、ステップ 2 から実行し直してください。各学習実行のディレクトリには、「この実行でどのデータを使ったか」の記録として dataset.json のコピーが自動的に保存されます。普段の作業ではこれを編集しないでください。

自分のデータに切り替える。 Alpaca(instruction/output)や ShareGPT(conversations)など、よく使われるオープンソースの SFT データ形式は、本質的にはこのプロジェクトと同じです。「会話メッセージのリスト + どれが期待される出力か」であり、JSON での並べ方が違うだけです。データを切り替えるときに主に変えるのは 2 か所です。データを train/val/test の 3 セットの会話に整理すること、そして必要に応じて D3 の build_messages を調整し、system プロンプトに何を入れるかを決めることです。

2.8 オプション:自分のパソコンでデータチェックを実行する

ここまでの内容は読むだけで理解できます。GPU を借りる前にトークン化の結果を自分の目で確かめたい場合や、データを編集した後にチェックしたい場合は、自分のパソコン(Mac または Linux、Python 3.11)に PyTorch を含まない軽量な環境を入れられます。数分で終わり、グラフィックカードは不要です。

python3.11 -m venv .venv-data
source .venv-data/bin/activate
python -m pip install -r requirements-data.txt
python -m lab.download --tokenizer-only
python -m lab.data --allow-draft

最後の行で {'train': 500, 'val': 50, 'test': 50} と表示されるはずです。--tokenizer-only はトークナイザーと設定(約 15 MB)だけをダウンロードし、8 GB の重みはダウンロードしません。そのため、ローカルの models/qwen3-4b/ に重みのシャードがないのは正常です。Transformers が None of PyTorch, TensorFlow ... have been found という警告を出すことがありますが、ローカルではトークナイザーしか使わないので無視してかまいません。今後新しいターミナルを開いたときは、まず source .venv-data/bin/activate を実行してください。