トークナイゼーション ── BPE・SentencePiece
LLM との関係:ユーザーの入力テキストが LLM に渡される最初の処理。トークナイゼーションの理解は、プロンプト設計・コスト計算・多言語対応の全てに直結する。

この章で何ができるようになるか:「1トークン ≠ 1単語」の理由と、BPE アルゴリズムの仕組みを説明でき、トークン数がコストとレイテンシにどう影響するかを理解できる。
なぜ「単語単位」ではダメか
問題1: 未知語
語彙にない単語は扱えない
"ChatGPT" → 語彙にない → [UNK](未知語トークン)→ 情報が失われる
問題2: 語彙サイズの爆発
英語だけでも 100万語以上の単語形(活用・複合語含む)
→ Embedding 行列が巨大になる(100万 × 4096 = 40億パラメータ)
問題3: 多言語対応
日本語・中国語・韓国語...全ての語彙を持つのは非現実的
解決策:単語より細かい「サブワード(subword)」単位に分割する。
BPE(Byte Pair Encoding)
1994年にデータ圧縮アルゴリズムとして発明され、2016年に NLP に応用された。
アルゴリズム
Step 1: 文字単位に分割
"lower" → ['l', 'o', 'w', 'e', 'r']
"lowest" → ['l', 'o', 'w', 'e', 's', 't']
"newer" → ['n', 'e', 'w', 'e', 'r']
Step 2: 最も頻出する隣接ペアをマージ(繰り返し)
最頻出: ('e', 'r') → 'er' にマージ
"lower" → ['l', 'o', 'w', 'er']
"newest" → ['n', 'e', 'w', 'er']
次の最頻出: ('l', 'o') → 'lo' にマージ
"lower" → ['lo', 'w', 'er']
"lowest" → ['lo', 'w', 'e', 's', 't']
... これを語彙サイズに達するまで繰り返す
from collections import Counter
def train_bpe(corpus: list[str], num_merges: int) -> dict:
"""BPE の語彙を訓練する"""
# Step 1: 単語を文字単位に分割(単語の末尾に </w> を付ける)
word_freqs = Counter()
for text in corpus:
for word in text.split():
chars = tuple(list(word) + ['</w>'])
word_freqs[chars] += 1
merges = {}
for i in range(num_merges):
# 最も頻出する隣接ペアを見つける
pair_freqs = Counter()
for word, freq in word_freqs.items():
for j in range(len(word) - 1):
pair_freqs[(word[j], word[j+1])] += freq
if not pair_freqs:
break
best_pair = pair_freqs.most_common(1)[0][0]
merges[best_pair] = best_pair[0] + best_pair[1]
# マージを適用
new_word_freqs = Counter()
for word, freq in word_freqs.items():
new_word = []
j = 0
while j < len(word):
if j < len(word) - 1 and (word[j], word[j+1]) == best_pair:
new_word.append(merges[best_pair])
j += 2
else:
new_word.append(word[j])
j += 1
new_word_freqs[tuple(new_word)] = freq
word_freqs = new_word_freqs
return merges
def tokenize_bpe(text: str, merges: dict) -> list[str]:
"""訓練済みの BPE で文字列をトークン化"""
tokens = list(text) + ['</w>']
while len(tokens) > 1:
# 適用可能なマージの中で最も優先度が高いものを適用
best_pair = None
best_idx = -1
for i in range(len(tokens) - 1):
pair = (tokens[i], tokens[i+1])
if pair in merges:
best_pair = pair
best_idx = i
break # 簡略化(実際は全ペアを評価)
if best_pair is None:
break
tokens = tokens[:best_idx] + [merges[best_pair]] + tokens[best_idx+2:]
return tokens
実際のトークナイゼーション例
# tiktoken(OpenAI の BPE 実装)
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
# 英語
tokens = enc.encode("Hello, world!")
print(tokens) # [9906, 11, 1917, 0](4トークン)
print([enc.decode([t]) for t in tokens])
# → ['Hello', ',', ' world', '!']
# 日本語
tokens = enc.encode("東京タワーの高さは333メートルです")
print(len(tokens)) # 約14トークン(日本語は英語より多くのトークンを消費)
# コード
tokens = enc.encode("def fibonacci(n):\n if n <= 1:\n return n")
print(len(tokens)) # 約20トークン(インデントもトークン化される)
日本語が「トークン効率が悪い」理由
英語 "Hello" → 1トークン(頻出語彙に登録済み)
日本語 "こんにちは" → 2〜3トークン(マルチバイト文字がサブワードに分割される)
GPT-4 の語彙(100,256トークン)の大半は英語のサブワード。
日本語・中国語・韓国語は頻度が低いため、
より細かいサブワードに分割される → トークン数が増える → コストが高くなる。
トークン数がコストとレイテンシに直結する理由
コスト:
API 課金はトークン数に比例
入力 1000 トークン + 出力 500 トークン = 1500 トークン分の課金
レイテンシ:
Attention の計算量は O(n²)(n = トークン数)
トークン数が 2倍 → Attention の計算量が 4倍
コンテキスト長:
モデルの最大コンテキスト長(例: 128K トークン)はトークン単位
冗長なプロンプトはコンテキストを無駄に消費する
プロンプト最適化のヒント
❌ 「以下の文章を読んで、内容を要約してください。要約は簡潔にしてください。」
→ 冗長な指示 → トークンの無駄
✅ 「以下を3文以内で要約:」
→ 短い指示 → トークン効率◎
SentencePiece:言語非依存のトークナイゼーション
Google が開発。BPE と Unigram の2つのアルゴリズムを実装。
BPE との違い:
BPE: 空白で単語を区切ってから文字単位に分割
→ 空白のない言語(日本語・中国語)にそのまま適用しにくい
SentencePiece: テキストを「バイト列」として扱い、空白も特別扱いしない
→ 言語に依存しない
→ 空白は ▁(下線)で明示
→ "Hello world" → ["▁Hello", "▁world"]
LLaMA、Claude、Gemini は SentencePiece 系のトークナイザを使用。
特殊トークン
[BOS] / <s> : 文の開始(Beginning of Sentence)
[EOS] / </s> : 文の終了(End of Sentence)
[PAD] : パディング(バッチ処理で長さを揃える)
[UNK] : 未知語(BPE ではほぼ発生しない)
[SEP] : 文の区切り(BERT で使用)
<|endoftext|> : テキスト終了(GPT で使用)
<|im_start|> : メッセージ開始(Chat フォーマット)
まとめ
| トークナイザ | アルゴリズム | 使用例 |
|---|---|---|
| BPE(tiktoken) | 頻出ペアのマージ | GPT-4, GPT-3.5 |
| SentencePiece(Unigram) | 尤度最大化で語彙を選定 | LLaMA, T5 |
| SentencePiece(BPE) | バイトレベル BPE | Claude, Gemini |
| WordPiece | BPE の変種(尤度ベース) | BERT |
トークナイゼーションは「地味だが重要」。プロンプトの書き方、コスト計算、多言語対応──全てに影響する。