トークナイザを1から — LLMが世界を切り刻む単位
LLMは文字も単語も読んでいない。BPEがどう語彙を作るか、SentencePieceが何を解決したか、なぜ日本語は同じ内容で損をするのか、語彙サイズを増やすと何と何が交換されるのかを、前提知識ゼロから手計算とコードで追う。
Neural Machine Translation of Rare Words with Subword Units
一次資料 — この記事の根拠
この解説の公開 2026-08-22
Neural Machine Translation of Rare Words with Subword UnitsarXiv:1508.07909論文ページ·PDFSubword Regularization: Improving Neural Network Translation Models with Multiple Subword CandidatesarXiv:1804.10959論文ページ·PDF
SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text ProcessingarXiv:1808.06226論文ページ·PDF
LLMは文字を読んでいない
「こんにちは」とモデルに送ったとき、中に入っていくのは文字ではありません。整数の並びです。文字列を整数列に変換する係がトークナイザ、変換の単位がトークンです。
番号の振り方——どこで文章を切り、何を1個と数えるか——を決めているのはニューラルネットではなく、学習が始まる前に固定される別のプログラムです。そしてこの切り方が、料金・文脈長・計算コスト・言語ごとの得手不得手まで決めてしまいます。地味な前処理に見えて、実際はモデルが世界を見るときの解像度そのものです。
比喩: レゴブロックの箱を設計する
文章を組み立てる作業をレゴに例えます。あなたは箱に入れるブロックの種類を、たとえば5万種類だけ選べます。
- 1×1の小さいブロックだけにする。何でも作れますが、城を建てるのに何万個も要ります。
- 「城」「家」「船」の完成品を入れる。1個で済んで速いですが、箱に無い「灯台」は作れません。
文字単位のトークナイザが前者、単語単位が後者です。前者は語彙が数百で未知語もゼロですが系列が極端に長い。後者は1トークンが意味を持つ代わりに語彙が数十万に膨れ、それでも新語・固有名詞・タイポはこぼれ落ちて「未知語(UNK)」という捨て札になります。
実用されているのは中間の設計、サブワードです。よく出る語はまるごと1個、珍しい語は破片の組み合わせで表す。「灯台」が箱に無くても「灯」+「台」で作れるようにしておく、という考え方です。
仕組み: BPE — 一番よく隣り合う2つをくっつける
サブワード語彙を決める最も広く使われている方法が BPE(Byte Pair Encoding) です。もとは1994年に提案されたデータ圧縮アルゴリズムで、2016年に機械翻訳へ持ち込まれました。出自が圧縮なのは偶然ではなく、「頻出パターンに短い符号を割り当てる」という発想はエントロピー符号化と同じ家系です。
手順は単純です。
- まず全部を1文字に分解する(これが初期語彙)
- コーパス全体で最も多く隣り合っている2つ組を数える
- その2つ組を新しいトークンとして語彙に追加し、該当箇所を全部くっつける
- 語彙が目標サイズになるまで 2〜3 を繰り返す
式(1)は「いま数えた中で、最も出現回数が多い隣接ペア を選ぶ」と言っているだけです。 は「最大にする引数を返す」記号、 は の直後に が来た回数です。
つまり、隣り合った2つ組ごとに正の字を書いていって、いちばん多かった組を指差す——それだけのことです。 の星印は「その回で選ばれた勝者」を指しています。
手で追ってみます。コーパスが 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 t で est——と、頻度の高い塊から順に大きくなっていく。結果として low や est のような、人間が見ても意味のありそうな断片が自然に生まれます。ただしこれは統計の副産物で、文法や辞書は一切使っていません。
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などが使用)は、マージ相手を生の頻度ではなく のようなスコアで選びます。「ただ多いペア」ではなく「単独で出るより一緒に出がちなペア」を優先する、という違いです。
Unigram言語モデル(SentencePieceの既定のひとつ)は発想が逆さまで、大きめの候補語彙から要らないものを削っていきます。各トークンに確率 を持たせ、分割全体の尤度を次で評価します。
式(2)は「その分割の尤もらしさは、使った各トークンの確率を全部かけ算したもの」という意味です。 は「並んだものを全部かけ算する」記号、 が分割結果(トークンの列)、 が 番目のトークンの出やすさです。
言い換えると、切り方の点数は「使った部品それぞれのありふれ度を掛け合わせた値」ということ。珍しい部品を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乗に比例します。
トークン効率の悪さは、料金・文脈・速度の3方向に同時に効きます。日本語に強いモデルが「トークナイザを鍛え直した」と強調するのは、精度の話であると同時に単価の話でもあるわけです。
語彙サイズの取引
では語彙を大きくすればよいのか。ここが設計上いちばん面白い交換です。
語彙 を増やすと1トークンが表す文字数が増え、系列が短くなります。系列が短ければ注意のコストは2乗で効いて下がり、同じ文脈長により多くの内容が入り、推論時にK・Vを溜めるKVキャッシュの消費も減ります。
代わりに払うものが3つあります。入口と出口が太る——埋め込み行列は ( は次元数)、出力側のsoftmaxも同じ形で、小さいモデルほどこの「両端」が全体を食います。珍しいトークンが育たない——語彙の末尾には滅多に出ないトークンが並び、その埋め込みは学習信号をほとんど受け取れません。出力層の計算が重くなる——生成は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系なら tiktoken(cl100k_base などのエンコーディング名)。オープンモデルなら HuggingFace の AutoTokenizer / tokenizers、学習側は sentencepiece(--vocab_size --model_type --character_coverage --byte_fallback)。語彙を足すときは tokenizer.add_tokens() と model.resize_token_embeddings() が対になります。
事故になる落とし穴。
- 文字数で見積もると外す。 課金・
max_tokens・文脈長は全部トークン基準。日本語では文字数と大きくずれるので、実測してから設計する。 - 末尾の空白。 多くのBPEでは空白が次の語の先頭にくっつきます。プロンプトを空白で終わらせると「空白+語」というトークンの出番を奪い、生成が不自然になることがあります。
- stop文字列がトークン境界と一致しない。 停止語は文字列でもモデルが吐くのはトークン。境界がずれると止まらない/余計に出ます。
- トークナイザとチェックポイントの不一致。 語彙を足して埋め込み行列のリサイズを忘れると、IDが範囲外になるか無言でずれます。足したトークンの埋め込みは乱数初期値なので、学習しなければ意味を持ちません。
- 数値の桁。 数字の塊の切れ方はトークナイザ依存で、割れ方が不揃いだと算術が不安定になります。計算はツール呼び出しに逃がす。
- 正規化で情報が消える。 SentencePieceの正規化(NFKC系)は全角・半角や一部記号をまとめます。その区別が意味を持つデータでは設定を確認する。
問われる形。 「日本語のプロンプトが英語より高くつくのはなぜか」——答えの筋道は、UTF-8のバイト長 → コーパスの偏りによる語彙の割り当て → 系列長 → 注意の2乗コストと課金、の4段です。「では語彙を大きくすれば解決か」と重ねられたら、両端のパラメータ増・低頻度トークンの学習不足・モデルサイズとの釣り合いを挙げれば十分です。
まとめ
- トークナイザは学習前に固定される別プログラムで、モデルが世界を見る解像度を決める
- BPEは「最頻の隣接ペアをくっつける」の繰り返し。WordPieceはスコアで、Unigramは尤度で選ぶ
- SentencePieceは空白前提を捨てて可逆にしたことで、日本語のような言語を土俵に乗せた
- 日本語の不利は、3バイト文字・英語偏りコーパス・空白なしの3段重ね。効くのは料金と速度
- 語彙サイズは「系列の短さ」と「両端のパラメータ・低頻度トークンの学習」の交換
次は、切り出したトークンを数値ベクトルに変える工程です。埋め込み(Embedding)を1から理解するへ続きます。
コメント
コメントにはログインが必要です