JA EN
体系Transformerの仕組み
·無料·論文·13分で読めます

トークナイザを1から — LLMが世界を切り刻む単位

LLMは文字も単語も読んでいない。BPEがどう語彙を作るか、SentencePieceが何を解決したか、なぜ日本語は同じ内容で損をするのか、語彙サイズを増やすと何と何が交換されるのかを、前提知識ゼロから手計算とコードで追う。

対象textタスクnlp

Neural Machine Translation of Rare Words with Subword Units


LLMは文字を読んでいない

「こんにちは」とモデルに送ったとき、中に入っていくのは文字ではありません。整数の並びです。文字列を整数列に変換する係がトークナイザ、変換の単位がトークンです。

番号の振り方——どこで文章を切り、何を1個と数えるか——を決めているのはニューラルネットではなく、学習が始まるに固定される別のプログラムです。そしてこの切り方が、料金・文脈長・計算コスト・言語ごとの得手不得手まで決めてしまいます。地味な前処理に見えて、実際はモデルが世界を見るときの解像度そのものです。

比喩: レゴブロックの箱を設計する

文章を組み立てる作業をレゴに例えます。あなたは箱に入れるブロックの種類を、たとえば5万種類だけ選べます。

文字単位のトークナイザが前者、単語単位が後者です。前者は語彙が数百で未知語もゼロですが系列が極端に長い。後者は1トークンが意味を持つ代わりに語彙が数十万に膨れ、それでも新語・固有名詞・タイポはこぼれ落ちて「未知語(UNK)」という捨て札になります。

実用されているのは中間の設計、サブワードです。よく出る語はまるごと1個、珍しい語は破片の組み合わせで表す。「灯台」が箱に無くても「灯」+「台」で作れるようにしておく、という考え方です。

仕組み: BPE — 一番よく隣り合う2つをくっつける

サブワード語彙を決める最も広く使われている方法が BPE(Byte Pair Encoding) です。もとは1994年に提案されたデータ圧縮アルゴリズムで、2016年に機械翻訳へ持ち込まれました。出自が圧縮なのは偶然ではなく、「頻出パターンに短い符号を割り当てる」という発想はエントロピー符号化と同じ家系です。

手順は単純です。

  1. まず全部を1文字に分解する(これが初期語彙)
  2. コーパス全体で最も多く隣り合っている2つ組を数える
  3. その2つ組を新しいトークンとして語彙に追加し、該当箇所を全部くっつける
  4. 語彙が目標サイズになるまで 2〜3 を繰り返す

(a,b)=argmax(a,b) count(a,b)(a,b)^{*} = \arg\max_{(a,b)} \ \mathrm{count}(a,b)
(1)

式(1)は「いま数えた中で、最も出現回数が多い隣接ペア (a,b)(a,b) を選ぶ」と言っているだけです。argmax\arg\max は「最大にする引数を返す」記号、count(a,b)\mathrm{count}(a,b)aa の直後に bb が来た回数です。

つまり、隣り合った2つ組ごとに正の字を書いていって、いちばん多かった組を指差す——それだけのことです。(a,b)(a,b)^{*} の星印は「その回で選ばれた勝者」を指しています。

手で追ってみます。コーパスが low ×5、lower ×2、newest ×6 なら、文字に割ると l o w / l o w e r / n e w e s t。一番多い隣接ペアは e s(6回)なので es を作り、次は es test——と、頻度の高い塊から順に大きくなっていく。結果として lowest のような、人間が見ても意味のありそうな断片が自然に生まれます。ただしこれは統計の副産物で、文法や辞書は一切使っていません。

def merge_once(corpus):                       # corpus: list[list[str]]
    pairs = {}
    for toks in corpus:
        for p in zip(toks, toks[1:]):
            pairs[p] = pairs.get(p, 0) + 1
    a, b = max(pairs, key=pairs.get)           # 式(1)そのもの
    return a + b, [_apply(t, a, b, a + b) for t in corpus]

学習はこれを数万回まわすだけ、推論時は「学習で決まったマージ規則を、決まった順番どおりに当てていく」だけです。順番が規則そのものなので、マージ表を1行入れ替えると別のトークナイザになります。

バイト単位BPE: 未知の文字を無くす

文字から始めると、学習データに無かった文字(絵文字、稀な漢字、キリル文字)は未知になります。GPT-2 が採った解決策が byte-level BPE で、文字ではなく UTF-8のバイトから始めます。バイトは256種類しかなく、あらゆるテキストがバイト列で表せるので、未知語が原理的に存在しなくなるわけです。

ここに伏線があります。UTF-8では英数字は1文字=1バイトですが、ひらがな・カタカナ・漢字はほとんどが1文字=3バイトです。日本語は、マージ規則が一つも当たらなければ1文字が最大3トークンに化けます。

WordPieceとUnigram: 頻度ではなく確率で選ぶ

BPEには兄弟が2つあります。WordPiece(BERTなどが使用)は、マージ相手を生の頻度ではなく count(ab)/(count(a)count(b))\mathrm{count}(ab)/(\mathrm{count}(a)\cdot\mathrm{count}(b)) のようなスコアで選びます。「ただ多いペア」ではなく「単独で出るより一緒に出がちなペア」を優先する、という違いです。

Unigram言語モデル(SentencePieceの既定のひとつ)は発想が逆さまで、大きめの候補語彙から要らないものを削っていきます。各トークンに確率 p(x)p(x) を持たせ、分割全体の尤度を次で評価します。

P(x)=i=1np(xi)P(\mathbf{x}) = \prod_{i=1}^{n} p(x_i)
(2)

式(2)は「その分割の尤もらしさは、使った各トークンの確率を全部かけ算したもの」という意味です。\prod は「並んだものを全部かけ算する」記号、x\mathbf{x} が分割結果(トークンの列)、p(xi)p(x_i)ii 番目のトークンの出やすさです。

言い換えると、切り方の点数は「使った部品それぞれのありふれ度を掛け合わせた値」ということ。珍しい部品を1つ挟むだけで小さい数が1回掛かるので、点は一気に下がります。だから「よく出る部品だけで、なるべく少ない個数で組み立てた切り方」が勝ちます。同じ文字列の分割候補は何通りもあるので、この値が最大になる切り方を選びます。候補は指数的にありますが、前から順に「この位置までの最良の分割」を埋めていく動的計画法で解けます。

利点は分割が1通りに固定されないことです。確率が付いているので、学習時にわざと2番目・3番目に尤もらしい切り方をサンプリングでき(サブワード正則化)、モデルが特定の切れ目に依存しすぎるのを防げます。

SentencePiece は、この上に「言語を仮定しない」設計を重ねたツールです。従来のトークナイザは「まず空白で単語に切る」前処理を暗黙に置いていましたが、空白で語を分けない日本語・中国語・タイ語ではこの前提が最初から崩れます。SentencePieceは入力を生の文字列のまま扱い、空白すら (U+2581)という普通の文字として語彙に入れます。連結して を空白に戻せば元の文字列が完全に復元できる——可逆であることが最大の売りです。

日本語はなぜ損をするのか

ここまでを踏まえると、日本語が不利になる理由が3つ積み重なっていると分かります。

1. バイトが重い。 UTF-8で仮名・漢字はおおむね3バイト。その文字を含むマージが語彙に無ければ、1文字が2〜3トークンに膨らみます。

2. 語彙は学習データの鏡。 BPEもUnigramも「コーパスでよく出る並び」を採用します。学習コーパスが英語中心なら、英語の語尾(ing, tion)には専用トークンが割り当たり、日本語の頻出表現には割り当たりません。

3. 空白が無い。 空白区切りを前提にした事前分割を持つ実装では、日本語は「1つの巨大な語」に見えます。SentencePieceはこれを解決しましたが、英語向けの正規表現で事前分割するトークナイザでは今も切れ味が落ちます。

結果、同じ内容の日本語文は英語より多くのトークンになりがちです。これは体感の問題では済みません。API料金はトークン課金、文脈長の上限もトークン数、さらに自己注意の計算量は系列長の2乗に比例します。

FIG 1横軸をトークン数だと思って見てください。同じ内容が2倍のトークンに膨らむと、線形なコストは2倍で済みますが、注意のn²は4倍に離れていきます

トークン効率の悪さは、料金・文脈・速度の3方向に同時に効きます。日本語に強いモデルが「トークナイザを鍛え直した」と強調するのは、精度の話であると同時に単価の話でもあるわけです。

語彙サイズの取引

では語彙を大きくすればよいのか。ここが設計上いちばん面白い交換です。

語彙 V|V| を増やすと1トークンが表す文字数が増え、系列が短くなります。系列が短ければ注意のコストは2乗で効いて下がり、同じ文脈長により多くの内容が入り、推論時にK・Vを溜めるKVキャッシュの消費も減ります。

代わりに払うものが3つあります。入口と出口が太る——埋め込み行列は V×d|V| \times ddd は次元数)、出力側のsoftmaxも同じ形で、小さいモデルほどこの「両端」が全体を食います。珍しいトークンが育たない——語彙の末尾には滅多に出ないトークンが並び、その埋め込みは学習信号をほとんど受け取れません。出力層の計算が重くなる——生成は1トークンごとに語彙全体のスコアを出して正規化します。

FIG 2棒の1本1本が語彙の1トークンだと思ってください。生成のたびに、この分布が語彙サイズぶんの幅で計算されます。トークナイザが決めるのは、この横軸そのものです

だから実際のモデルは、狙う言語の分布とモデルサイズを見て語彙サイズを決めています。GPT-2 は 50,257、Llama 2 は 32,000、Llama 3 は 128,256、Gemma は 256,000。大きいほど新しくて偉いのではなく、多言語を狙うほど、モデル本体が大きいほど、大きい語彙のコストを払いやすいという関係です。

コードで触ってみる

自分のテキストが何トークンになるかは数行で測れます。

from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("gpt2")

s = "トークナイザは前処理ではなく設計だ"
ids = tok.encode(s)
print(len(s), len(ids))                 # 文字数 vs トークン数
print(tok.convert_ids_to_tokens(ids))   # どこで切れたかを目で見る

数を眺めるだけでなく、切れ目を目で確認するのがコツです。数字だけでは、固有名詞が1文字ずつバラバラになっている類の問題に気づけません。自前で語彙を学習するなら SentencePiece が短く書けます。

import sentencepiece as spm
spm.SentencePieceTrainer.train(
    input="corpus.txt", model_prefix="ja", vocab_size=32000,
    model_type="unigram", character_coverage=0.9995, byte_fallback=True)

character_coverage は文字種のうち何割を語彙で直接カバーするかで、文字種の多い日本語・中国語では 1.0 ではなく 0.9995 あたりが推奨されています。残りをUNKにせず拾うのが byte_fallback です。

現場ではこう使う

誰が、いつ触るか。 LLMアプリのエンジニアが料金と文脈長を見積もるとき、ファインチューニング担当が独自ドメインの語彙を検討するとき、多言語対応の担当者が「なぜこの言語だけ遅くて高いのか」を説明するときです。

触るツール・パラメータ。 OpenAI系なら tiktokencl100k_base などのエンコーディング名)。オープンモデルなら HuggingFace の AutoTokenizer / tokenizers、学習側は sentencepiece--vocab_size --model_type --character_coverage --byte_fallback)。語彙を足すときは tokenizer.add_tokens()model.resize_token_embeddings() が対になります。

事故になる落とし穴。

問われる形。 「日本語のプロンプトが英語より高くつくのはなぜか」——答えの筋道は、UTF-8のバイト長 → コーパスの偏りによる語彙の割り当て → 系列長 → 注意の2乗コストと課金、の4段です。「では語彙を大きくすれば解決か」と重ねられたら、両端のパラメータ増・低頻度トークンの学習不足・モデルサイズとの釣り合いを挙げれば十分です。

まとめ

次は、切り出したトークンを数値ベクトルに変える工程です。埋め込み(Embedding)を1から理解するへ続きます。

参考文献

  1. Neural Machine Translation of Rare Words with Subword Units. arXiv:1508.07909論文ページ·PDF
  2. Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates. arXiv:1804.10959論文ページ·PDF
  3. SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. arXiv:1808.06226論文ページ·PDF

本記事は上記論文の本文にもとづいて執筆しています。数値・主張は原典を優先してください。

コメント

コメントにはログインが必要です