トークナイザーは日本語・多言語でどう効くか — Fertilityと語彙配分の理論

「こんにちは、世界」と “Hello, world” ——意味はほぼ同じなのに、LLM に入れると日本語のほうが多くのトークンを消費することをご存じでしょうか。API 料金はトークン数で決まり、モデルが一度に読める長さ(コンテキスト窓)もトークン数で頭打ちになります。つまり同じ内容を扱っても、日本語ユーザーは英語ユーザーより高いコストを払い、短い文脈しか使えない——そんな構造的な不平等が、トークナイザーの中で静かに起きています。

なぜこんなことが起きるのでしょうか。犯人は「トークナイザー」です。トークナイザーは文章をサブワード(部分文字列)の列に切り分ける前処理で、その語彙の多くは英語をはじめとする空白区切りの言語で学習されています。日本語や中国語のように空白で単語を区切らない言語、そして漢字・かなのように1文字が UTF-8 で3バイトを占める文字体系は、この仕組みと相性が悪く、細かく刻まれてしまうのです。

この記事を読むと、次のような実務的な判断ができるようになります。

  • LLM のコスト見積もり: 日本語アプリのトークン消費がなぜ英語より多いのかを定量的に説明できる
  • トークナイザーの選定・再学習: 日本語中心のサービスで語彙を作り直すべきか、既製品で足りるかを判断できる
  • 多言語モデルの公平性: なぜ低リソース言語が不利になるのか(語彙配分問題)を理解し、緩和策を選べる

本記事では、Fertility(肥沃度)という1つの指標を軸に、「トークナイザーが言語ごとにどう効くか」を根っこから解き明かします。スクラッチで書いた byte-level BPE を実際に多言語コーパスに学習させ、日本語・中国語が3倍近く非効率になる様子を実測し、さらにそれを改善する方法まで見ていきましょう。

同じ意味の一文でも、日本語・中国語はトークン数が数倍に膨らむ

上の図は、この記事の出発点です。英語中心で学習した同じトークナイザーで、同じ内容の一文を分割すると、英語10トークンに対し日本語は60トークン——実に6倍です。この差がどこから来て、どう縮められるのか。順に見ていきます。

本記事の内容

  • トークナイザーの効率を測る指標 Fertility(肥沃度)とは
  • 空白のない言語(日本語・中国語)がなぜ難しいのか
  • UTF-8 と byte-level BPE:漢字が3トークンに「爆発」する仕組み
  • 多言語モデルの語彙配分問題と、サンプリング温度による緩和
  • スクラッチ byte-level BPE での実測と、日本語再学習の効果

前提知識

以下の記事を先に読むと、本記事の理解が深まります。

サブワード分割(BPE など)の基本的な仕組みを知っていれば、あとは本文で説明します。

トークナイザーの効率をどう測るか — Fertility

まず、「トークナイザーが効率的かどうか」を測るものさしを用意しましょう。使うのは Fertility(肥沃度) という指標です。名前は仰々しいですが、意味はとてもシンプルです。

イメージとしては、「1つの文字(または単語)が、平均していくつのトークンに割れてしまうか」を表す数字です。1トークンで長い意味のかたまりを運べるなら効率的、1文字ごとにバラバラに刻まれるなら非効率、というわけです。

$$ \text{Fertility} = \frac{\text{トークン数}}{\text{文字数(または単語数)}} $$

Fertility が低いほど効率的です。たとえば英語の “river” が1トークンにまとまれば、5文字で1トークンなので fertility は $5$ 分の $1 = 0.2$。逆に日本語の「川」が UTF-8 の3バイトにバラバラに分解されると、1文字で3トークンなので fertility は $3.0$ です。同じ「1文字」でも、15倍の開きが出ることになります。

Fertility(肥沃度)の定義。トークン数÷文字数で効率を測る

この図が示すとおり、fertility は「トークナイザーがその言語をどれだけ手際よく畳めているか」の通信簿です。英語なら 0.3 前後、日本語や中国語だと 2〜3 になりがち——これがトークン数の不平等の正体です。では、なぜ言語によってここまで差がつくのでしょうか。理由は大きく2つあります。「空白の有無」「文字のバイト数」です。順に見ていきましょう。

理由その1:空白のない言語は「境界」を自力で探すしかない

英語の文には、単語の切れ目に空白があります。”the quick brown fox” と書けば、どこからどこまでが1単語かは一目瞭然です。トークナイザーにとって、この空白はタダでもらえる「単語境界のヒント」です。実際、ほとんどのトークナイザーは最初に空白で文を大まかに区切ってから(pre-tokenization)、各単語の中をサブワードに分けます。

ところが日本語・中国語・タイ語には、単語のあいだに空白がありません。「すばやい茶色の狐」という文字列のどこで区切るべきかは、書いてある文字だけからはわかりません。「すばやい/茶色/の/狐」なのか「すば/やい/茶/色/の/狐」なのか——境界はデータから学ぶしかないのです。

空白のある言語とない言語のpre-tokenization。英語は境界が無料、日本語は学習が必要

この違いは決定的です。英語では空白のおかげで「単語」という自然なまとまりが最初から手に入るので、トークナイザーは「よく出る単語」を丸ごと1トークンに割り当てられます。日本語ではその足がかりがないため、境界の学習に大量のデータが必要になり、学習コーパスに日本語が少ないと、結局うまくまとめられず細切れになります。

ただ、空白の有無だけが問題なら、日本語コーパスをたっぷり学習させれば解決するはずです。実は、それでも残るもう1つの根深い理由があります。文字が占めるバイト数です。

理由その2:UTF-8 のバイト数と「byte-level BPE」

現代の多くのトークナイザー(GPT 系など)は、文字そのものではなく UTF-8 のバイト列を最小単位にした byte-level BPE を使います。なぜバイト単位かというと、こうすれば世界中のどんな文字(未知の絵文字や珍しい漢字も含めて)も必ず256種類のバイトの組み合わせで表現でき、「未知語で壊れる」ことがなくなるからです。堅牢性のための賢い設計です。

しかし、この設計は文字体系によって恩恵が偏ります。UTF-8 では、文字によって必要なバイト数が違うのです。

UTF-8のバイト構造。ASCIIは1バイト、かな・漢字は3バイト

図のとおり、英語の A は1バイト、アクセント付きの é は2バイト、そしてひらがな・カタカナ・漢字は3バイトを占めます。つまり日本語は、そもそもの出発点で英語の3倍のバイトを持っているのです。

ここに byte-level BPE が絡むと、こうなります。BPE は「よく隣り合うバイトのペア」を繰り返しマージして語彙を作ります。英語中心のコーパスで学習すると、英語の頻出バイト列はどんどんマージされて「river」のような長いトークンになります。ところが、学習コーパスにほとんど現れない漢字のバイト列はマージされる機会がなく、バラバラの単独バイトのまま残ります。

語彙にない漢字がbyte-level BPEで3バイトトークンに爆発する

結果として、語彙に入りそこねた漢字1文字は「3つのバイトトークン」に爆発します。英語の1単語がしばしば1トークンで済むのと比べると、この差は歴然です。次の節では、この現象を実際にスクラッチ実装で再現し、fertility を測ってみます。

スクラッチ byte-level BPE で実測する

言葉で説明するより、動かして確かめましょう。byte-level BPE を最小限のコードで実装します。単位は UTF-8 のバイト(0〜255)です。

from collections import Counter

def get_stats(vocab):
    pairs = Counter()
    for word, freq in vocab.items():
        for i in range(len(word) - 1):
            pairs[(word[i], word[i + 1])] += freq
    return pairs

def merge_vocab(pair, vocab):
    a, b = pair
    new = {}
    for word, freq in vocab.items():
        w, i, out = word, 0, []
        while i < len(w):
            if i < len(w) - 1 and w[i] == a and w[i + 1] == b:
                out.append(a + b); i += 2      # 2つのトークン(tuple)を結合
            else:
                out.append(w[i]); i += 1
        t = tuple(out)
        new[t] = new.get(t, 0) + freq
    return new

各「トークン」をバイトのタプルで表しているのがポイントです。最初は1バイト=1トークン((0xE5,) のような長さ1のタプル)で、マージするとタプルが連結されて長いトークンになります。次に、テキストをバイト列に変換する部分と、学習ループです。

def to_words(text, has_space):
    # 空白言語は空白区切り(先頭に空白を付与)、非空白言語は一文をそのまま
    chunks = [" " + w for w in text.split()] if has_space else [text]
    return [tuple((b,) for b in ch.encode("utf-8")) for ch in chunks]

def train_bpe(corpus, num_merges):
    vocab = Counter()
    for text, has_space in corpus:
        for w in to_words(text, has_space):
            vocab[w] += 1
    vocab, merges = dict(vocab), []
    for _ in range(num_merges):
        pairs = get_stats(vocab)
        if not pairs: break
        best = max(pairs.items(), key=lambda kv: (kv[1], kv[0]))[0]  # 頻度最大(tie=辞書順)
        vocab = merge_vocab(best, vocab)
        merges.append(best)
    return merges

def encode(text, has_space, merges):
    toks = []
    for w in to_words(text, has_space):
        symbols = list(w)
        for a, b in merges:
            i, out = 0, []
            while i < len(symbols):
                if i < len(symbols)-1 and symbols[i]==a and symbols[i+1]==b:
                    out.append(a + b); i += 2
                else:
                    out.append(symbols[i]); i += 1
            symbols = out
        toks += symbols
    return toks

これで、テキストを空白言語なら空白区切りで、非空白言語なら一文まるごとバイト列にして、学習したマージ規則で符号化できます。わざと英語を多め(3倍)にした多言語コーパスで学習し、各言語のテスト文を分割してみましょう。現実のトークナイザーが英語中心のデータで作られている状況を模します。

en = ("the quick brown fox jumps over the lazy dog near the river "
      "children are playing in the park and the sun is shining today "
      "learning a new language takes time but the reward is great "
      "she sells sea shells by the sea shore every single morning ")
es = ("el rapido zorro marron salta sobre el perro perezoso "
      "los ninos juegan en el parque y el sol brilla hoy ")
corpus = [(en, True)] * 3 + [(es, True)]     # 英語を3倍にして高リソース偏りを作る
merges = train_bpe(corpus, 400)

TEST = {"英語": ("the fox jumps over the lazy dog by the river", True),
        "スペイン語": ("el zorro salta sobre el perro junto al rio", True),
        "日本語": ("茶色の狐が川のそばで怠けた犬を飛び越える", False),
        "中国語": ("棕色的狐狸在河边跳过了懒狗", False)}
for lang, (sent, sp) in TEST.items():
    n = len(encode(sent, sp, merges))
    nchar = len(sent.replace(" ", ""))
    print(f"{lang}: {n} トークン, fertility={n/nchar:.2f}")

このコードを実行すると、次のような結果になります(実測値):

英語: 10 トークン, fertility=0.29
スペイン語: 15 トークン, fertility=0.44
日本語: 60 トークン, fertility=3.00
中国語: 39 トークン, fertility=3.00

言語別fertilityの実測。英語0.29に対し日本語・中国語は3.0

数字がはっきり物語っています。英語の fertility は 0.29、スペイン語は 0.44——ラテン文字どうしはバイトを共有するので、英語で学習した語彙がスペイン語にもそこそこ流用できます。ところが日本語・中国語はきっかり 3.00。これは「すべての文字が、マージされないまま UTF-8 の3バイトに分解された」ことを意味します。学習コーパスに日本語が1文字も入っていないので、漢字やかなのバイト列は一度もマージされず、byte-level のフォールバックがそのまま出ているのです。20文字の日本語文が60トークンになるのは、まさにこの $20 \times 3$ です。

この結果は、実務でよく言われる「日本語は英語よりトークンを食う」という経験則の、根っこにあるメカニズムそのものです。しかもこの差は、単なる豆知識では済みません。トークン数の多寡は、実務の3つの場面で直接コストとして跳ね返ってきます。次にそれを確認してから、改善策に進みましょう。

トークン数の差が効く3つの実務場面

Fertility の差を「机上の数字」と侮ってはいけません。トークン数は、LLM を使うシステムの根幹的なコストに直結しています。

1つめは、料金です。 商用 LLM の API はほぼすべて「トークン数」で課金されます。同じ内容の問い合わせでも、日本語が英語の3倍のトークンを使うなら、単純計算で入力コストが3倍になります。日本語で大量の文書を処理するサービスでは、この差がそのまま運用費に効いてきます。fertility を 3.0 から 1.2 に下げられれば、コストを半分以下にできる計算です。

2つめは、コンテキスト窓です。 モデルが一度に読める長さは「〇〇トークンまで」と決まっています。日本語は1文字あたりのトークンが多いぶん、同じトークン上限でも実際に詰め込める文字数(=情報量)が少なくなります。長文の要約や、たくさんの資料を渡す RAG(検索拡張生成)では、この「実効的な文脈の短さ」がボトルネックになります。英語なら1回で収まる資料が、日本語だと分割せざるを得ない、という事態が起きるのです。

3つめは、速度とレイテンシです。 生成モデルはトークンを1つずつ順番に出力します。出力すべきトークンが多ければ、それだけ生成に時間がかかり、計算資源も食います。日本語の応答が英語より遅く感じられる一因は、ここにもあります。

つまり fertility は、コスト・文脈・速度という3つの軸すべてに効く「効率の総合指標」なのです。この数字を下げることには、はっきりした実利があります。では、この非効率はどう改善できるのでしょうか。

改善策1:対象言語をコーパスに入れて再学習する

もっとも直接的な対策は、トークナイザーの学習コーパスに日本語を入れて作り直すことです。日本語が十分に含まれれば、頻出する漢字・かなのバイト列がマージされ、「川」「狐」といった文字が1〜2トークンにまとまるようになります。実験で確かめましょう。先ほどのコーパスに日本語の文を加えて、もう一度学習します。

ja = ("すばやい茶色の狐が怠けた犬を飛び越える"
      "子供たちは公園で遊んでいて今日は太陽が輝いている"
      "新しい言語を学ぶには時間がかかる")
corpus_ja = [(en, True)] * 3 + [(es, True)] + [(ja, False)] * 2
merges_ja = train_bpe(corpus_ja, 400)
# 同じ TEST で fertility を測り直す

日本語を加えた再学習でfertilityが改善。日本語3.0→1.2

図の灰色(英語中心)とオレンジ(日本語も追加)を比べてください。日本語の fertility は 3.00 から 1.20 へと激減しました。日本語のバイト列がマージされて、複数文字が1トークンにまとまるようになったからです。一方で英語・スペイン語の効率は変わらず、日本語を足したことで既存言語が犠牲になってはいません。

興味深いのは中国語です。fertility は 3.00 → 2.85 とほとんど改善していません。今回は学習コーパスに日本語しか足さなかったので、中国語の漢字(日本語と一部しか重ならない)はやはりマージされないままなのです。これは大切な教訓を含んでいます——「対象言語そのものを入れないと、その言語は救われない」。多言語対応とは、言語を1つずつコーパスに含めていく地道な作業なのです。

ここで自然な疑問が湧きます。「では、世界中の言語を全部コーパスに入れれば公平になるのか?」——話はそう単純ではありません。次はその落とし穴、語彙配分問題を見ます。

語彙配分問題とサンプリング温度

多言語トークナイザーを作るとき、コーパスは言語ごとにデータ量が大きく違います。英語は Web に大量にありますが、たとえばスワヒリ語やアイスランド語は桁違いに少ない。この状態で素直に学習すると、データの多い言語(高リソース言語)が語彙を独占してしまいます。BPE は頻度の高いペアからマージするので、英語のバイト列ばかりがマージされ、低リソース言語は後回し——結果、fertility が高いまま放置されるのです。

語彙サイズを増やしても、学習コーパスに無い言語は改善しない

この図は「語彙サイズ(マージ回数)を増やせば解決するのでは?」という期待に冷や水を浴びせます。英語中心コーパスで学習した場合、語彙をいくら大きくしても、コーパスに入っていない日本語・中国語の fertility は 3.0 に張り付いたままです。効率が上がるのは、データが存在する英語・スペイン語だけ。語彙の大きさではなく、そもそもの「データの取り分」が問題なのです。

そこで実際の多言語モデル(XLM-R や mT5 など)が使うのが、サンプリング温度による平滑化です。各言語のデータ量 $n_i$ をそのまま使うのではなく、$\alpha$ 乗して確率を作ります:

$$ p_i = \frac{n_i^{\alpha}}{\sum_j n_j^{\alpha}} $$

$\alpha = 1$ なら素のデータ比率そのまま(高リソースが独占)。$\alpha$ を小さく(たとえば 0.3)すると、大きい言語の取り分が抑えられ、小さい言語が相対的に持ち上げられます。極端に $\alpha \to 0$ にすると全言語が均等になります。

サンプリング温度αを下げると低リソース言語の取り分が増える

図を見ると、$\alpha=1.0$ では英語が7割以上を占めていますが、$\alpha=0.3$ まで下げると各言語の比率がぐっと平らになり、低リソース語の取り分が増えているのがわかります。これにより、低リソース言語の語彙が確保され、fertility の不平等が緩和されます。ただしトレードオフもあります——高リソース言語を人為的に薄めるので、英語などの性能はわずかに犠牲になります。「どの言語をどれだけ大事にするか」という設計判断が、この $\alpha$ 一つに凝縮されているわけです。

なぜ「語彙を巨大にして全部1トークンに」しないのか

ここで、素朴な疑問が浮かびます。「fertility を下げたいなら、語彙をものすごく大きくして、あらゆる単語・フレーズを1トークンに割り当てればいいのでは?」——実は、それはできません。fertility を下げることには、逆向きのコストがついて回るからです。

第一に、語彙サイズは埋め込み行列の大きさに直結します。語彙を10万から100万に増やせば、モデルの入力・出力の埋め込みパラメータもそれだけ膨らみ、メモリと計算が重くなります。第二に、まれなトークンは十分に学習されません。ある単語を専用トークンにしても、それが訓練データにめったに出てこなければ、その埋め込みは「訓練不足」のまま放置され、かえって性能を落とします。頻度の低いものを無理に1トークンにするのは逆効果なのです。

情報理論の言葉でいえば、トークナイザーは「短い符号(=低い fertility)」と「符号表の小ささ(=小さい語彙)」のあいだのトレードオフを解いています。よく出るものだけを長いトークンにまとめ、まれなものはサブワードやバイトに任せる——この頻度に応じた按分こそ、BPE や Unigram がやっていることの本質です。だからこそ「全部1トークン」は最適解にならず、言語ごとに適切な粒度を探すことになります。

このトレードオフを踏まえたうえで、温度による平滑化はサブワードの学習量を調整する策でした。日本語にはもう1つ、まったく別のアプローチがあります。形態素解析です。

改善策2:形態素解析による前区切り

日本語の「境界がわからない」問題に正面から取り組むのが、形態素解析(MeCab、Sudachi など)です。これは辞書と文法を使って、文を「意味のある最小単位(形態素)」にあらかじめ区切る技術です。「茶色の狐が」を「茶色/の/狐/が」のように分けてくれます。

トークナイザーの前段でこの形態素解析をかけておくと、英語の空白と同じ役割を果たします。つまり日本語にも「単語境界のヒント」を人工的に与えられるのです。そのうえでサブワード分割をすれば、境界をゼロから学ぶ負担が減り、語彙が効率的にまとまります。実際、日本語特化の BERT の多くは、MeCab などで前区切りしてから WordPiece や Unigram を適用しています。

一方で、形態素解析にもコストはあります。辞書のメンテナンスが必要で、新語や崩れた表記に弱く、処理も重くなります。「純粋なサブワードだけで済ませるか、形態素解析を挟むか」は、アプリの性質(新語の多さ、速度要件、精度要件)で選ぶことになります。

ここまでの選択肢を、状況別に整理しておきましょう。

日本語・多言語でのトークナイザー選択の早見表

この早見表のとおり、正解は1つではありません。英語中心なら既製品で十分ですし、日本語中心なら語彙の再学習が効きます。多言語の公平性を重視するならサンプリング温度、形態素の粒度が大事なら前区切り——アプリが何を最優先するかで、打つ手が変わるのです。

最後に、こうした原理が実在のトークナイザーでどう現れているかを見ておきましょう。私たちのスクラッチ実験は、現実のシステムの縮図になっています。

実際のトークナイザーの事情

本記事で作った byte-level BPE は、おもちゃではありますが、実在のトークナイザーと同じ原理で動いています。代表的なものを整理しておきます。

GPT 系の byte-level BPE。 GPT-3.5 や GPT-4 で使われるトークナイザー(cl100k_base と呼ばれる、約10万語彙)は、まさに本記事と同じ byte-level BPE です。登場初期は日本語がかなり細切れで、英語の数倍のトークンを消費していました。その後の世代(GPT-4o 系で使われる約20万語彙の o200k_base など)では、多言語データを増やして語彙を拡張したことで、日本語や中国語の効率が目立って改善しました。これは本記事の「対象言語をコーパスに入れて再学習する」という改善策を、実際に世界規模でやった結果だと理解できます。

SentencePiece の Unigram。 一方、mT5 や XLM-R といった多言語モデルの多くは、Google の SentencePiece が実装する Unigram 言語モデル方式のトークナイザーを使います。SentencePiece の設計思想は「言語を選ばない」ことにあり、空白すら1つの記号()として扱い、生のテキストから直接サブワードを学習します。空白のない日本語・中国語でも特別な前処理なしに動くよう作られているのが特徴です。Unigram 方式の仕組み自体は奥が深いので、別記事で詳しく扱います。

日本語特化モデルの前区切り。 日本語に特化した BERT の多く(東北大版など)は、MeCab のような形態素解析器で文をあらかじめ区切ってから、WordPiece や Unigram を適用します。本記事の「形態素解析による前区切り」を実装したもので、日本語の語彙効率と精度を両立させる定番の構成です。

いずれのアプローチも、突き詰めれば本記事で見た「空白・バイト数・語彙配分」という3つの論点への、それぞれ異なる回答です。トークナイザーの中身を一度理解しておくと、新しいモデルが出てきたときも「このモデルは日本語をどう畳んでいるのか」を自分で見積もれるようになります。

まとめ

本記事では、トークナイザーが日本語・多言語でどう効くかを、Fertility を軸に解き明かしました。要点を整理します。

  • Fertility(肥沃度)= トークン数 ÷ 文字数。低いほど効率的。英語は 0.3 前後、日本語・中国語は 2〜3 になりがち
  • 不平等の2大原因は「空白の有無」(境界ヒントの有無)と「UTF-8 バイト数」(漢字・かなは3バイト)
  • byte-level BPE は堅牢だが、学習コーパスに無い文字はマージされず、1文字が3バイトトークンに爆発する(実測で英語10トークンの内容が日本語60トークン)
  • 改善策:対象言語をコーパスに入れて再学習(日本語 fertility 3.0→1.2 を実測)、サンプリング温度 $\alpha$ での語彙配分の平滑化、形態素解析による前区切り
  • 語彙サイズを増やすだけでは、そもそもデータに無い言語は救われない

トークナイザーは「ただの前処理」と見過ごされがちですが、そこにこそ言語間のコストと性能の不平等が埋め込まれています。日本語を扱うなら、モデル本体だけでなく、その入口にあるトークナイザーが自分の言語をどう畳んでいるかに、一度目を向けてみる価値があります。

次のステップとして、以下の記事も参考にしてください。