トークン:モデルから見た文字
DeepSeek と OpenAI のトークナイザーで中国語、英語、漢文、コードを実際に分割し、モデルが文字を何に切り分けているのか、それがなぜ料金を決めるのか、そしてモデルが文字数をうまく数えられない理由を見ていきます。
- 約 35 分
- 難易度:入門
- 検証:2026-09-14 deepseek-flash、DeepSeek V4 トークナイザー、tiktoken o200k_base
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
LLM はトークン(token)単位で課金され、コンテキストウィンドウもトークンで数え、出力の上限もトークンで設定します。前の課で usage の prompt_tokens と completion_tokens を見ました。では、トークンとはいったい何でしょうか。漢字 1 文字が 1 トークンでしょうか。英単語 1 語ならどうでしょう。
これは料金計算だけの問題ではありません。モデルには奇妙な癖があります。たとえば、文の文字数を正しく数えられないことがある。その根っこもトークンにあります。この課では文字を実際に切り分けて見てみます。
モデルは文字を読めない、読めるのは数字だけ
ニューラルネットワークが扱えるのは数字だけです。そのため文字はモデルに入る前に、トークナイザー(tokenizer)と呼ばれるプログラムを通ります。トークナイザーは固定の語彙表を持っていて、文字を語彙表にある断片に切り分け、断片ごとに番号を割り当てます。モデルが見るのは番号の列で、出力するのも番号です。最後にトークナイザーがそれを文字に戻します。
この断片がトークンです。1 文字のこともあれば、1 語、英単語の半分、句読点、数個の空白のこともあります。DeepSeek の語彙表には 129,280 個のトークンがあり、OpenAI の GPT-4o 世代が使う o200k_base の語彙表には 200,019 個あります。
語彙表はモデルを学習する前に、大量のテキストの統計から作られます。よく一緒に現れる文字の組み合わせが、一つのトークンにまとめられるのです。このアルゴリズムはモジュール 09 の第 1 課でゼロから実装します。今は結論だけ覚えておいてください。よくある組み合わせは一つのトークンにまとめられ、まれなものはバラバラにされる。
実際に切ってみる
DeepSeek はトークナイザーを公開していて、ダウンロードして手元で使えます。ダウンロード先から入手して展開すると、deepseek_v4_tokenizer ディレクトリができます。さらにパッケージを二つ入れます。
uv add tokenizers tiktoken
tokenizers は DeepSeek のトークナイザーファイルを読み込むため、tiktoken は OpenAI のトークナイザーです。次のスクリプトは同じ文章をいくつか二つのトークナイザーに渡し、切り出された断片を一つずつ表示します。
import tiktoken
from tokenizers import Tokenizer
deepseek = Tokenizer.from_file("deepseek_v4_tokenizer/tokenizer.json")
openai_enc = tiktoken.get_encoding("o200k_base") # GPT-4o 等 OpenAI 模型用的分词器
samples = [
"人工智能正在改变软件开发的方式,越来越多的程序员开始用大模型写代码。",
"Artificial intelligence is changing how software is built, and more programmers now write code with large language models.",
"学而时习之,不亦说乎?有朋自远方来,不亦乐乎?",
"def add(a, b):\n return a + b\n",
"3.1415926535897932384626",
"😀",
]
def deepseek_pieces(text):
ids = deepseek.encode(text, add_special_tokens=False).ids
return [deepseek.decode([i]) for i in ids]
def openai_pieces(text):
pieces = []
for i in openai_enc.encode(text):
raw = openai_enc.decode_single_token_bytes(i)
# 一个汉字或表情可能被拆成几个字节,单独拿出来不是合法的 UTF-8
pieces.append(raw.decode("utf-8", errors="replace"))
return pieces
for text in samples:
ds, oa = deepseek_pieces(text), openai_pieces(text)
print(f"原文({len(text)} 个字符):{text!r}")
print(f" DeepSeek {len(ds):3d} 个词元:{ds}")
print(f" OpenAI {len(oa):3d} 个词元:{oa}")
print()
実行結果です。
原文(34 个字符):'人工智能正在改变软件开发的方式,越来越多的程序员开始用大模型写代码。'
DeepSeek 15 个词元:['人工智能', '正在', '改变', '软件开发', '的方式', ',', '越来越多的', '程序员', '开始', '用', '大', '模型', '写', '代码', '。']
OpenAI 21 个词元:['人工', '智能', '正在', '改变', '软件', '开发', '的', '方式', ',', '越来越', '多', '的', '程序', '员', '开始', '用', '大', '模型', '写', '代码', '。']
原文(122 个字符):'Artificial intelligence is changing how software is built, and more programmers now write code with large language models.'
DeepSeek 20 个词元:['Artificial', ' intelligence', ' is', ' changing', ' how', ' software', ' is', ' built', ',', ' and', ' more', ' programmers', ' now', ' write', ' code', ' with', ' large', ' language', ' models', '.']
OpenAI 20 个词元:['Artificial', ' intelligence', ' is', ' changing', ' how', ' software', ' is', ' built', ',', ' and', ' more', ' programmers', ' now', ' write', ' code', ' with', ' large', ' language', ' models', '.']
原文(23 个字符):'学而时习之,不亦说乎?有朋自远方来,不亦乐乎?'
DeepSeek 22 个词元:['学', '而', '时', '习', '之', ',', '不', '亦', '说', '乎', '?', '有', '朋', '自', '远方', '来', ',', '不', '亦', '乐', '乎', '?']
OpenAI 21 个词元:['学', '而', '时', '习', '之', ',不', '亦', '说', '乎', '?', '有', '朋', '自', '远', '方', '来', ',不', '亦', '乐', '乎', '?']
原文(32 个字符):'def add(a, b):\n return a + b\n'
DeepSeek 12 个词元:['def', ' add', '(a', ',', ' b', '):\n', ' ', ' return', ' a', ' +', ' b', '\n']
OpenAI 12 个词元:['def', ' add', '(a', ',', ' b', '):\n', ' ', ' return', ' a', ' +', ' b', '\n']
原文(24 个字符):'3.1415926535897932384626'
DeepSeek 10 个词元:['3', '.', '141', '592', '653', '589', '793', '238', '462', '6']
OpenAI 10 个词元:['3', '.', '141', '592', '653', '589', '793', '238', '462', '6']
原文(1 个字符):'😀'
DeepSeek 2 个词元:['�', '�']
OpenAI 1 个词元:['😀']
この 1 画面の出力には、じっくり見る価値のある点がいくつもあります。
現代中国語は、DeepSeek のほうが粗く切る。「人工智能」(人工知能)、「软件开发」(ソフトウェア開発)、「越来越多的」(ますます多くの)は、DeepSeek の語彙表ではどれも 1 トークンです。同じ文を DeepSeek は 15 トークン、OpenAI は 21 トークンで表します。語彙表は学習コーパスの統計で作られ、DeepSeek のコーパスには中国語が多いので、より多くの中国語の語句がまとめられています。同じ中国語の内容なら、トークンが少ないほど費用は安く、一度にコンテキストへ詰め込める量も増えます。
英語は、両社でまったく同じ。よくある英単語はほぼ 1 トークンで、空白は後ろの単語にくっつきます。' intelligence' の前に空白が一つ付いています。122 文字が 20 トークンで、ほぼ 1 単語 1 トークンです。
漢文は、ほぼ 1 文字 1 トークン。「学而时习之」はどちらの語彙表でも単独の 5 文字です。現代中国語でよく使うのは「学习」(学習)や「时间」(時間)といった組み合わせで、「时习」や「不亦」のような組み合わせはまれなので、まとめられていません。同じ 23 文字でも、漢文は現代中国語の倍のトークンを使います。
コードの空白もお金になる。インデントの 4 つの空白のうち、3 つの空白が独立した 1 トークンになっています。Python のコードはインデントが深くなるほど、空白が占めるトークンが増えます。
数字は 3 桁ずつに切られる。3.1415926... は 141、592、653 といった塊に切られています。モデルに見えているのは一つの完全な数ではなく、いくつかの「3 桁の断片」です。これはモデルが多桁の計算を間違えやすい理由の一つです。筆算では 1 桁ずつそろえる必要があるのに、入力そのものが 1 桁ずつになっていないのです。
絵文字はバイト単位に分解されることがある。DeepSeek は 😀 を 2 トークンに切っており、単独でデコードするとどちらも文字化けの � になります。各トークンはこの絵文字のエンコードの一部のバイトにすぎず、両方をつなげて初めて完全な文字になるからです。
料金計算でトークン数を見積もるには
最も正確な方法は二つあります。公式のトークナイザーで手元で数えるか、API が返す usage を直接見ることです。
それが難しいときは、ざっくり見積もることもできます。DeepSeek の公式ドキュメントでは、英語 1 文字が約 0.3 トークン、中国語 1 文字が約 0.6 トークンという換算が示されています。API でいくつかの文章を実測したところ、結果は公式の数字より低くなりました。
| テキスト | 文字数 | トークン数 | 1 文字あたりのトークン数 |
|---|---|---|---|
| 現代中国語(上の文) | 34 | 15 | 0.44 |
| 英語(上の文) | 122 | 20 | 0.16 |
| 漢文(上の文) | 23 | 22 | 0.96 |
| Python のコード | 32 | 12 | 0.38 |
| 絵文字 5 個 | 5 | 10 | 2.00 |
測り方は次のとおりです。同じ質問で max_tokens=1 にして、prompt_tokens だけを見ます。まず文字 a だけのメッセージを送り、その入力トークン数を基準(書式マーカーの分)として記録し、次に測りたい文章を送って、二つの差に 1 を足したものがその文章自体のトークン数です。
def count_tokens(text):
r = client.chat.completions.create(
model="deepseek-flash",
messages=[{"role": "user", "content": text}],
max_tokens=1,
extra_body={"thinking": {"type": "disabled"}},
)
return r.usage.prompt_tokens
base = count_tokens("a") # 格式标记加上字母 a 本身,我测到的是 5
print(count_tokens("学而时习之,不亦说乎?有朋自远方来,不亦乐乎?") - base + 1)
公式ドキュメントの数字は控えめなので、予算を立てるにはちょうどよく、超過しにくくなります。ただしトークン数は内容次第であることを覚えておいてください。古文、珍しい漢字、絵文字、あまり使われない言語は、ふだんの中国語より「高く」つきます。
モデルが文字数を数えられない理由
有名な例があります。モデルに「strawberry に r はいくつある?」と聞くと、多くのモデルが 2 と答えますが、正解は 3 です。
トークナイザーがこの単語をどう扱うか見てみましょう。DeepSeek は単独の strawberry を st、raw、berry の 3 つに切りますが、前に空白の付いた strawberry(文の途中に出てくるとこうなります)は丸ごと 1 トークンです。モデルが受け取るのは一つの番号(79430)で、中の文字を直接「見て」いるわけではありません。この単語に r がいくつあるかをモデルが知っているとすれば、それは学習中に似た議論を見たことがあるからにすぎません。
とはいえ、この例はあまりに有名なので、今のモデルはたいてい見たことがあります。deepseek-flash にいくつかの聞き方で答えさせ、それぞれ思考オフと思考オンで 3 回ずつ聞いてみました。
| 質問 | 思考オフ | 思考オン |
|---|---|---|
| 英語で strawberry の r の数を聞く | 3, 3, 3 | 3, 3, 3 |
| 中国語で strawberry の r の数を聞く | 2, 3, 3 | 3, 3, 3 |
| 中国語で raspberry の r の数を聞く | 3, 2, 3 | 3, 3, 3 |
| 「秋天的叶子一片片落下」(秋の葉が一枚また一枚と落ちる)は何文字か | 10, 10, 11 | 10, 10, 10 |
あの古い問題は英語で聞けば全問正解になっていますが、中国語で聞く、単語を替える、漢字の数を数えさせる、とすると、思考オフではときどき間違えます。思考オンでは全問正解です。モデルが思考過程で単語を 1 文字ずつ書き出してから数えるので、自分でトークンを分解しているのと同じことになるからです。
実際の開発では、文字数のカウント、文字列の長さ、テキストの切り詰めなど、正確に数える必要があることはモデルに任せず、コードで処理してください。len("秋天的叶子一片片落下") はいつでも 10 です。
よくある質問
モデルが違ってもトークン数はそのまま比較できますか? できません。同じ中国語の文章でも、DeepSeek と OpenAI ではトークン数が 3 割違います。二つのモデルの料金を比べるときは、同じ実際の業務テキストでそれぞれトークン数を出し、各自の単価を掛けて比べます。単価だけを比べてはいけません。
コードで tiktoken を使って DeepSeek のトークン数を数えてもいいですか? 英語とコードならほぼ同じですが、中国語は多めに出ます。正確さが必要なら DeepSeek 自身のトークナイザーを使ってください。
練習問題
- あなたの名前、住んでいる都市の名前、方言の一文を
tokens.pyの二つのトークナイザーに渡し、それぞれどう切られるか見てください。 count_tokensで試してみてください。同じ内容を中国語で書いた場合と英語で書いた場合、どちらのトークンが多いでしょうか。仕事で実際に使う文章でも試してみましょう。- JSON データ(たとえば何かの API の戻り値)を用意してトークン数を数え、それを 1 行に圧縮して空白をすべて取り除いてからもう一度数えてください。トークン数はどれだけ減りましたか。大量のデータをモデルに渡す必要があるときに役立ちます。
確認テスト
1. 同じ現代中国語の文でも、DeepSeek のトークン数は OpenAI のトークナイザーより少なくなります。なぜでしょうか。それはあなたにどう影響しますか?
トークナイザーの語彙表は学習コーパスの統計から作られます。DeepSeek のコーパスは中国語の比率が高いので、より多くの中国語の語句が 1 トークンにまとめられています。影響としては、同じ中国語の内容なら DeepSeek のほうが費用が安く、一度にコンテキストに入れられる量も多くなります。モデルの料金を比べるときは、実際のテキストでトークン数を出してから比べます。
2. モデルが単語の中の特定の文字の数を数え間違えやすいのはなぜですか?
モデルに見えているのはトークンの番号で、文字ではありません。よくある単語はたいてい 1 トークンなので、モデルは中の文字を直接「見る」ことができず、学習時の記憶に頼って答えるしかありません。思考をオンにすると、モデルは先に単語を 1 文字ずつ書き出してから数えるので、正答率がずっと上がります。正確に数える必要があるときは、モデルではなくコードを使うべきです。
3. 漢文が同じ文字数の現代中国語より「高く」つくのはなぜですか?
語彙表がまとめるのは、学習コーパスでよく現れる文字の組み合わせです。現代中国語の常用語(「人工智能」「程序员」(プログラマー))はよく現れるので 1 トークンにまとめられていますが、漢文の組み合わせ(「时习」「不亦」)はまれなのでまとめられず、1 文字 1 トークンになります。トークンが多ければ、費用も高くなります。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…