一文まとめ: コンピュータはテキストを直接処理できません。トークナイゼーションはテキストをトークン ID に変換し、その ID がベクトルへとマッピングされます。
4.1 なぜトークナイゼーションが必要なのか
前章では、Transformer のパイプラインの最初のステップを次のように示しました。
text -> token IDs
本章ではこのステップを説明します。
4.1.1 コンピュータには数字が必要
コンピュータは私たちと同じようには文を見ていません。たとえば次の文を見ても、
つげ義春はねじ式を描いた。
それが意味を持つ単語の連なりであることはわかりません。コンピュータには数値の単位が必要なのです。
トークナイゼーションとは、テキストを数値の列に変換するプロセスです。各数値単位を トークン ID(テキストを表す番号)と呼びます。そして、トークン ID に対応するテキストの断片そのものを トークン(モデルが扱うテキスト片)と呼び、トークナイゼーションを実行する仕組みを トークナイザ と呼びます。
4.1.2 アーキテクチャ上の位置づけ
トークナイゼーションは入り口に位置します。
raw text -> token IDs -> embeddings -> position -> Transformer blocks
トークナイゼーションがなければ、モデルの残りの部分は処理する材料を持てません。
4.2 トークナイズの2つの方法
最も単純な発想は、すべての文字に番号を割り当てることです。しかし、実際の LLM はもう少し賢いやり方を採用しています。
4.2.1 方法その1: 文字単位の ID
日本語の文に対して、素朴な文字単位のトークナイザは次のように番号を割り振るかもしれません。
つ -> 1
げ -> 2
義 -> 3
春 -> 4
は -> 5
ね -> 6
じ -> 7
式 -> 8
...
仕組みは理解しやすく、すべての文字が数字になります。
しかし、いくつか問題があります。
- トークン数が多すぎる: 1単語が複数の文字に分解されてしまいます。
- 意味の単位が弱い:
つげ義春は一つの人名なのに、複数の文字へ分解されてしまいます。 - コンテキストの使い方が非効率: 長いテキストはコンテキスト長をすぐに使い切ってしまいます。
文字単位のトークナイゼーションが間違いというわけではありませんが、現代の LLM にとって最良の選択肢になることはほとんどありません。
4.2.2 方法その2: バイトレベルBPE
OpenAIの tiktoken は BPE(Byte Pair Encoding)を使います。バイトレベルの土台があるため、あらゆるUnicode文字を個別の語彙項目として持たなくても、任意のテキストをバイト列へフォールバックできます。
考え方は次の通りです。
- 頻出するまとまりは1つのトークンになる
- 珍しい単語はより小さい トークン片(サブワード単位の小片)に分割できる
- 語彙のサイズは有限のまま保てる
- 未知のテキストにも対応できる
OpenAI の cl100k_base トークナイザを使うと、次のテキストは、
つげ義春はねじ式を描いた。
このようにエンコードされます。
[59739, 2243, 240, 47453, 102, 11881, 98, 15682, 2243, 255, 100204, 29430, 30512, 21441, 237, 16995, 28713, 1811]
各トークン片の対応は以下の通りです。
59739 -> "つ"
2243 + 240 -> "げ" のUTF-8バイト片
47453 + 102 -> "義" のUTF-8バイト片
11881 + 98 -> "春" のUTF-8バイト片
15682 -> "は"
100204 -> "じ"
29430 -> "式"
1811 -> "。"
一つの日本語文字が、複数のTokenへ分かれる場合があります。バイトレベルBPEでは正常な挙動です。
4.2.3 コンテキスト長
コンテキスト長とは、モデルに設定されたシーケンスの予算です。生成 API では、通常プロンプトと生成トークンがその予算を共有します。
128,000 トークンに対応するモデルは、英単語 128,000 個を処理できるという意味ではありません。あくまでトークナイザの単位で 128,000 個という意味です。
コンテキスト長は特定のモデルとサービング設定に属する数値で、製品表はすぐ古くなります。直感を作るには、論文で安定して確認できる2例で十分です。
| モデル | コンテキスト長 |
|---|---|
| GPT-3論文 (2020) | 2,048 トークン |
| Llama 2 (2023) | 4,096 トークン |
現代の製品モデルはより長いウィンドウをサポートし得ますが、正確な入出力予算は現在のモデルドキュメントで確認すべきです。
言語や文字体系によってトークン効率は異なりますが、「中国語1文字は常に何トークン」という固定比率はありません。cl100k_base では 中华人民共和国 は7文字・7トークンですが、小沈阳江西演唱会邀请了, は12文字・16トークンです。文やトークナイザが変われば比率も変わります。
LLM APIがトークン単位で計測するのは、トークンがモデルの処理単位だからです。その結果、人間にとって同程度の長さの文章でも、言語によって料金や使えるコンテキスト量が変わり得ます。
4.3 トークンから埋め込みへ
トークン ID だけではまだ不十分です。モデルは各 ID をベクトルに変換しなければなりません。
これを 埋め込み(Embedding) と呼びます。
4.3.1 埋め込みのルックアップテーブル
モデルの内部には大きな表があります。
[vocab_size, d_model]
ここで、
- vocab_size はトークナイザが知っているトークン ID の総数(語彙のサイズ)です。
- d_model はモデルが扱うベクトルの幅です。
説明のための丸めた仮想例として、
vocab_size = 100000
d_model = 64
であれば、埋め込みテーブルが含む数の個数は次のようになります。
100000 x 64 = 6,400,000 個
これらの数値はすべて学習可能なパラメータです。
4.3.2 ルックアップの流れ
例として次の文を考えます。
つげ義春はねじ式を描いた。
トークナイゼーションすると、
[59739, 2243, 240, 47453, 102, 11881, 98, 15682, 2243, 255, 100204, 29430, 30512, 21441, 237, 16995, 28713, 1811]
そしてモデルはテーブル参照を行います。
token 59739 -> row 59739 -> vector
token 2243 -> row 2243 -> vector
token 240 -> row 240 -> vector
...
結果として得られるのは行列です。
[context_length, d_model]
この文は18個のトークンを持つので、d_model = 64 なら行列の形状は次のようになります。
[18, 64]
この行列が、Transformer ブロックに送られる数値表現です。
4.3.3 なぜベクトルを使うのか
トークン ID をそのまま使えばよいのでは、と思うかもしれません。
しかし、ID には幾何構造がありません。トークン ID 791 がトークン ID 792 に意味的に「近い」とは言えないのです。
学習されたベクトルは有用な関係を表現できますが、2つの段階を分けて考えましょう。
- 埋め込みテーブルは各トークンIDに、文脈をまだ含まない出発ベクトルを与える
- Transformerブロックがその出発ベクトルを文脈付きの隠れ状態へ変える
つげ義春のような人名の意味は、通常は複数トークンと複数層の間で合成される
埋め込みベクトルは、トークン ID 自体に意味があるかのように扱うことなく、言語を行列演算の世界に持ち込む手段です。初期埋め込みの近さは有用なこともありますが、意味的に関連するすべての単語や句が必ず近傍に並ぶという保証ではありません。
4.4 tiktoken で試してみる
OpenAI のトークナイザライブラリを使えば、トークナイゼーションの様子を実際に観察できます。
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
text = "つげ義春はねじ式を描いた。"
tokens = enc.encode(text)
print(f"Token IDs: {tokens}")
print(f"Token count: {len(tokens)}")
print(f"Decoded: {enc.decode(tokens)}")
for token_id in tokens:
token_bytes = enc.decode_single_token_bytes(token_id)
token_text = token_bytes.decode("utf-8", errors="replace")
print(f"{token_id} -> bytes={token_bytes!r}, text={token_text!r}")
実行結果のイメージは次のようになります。
Token IDs: [59739, 2243, 240, 47453, 102, 11881, 98, 15682, 2243, 255, 100204, 29430, 30512, 21441, 237, 16995, 28713, 1811]
Token count: 18
Decoded: つげ義春はねじ式を描いた。
59739 -> bytes=b'\xe3\x81\xa4', text='つ'
2243 -> bytes=b'\xe3\x81', text='�'
240 -> bytes=b'\x92', text='�'
...
この小さな実験はぜひやってみてください。decode_single_token_bytes() を意図的に使っています。1つのトークンがUTF-8文字の一部のバイトだけを持つことがあり、トークンごとに単独の文字列としてデコードすると損失が起き得るからです。
4.5 埋め込み層のパラメータ数
埋め込み層は無視できない数のパラメータを保持します。
4.5.1 計算式
embedding parameters = vocab_size x d_model
4.5.2 例
| モデル | 語彙 | 幅 | パラメータ数 |
|---|---|---|---|
| GPT-2 Small | 50,257 | 768 | 約 38.6M |
| GPT-2 Large | 50,257 | 1,280 | 約 64.3M |
| GPT-3 | 50,257 | 12,288 | 約 618M |
| LLaMA-2-7B | 32,000 | 4,096 | 約 131M |
埋め込みは小さな前処理の細部ではありません。学習されたパラメータテーブルそのものであり、無視できない存在です。
入力埋め込み行列と出力射影を共有するアーキテクチャもあります。その場合、同じ重みが2つの役割を持つため、モデル全体のパラメータ数で二重に数えてはいけません。
4.6 章のまとめ
4.6.1 重要な概念
| 概念 | 意味 |
|---|---|
| トークナイゼーション | テキストをトークナイザの単位に変換する |
| トークン | モデルが読める形のテキスト片 |
| トークン ID | トークンに対応する数値 ID |
| ボキャブラリサイズ | 既知のトークン ID の数 |
| 埋め込み | トークン ID をベクトルに対応付ける |
| d_model | モデル内部のベクトルの幅 |
| コンテキスト長 | 設定されたシーケンス予算で、通常は入力と生成出力が共有する |
4.6.2 流れ
"つげ義春はねじ式を描いた。"
|
| Tokenization
v
[59739, 2243, 240, 47453, ...]
|
| Embedding lookup
v
[context_length, d_model] matrix
4.6.3 核心のメッセージ
トークナイゼーションと埋め込みは、テキストを Transformer に入力する仕組みそのものです。トークナイゼーションがテキストをモデルに読める単位に切り分け、埋め込みがその単位を行列演算に参加できるベクトルへと変換します。
チャプターチェックリスト
本章を読み終えたあと、次のことができるようになっているはずです。
- なぜトークナイゼーションが必要なのかを説明できる
- 文字単位のトークナイゼーションと BPE 系のトークナイゼーションの違いを述べられる
-
vocab_size、d_model、コンテキスト長の意味を説明できる - トークン ID をベクトルに変換する理由を説明できる
- 埋め込みテーブルのパラメータ数を計算できる
次章に向けて
ここまでがトークナイゼーションの全体像です。次に API がトークン単位で課金してきたら、それが何を数えているのかを正確に理解できているはずです。
これでテキストはベクトルになりました。しかし、まだ大事なものが一つ欠けています。それが 位置(position) です。
たとえば次の二つの文を見てください。
漫画家が貸本屋を訪ねた。
貸本屋が漫画家を訪ねた。
ほぼ同じ単語が並んでいるのに、意味は異なります。第5章では、モデルがそれぞれのトークンが系列のどこにあるかをどう知るのかを説明します。
ここまでお付き合いいただきありがとうございました。次の章でまたお会いしましょう。