目次を表示する

LLM完全解説:スクラッチから理解する大規模言語モデル

トークナイゼーション ── BPE・SentencePiece

トークナイゼーション ── BPE・SentencePiece

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


トークナイゼーション — BPE・SentencePiece・トークン経済学

この章で何ができるようになるか:「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)バイトレベル BPEClaude, Gemini
WordPieceBPE の変種(尤度ベース)BERT

トークナイゼーションは「地味だが重要」。プロンプトの書き方、コスト計算、多言語対応──全てに影響する。