日本語はなぜトークン代が高いのか。BPEを一から実装して測ったら、「3倍高い」は言い過ぎだった
約7分で読めます

日本語はなぜトークン代が高いのか。BPEを一から実装して測ったら、「3倍高い」は言い過ぎだった


LLMの料金はトークン単位で決まる。そして「日本語は英語よりトークンを食うので高くつく」という話をよく聞く。実務でAPIを叩いていると無視できない話だが、実際どれくらいなのかを自分で測ったことがなかった。

そこで、トークナイザの中身であるBPE(Byte Pair Encoding)をゼロから実装し、仕組みを理解したうえで、自分のブログ49記事を実際のトークナイザにかけて測ってみた。結果は、よく言われる「3倍」という数字が半分正しくて半分ミスリードだという、思ったより面白いものだった。

BPEの仕組み: 頻出ペアを繰り返し合体させるだけ

BPEのアルゴリズムは驚くほど単純だ。テキストをバイト列にして、一番よく隣り合っているペアを1つの新しい記号に置き換える。これを語彙サイズの分だけ繰り返す。

for k in range(num_merges):
    pair, freq = get_stats(ids).most_common(1)[0]  # 最頻ペアを見つけて
    ids = merge(ids, pair, 256 + k)                # 新しいIDに置き換える

「低い 高い 高低差」の繰り返しで学習させると、最初はバイトの断片(日本語の1文字は3バイトなので、まず文字を組み立てるところから)、やがて「高い」「低い 高い」のような意味のある単位が語彙として生まれてくる。言語の知識は一切与えていないのに、頻度統計だけで単語らしきものが浮かび上がるのが面白い。

ハンデは出発点から: 日本語1文字=3バイト

そもそもBPEはバイト列から出発する。ここに最初の非対称がある。

UTF-8バイト数の比較。「あ」は3バイトなのに"a"は1バイト。「日本語」9バイト vs "abc" 3バイト。「拡散モデル」15バイト vs "diffusion" 9バイト

英語のアルファベットは1文字1バイトだが、日本語は1文字3バイト。マージを始める前の時点で、日本語は3倍の長さの列から出発することになる。

自作BPEで測る: 語彙を増やしても差は縮まらない

自分のブログ49記事(12万字)と、比較用の英語テキスト(『不思議の国のアリス』を同じ文字数だけ)で、語彙サイズを変えながら圧縮率を測った。

左: 語彙数を増やすほど1トークンあたりの文字数が増えるが、英語(青)は常に日本語(赤)の上を行く。語彙3000で英語4.38文字/トークン、日本語1.85文字/トークン。右: 英語÷日本語の効率比は語彙を増やしても2.4倍前後で横ばい

語彙を増やせばどちらも効率は上がる。だが両者の比は2.4倍前後のまま、ほとんど縮まらない。日本語は「文字を組み立てる」ためにまず語彙を消費してしまうので、単語レベルの圧縮に回せる余力が英語より少ないのだ。

実際のトークナイザで測る

自作BPEはおもちゃなので、本物(GPT-4o系のo200k_base)でも測った。

  • 日本語(自ブログ12万字): 88,071トークン = 1.38文字/トークン
  • 英語(アリス12万字): 31,070トークン = 3.93文字/トークン
  • 同じ文字数なら、日本語は英語の2.83倍のトークンを消費

ちなみに一世代前のcl100k_base(GPT-3.5/4系)では日本語1.04文字/トークンで、比は3.75倍だった。新しいトークナイザで日本語の効率は3割ほど改善している——「日本語は3倍高い」という通説は、少し古い世代の数字なのだ。

ただし「同じ意味」で比べると1.38倍まで縮む

ここが今回いちばん面白かった点だ。文字数を揃えた比較はフェアではない。日本語の1文字は英語の1文字より多くの情報を運ぶからだ。そこで同じ内容の対訳ペアで測り直した。

対訳4組のトークン数比較。「こんにちは、世界。」4tok vs "Hello, world." 4tok(1.0倍)、「拡散モデルをnumpyで一から実装しました。」13tok vs 英訳10tok(1.3倍)、機械学習の文25tok vs 14tok(1.8倍)、統計の文23tok vs 16tok(1.4倍)。平均1.4倍

平均1.38倍。文字数ベースの2.83倍と比べると、半分以下だ。「こんにちは、世界。」と”Hello, world.”に至っては4トークンで同点である。

実際の分割を並べると、その差の正体が見える。

同じ意味の文のトークン分割。日本語25トークンは「この記事」「では」「、」「機」「械」「学」「習」…と1文字単位に砕けている部分が多い。英語14トークンは"In" "␣this" "␣article" "␣we" "␣verify"…と単語ごとにまとまっている

英語は単語がそのまま1トークンになる一方、日本語は「機」「械」「学」「習」と1文字ずつに砕けてしまう。ただし「この記事」「では」「ます」のように、頻出する塊はちゃんと1トークンに圧縮されている。日本語は不利だが、言われるほど絶望的でもない

12万字を入力した場合の実コスト(3/Mトークン換算)は日本語3/Mトークン換算)は日本語0.264 / 英語$0.093。同じ文字数なら約2.8倍だが、同じ内容を伝えるコストなら1.4倍程度、というのが実測から言える結論だ。

手を動かして意外だったこと

「日本語は3倍高い」を検証するつもりが、何と比べるかで答えが2.83倍にも1.38倍にもなるという、比較の設計そのものの話になった。文字数を揃えた比較は一見公平だが、実際には「同じ文字数でより多くを語れる」という日本語の性質を無視している。ベンチマークの数字を読むときは、揃えているものが本当に揃えるべきものかを疑うべきだ——Datasaurusの回から続く「数字は嘘をつかないが、何を測っているかは別問題」の系譜に、また1つ実例が増えた。

もう1つは、トークナイザが世代交代で日本語に優しくなっていること(3.75倍→2.83倍)。語彙を9万から20万に増やす際、日本語の頻出パターンに多く割り当てられた結果だ。トークナイザの語彙配分は、どの言語のユーザーを大事にするかという設計判断そのものでもある。

実務的な結論としては、日本語プロンプトのコストを見積もるとき「文字数÷1.4」がざっくりのトークン数になる(英語なら「文字数÷3.9」)。長いシステムプロンプトを英語で書くとコストが減るのは事実だが、それは1.4倍分であって、3倍ではない。

まとめ

  • BPE(頻出バイトペアを繰り返し合体させる圧縮)をゼロから実装。言語知識ゼロでも頻度統計だけで単語らしき語彙が生まれる
  • 出発点の非対称: UTF-8で日本語1文字=3バイト、英語1文字=1バイト
  • 自作BPEでの測定: 語彙を3000まで増やしても、英語と日本語の効率比は2.4倍前後で横ばい
  • 実トークナイザ(o200k_base)で自ブログ12万字を測定: 日本語1.38文字/トークン vs 英語3.93文字/トークン = 同じ文字数なら2.83倍。ただし旧世代のcl100k_baseでは3.75倍で、新トークナイザは日本語に3割優しくなっている
  • 同じ意味の対訳で比べると差は平均1.38倍まで縮む。「日本語は3倍高い」は、文字数を揃えた比較のときだけ正しい