MiniMind — 小さな言語モデルを最初から最後まで自分で学習する
借りてきたトークナイザーはハングルをバイトに刻む
一言でいうと
トークナイザーは、文章をモデルが食べる番号(トークン)に変える辞書です。MiniMindは、語彙6,400個のバイトレベルBPEを使っていて、このサイズは、小さなモデルで埋め込みがパラメーターを食い尽くさないように選んだ値です。ところが、その辞書は中国語・英語で学習されているため、韓国語をほとんどバイト単位で切ります。このモジュールでは、私たちのコーパスで辞書を新しく学習し、同じ文章が何トークンになるのかを、数字で比べます。
なぜ必要なのか
言語モデルは、文字を知りません。整数の番号の並びだけを受け取り、番号ごとに埋め込みベクトルを1行取り出して使います。そのため、2つのことが一度に決まります。
- 長さ: 同じ文が20トークンなら20回、60トークンなら60回計算します。アテンションは長さの2乗で高くなり、1回に見られるコンテキスト(128トークン、512トークン)もトークン数で測ります。細かく切るほど、同じコンテキストウィンドウに収まる文章が減ります。
- パラメーター: 埋め込み表は
어휘 × 차원です(プレースホルダーは語彙と次元です)。MiniMind-3(768次元)で語彙が6,400なら、約490万個で、全体の64Mの8%ほどです。ところが、私たちが作る128次元のモデルに語彙6,400を使うと、埋め込みだけで82万個になり、モデルの半分近くになります。MiniMindのREADMEが「小さなモデルには語彙を小さくしておくほうが合っている」と書く理由です。
MiniMindは、またトークナイザーを再学習しないように勧めています。辞書が変わると、重み・データ形式・推論インターフェースがすべて一緒に変わって、他人とモデルを共有できなくなり、トークン単位で計算するパープレキシティ(PPL)も、辞書が違うと互いに比べられないからです。このコースは、その勧告を知りながら、あえて新しく学習します。韓国語のコーパスでは、借りてきた辞書があまりに高くつくことを、自分で測ってみるためです。
どう動くのか
MiniMindのtrainer/train_tokenizer.pyは、HuggingFaceのtokenizersで3つの部品を組み立てます。
tok = Tokenizer(models.BPE())
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
trainer = trainers.BpeTrainer(vocab_size=6400,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), # 바이트 256개를 처음부터
special_tokens=["<|endoftext|>", "<|im_start|>", "<|im_end|>", ...])
tok.decoder = decoders.ByteLevel()
バイトレベル(ByteLevel)は、文章をまずUTF-8のバイトに変え、バイト256個を基本の文字にします。どんな文字もバイトで表現できるので、「知らない文字」は生まれません。ハングル1文字は、UTF-8で3バイトなので、辞書にその文字をまとめたトークンがなければ、1文字が最大3トークンになります。
BPEは、最も頻繁に並んで出てくる2つの断片を1つにまとめる作業を、語彙が埋まるまで繰り返します。まとめる組が先になくなると、語彙サイズを埋めきれずに止まります。このコースのコーパスは、単語の種類が少ないので、1,024を与えても、700個ほどで止まります。ラボで実際に見ることになります。
特殊トークンは、学習の前に先頭の位置に入れておきます。MiniMindは、ID 0は<|endoftext|>(パディング)、ID 1は<|im_start|>(bos)、ID 2は<|im_end|>(eos)を使い、ツール呼び出し・思考モード用のトークンまで、36個の位置を予約しています。このIDは、チャット形式と損失のマスキングが頼りにする目印なので、バイトに切られてはいけません。
現場での姿
社内文書でオープンモデルをファインチューニングしようとして、コンテキストウィンドウがすぐ埋まり、長い文書が切れてしまうなら、まず、そのモデルのトークナイザーが韓国語を何トークンに切るのかを測ってみる必要があります。同じコストで入れられる文章の量は、トークナイザーによって2–3倍変わります。ただし、すでに学習されたモデルの辞書は変えられません。辞書を変えた瞬間、埋め込み表が使い物にならなくなるからです。辞書を選ぶことは、最初から学習するときにしかできない決定で、だからMiniMindは、辞書を一度決めたら使い続けます。
モデル同士の品質を比べるときも、辞書が違うと、トークンあたりの損失をそのまま比較してはいけません。トークンが長いと、トークン1つを当てるのが難しく、短いと簡単です。MiniMindのREADMEが、トークナイザーが違うときはバイトあたりのビット数(BPB)を使うよう勧める理由で、このコースの最後のモジュールで、BPBを実際に測ります。
MiniMindのオリジナルとこのコースの違い
MiniMindのtrain_tokenizer.pyは、SFTデータ(sft_t2t_mini.jsonl)の会話内容で語彙6,400を学習し、特殊トークンの枠を36個取っておきます。ツール呼び出し(<tool_call>)・思考モード(<think>)・今後使う予備の枠(<|buffer1|>…)までです。このコースは、3つを変えます。学習データは、事前学習コーパスのtextだけを使い(採点ツールが同じ条件で学習し直して比べられるように)、語彙は1,024に減らし、特殊トークンは、チャットに必須の3つだけにします。ツール呼び出しと思考モードを扱わないからです。その代わり、MiniMindと同じ名前・同じID(0・1・2)を守り、以後のモジュールのチャット形式と損失のマスキングが、MiniMindのコードとまったく同じように動くようにします。
次のラボですること
MiniMindの辞書で、私たちの韓国語コーパスを切り、1文字あたりのトークン数を測り、同じ組み立てで語彙1,024の辞書を学習して比べます。語彙を384・512・1,024に変えると、圧縮率がどう変わるか、語彙サイズが埋め込みの割合をどれだけ変えるかまで、数字で残します。