4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「OpenAI APIのトークン消費量をざっくり計算して」と頼まれて調べたら絶望した話

4
Posted at

この記事の対象読者

  • 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バイト a0x61
ひらがな・カタカナ 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時間

まとめ

「ざっくり計算して」という一言から始まった調査は、想像以上に深い沼だった。

分かったこと:

  1. 日本語のトークン数は文字数から正確に推定できない。 ひらがな・カタカナ・漢字・英数字の混合比率、さらには個々の漢字の出現頻度によって大きく変動する
  2. 日本語は英語の2〜4倍のトークンを消費する。 これはBPEの学習データにおける英語の圧倒的な出現頻度が原因で、構造的な問題
  3. エンコーディングによってトークン数が変わる。 o200k_basecl100k_baseより日本語のトークン効率が改善されている
  4. 正確な見積もりにはtiktokenによる実測が不可欠。 「文字数 × 係数」は桁感を掴む以上の精度は期待できない

正直、日本語話者としてはこの「トークン効率の格差」にやるせなさを感じる。英語なら "Happy Birthday" が2トークンで済むのに、「お誕生日おめでとう」は8トークン。同じ祝福なのに4倍課金される世界線。

とはいえ、GPT-4o世代のエンコーディングで日本語の効率は着実に改善されてきている。o200k_baseの語彙サイズ拡大は、非英語圏への確かな一歩だ。今後のさらなる改善に期待しつつ、現時点ではtiktokenで実測するのが最も誠実な見積もり方法だと結論づけたい。


参考文献


4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?