この記事の対象読者
- OpenAI APIを使ったアプリケーションを開発している(またはこれからする)人
- 「日本語だとトークン数どれくらいになるの?」と聞かれて困った経験がある人
- APIのコスト見積もりを上司やクライアントに提出する必要がある人
- LLMのトークナイゼーションの仕組みに興味がある人
この記事で得られること
- なぜ日本語のトークン数を「ざっくり」計算するのが絶望的に難しいか
- BPE(Byte Pair Encoding)の仕組みと、日本語が不利になる構造的理由
- エンコーディング別(
cl100k_base/o200k_base)の日本語トークン効率の実態 -
tiktokenを使った正確なトークン数カウント方法と、コスト見積もりの実践的アプローチ
この記事で扱わないこと
- OpenAI API自体の使い方チュートリアル
- ファインチューニングやEmbeddingのトークン計算
- Anthropic / Google等の他社APIのトークナイザー詳細
1. 事の発端: 「ざっくりでいいから」
ある日、こう頼まれた。
「OpenAI APIを使ったチャットボット、月額どれくらいかかるかざっくり見積もって」
ざっくり。なるほど。英語なら「1単語 ≒ 1.3トークン」みたいな目安がある。じゃあ日本語は?
「1文字1トークンくらいでしょ?」
...いや、違う。
「じゃあ1文節1トークン?」
...それも違う。
日本語のトークン数は、同じ文字数でも内容によって2倍以上変動する。 ざっくり計算なんて概念がそもそも成立しない。
調べれば調べるほど深みにハマったので、その絶望の記録をここに残す。
2. 前提知識: トークンとは何か — 「両替所」の比喩で理解する
トークナイゼーションを理解するために、ここでは「両替所」の比喩を使って説明する。
LLMは人間の言葉をそのまま理解できない。内部では全てをトークンという「通貨」に両替してから処理する。そしてOpenAI APIは、この両替後のトークン数に対して課金する。
ここで問題になるのが、両替レートが言語によって全く違うこと。英語は「優遇レート」で少ないトークンに変換されるが、日本語は「割高レート」で大量のトークンを消費する。
OpenAI APIの料金はトークン単位で課金される。同じ内容の文章でも、日本語で書くと英語の3〜4倍のトークンを消費することがある。コスト見積もりで言語の違いを無視すると、予算が大幅に狂う。
3. BPE(Byte Pair Encoding)— 両替レートはこうして決まる
では、この「両替レート」はどうやって決まるのか。OpenAIのトークナイザーは**BPE(Byte Pair Encoding)**というアルゴリズムを採用している。
3.1 BPEの仕組み — 「よく使う組み合わせを覚える」
BPEを両替所の比喩で言えば、「頻繁に使われる紙幣の組み合わせには専用のトークンを用意する」という仕組みだ。
たとえば英語のテキストでは th というバイト列が大量に出現する。BPEはこれを1つのトークンにまとめる。さらに the も頻出するので、これも1トークンになる。最終的に " the" (スペース含む)が丸ごと1トークンとして登録される。
英語では「単語」や「単語の一部」が効率よくトークン化される。 これが「優遇レート」の正体だ。
3.2 日本語が不利になる構造的理由
一方、日本語はどうか。ここが絶望ポイントの核心。
理由1: UTF-8エンコーディングでバイト数が多い
BPEは文字を直接見ているのではなく、UTF-8のバイト列を見ている。
| 文字種 | UTF-8バイト数 | 例 |
|---|---|---|
| ASCII(英数字) | 1バイト |
a → 0x61
|
| ひらがな・カタカナ | 3バイト |
あ → 0xE3 0x81 0x82
|
| 漢字 | 3バイト |
猫 → 0xE7 0x8C 0xAB
|
| 絵文字 | 4バイト |
🔥 → 0xF0 0x9F 0x94 0xA5
|
英語の cat は3バイト。日本語の 猫 も3バイト。だが、英語の cat はBPEの学習データに大量に出現するため 1トークン になる。一方 猫 は出現頻度が低いため、バイト単位で分割されて 2〜3トークン になる。
同じ「3バイト」でも、BPEの学習データにおける出現頻度によってトークン数が全く変わる。日本語は英語より圧倒的に学習データが少ないため、バイト列のマージが進みにくく、トークン効率が悪い。
理由2: Unicode CJK統合漢字が多すぎる
Unicode 15.1には97,000以上のCJK統合漢字が存在する。cl100k_baseの語彙サイズは約100,000トークンなので、仮にCJK漢字を全部収録したら語彙がそれだけで埋まってしまう。結果として、個々の漢字はバイト列のまま分割されてトークン化される。
理由3: 日本語には「スペースで区切る」という概念がない
英語は単語間にスペースがある。BPEの前処理で " the" のように単語単位のチャンクを作りやすい。日本語は「私は猫が好きです」のように全部くっついている。BPEの正規表現ベースの前処理が、日本語では英語ほどうまく機能しない。
4. 実験: 同じ意味の文を日英で比較してみた
ここからは実際に tiktoken を使って検証する。両替所に日本語と英語を持ち込んで、レートの差を体感しよう。
4.1 環境構築
pip install tiktoken
4.2 検証コード
import tiktoken
def compare_tokens(text_ja: str, text_en: str, encoding_name: str = "o200k_base"):
"""日英テキストのトークン数を比較する"""
enc = tiktoken.get_encoding(encoding_name)
tokens_ja = enc.encode(text_ja)
tokens_en = enc.encode(text_en)
print(f"=== {encoding_name} ===")
print(f"日本語: 「{text_ja}」")
print(f" 文字数: {len(text_ja)}, トークン数: {len(tokens_ja)}, 文字/トークン比: {len(text_ja)/len(tokens_ja):.2f}")
print(f"英語: \"{text_en}\"")
print(f" 文字数: {len(text_en)}, トークン数: {len(tokens_en)}, 文字/トークン比: {len(text_en)/len(tokens_en):.2f}")
print(f" トークン比(日/英): {len(tokens_ja)/len(tokens_en):.2f}倍")
print()
return tokens_ja, tokens_en
# テストケース
pairs = [
("こんにちは", "Hello"),
("私は猫が好きです", "I like cats"),
("本日は晴天なり", "It is sunny today"),
("機械学習モデルの性能を最適化する方法について解説します",
"This article explains how to optimize machine learning model performance"),
("お誕生日おめでとう", "Happy Birthday"),
]
for ja, en in pairs:
compare_tokens(ja, en)
4.3 実行結果 — 絶望のテーブル
| 日本語 | 英語 | JP文字数 | JPトークン | EN文字数 | ENトークン | JP/EN比 |
|---|---|---|---|---|---|---|
| こんにちは | Hello | 5 | 3 | 5 | 1 | 3.0倍 |
| 私は猫が好きです | I like cats | 8 | 6 | 11 | 3 | 2.0倍 |
| 本日は晴天なり | It is sunny today | 7 | 5 | 17 | 4 | 1.25倍 |
| 機械学習モデルの... | This article... | 26 | 15 | 76 | 11 | 1.36倍 |
| お誕生日おめでとう | Happy Birthday | 9 | 8 | 14 | 2 | 4.0倍 |
...orz
「お誕生日おめでとう」が8トークン、"Happy Birthday"が2トークン。4倍。
同じ気持ちを伝えるのに、日本語は英語の4倍のAPI料金がかかる。感情に課金される時代が来てしまった。
5. さらなる絶望: トークン分割の中身を覗く
ここで「じゃあ日本語は1文字何トークンなの?」と思うだろう。覗いてみよう。
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
text = "お誕生日おめでとう"
tokens = enc.encode(text)
print(f"テキスト: {text}")
print(f"トークン数: {len(tokens)}")
print(f"トークンID: {tokens}")
print()
for token_id in tokens:
token_bytes = enc.decode_single_token_bytes(token_id)
try:
decoded = token_bytes.decode("utf-8")
except UnicodeDecodeError:
decoded = repr(token_bytes)
print(f" ID:{token_id:>8} → {repr(token_bytes):>20} → {decoded}")
o200k_base(GPT-4o)での結果:
テキスト: お誕生日おめでとう
トークン数: 8
ID: 8930 → b'\xe3\x81\x8a' → お
ID: 9697 → b'\xe8\xaa' → (不完全)
ID: 243 → b'\x95' → (不完全)
ID: 128225 → b'\xe7\x94\x9f\xe6\x97\xa5' → 生日
ID: 8930 → b'\xe3\x81\x8a' → お
ID: 17693 → b'\xe3\x82\x81' → め
ID: 43834 → b'\xe3\x81\xa7\xe3\x81\xa8' → でと
ID: 12735 → b'\xe3\x81\x86' → う
(;゚д゚)ポカーン
「誕」が2つのトークンに引き裂かれている。b'\xe8\xaa' と b'\x95' は 誕 のUTF-8バイト列 E8 AA 95 が途中で切断されたものだ。一方「生日」は2文字で1トークンにまとまっている。
つまり:
| 文字 | トークン数 | 理由 |
|---|---|---|
| お | 1 | 頻出ひらがな。専用トークンあり |
| 誕 | 2 | 珍しい漢字。バイト列が分割される |
| 生日 | 1 | 「生日」は中国語でも「誕生日」の意味。BPEの学習データに頻出 |
| め | 1 | 頻出ひらがな |
| でと | 1 | 頻出ひらがなの組み合わせ。マージ済み |
| う | 1 | 頻出ひらがな |
同じ日本語の文の中でも、文字ごとにトークン効率がバラバラ。ひらがなは比較的効率が良く(1文字≒1トークン)、漢字は頻度によって1〜3トークンに揺れる。この不規則性が「ざっくり見積もり」を絶望的にする根本原因。
6. エンコーディングの違い — 両替所にも種類がある
OpenAIのモデルごとに使用するエンコーディング(両替所)が異なる。
| エンコーディング | 語彙サイズ | 対応モデル |
|---|---|---|
r50k_base |
約50,000 | GPT-3 |
cl100k_base |
約100,000 | GPT-4, GPT-3.5-turbo |
o200k_base |
約200,000 | GPT-4o, GPT-4o-mini |
語彙サイズが大きいほど、より多くのバイト列の組み合わせを「既知のトークン」として持てる。つまり両替所の品揃えが良くなる。
エンコーディング別の日本語トークン効率
import tiktoken
text = "お誕生日おめでとう"
for enc_name in ["r50k_base", "cl100k_base", "o200k_base"]:
enc = tiktoken.get_encoding(enc_name)
tokens = enc.encode(text)
print(f"{enc_name:>15}: {len(tokens)} tokens")
| エンコーディング | 「お誕生日おめでとう」のトークン数 |
|---|---|
r50k_base |
14 |
cl100k_base |
9 |
o200k_base |
8 |
r50k_base(GPT-3時代)だと14トークン。o200k_base(GPT-4o)だと8トークン。同じ文なのにエンコーディングによって倍近く変わる。
APIのコスト見積もりをするとき、どのモデルを使うかでトークン数自体が変わることを忘れてはいけない。「トークン単価 × トークン数」の両方が変数になるため、モデル選択はダブルで効いてくる。
7. 「ざっくり」の限界と、現実的な見積もり手法
ここまでの調査で分かったのは、日本語のトークン数を「文字数 × 係数」で見積もるのは原理的に不正確だということ。それでもビジネスの現場では見積もりが必要だ。現実的なアプローチを3段階で紹介する。
7.1 レベル1: 超ざっくり(プレゼン・初期提案向け)
精度は低いが、桁感を掴むにはこれで十分。
| テキスト種別 | 目安の係数(文字数 → トークン数) |
|---|---|
| ひらがな中心の文章 | × 0.7〜1.0 |
| 漢字が多い文章 | × 1.0〜1.5 |
| 混合(一般的な日本語) | × 0.8〜1.2 |
| 英語 | × 0.25〜0.35 |
一般的な日本語なら「文字数 ≒ トークン数」が最も雑だが使える近似。
この係数は o200k_base(GPT-4o系)での目安。cl100k_base(GPT-4)ではさらに1.1〜1.3倍程度多くなる。
7.2 レベル2: サンプリング計測(見積書・予算申請向け)
実際のユースケースに近いテキストをサンプルとして用意し、tiktokenでカウントする。
import tiktoken
def estimate_monthly_cost(
sample_texts: list[str],
avg_requests_per_day: int,
model: str = "gpt-4o",
input_price_per_1m: float = 2.50, # $/1Mトークン
output_price_per_1m: float = 10.00, # $/1Mトークン
avg_output_ratio: float = 1.5, # 出力/入力のトークン比
):
"""サンプルテキストからAPI月額コストを見積もる"""
enc = tiktoken.encoding_for_model(model)
token_counts = [len(enc.encode(text)) for text in sample_texts]
avg_input_tokens = sum(token_counts) / len(token_counts)
avg_output_tokens = avg_input_tokens * avg_output_ratio
monthly_requests = avg_requests_per_day * 30
total_input_tokens = avg_input_tokens * monthly_requests
total_output_tokens = avg_output_tokens * monthly_requests
input_cost = (total_input_tokens / 1_000_000) * input_price_per_1m
output_cost = (total_output_tokens / 1_000_000) * output_price_per_1m
print(f"=== 月額コスト見積もり ===")
print(f"モデル: {model}")
print(f"サンプル平均入力トークン: {avg_input_tokens:.0f}")
print(f"推定平均出力トークン: {avg_output_tokens:.0f}")
print(f"月間リクエスト数: {monthly_requests:,}")
print(f"月間入力トークン: {total_input_tokens:,.0f}")
print(f"月間出力トークン: {total_output_tokens:,.0f}")
print(f"入力コスト: ${input_cost:.2f}")
print(f"出力コスト: ${output_cost:.2f}")
print(f"合計: ${input_cost + output_cost:.2f}/月")
return input_cost + output_cost
# 使用例: カスタマーサポートBotの見積もり
sample_queries = [
"商品の返品方法を教えてください。注文番号は12345です。",
"先月購入したノートPCのバッテリーが膨張しています。交換対応は可能でしょうか?",
"配送状況を確認したいのですが、追跡番号がわかりません。名前と住所で検索できますか?",
"クレジットカードの請求額が注文金額と異なるのですが、内訳を確認できますか?",
"解約手続きをお願いします。今月末で契約終了にしてください。",
]
estimate_monthly_cost(sample_queries, avg_requests_per_day=500)
7.3 レベル3: 本番ログ分析(運用最適化向け)
実際にAPIを運用し始めたら、ログからトークン消費を分析して見積もりを修正する。OpenAI APIのレスポンスには usage フィールドが含まれている。
# APIレスポンスの usage フィールド例
{
"usage": {
"prompt_tokens": 156,
"completion_tokens": 342,
"total_tokens": 498
}
}
この usage データを蓄積・集計して、自社のユースケース固有の平均トークン数を算出するのが最も正確なアプローチ。
8. よくあるエラーと対処法
| 症状 | 原因 | 対処法 |
|---|---|---|
tiktokenのインストールでエラー |
Rust toolchainが必要な場合がある |
pip install tiktoken で通常は解決。Rust未インストールの場合は rustup をインストール |
encoding_for_model("gpt-4o") でKeyError |
tiktokenのバージョンが古い |
pip install --upgrade tiktoken で最新版に更新 |
| 日本語のトークンをdecodeしたら文字化け | トークンがUTF-8文字の途中で分割されている |
decode_single_token_bytes() で取得したバイト列を連結してからdecodeする |
| 見積もりと実際のAPI課金額が大きく乖離 | system promptやchat履歴のトークンを見落としている | APIに送信する全メッセージ(system, user, assistant全て)のトークンを合算する |
| 同じ文でもAPI呼び出しごとにトークン数が変わる | 通常は変わらない。変わる場合はテキストに不可視文字が混入している可能性 | テキストを正規化(NFKC等)してから計測 |
最も多い見積もりミス: ユーザーの入力トークンだけを計算して、system promptとchat履歴のトークンを忘れること。実際のAPI課金は「system prompt + 全会話履歴 + 今回の入力 + 出力」の合計に対して発生する。会話が長くなるほど、履歴のトークンが雪だるま式に増えていく。
9. 診断スクリプト — 自分のテキストのトークン効率を一発チェック
以下のスクリプトをコピペすれば、任意のテキストのトークン効率を即座に診断できる。
import tiktoken
import sys
def diagnose_tokens(text: str):
"""テキストのトークン効率を診断する"""
encodings = {
"o200k_base (GPT-4o)": "o200k_base",
"cl100k_base (GPT-4)": "cl100k_base",
}
print(f"入力テキスト: {text[:50]}{'...' if len(text) > 50 else ''}")
print(f"文字数: {len(text)}")
print(f"UTF-8バイト数: {len(text.encode('utf-8'))}")
print()
for label, enc_name in encodings.items():
enc = tiktoken.get_encoding(enc_name)
tokens = enc.encode(text)
ratio = len(text) / len(tokens) if tokens else 0
byte_ratio = len(text.encode('utf-8')) / len(tokens) if tokens else 0
print(f"--- {label} ---")
print(f" トークン数: {len(tokens)}")
print(f" 文字/トークン比: {ratio:.2f} (1.0に近いほど効率的)")
print(f" バイト/トークン比: {byte_ratio:.2f}")
# トークン効率の評価
if ratio >= 1.0:
print(f" 評価: ✅ 良好(1文字1トークン以下)")
elif ratio >= 0.5:
print(f" 評価: ⚠️ 普通(日本語の平均的な効率)")
else:
print(f" 評価: ❌ 非効率(漢字・特殊文字が多い可能性)")
print()
# 使用例
if __name__ == "__main__":
test_texts = [
"こんにちは、今日はいい天気ですね。",
"機械学習における勾配降下法の最適化手法について概説する。",
"Hello, it's a nice day today.",
"RTX 5090でCUDA環境を構築してローカルLLMを動かす手順を解説します。",
]
for text in test_texts:
diagnose_tokens(text)
print("=" * 60)
10. 2026年版: モデル別コスト早見表
APIコストの計算式は単純だが、変数が多い。整理しておく。
\text{コスト} = \frac{\text{入力トークン数}}{1{,}000{,}000} \times P_{\text{in}} + \frac{\text{出力トークン数}}{1{,}000{,}000} \times P_{\text{out}}
主要モデルの料金(2026年3月時点):
| モデル | エンコーディング | 入力 ($/1Mトークン) | 出力 ($/1Mトークン) |
|---|---|---|---|
| GPT-4o | o200k_base | $2.50 | $10.00 |
| GPT-4o-mini | o200k_base | $0.15 | $0.60 |
| GPT-4 | cl100k_base | $30.00 | $60.00 |
GPT-4oはGPT-4と比べて、トークン単価が約1/12に下がっているだけでなく、o200k_baseエンコーディングにより日本語のトークン効率も改善されている。日本語ユースケースでは「単価の安さ × トークン効率の向上」でダブルの恩恵がある。
日本語1,000文字あたりの目安コスト
| モデル | 入力1,000文字 | 出力1,000文字 |
|---|---|---|
| GPT-4o | 約$0.0025 | 約$0.010 |
| GPT-4o-mini | 約$0.00015 | 約$0.0006 |
| GPT-4 | 約$0.039 | 約$0.078 |
※ 日本語1,000文字 ≒ 1,000トークンとして概算(o200k_baseの場合。cl100k_baseはこの1.1〜1.3倍)
11. 学習ロードマップ
この記事で日本語トークナイゼーションの「闇」に触れた。次のステップとして以下を推奨する。
| ステップ | リソース | 所要時間 |
|---|---|---|
| tiktoken公式リポジトリ | openai/tiktoken | 30分 |
| OpenAI Cookbook トークン計数 | How to count tokens | 1時間 |
| Tiktokenizer(ブラウザで試せる) | tiktokenizer.vercel.app | 15分 |
| BPEの原論文 | Sennrich et al., 2016 "Neural Machine Translation of Rare Words with Subword Units" | 2時間 |
| Karpathy氏のTokenizer解説 | Let's Build the GPT Tokenizer | 3時間 |
まとめ
「ざっくり計算して」という一言から始まった調査は、想像以上に深い沼だった。
分かったこと:
- 日本語のトークン数は文字数から正確に推定できない。 ひらがな・カタカナ・漢字・英数字の混合比率、さらには個々の漢字の出現頻度によって大きく変動する
- 日本語は英語の2〜4倍のトークンを消費する。 これはBPEの学習データにおける英語の圧倒的な出現頻度が原因で、構造的な問題
-
エンコーディングによってトークン数が変わる。
o200k_baseはcl100k_baseより日本語のトークン効率が改善されている -
正確な見積もりには
tiktokenによる実測が不可欠。 「文字数 × 係数」は桁感を掴む以上の精度は期待できない
正直、日本語話者としてはこの「トークン効率の格差」にやるせなさを感じる。英語なら "Happy Birthday" が2トークンで済むのに、「お誕生日おめでとう」は8トークン。同じ祝福なのに4倍課金される世界線。
とはいえ、GPT-4o世代のエンコーディングで日本語の効率は着実に改善されてきている。o200k_baseの語彙サイズ拡大は、非英語圏への確かな一歩だ。今後のさらなる改善に期待しつつ、現時点ではtiktokenで実測するのが最も誠実な見積もり方法だと結論づけたい。
参考文献