🤖

久しぶりにtiktokenでトークナイザの性能変遷を確認してみる

に公開

執筆日

2025/09/16

概要

最近トークナイザの変化についてリリースノートにあんまり書かれてないなと思ったので今一度トークン化の効率を確認してみようという趣旨です。

結論として、GPT-4o以降はトークナイザは変更がなく、共通のo200k_baseを使っていてトークン化の効率は変わっていませんでした。
そんなにトークナイザを作り直すメリットないのか、既に十分最適化されていてこれ以上になることが期待できないのか、トークナイザを作り直したときに書き直さなければならない諸々のことを考えると現実的ではないのか。その辺りの事情は分かりません。

検証

インストール

$ pip install tiktoken=0.11.0

コード

count_tokens.py
import tiktoken

def count_tokens(text, model="gpt-4o"):
    encoder = tiktoken.encoding_for_model(model)
    tokens = encoder.encode(text)
    token_count = len(tokens)
    
    return token_count

text = "<日英を含む2000字弱のサンプルテキスト>"
print(f"テキスト: {len(text)} 文字")

token_count = count_tokens(text, model="gpt-3.5-turbo")
print("# GPT-3.5-turbo")
print(f"トークン数: {token_count}")
print(f"token per char: {token_count / len(text):.3f}")

token_count = count_tokens(text, model="gpt-4")
print("# GPT-4")
print(f"トークン数: {token_count}")
print(f"token per char: {token_count / len(text):.3f}")

token_count = count_tokens(text, model="gpt-4o")
print("# GPT-4o")
print(f"トークン数: {token_count}")
print(f"token per char: {token_count / len(text):.3f}")

token_count = count_tokens(text, model="gpt-4.1")
print("# GPT-4.1")
print(f"トークン数: {token_count}")
print(f"token per char: {token_count / len(text):.3f}")

token_count = count_tokens(text, model="gpt-5-chat-latest")
print("# GPT-5-chat-latest")
print(f"トークン数: {token_count}")
print(f"token per char: {token_count / len(text):.3f}")

実行結果

$ python count_tokens.py

テキスト: 1756 文字
# GPT-3.5-turbo
トークン数: 1328
token per char: 0.756
# GPT-4
トークン数: 1328
token per char: 0.756
# GPT-4o
トークン数: 970
token per char: 0.552
# GPT-4.1
トークン数: 970
token per char: 0.552
# GPT-5-chat-latest
トークン数: 970
token per char: 0.552

GPT-4oからトークナイザの変更がないことがわかります。

トークナイザの確認方法

tiktokenライブラリのtiktoken/model.pyMODEL_PREFIX_TO_ENCODINGMODEL_TO_ENCODINGを確認することで使用されているトークナイザが確認できます。
今回初めてちゃんと各モデルのトークナイザを確認していて気付いたのですが、embeddingモデルはGPT-4くらいの時に開発されたtext-embedding-3-large(small)で止まっているため、GPT-4と同じく1つ古いcl100k_baseを使っておりあまりトークン化の効率は良くないんですよね……。

余談

トークナイザの名前

  • cl100k_base: Chat Languageのcl、100,000個のユニークトークンの辞書、基本系(何かに特化した辞書ではなく汎用的な辞書)を示すbase
  • o200k_base: omniのo、200,000個のユニークトークンの辞書、基本系を示すbase

というようなネーミング規則になっているようです。漢字とかも学習してるのにそんな数でいいんだ?と思ったのですが、UTF-8をバイト分割してるから漢字や日本語は1文字1トークンよりさらに小さく分割されることもあるみたいです。なるほど。
最近出たOSSのgpt-oss-シリーズでは、o200k_harmonyというトークナイザが使われているようです。(harmony系の詳細についてはこの辺りを読めばわかりそう)

トークン分割

o200k_baseが実際どんなトークン分割をしているのか調べている人がいました。
https://zenn.dev/hellorusk/articles/27684d0ed96c4c
単語が長くても頻出する場合は1つのトークンにしているのがわかりますが、出てきたサンプルが明らかに2(5)ちゃんねるのテキストが大量に学習に使われ、"風吹けば名無し""VIPがお送りします"が1トークンになっているのが少し面白いですね。(GPTが台頭してきたときに掲示板風のテキストを生成させる遊びがSNSで流行っていたのも原因なのでは……?という気もします)
GPTが採用しているByte Pair Encoding (BPE)の特徴をGPTに教えてもらいました

仕組み

元々はデータ圧縮の手法を NLP に応用したもの。
学習時に以下を繰り返す:

  1. コーパスを文字単位で分割する(初期状態)。
  2. 頻度が最も高い文字ペアを探す。
  3. それを新しい「トークン」として辞書に追加。
  4. それをコーパスに適用して再構築。

このペアマージを繰り返し、辞書サイズが指定の大きさになるまで拡張する。

特徴

  • 決定的(デターミニスティック):ルールが一意に決まる。
  • 頻出語に強い:よく使われる単語は短いトークンで表せる。
  • 希少語に弱い:未知語は細かく分割されがち。
ヘッドウォータース

Discussion