Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /home/zhenxiangba/zhenxiangba.com/public_html/phproxy-improved-master/index.php on line 456
LLMのトークン
[go: Go Back, main page]

LLMのトークン

LLM(大規模言語モデル)は文字ではなくトークン(token)というものを単位として処理します。トークンは単語に近いものですが、必ずしも単語とは一致しません。

文字列をトークンに分割するツールをトークナイザ(tokenizer)といいます。OpenAIのLLMでは tiktoken というトークナイザが使われます。tiktoken はいくつかのエンコーディング(トークン分割方式)に対応しており、古くは cl100k_base というエンコーディングが使われていましたが、GPT 4oからは o200k_base に変わりました。gpt-oss は o200k_harmony ですが、特殊トークンが増えた以外は o200k_base と変わりなさそうです。

pip install tiktoken して試してみましょう:

import tiktoken

enc = tiktoken.get_encoding("o200k_base")
# または enc = tiktoken.encoding_for_model("gpt-4o")

次の例で試してみましょう(山本義隆『熱学思想の史的展開』(現代数学社,1987年)より):

s = "「何人ものニュートンがいた(There were several Newtons)」と言ったのは,科学史家ハイルブロンである.同様にコーヘンは「ニュートンはつねに二つの貌を持っていた(Newton was always ambivalent)」と語っている."
e = enc.encode(s)
for i in e:
    c = enc.decode([i])
    if len(c) == 1 and ord(c) == 65533:  # 65533は「�」
        print(i, end="|")
    else:
        print(repr(c)[1:-1], end="|")
print()

次のように67トークンに分割されていることがわかります:

「|何|人|もの|ニュ|ート|ン|が|いた|(|There| were| several| Newton|s|)」|と言|った|の|は|,|科学|史|家|ハ|イル|ブ|ロン|で|ある|.|同|様|に|コ|ー�|246|ン|は|「|ニュ|ート|ン|は|つ|ね|に|二|つ|の|貌|を|持|って|いた|(|Newton| was| always| amb|ivalent|)」|と|語|って|いる|.|

日本語は必ずしもUTF-8の文字の境界で切れるわけではなく、上の「コーヘン」のように文字の途中で切れることもあります。

別の例です。これらは文字単位の処理が苦手なLLMの弱点を突くプロンプトとして有名なものです:

How| many| r|'s| are| in| strawberry|?|

「|いっぱい|」の|「|い|」を|「|お|」に|変|えて|ください|。|

このように、" strawberry" や "いっぱい" は1トークンになるので、その中にどういう文字が含まれるかは別に学習していないとこれらの問題は解けません。

日本語ではどんなものがトークンとして登録されているのでしょうか。ちょっと調べてみました。

import tiktoken
import re

enc = tiktoken.get_encoding("o200k_base")

japanese_re = re.compile('[\u3000-\u30ff\u4e00-\u9fff]')

# 3000-30ff CJK記号と句読点、ひらがな、カタカナ
# 4e00-9fff CJK統合漢字
## 31f0-31ff カタカナ追加
## 3220-325f, 3280-33ff CJK文字いろいろ
# 3400-4dbf CJK統合漢字拡張A
# f900-faff CJK互換漢字

words = {}
for i in range(enc.n_vocab):
    s = enc.decode([i])
    words[i] = s
sorted_words = sorted(words.items(), key=lambda item: len(item[1]), reverse=True)
# for i, s in sorted_words[:100]:
#     print(i, s)

for i, s in sorted_words[:100000]:
    if japanese_re.search(s):
        print(i, repr(s)[1:-1])

一部を抜き出します:

(前略)
113862 ありがとうございました
181081  微信公众号天天中彩票
185118 _日本毛片免费视频观看
93926 ありがとうございます
147058 VIPがお送りします
170996  微信上的天天中彩票
187716  微信里的天天中彩票
188394  天天中彩票大神推荐
46669  天天中彩票app
55935  彩神争霸大发快三
(中略)
77298 @お腹いっぱい
(中略)
87123 風吹けば名無し
(中略)
123086 がお送りします
(中略)
44948  名無しさん
(後略)

何を学習させたのか想像がついてしまいます。

中国語のものは微信(WeChat)の宝くじ関係のものが多いようですが、アダルト関係も混ざっているようです。

「_日本毛片免费视频观看」は「_日本ポルノ無料動画視聴」という意味ですが、不思議なことにGPT-4oはこのトークンが読めないようです。「_日本毛片免费视频观看」を日本語に訳してくれと言っても何も見えないらしいのです(GPT-5で修正されました)。初期のChatGPTのトークナイザ p50k_base では「 SolidGoldMagikarp」などがChatGPTが読めないトークンとして有名でしたが、新トークナイザでも同じようなことが起こっているのかもしれません(→グリッチトークン参照)。


大規模言語モデル PLaMo 2 のためのトークナイザ性能改善という記事を見て、PLaMo 2のトークナイザも試してみたくなりました。

from mlx_lm import load, generate

model, tokenizer = load("mlx-community/plamo-2-8b-bf16",
                        tokenizer_config={"trust_remote_code": True})
tokenizer.add_eos_token("<|plamo:bos|>")
s = "「何人ものニュートンがいた(There were several Newtons)」と言ったのは,科学史家ハイルブロンである.同様にコーヘンは「ニュートンはつねに二つの貌を持っていた(Newton was always ambivalent)」と語っている."

e = tokenizer.encode(s)
for i in e:
    c = tokenizer.decode(i)
    print(repr(c)[1:-1], end="|")
print()
<|plamo:bos|>|「|何人|も|の|ニュー|トン|がいた|(|There were| several| Newton|s|)」|と言った|の|は,|科学|史|家|ハイ|ル|ブロン|である|.|同様に|コー|ヘン|は「|ニュー|トン|は|つねに|二つ|の|貌|を持っていた|(|Newton| was always| |ambi|valent|)|」と|語っている|.|
japanese_re = re.compile('[\u3000-\u30ff\u4e00-\u9fff]')
japanese_words = [(k, v) for k, v in tokenizer.vocab.items() if japanese_re.search(k)]
sorted_japanese_words = sorted(japanese_words, key=lambda x: len(x[0]), reverse=True)

for k, v in sorted_japanese_words[:30]:
    print(v, repr(k))
58984 'いただきありがとうございました。'
68757 'あけましておめでとうございます。'
54912 'いただきありがとうございます。'
66397 '明けましておめでとうございます'
51379 'お気軽にお問い合わせください'
59328 '本当にありがとうございました'
63073 'してみてはいかがでしょうか。'
74903 'いつもありがとうございます。'
78833 'いただきありがとうございます'
81104 'ことができるようになります。'
85013 'もいるのではないでしょうか。'
50286 'よろしくお願いいたします。'
57225 'させていただいております。'
64602 'お気軽にお問い合わせ下さい'
70922 'することをおすすめします。'
75898 'できるようになっています。'
76399 'というわけではありません。'
85032 'いつもありがとうございます'
48448 'ありがとうございました。'
58809 'できるようになりました。'
58983 '心よりお待ちしております'
61422 'みてはいかがでしょうか。'
61959 'するものではありません。'
65294 'よろしくお願いいたします'
65859 'お気軽にお問合せください'
66682 '頂きありがとうございます'
68465 'させていただきますので、'
70634 'をさせていただきました。'
70678 'YouTubeチャンネル'
75441 'させていただいています。'

OpenAIのものと比べて、とてもまともです。

PLaMo 2トークナイザは10万語彙すべて自分が目を通しているのでグリッチトークンはありません。秒間10語彙見れば半日で全部確認できるのに大手がそれをしていないのはちょっと手抜きでh(ry

— いもす (@imos) September 13, 2025

GoogleのオープンなモデルGemma 3のトークナイザを調べてみましょう。ここではMLX版を使います。

from mlx_lm import load

model, tokenizer = load("mlx-community/gemma-3-27b-it-8bit")

s = "昔々あるところにおじいさんとおばあさんがいました。"

e = tokenizer.encode(s)
for i in e:
    c = tokenizer.decode(i)
    if len(c) == 1 and ord(c) == 65533:  # 65533は「�」
        print(i, end="|")
    else:
        print(repr(c)[1:-1], end="|")
print()
<bos>|昔|々|ある|ところ|にお|じ|い|さんと|お|ば|あ|さんが|いました|。|
import re

japanese_re = re.compile('[\u3000-\u30ff\u4e00-\u9fff]')

# words = list(tokenizer.vocab.items())
japanese_words = [(k, v) for k, v in tokenizer.vocab.items() if japanese_re.search(k)]
sorted_japanese_words = sorted(japanese_words, key=lambda x: len(x[0]), reverse=True)

for k, v in sorted_japanese_words[:30]:
    print(v, repr(k))
120581 '▁中国证券投资基金业协会'
107678 'ありがとうございました'
196460 'よろしくお願いします'
169209 '▁スタッドレスタイヤ'
202172 'のではないでしょうか'
86108 'ありがとうございます'
155504 'する必要があります'
118906 '证券投资基金业协会'
109516 'することができます'
208832 '▁ショルダーバッグ'
128544 'させていただきます'
179513 'コミュニケーション'
234640 'クレジットカード'
178498 'お願いいたします'
211554 '▁ホイールセット'
229394 'きたいと思います'
158641 'してみてください'
228487 'ようになりました'
171006 'ることができます'
149888 'アプリケーション'
146623 '存于互联网档案馆'
202105 '可能性があります'
102788 '場合がございます'
127161 'することができる'
209893 'かもしれませんが'
231432 'したいと思います'
171559 '必要があります'
152335 '中华人民共和国'
157513 '▁ファッション'
216277 '▁生命周期函数'

Geminiのトークナイザは公開されていないようですが、トークンの個数は数えられます。

from google import genai

client = genai.Client()

total_tokens = client.models.count_tokens(
    model="gemini-2.5-pro", contents="hello"
)
print("total_tokens:", total_tokens) # 2

おそらく <bos> が先頭に付くため 1 だけ多くなります。

いろいろやってみると、Gemini 3のトークナイザと同じでないかという気がしてきました。