トークンって結局なに? — エンジニアが tiktoken で5分で腹落ちする入門
結論
- トークンとは、AIが文章を扱うときの最小単位。文字でも単語でもなく、その中間(サブワード)であることが多い。
- LLMの課金もコンテキスト長も処理も、文字数ではなくトークン数で動く。だから「トークンを数える」のは料金とプロンプト設計に直結する実用知識だ。
- トークンへ切り分ける部品をトークナイザ、その処理を**トークン化(tokenization)**と呼ぶ。本記事はこの3語を
tiktokenで実際に動かして腹落ちさせる。 - そして同じ意味の文でも日本語は英語よりトークンを食う。その理由を5つのトークナイザで解剖したのが応用編。本記事はその前段だ。
この記事は手を動かす前提で書いている。Qiitaにトークナイザ入門は多いが、ここでは用語を1つずつ
tiktokenで確かめ、数字を自分の目で見て進む。読み終えたら、トークン数を自分で数えられるようになっているはずだ。
まず動かす:tiktoken でトークンを数えてみる
理屈の前に1回動かす。OpenAIのトークナイザ実装 tiktoken を使う。
pip install tiktoken
import tiktoken
# GPT-4o系のトークナイザを読み込む
enc = tiktoken.get_encoding("o200k_base")
text = "Hello, world!"
ids = enc.encode(text) # 文字列 → トークンID列
tokens = [enc.decode([i]) for i in ids] # 各IDを文字列に戻して中身を見る
print(f"文字数: {len(text)}")
print(f"トークン数: {len(ids)}")
print(f"トークンID: {ids}")
print(f"分割: {' | '.join(tokens)}")
ここで出てくる2つのメソッドが、トークナイザの本体だ。
- エンコード(encode):文字列をトークンID(整数)の列に変換する。AIが実際に受け取るのはこの整数列で、文字そのものではない。
- デコード(decode):トークンIDを文字列に戻す。上のコードでは「各IDが元どんな文字だったか」を覗くために1個ずつ戻している。
「ID列に変換する部品」がトークナイザ、「変換そのもの」がトークン化だ。enc.encode() を呼んだ瞬間が、まさにトークン化が起きている場所になる。
実際に英単語をいくつか通すと、トークンの「単位」の感覚がつかめる。
for w in ["world", "tokenization", "unbelievable", "ChatGPT"]:
ids = enc.encode(w)
print(f"{w:14} → {len(ids)}トークン : {' | '.join(enc.decode([i]) for i in ids)}")
world のような頻出語は丸ごと1トークンだが、tokenization のような長め・複合的な語は複数トークンに割れる。「1単語=1トークン」ではないことがここで分かる。この「単語より細かく、文字よりは大きい」単位の正体が、次に説明するサブワードだ。
トークンとは何か:文字でも単語でもない「サブワード」
トークン化の素朴な作り方は2つ考えられる。どちらも実は具合が悪い。
案1:1文字=1トークン
語彙(後述)は文字種だけで済むので小さくて済む。だが文章が文字数ぶんのトークン列になり、系列が長くなりすぎる。tokenization がそれだけで12トークンを食う。
案2:1単語=1トークン
系列は短くなる。だが世の中の単語をすべて登録するのは不可能で、新語・固有名詞・タイポに出会うたび「知らない単語」になる。これを**未知語(OOV: Out-Of-Vocabulary)**と呼ぶ。tokenizationnn のようなちょっとした崩れすら扱えなくなる。
そこで現代のトークナイザは、その中間をとる。よく出る文字の並びをまとめて1単位にする——これがサブワードだ。token はそのまま1トークン、ization は接尾辞としてまとめて1トークン、というように、頻度の高いまとまりを語彙に持つ。
ここで使った用語を整理しておく。
-
語彙(vocabulary):そのトークナイザが知っているトークンの一覧表。「ID 1234 =
token」のような対応表だと思えばいい。語彙の大きさを語彙サイズと呼ぶ。 - 未知語(OOV):語彙に載っていない入力。サブワード方式は、未知語をより小さい既知のかけらに分解することで、原理的にOOVをなくせるのが強みだ(最悪、1文字やバイト単位まで落とせば必ず表現できる)。
サブワードの利点は「語彙サイズを現実的に抑えつつ、未知語をなくせる」ことに尽きる。文字単位の短所(系列が長い)と単語単位の短所(未知語が出る)を、同時に回避するための折衷案だと捉えると分かりやすい。
なぜ単語でも文字でもないのか:語彙サイズと未知語のトレードオフ
ここを一段だけ掘り下げる。トークナイザの設計は、結局この綱引きで決まる。
| 単位 | 語彙サイズ | 系列の長さ | 未知語 |
|---|---|---|---|
| 文字単位 | 小さい | 長い(不利) | 出ない |
| 単語単位 | 巨大(不利) | 短い | 出る(不利) |
| サブワード | 中くらい | 中くらい | 出ない |
語彙を大きくすれば長いまとまりを1トークンにでき、系列は短くなる。だが語彙が大きいほどモデルの出力層も重くなる。逆に語彙を小さくすると系列が伸びる。「語彙サイズ」と「系列の長さ」はトレードオフの関係にあり、サブワード方式はこのバランス点を、実際のテキストの頻度統計から学習して決める。
つまりトークナイザは、ルールで手書きされたものではなく、大量のテキストから「どの並びをまとめると効率がいいか」を学習して作られた表だ。この「何のテキストで学習したか」が、後の応用編で効いてくる。英語中心のテキストで語彙を作れば、英語は長く・日本語は細かく割れる——という偏りがここで生まれる。
方式の名前だけ知っておく:BPE / WordPiece / Unigram
サブワード語彙の作り方にはいくつか流派がある。入門段階では名前と一言の方針だけ押さえれば十分だ。
-
BPE(Byte Pair Encoding):最初は文字単位から始め、いちばん頻繁に隣り合うペアを繰り返しくっつけていく。
t+h→th、th+e→the、という調子で頻出のまとまりを育てる。GPT系(tiktoken)やMistralが採用。 - WordPiece:BPEに似るが、くっつける基準が「頻度」ではなく「くっつけると尤度がどれだけ上がるか」。BERT系が採用。
- Unigram:逆方向。大きめの語彙から始めて、要らないトークンを削っていく。SentencePieceでよく使われる。
バイトレベルBPEという言葉も覚えておくとよい。文字ではなくバイトを最小単位にするBPEで、GPT系が使っている。どんな文字でも最悪バイトまで分解すれば表現できるので、
[UNK](未知語の取りこぼし)が原理的に出ない。応用編では、この「バイトまで落ちる」挙動が日本語のトークン数に効いてくる場面を実測している。
方式ごとの細かい違いは応用編で実物を見せる。ここでは「作り方が何種類かあって、モデルごとに採用が違う」とだけ分かっていれば先へ進める。
トークン数が「効く」理由:課金・コンテキスト長・処理単位
なぜトークン数をわざわざ数えるのか。理由は3つあり、どれも実務に直結する。
1. 課金がトークン単位
ほとんどのLLM APIは、入力・出力のトークン数で課金する。文字数でも単語数でもない。だから「短く書いたつもり」でもトークンが多ければ高くつく。プロンプトのトークン数を見積もれると、コストを事前に概算できる。
2. コンテキスト長がトークン単位
モデルが一度に扱える入力の上限を**コンテキスト長(context window)**と呼ぶが、この上限もトークンで数える。「128kトークン」のような表記がそれだ。長い文書を渡すとき、文字数ではなくトークン数で上限に当たる。
3. 処理がトークン単位
モデルは内部でトークン列を受け取り、次のトークンを予測して進む。だから生成の速さ(レイテンシ)もトークン数に効く。
実際に、プロンプトのトークン数を見積もるのはこれだけで書ける。
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
def count_tokens(text: str) -> int:
return len(enc.encode(text))
prompt = "次の文章を3行で要約してください:\n" + "..." * 100
print(f"このプロンプトは約 {count_tokens(prompt)} トークン")
API課金やコンテキスト長の見積もりは、本質的にこの count_tokens の一行に集約される。トークナイザを意識せず tokenizer(text) の裏で通り過ぎていた部分が、料金と上限を決めていたわけだ。
締め:日本語だと、話が変わる
ここまでは主に英語で見てきた。だが日本語を通すと事情が一変する。最後に、その入口だけ見せておく。
同じ意味の技術文を、英語と日本語でGPT-4o(o200k)に通すと、こうなる。
| 文字数 | トークン数 | |
|---|---|---|
| 英語:We performed cross-validation ... | 112字 | 18トークン |
| 日本語:機械学習モデルの精度を評価するために... | 40字 | 30トークン |
英語のほうが文字数は約3倍長いのに、トークン数は日本語のほうが多い。字数とトークン数が逆転している。理由を分割の様子から覗くと、
英語 : We | performed | cross | -validation | using | a | ...(ほぼ単語ごと)
日本語: 機 | 械 | 学 | 習 | モデル | の | 精 | 度 | を | 評価 | ...(漢字が1文字ずつ)
英語は performed validation が単語まるごと1トークンに収まる一方、日本語は漢字が1文字ずつに割れて、「機械学習」だけで4トークンを食っている。なぜこうなるのか——そして「日本語が高い」のは宿命なのか、語彙設計で手当てできるのか。GPT系・Mistral・多言語BERT・日本語特化BERTの5つのトークナイザを実測して解剖したのが応用編だ。
👉 応用編:日本語はなぜトークンを食うのか — 5つのトークナイザを実測して解剖する
日本語はなぜトークンを食うのか — 5つのトークナイザを実測して解剖する
参考:今回使ったメソッドだけまとめ
import tiktoken
enc = tiktoken.get_encoding("o200k_base") # トークナイザを読み込む
enc.encode("text") # 文字列 → トークンID列(これがトークン化)
enc.decode([123, 456]) # トークンID列 → 文字列
len(enc.encode("text")) # トークン数を数える
o200k_base はGPT-4o系のトークナイザ。GPT-3.5/4系なら cl100k_base を指定する。同じ文でも世代で結果が変わる——という話も、応用編で実測している。





