TT Lab
はじめる
学ぶ 学習パス コース

MiniMind — 小さな言語モデルを最初から最後まで自分で学習する

チャット形式で SFT し、損失マスキングが何を変えるか測る

TT Labで続きを見る

目標

MiniMindのチャット形式(<|im_start|>역할\n…<|im_end|>\n)で対話を切り(プレースホルダーは役割です)、generate_labelsのように、答え(assistant)だけを残すラベルを自分で作ります。基準の事前学習の重みから、マスキングしたモデルと外したモデルを同じ条件で学習して、取り分けておいた対話で、答えトークンと質問トークンの損失がどう分かれるかを数字で見ます。

なぜ重要なのか

SFTは新しいアルゴリズムではなく、同じ次トークン予測です。違うのは、何を正解にするかの1点だけです。対話全体を正解にすると、モデルは質問を真似ることに学習信号を分けて使い、答えが終わったあと、自分で次の質問を作り出すこともあります。答えだけを正解にすると、質問は条件になり、終わりの目印まで正解に入れてはじめて、いつ止まるかを学びます。 この境界は、ラベル配列の-100の数個で決まり、間違えてもエラーが出ません。そのため、ラベルを自分で出力して見て、マスキングが実際に何を変えるのかを、2つのモデルで測ってみるのが、最も確実な確認です。

ステップ

  1. /opt/mm/data/sft.jsonlの最初の対話をチャット形式の文字列に変えて、保存してください(出力先: /root/mm/sft/sample0.txt)。
  2. スクリプト(/root/mm/sft/sftlib.py)にencode(대화, max_len=64, mask=True)(プレースホルダーは対話です)を作り、最初の20個の対話のinput_idsとlabelsを保存してください(出力先: /root/mm/sft/labels.json)。
  3. SFTデータ全体で、パディングを除いた実際のトークン数と、ラベルが残ったトークン数、その割合を書いてください(出力先: /root/mm/sft/ratio.json)。
  4. 基準の事前学習の重み(/opt/mm/ref/pretrain.pth)から、マスキングしたラベルで300学習ステップSFTして、保存してください(出力先: /root/mm/sft/sft_masked.pth)。
  5. 同じ条件で、ラベルに入力全体(パディングだけ-100)を入れて学習し、保存してください(出力先: /root/mm/sft/sft_nomask.pth)。
  6. 取り分けておいた対話(sft_val.jsonl)で、2つのモデルの答えトークンの損失と、質問側のトークンの損失を書いてください(出力先: /root/mm/sft/compare.json)。
  7. マスキングしたモデルで、取り分けておいた質問20個の答えを、グリーディ生成で取り出して保存してください(出力先: /root/mm/sft/answers.jsonl)。
  8. ## 채팅 형식 ## 손실 마스킹 ## 한계の3つのセクションを書き、マスキングしたモデルの質問の損失とステップ3の割合を入れてください(出力先: /root/mm/sft/report.md)。韓国語の見出しは、順に「チャット形式」「損失のマスキング」「限界」という意味です。

参考

対話を1行にする

/opt/mm/data/sft.jsonlの最初の行のconversationsを、MiniMindのチャット形式(番ごとに<|im_start|>역할\n내용<|im_end|>\n。プレースホルダーは、順に役割と内容です)に変えて、保存してください(出力先: /root/mm/sft/sample0.txt)。

mmkit.chat_text(대화)が、この形式を作ります(プレースホルダーは対話です)。自分で作ってもかまいません。役割の後ろに改行、内容の後ろに<|im_end|>と改行です。思考モード用の<think>タグとsystemプロンプトは入れません。

答えだけを残すラベルを作る

スクリプト(/root/mm/sft/sftlib.py)に、encode(대화, max_len=64, mask=True)(プレースホルダーは対話です)を作ってください。チャット形式を/opt/mm/ref/tokenizer.jsonで切り、64トークンに切ってパディング(0)で埋めたinput_idsと、答えの開始の目印の後ろから終わりの目印までだけが元のトークンで、残りは-100のlabelsです。最初の20個の対話の結果を、[{"input_ids": […], "labels": […]}, …]の形で保存してください(出力先: /root/mm/sft/labels.json)。

MiniMindのgenerate_labelsのように、トークン列を走査して、開始の目印の断片と同じ区間を探し、その後ろから、終わりの目印の断片が終わるまでラベルを埋めます。2往復の対話なら、区間が2回できます。採点ツールは、同じ規則で作ったラベルと、1つずつ比べます。

損失に入るトークンが何%かを数える

sft.jsonl全体をencodeで変換して、パディングでない入力トークン数(real_tokens)と、-100でないラベルの数(label_tokens)、その割合(ratio)を書いてください(出力先: /root/mm/sft/ratio.json)。

質問と役割の目印が条件にすぎないなら、学習信号は残りからしか出ません。事前学習と同じ学習ステップ数でも、SFTが実際に学ぶトークンは、この割合だけです。

マスキングしたSFT

スクリプト(/root/mm/sft/train_sft.py、引数: --nomask・--out)を書いて、/opt/mm/ref/pretrain.pthから始めて、マスキングしたラベルで300学習ステップ(バッチ16・長さ64・lr 1e-3・MiniMindのコサイン・シード42)SFTし、保存してください(出力先: /root/mm/sft/sft_masked.pth)。

前のモジュールのループから、データだけが変わります。model(X[ix], labels=Y[ix])です。採点ツールは、取り分けておいた対話で答えの損失が1.2を下回るか、そして質問側の損失が高く(3以上)残っているかを見ます。マスキングしたなら、質問は学んでいないからです。

マスキングを外したSFT

ステップ4とすべての条件を同じにして、ラベルだけをinput_idsそのまま(パディングの位置だけ-100)に変えて学習し、保存してください(出力先: /root/mm/sft/sft_nomask.pth)。

encode(대화, mask=False)のように(プレースホルダーは対話です)、1つの引数で2通りを作れるようにしておくと、条件が同じであることがコードで保証されます。採点ツールは、このモデルの質問側の損失が低いか(1.5未満)を見ます。質問も学んだという証拠です。

2つのモデルを取り分けておいた対話で測る

sft_val.jsonlのすべての対話を2つのモデルに入れて、答えの区間のラベルだけを残した損失(answer_loss)と、答えでない位置(パディングを除く)だけを残した損失(prompt_loss)を、{"masked": {…}, "nomask": {…}}の形で書いてください(出力先: /root/mm/sft/compare.json)。

同じ入力に、ラベルだけを2通りに隠して、cross_entropy(…, ignore_index=-100)を2回計算すればよいです。ロジットは1つずらして比べます(logits[:, :-1]とlabels[:, 1:])。同じ学習ステップ数での、2つのモデルの答えの損失も比べてみてください。

取り分けておいた質問に答える

マスキングしたモデルで、sft_val.jsonlの先頭20個の対話の最初の質問に対する答えを、グリーディ生成で取り出し、1行に1つずつ{"q": 질문, "a": 답, "ref": 자료의 답}の形で保存してください(プレースホルダーは、順に質問・答え・データの答えです。出力先: /root/mm/sft/answers.jsonl)。

mmkit.greedy(model, tok, 질문)は、チャット形式に<|im_start|>assistant\nを付けて生成し、<|im_end|>で止まります(プレースホルダーは質問です)。取り分けておいた質問は、SFTで見たことのない質問の形なので、形式は合っているのに内容が間違った答えが出るのが正常です。最後のモジュールで、その理由を測ります。

マスキングの効果を残す

## 채팅 형식 ## 손실 마스킹 ## 한계の3つのセクションを書き、ステップ6のマスキングしたモデルのprompt_lossと、ステップ3のratioを数字で入れてください(出力先: /root/mm/sft/report.md)。韓国語の見出しは、順に「チャット形式」「損失のマスキング」「限界」という意味です。

質問の損失が高いことが、なぜ「良い」サインなのかを、1行書いてください。限界のセクションには、ステップ7で間違った答えの形を書きます。