トークン化 入門

モデルが読むのは整数であって、テキストではない。片方をもう片方に変えるのは、最頻のバイトのペアを何度も融合する 4 行のループ だ —— そして残された語彙が、文脈長も、API の請求額も、モデルが単語の文字数を数えられるかどうかも決める。

01

何を 1 トークンとするか

モデルが掛け算を始める前に、テキストは整数の列にならなければならない。その 1 整数が何を指すのかが、この主題のすべてだ。

モデルはあなたの文字列を見ていない。見ているのは整数の列で、どれもある表の行番号だ。どの行かを知っているのは tokenizer だけ。下は一文と、それが変わる整数だ ——キャレットを沿って動かしてほしい:

1 番目のトークン —— id 258
·the の id は 258

id が恣意的でないことに注目してほしい。最初の 256 はバイト値で、その上はいつ学習されたかで番号がつく。だから ·the が 258 なのは、訓練テキストが 3 番目に求めたものだからだ。 id は履歴の中の位置であり、2 つの tokenizer が表を交換できない理由もそれだ。

「1 行とは何か」への答えは長らく 3 つ並んでいて、どれもどこかで壊れた。 1 文字 1 行は表を小さく保つが、系列は 5 倍に伸びる —— アテンションのコストはその 2 乗だ。1 単語 1 行は系列を短く保つが、表に上限がなく、見たことのない語はそのまま表の穴になる。実際に出荷されるのは、両者のあいだのつまみだ。

下は同じ文で、つまみは左端、生のバイトがそれぞれ 1 トークンになっている。スライダーを引くと 1 段ごとに訓練テキスト中の最頻ペアが融合し、トークンがバイトから語片へ、そして単語へと育つ:

語彙 256 項目:この文は 16 トークン

3 つの水準のあいだで何も選んでいない —— ひとつのループが 3 つとも生む。最初の画面であるマージ 0 回では語彙は 256 のバイト値で、この文は 16 トークン。では 288 になり、·the、·model、·read がそれぞれ 1 項目になって 4 トークンだ。点は空白で、どの GPT 語彙でも単語は直前の空白を連れている。

つまみの両端にはコストがあり、その 2 つは同じスライダーを反対側から読んだものだ —— モデルがアテンションをかける系列と、保持して採点する語彙の行数:

16 トークン対 256 行の語彙

アテンションは系列長の 2 乗、埋め込み表は語彙サイズの 1 乗なので、つまみの左端は 1 ステップあたりが高く、右端は 1 行あたりが高い。ただの設定はどこにもない。 §06 で両側に実際の数字を置く。

単語側には、あの図に載っていないもう 1 つのコストがある。単語まるごとの語彙は、すでに見たものしか返せない。訓練テキストにあった語を通り越すまでスライダーを進めてほしい:

the:単語語彙は 1 トークンを返し、バイト語彙は 1 トークンを返す

を見てほしい。単語語彙はこの項目を持たず、<unk> —— ここに何かがあったとしか言わない id —— を返す。バイト語彙は 12 トークンで綴ってしまう。出荷されるすべての tokenizer が下に 256 のバイト値を置いている理由がこれだ。アルファベットがファイルのアルファベットである以上、語彙外の入力は存在しない。

ただし下の床は文字ではなくバイトで、その 2 つが一致するのは ASCII だけだ。キャレットをラテンの行と日本語の行に沿って動かし、 1 文字のコストを読んでほしい:

1 バイト目:文の中で、その文字は 3 バイト
1 バイト目:文の中で、その文字は 3 バイト

ラテン文字は 1 バイト、日本語の文字は 3 バイト。ある文字体系のマージを何も学んでいない tokenizer は、ほかの何を払うより先に 1 文字あたり 3 トークンを払う —— そして §04 は、それを直したはずのマージがどこか別の場所に使われたときに何が起きるかだ。

02

マージのループ

バイトペア符号化は 4 行で、2016 年からずっと同じ 4 行だ。残りはすべて「何を数えるか」の選択にすぎない。

テキストの山の中で隣接ペアをすべて数え、最頻のものを 1 つの新しいシンボルに融合し、その融合を書き留めて、もう一周する。語彙はその融合の産物だ。ただし、単語境界をまたぐ計数は 1 つもない —— ループが走る前に、テキストは切られているからだ。

その切れ目はただの正規表現で、·the と the が別々の項目になる理由でもある。それが生む断片に沿ってスライダーを動かし、各断片の下のバイト数を読んでほしい:

1 番目の断片:·the
1 番目の断片 ·the は 4 バイトで、どのマージもここから出られない

境界がどこに落ちるかに注目してほしい。先頭の空白は後ろの語につくので、単語は自分の空白を連れている。句読点は単独になる。そして空白を持たない中国語と日本語は 1 文字 1 断片になる。どのマージもこの線を越えられない —— 見た目より強い保証で、語彙が ·the·model を 1 トークンとして学ぶのを止めているのはこれだ。

ではループ本体を、11 の語形を 11 の異なるバイトで書いたコーパスの上で。コーパス中の隣接ペアはすべて、その語の出現頻度で重みづけて数えられ、最頻のペアが勝つ:

0 回のマージ後、最頻ペアは ·l で 21 回出現

最初のパスでは ·l が 21 で es の 16 を上回る —— low、lower、lowest、less、list の「空白+l」の合計だ。同点は絶えず起き、では先頭が 4 の三つ巴になる。ここでは走査で先に出会ったペアを取るので、同点規則の違う 2 つのライブラリは、同じテキストから別々の語彙を作る。

マージのたびにコーパス全体が書き換わり、次のパスは残ったものを数える。再生を押して、生バイトが語彙が抱える語片へ融合するのを見てほしい:

0 回のマージ:コーパスは 62 シンボル

その形に注目してほしい。最初のマージは語幹を買う —— ·l、·lo、·low —— 次に共有接尾辞 est、そして単語まるごと。14 回のマージでコーパスは 62 シンボルから 21 になり、スクラバーがそこで止まるのは、14 がこの図に与えられた予算だからだ。ループ自体は終わっていない。2 回以上現れるペアはまだ 10 組あり、最後の重複が消えるまであと 10 回かかる。実テキストではその点にそもそも到達しない —— 必ず予算で止まり、その予算をどう選ぶかが §04 の主題そのものだ。

マージ 1 回は表の 1 行で、もとから完成しているアルファベットの上に載る。スライダーを引いて、学習した項目が 1 つずつ届くのを見てほしい:

0 回のマージを学習:語彙は 256 項目

土台が固定でマージ 1 回がちょうど 1 項目を足すので、語彙サイズは手探りで調整するハイパーパラメータではなく算術だ:256 + マージ数 + 特殊トークン。GPT-2 は 256 + 50,000 + 1 = 50,257 で、その 1 つの特殊トークンが <|endoftext|> だ。§04 の梯子の数字もすべて同じように分解できる。

符号化はその表を 1 語の上で再生することだ。語の中でランクが最小のペアを繰り返し見つけて適用する。訓練テキストに一度も現れなかった語を、その生バイトから 1 歩ずつ進めてほしい:

0 番目まで再生すると、この語は 5 トークン

再生が学習した順に従うので、生まれる分割は、訓練当時にその語に対して生まれたはずの分割とぴったり同じになる。仕組み全体が乗っている不変条件がこれだ ——符号化は訓練を再現する。ここでの順序は最適化ではなく、定義そのものだ。

どちらも安く、どちらもボトルネックではない。訓練と符号化で高いところが違う。素朴な訓練は O(k · N) —— マージのたびにコーパスを走査し直す —— で、実運用の訓練器はペア頻度をヒープで差分更新して O(N log N) 近くまで持っていく。b バイトの語の符号化は素朴には O(b²)、ヒープなら O(b log b)。実テキストでは事前分割の正規表現がいちばん遅いのがふつうだ。

だから、いかにもな近道がバグになる。マージを再生するかわりに各位置で語彙にあるいちばん長い項目を取ることにしても、型検査はすべて通る。スイッチを切り替えて、同じ語に対する 2 つの答えを読んでほしい:

学習した順:2 トークン

マージの再生は ·n est —— 2 トークンを返す。最長一致は ·ne s t —— 3 つの id を返す。どれも語彙にあり、しかしモデルが訓練されたことのない並びだ。例外は投げられない。モデルは黙って悪くなるだけで、これは重みと 1 バージョンずれた merge ファイルを出荷するのと同じ失敗だ。

03

もう 2 つの選び方

WordPiece と Unigram は語片を残したまま、問いのほうを変える。片方はペアの採点を変え、もう片方はペアの採点をやめる。

BPE が問うのは、どのペアが最も頻繁かだ。それは頻度であって、それ以上でも以下でもない。WordPiece —— BERT の tokenizer で、多くのエンコーダモデルの祖先 —— が問うのは、どのペアが最も意外か。ペアの頻度を、その両半分が離れて出る頻度で割る。それは頻度ではなく尤度比だ。

同じコーパスを、もう一方の採点で。最初は尤度の基準に止まっていて、頻度の基準はスイッチひと押し先だ:

尤度では、勝つペアは id

尤度がどのペアを押し上げるかに注目してほしい。id は 8 回、·l の 21 回に対して少ない。だがこのコーパスで i と d はほとんど離れて現れないので、その結びつきは声こそ小さいがテキスト中で最も強い証拠なのだ。頻度はいちばんよく見るペアを買い、尤度は意味のあるペアを買う。

基準は損失曲線の細部ではなく、別の語彙そのものだ。下の 2 つの表は番号のついた 10 の空きから始まる —— スライダーを引くと各基準が自分の行を 1 マージずつ埋めていくので、頻度が買ったものと尤度が買ったものを比べられる:

それぞれ 0 マージ後、2 つの語彙は 0 項目を共有する

10 マージずつのあと、2 つの表に共通する項目はちょうど 1 つ。tokenizer が部品として差し替えられないのはこのためで、BERT と GPT-2 の語彙は同じ言語の別々の分割だ。

Unigram —— SentencePiece のもう一つのモードで、Llama や T5 が使うもの —— はペアのループそのものを捨てる。大きな候補語彙から始め、各片に確率を与え、語に対して違う問いを立てる。どう組み立てるかではなく、どの切り方がいちばん確からしいかだ。すべての合法な切り方の順位をたどってほしい:

13 通り中 1 番目
13 通り中 1 番目、対数確率 -2.64

13 通りあり、1 位は BPE も返す切り方だ —— 対数確率 −2.64、2 位は −5.20。だが残りもそこにあり、それが要点だ。候補に順位をつけられるモデルはサンプリングもできる。サブワード正則化が 1 つの文字列の複数の分割で訓練できるのはこれだ。

04

同じ文が日本語だと高くつく理由

日本語が難しいからではない。マージが配給制で、配給はコーパスに従うからだ。

マージ予算は訓練が始まる前に決まっている —— GPT-2 は 50,000、cl100k は 100,000。ラテン語片に使った 1 マージは、漢字に使わなかった 1 マージだ。そしてループは頻度がいちばん高いところに使う。つまりコーパスの出どころに使う。

下は 32 マージの予算を 2 つの言語が分け合う図。上の帯はマージの行き先、下の 2 本は同じ内容の一文が英語と日本語でいくらかかるかだ。コーパスを引いてほしい:

日本語 0% のとき:英文は 4 トークン、和文は 18 トークン

最初の画面ではコーパスはすべて英語で、マージはすべてラテン。2 つの文はそれぞれ 4 トークンと 18 トークン —— 同じ量のテキストに 4.5 倍の請求だ。まで押すと 5 対 15、まで押すと、ちょうど反転する。16 対 6。非対称なのは文字体系ではなく、誰のテキストが山に入っていたかだ。

同じスライダーを、その日本語の文そのものに。裸のバイトひとつだけの箱は、 1 文字の 3 分の 1 が 1 文字の代わりに立っている姿だ:

日本語 0% のとき、この文は 18 トークン

コーパスに日本語がなければ、1 文字は 3 バイトで、1 バイトが 1 トークンになる —— 6 文字に 18 の id、そのどれも 1 文字ではない。バイトフォールバックのおかげで表現できないものはないので、モデルは読める。だが日本語話者が 1 つと見る記号に、文脈 3 つ分、埋め込み参照 3 回、アテンション 3 行を使っている。

この 6 年の語彙の梯子が買ってきたのはこれだ。出荷されたサイズをたどり、手元の段の分解を読んでほしい:

GPT-2 は 50,257 項目

どれも 256 + マージ数 + 特殊トークンだ —— cl100k と o200k は、どの文字列にも割り当てられなかった id の塊もそれぞれ抱えていて、それが特殊トークン数と合計の差のほとんどを占める —— 増えたのはほぼマージだけだ。 2019 年 GPT-2 の 50,257、2022 年 cl100k の 100,277、 2024 年 o200k の 200,019、Gemma の 256,000。5 年で予算を 4 倍にしたのが、多言語とコードの被覆を買った方法だ —— そして §06 がその値段だ。

05

これが壊す 4 つのもの

どれも例外を投げない。すべて同じバグだ。モデルは文字列を見ておらず、マージループがたまたま吐いた id しか見ていない。

まず算術から。マージループは数が何かを知らない。コーパスで頻出だった数字列を融合し、残りは放っておく。数直線を歩いて、切れ方が手元で変わるのを見てほしい:

380 は 1 トークン

最初の画面の 380 は 1 トークン。 は 2 つで、38 と 1。 はまた 1 つだ。年号が頻出だったからである。 3 桁の数 900 個のうち単一 id なのはここでは 108 個だけで、どの 108 個かは頻度の偶然にすぎない。この図の下の語彙は合成の数値コーパスで訓練したものなので、その 108 個は出荷済み tokenizer のものではなくこちらのものだ —— 合成でないのは、この不揃いさそのものである。つまりモデルが 2 つの数を足すとき、オペランドの形がそもそも違う。Llama 3 が数字を最大 3 桁の組に強制し、 Llama 2 と Gemma が 1 桁ずつ切ってループに任せないのはそのためだ。

同じ盲目さが、文字数を数える失敗も説明する。語のリストを歩き、モデルが受け取るものと下の文字を比べてほしい:

·model:6 文字、1 トークン

最初の画面では ·model は 6 文字を覆う 1 つ の id だ。文字が何個かと聞かれても、モデルはその id についてのその事実を暗記しているしかない —— 見ることはできない。文字が入力に入っていないからだ。稀な語のほうがうまくいくのは、まさに断片で届くからだ。この tokenizer は頻出語の綴りを隠し、誰も訓練していない語の綴りだけを露出させる。

3 つ目は空白。単語は直前の空白を連れているので、動かすと引く項目が変わる。同じ 2 語の 4 通りの並びを 1 歩ずつ進めてほしい:

1 番目:4 トークン

トークン数を見てほしい。ふつうの空白は 4。は 5 になる。空白が次の語に乗れず、それ自体が 1 トークンとして取り残されるからだ。空白で終わるプロンプトが、空白なしの同じプロンプトより悪くふるまう理由はすべてこれだ。迷信ではなく、モデルは本当に訓練時の分布から外れている。

4 つ目、1 つの語が 4 つの項目になる。大文字小文字も先頭の空白も鍵の一部で、この 4 つの綴りのうち 3 つは生バイトに落ちる:

·the は 1 トークン

·the は 1 id。行頭で空白のないは 3 つ、頭を大文字にすると 4 つ。この 4 つの id 列が 1 つの語を指すことを、モデルはデータだけから学ばなければならない。だいたいは学ぶ —— 学びそこねる場合があることが、同じベンチマークでプロンプトを Title Case に書き換えると点が変わる理由だ。

06

語彙の値段

項目 1 つにつき埋め込み 1 行、そして生成トークンごとに出力ロジット 1 つ。それが無料かどうかは、モデルの幅で完全に決まる。

デコーダブロックはおよそ 12·L·d² パラメータ —— d×d のアテンション射影 4 つと 4 倍幅の MLP —— で、埋め込み表は V·d だ。語彙を引いて、その和の 2 つの半分を見てほしい:

語彙 50,257 項目、d = 768 のとき、埋め込み表はモデルの 31.2%

最初の画面は GPT-2 small、d = 768、GPT-2 自身の語彙。埋め込み表は 3860 万で、12 ブロックの 8490 万に対する —— モデルの 31% が語彙だ。この式の合計 1.235 億は公表値 1.24 億と 4 サイズすべてで誤差 0.5% 以内なので、報告される内訳は本物だ。

同じ分割を、GPT-2 が実際に出荷した 4 サイズそれぞれについて描く。 1 本目のスライダーは語彙のまま、 2 本目 —— 新しいほう —— が幅の梯子のどの段を主役にするかを決める:

語彙 50,257 項目、d = 768 のとき、埋め込み表はモデルの 31.2%

触らなくても埋め込みの帯が下へ縮むことに注目してほしい。31.2%、14.6%、8.3%、そして では 5.2%。ブロックは d²、表は d でしか伸びないからだ。§04 の梯子はこれで説明がつく。

def train(corpus, k):
    seq = [list(w.encode()) for w in corpus]
    merges = []
    for _ in range(k):
        p = top_pair(seq)
        if p is None:
            break
        seq = [fuse(s, p) for s in seq]
        merges.append(p)
    # the order IS the model
    return merges

def encode(word, merges):
    s = list(word.encode())
    rank = {p: i for i, p in enumerate(merges)}
    while (p := best(s, rank)) is not None:
        # lowest rank first, always
        s = fuse(s, p)
    return s

重みは 2 行にかかる。merges は集合ではなく順序つきリストで、best はランクで選ぶ。だから merge ファイルは隣に置かれた設定ではなく、モデルの一部だ。重みを訓練したときのリストから 1 項目を削って、モデルが代わりに受け取るものを読んでほしい:

マージ表が完全なら、id はすべて一致する

何も例外は上がらないことに注目してほしい。下段の id はどれも正当で、 tokenizer は警告なしにそれを返す —— 削られたマージより後ろの id が一斉に 1 つずれただけだ。を削れば、文のほとんどが動く。§02 の最長一致エンコーダと同じ失敗で、症状はどちらも、静かに性能の落ちたモデルだけだ。