今回は、同じ文字列を
- Unicode tokenizer
- UTF-8 Byte tokenizer
- BPE tokenizer
の3種類でToken化し、Vocabulary sizeとToken数、圧縮率を比較してみる。
実験対象
今回使用する文字列は以下。
Hello, 🌍! こんにちは!
UTF-8でのサイズを確認count_utf8_bytes.pyすると、29バイトとなる。
文字列を入力してください: Hello, 🌍! こんにちは!
文字数: 16
UTF-8 バイト数: 29
この29 bytesの文字列を、それぞれ異なる方法でToken化する。
圧縮率は以下として計算する。
compression ratio = UTF-8 bytes / Token count
Unicode tokenizer
前提:UnicodeのCode PointをそのままToken IDとして使用する。
Unicode Code Pointに変換test_unicode.pyした結果は以下。
=== Unicode tokenizer ===
text: Hello, 🌍! こんにちは!
tokens: [72, 101, 108, 108, 111, 44, 32, 127757, 33, 32, 12371, 12435, 12395, 12385, 12399, 33]
UTF-8 bytes: 29
token count: 16
vocabulary size: 127758
compression ratio: 1.8125
今回の文字列は16個のUnicode Code Pointで構成されているため、1 Code Pointを1 Tokenとして扱うと、Token数は16となる。
つまりUnicode tokenizerでは基本的に、
1 Code Point = 1 Token
となる。
圧縮率は、
29 / 16 = 1.8125 bytes/token
Unicode方式の問題
しかし、今回の方式ではToken IDの範囲が大きくなる。
今回では、一番大きなToken IDは「127757」になるため、今回の入力を表現するだけでも少なくとも、
0 ~ 127757
までのIndexを扱える必要がある。
そのため、今回の実験では、
max(Token ID) + 1
= 127757 + 1
= 127758
となった。
(ここでの127758はUnicode全体のVocabulary sizeではなく、今回出現した最大のCode PointをそのままToken IDとして使用した場合に必要となるIndex空間である)
では、「127757」って何?decodeしてみよう。decode_127757.py
🌍 = 127757
今回16文字しか入力していないにもかかわらず、🌍というあんまり使わない1文字が存在するだけでVocabularyの必要サイズが非常に大きく127758になる。
UTF-8 Byte tokenizer
前提:文字列をUTF-8に変換し、1 Byteを1 Tokenとして扱う。
UTF-8のByteは、
0 ~ 255
の256種類しか存在しない。
そのためVocabulary sizeは、
256
実行結果test_utf8.py:
=== Byte tokenizer ===
text: Hello, 🌍! こんにちは!
tokens: [72, 101, 108, 108, 111, 44, 32, 240, 159, 140, 141, 33, 32, 227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, 175, 33]
UTF-8 bytes: 29
token count: 29
vocabulary size: 256
compression ratio: 1.0
UTF-8では29 Byteなので、
29 bytes
↓
29 tokens
となった。
つまり、
compression ratio = 29 / 29 = 1.0
である。
UTF-8 Byte tokenizerのメリット
Unicode tokenizerでは、
vocabulary size = 127758
だった。
一方、UTF-8 Byte tokenizerでは、
vocabulary size = 256
しか必要ない。
しかもUTF-8で表現できる文字であれば、どの言語でも表現できる。
Vocabularyの観点では非常に扱いやすい。
UTF-8 Byte tokenizerの問題
UTF-8 Byte tokenizerでは、1 Byteを1 Tokenとして扱う。
今回の入力はUTF-8で29 Byteだったため、
29 Byte
→ 29 Token
となった。
一方、同じ文字列をUnicode tokenizerで処理すると16 Token、後ほどで検証するBPE tokenizerでは9 Tokenだった。
今回の実験では、UTF-8 Byte tokenizerは3方式の中で最もToken数が多くなった。
つまり今回の実験から、UTF-8 Byte tokenizerはVocabulary sizeを256に抑えられる一方で、UTF-8で複数Byteを必要とする文字ではToken数の増えることが確認できた。
また、1 Byteを必ず1 Tokenとして扱うため、この方式ではcompression ratioは常に1.0 bytes/tokenになる。
BPE tokenizer
最後にBPE tokenizer(gpt-4)を使用する。
今回はtiktokenを使用した。(pythonの場合)
pip install tiktoken
pythonを利用したくない方はtiktokenizerへどうぞ
=== BPE tokenizer ===
text: Hello, 🌍! こんにちは!
tokens: [9906, 11, 11410, 234, 235, 0, 220, 90115, 0]
UTF-8 bytes: 29
token count: 9
vocabulary size: 100277
compression ratio: 3.2222222222222223
Token数は、
9
になった。
圧縮率は、
29 / 9
= 3.22 bytes/token
となる。
結果比較
今回の結果をまとめる。
検証文字列:
Hello, 🌍! こんにちは!
結果:
| Tokenizer | Vocabulary size | Token count | Compression ratio |
|---|---|---|---|
| Unicode | 127758 | 16 | 1.8125 |
| UTF-8 Byte | 256 | 29 | 1.0 |
| BPE | 100277 | 9 | 3.22 |
かなり特徴が違う。
Unicode tokenizerは、
Vocabularyが大きい
↓
しかしToken数は比較的少ない
UTF-8 Byte tokenizerは、
Vocabularyは256と非常に小さい
↓
しかしToken数が多い
BPEは、
Vocabularyは大きくなる
↓
その代わりToken数を大きく減らせる
という結果になった。
結論
今回の実験では、同じ文字列をUnicode Code Point、UTF-8 Byte、BPEの3方式でToken化して比較した。
結果は以下となった。
| tokenizer | Token数 |
|---|---|
| Unicode | 16 Token |
| UTF-8 Byte | 29 Token |
| BPE | 9 Token |
-
Unicode Code PointをそのままToken IDとして利用することはできるが、Code Pointの値によって必要なID空間が大きくなる。
-
一方、UTF-8 ByteをTokenとして利用すればVocabulary sizeを256に固定できるが、1 Byte = 1 Tokenとなるため、今回の入力では29 Byteがそのまま29 Tokenになった。
-
そしてBPEでは、同じ29 Byteの入力が9 Tokenまで少なくなった。
つまり今回の実験から、Tokenizerには単に文字を数字へ変換するだけではなく、LLMが処理するToken列を短くする役割もあることが確認できた。
ではBPEは、どのような処理によって、
29 Byte
↓
9 Token
までToken数を減らすことができたのか。
次はBPEの内部で何が起きているのかを確認してみる。