はじめに
この記事はAIで作成されています。multi-modal LLM関連のメモを記載しています。
目次
Tokenization
LLM は、入力されたテキストをそのまま処理するのではなく、テキストをトークンに分割し、それぞれをトークン ID に変換して処理する。この処理をトークナイズ(Tokenization)と呼ぶ。LLM では、単語をそのままトークンにするのではなく、単語より小さなサブワード単位に分割する方式が広く使われている。
代表的な方式には、次のものがある。
| 方式 | 語彙の構築方法 | 基本的な考え方 | 代表例・関連技術 |
|---|---|---|---|
| ① BPE | トークンをマージして語彙を増やす | 頻出する隣接ペアを統合する | GPT 系の各種トークナイザー |
| ② Byte-level BPE | バイトをマージして語彙を増やす | バイト列に BPE を適用する | GPT-2、tiktoken |
| ③ WordPiece | スコアに基づいて語彙を構築する | トークンの組み合わせの有用性を評価する | BERT |
| ④ Unigram | 大きな候補語彙からトークンを削除する | 確率モデルでテキストを説明する | SentencePiece の Unigram モデル |
なお、BPE、WordPiece、Unigram はトークナイズのアルゴリズムであり、SentencePiece や tiktoken はトークナイザーの実装・ライブラリである。これらは同じ階層の概念ではない。
現在の主流
LLM では、BPE 系のトークナイザーが広く使われている。一方、BERT 系では WordPiece、SentencePiece を利用するモデルでは BPE や Unigram などが使われている。トークナイザーの設計は、語彙サイズ、入力系列の長さ、多言語対応、未知の文字列への対応などに影響する。
① BPE(Byte Pair Encoding)
論文名: Neural Machine Translation of Rare Words with Subword Units
URL: https://arxiv.org/abs/1508.07909
発表日: 2015-08-31
✅ ポイント
- 隣接するトークンのペアを出現頻度に基づいてマージし、新しいトークンを作るアルゴリズム。
- 最初は文字などの小さな単位から始め、頻出ペアを統合することで語彙を増やしていく。
- マージのたびに新しいトークンが語彙に追加される。
- 目標の語彙サイズやマージ回数に達するまで、この処理を繰り返す。
- 学習時に獲得したマージ規則を使い、推論時のテキストをトークンに分割する。
🧪 学習の疑似コード
```python
# 1. コーパスを読み込む
corpus = load_corpus()
# 2. 初期トークン列を作成する(例:文字やバイト単位)
tokens = initialize_tokens(corpus)
# 3. 初期語彙を作成する
vocabulary = initialize_vocabulary(tokens)
# 4. マージ規則を初期化する
merge_rules = []
# 5. 語彙サイズが目標に達するまで学習する
while len(vocabulary) < target_vocab_size:
# 隣接するトークンペアの出現頻度を集計
pair_counts = count_adjacent_pairs(tokens)
# 最も頻繁に出現するペアを選択
best_pair = argmax(pair_counts)
# ペアをマージして新しいトークンを作成
new_token = merge_pair(best_pair)
# 語彙に新しいトークンを追加
vocabulary.add(new_token)
# マージ規則を追加(学習順が優先順位になる)
merge_rules.append(best_pair)
# コーパス内の該当するペアを置換
tokens = replace_pair(tokens, best_pair, new_token)
# 6. 学習済みの語彙とマージ規則を保存する
save_tokenizer(vocabulary, merge_rules)
実際の実装では、単語境界を扱う前処理や、マージの優先順位を管理する仕組みなども必要になる。
🔎 推論時の疑似コード
text = input_text()
# 学習済みの語彙とマージ規則を読み込む
vocabulary, merge_rules = load_tokenizer()
# テキストを初期単位に分割
tokens = initialize_tokens(text)
# 学習済みのマージ規則を適用
tokens = apply_merge_rules(tokens, merge_rules)
# トークンを ID に変換
token_ids = convert_to_ids(tokens, vocabulary)
推論時には、通常、語彙やマージ規則を再学習しない。学習済みの規則を使ってトークン化する。
🧮 BPE の処理イメージ
コーパスに次の単語があるとする。
low
lower
lowest
最初は文字単位に分割する。
l o w
l o w e r
l o w e s t
例えば、l と o が頻出ペアであれば、これらをマージする。
lo w
lo w e r
lo w e s t
次に lo と w をマージする。
low
low e r
low e s t
さらにマージを繰り返すことで、lower や lowest などが1つのトークンになる場合もある。
このように、頻出する短い単位を段階的にまとめて、より長いトークンを作る。
❓ よくある質問
Q. なぜ頻出ペアをマージするの?
- 頻繁に現れる文字列を1つのトークンにまとめることで、トークン数を減らしやすくするため。
- 一般的な単語やよく使われる文字列を効率的に表現できるようにするため。
Q. BPE は単語単位で処理するの?
- 実装によって前処理は異なるが、一般的な BPE では、単語などの単位に分けてから、その内部をサブワードに分割することがある。
- Byte-level BPE では、バイト列を基本単位として扱う。
Q. 語彙サイズはどのように決まるの?
- 初期語彙に、マージで追加されるトークンを加えていく。
- 目標の語彙サイズに達するまでマージを繰り返す。
- 実際の語彙サイズは、モデルの設計によって決まる。
Q. BPE は未知語に対応できるの?
- 文字などの初期単位に分解できれば、未知の単語でも既存の単位の組み合わせとして表現できる。
- ただし、初期単位にない文字の扱いは実装に依存する。この問題に対応しやすい方式が Byte-level BPE である。
② Byte-level BPE
代表例: GPT-2、tiktoken 系のトークナイザー
GPT-2 論文: Language Models are Unsupervised Multitask Learners
発表日: 2019-02-14
✅ ポイント
- 通常の BPE が文字などを初期単位として扱うのに対し、Byte-level BPE はバイトを初期単位として扱う。
- UTF-8 などのバイト列に変換してから BPE を適用する。
- 基本のバイト値は 256 種類であり、すべての Unicode 文字を初期語彙に登録する必要がない。
- バイト列に変換できるため、多言語のテキストや未知の文字列を扱いやすい。
- GPT-2 が採用した代表的なトークナイズ方式である。
🧪 学習の疑似コード
# テキストを読み込む
corpus = load_corpus()
# テキストをバイト列に変換
byte_sequences = encode_utf8(corpus)
# 256 種類のバイト値を初期語彙とする
vocabulary = initialize_byte_vocabulary()
# あとはBPEと同様
※ 実際の実装では、正規表現などによる前処理や、特殊トークンの管理なども行う。
🔎 推論時の疑似コード
text = input_text()
# 学習済みのバイト語彙とマージ規則を読み込む
vocabulary, merge_rules = load_tokenizer()
# UTF-8 のバイト列に変換
byte_sequence = encode_utf8(text)
# あとはBPEと同様
🧮 バイト列への変換例
ASCII 文字の a は UTF-8 では1バイトである。
a → 61
一方、日本語の あ は UTF-8 では3バイトである。
あ → E3 81 82
※ 数値は16進数表記。
Byte-level BPE では、このようなバイト列を基本単位として扱う。
例えば、日本語の文字が初期語彙に登録されていなくても、バイト列に変換できれば、そのバイトの組み合わせとして表現できる。
ただし、実際のトークナイザーでは、マージ規則や前処理によって最終的なトークンの分割結果が異なる。
❓ よくある質問
Q. なぜバイト単位にするの?
- Unicode の文字をすべて初期語彙に用意する必要がないため。
- 256 種類のバイト値を基本単位にできるため。
- 初期語彙にない文字列でも、バイト列に変換して表現しやすいため。
Q. どんな文字列でも処理できるの?
- UTF-8 などでバイト列に変換できる文字列に幅広く対応できる。
- ただし、入力の正規化や特殊トークンの扱いなどは別途考慮する必要がある。
- また、処理できることと、少ないトークン数で効率よく表現できることは別である。
Q. 通常の BPE との違いは?
- 通常の BPE は文字などを初期単位にする場合がある。
- Byte-level BPE はバイトを初期単位にする。
- そのため、Byte-level BPE は未知の文字列に対応しやすい。
Q. GPT-2 と tiktoken は同じもの?
- GPT-2 は言語モデルであり、Byte-level BPE を採用している。
- tiktoken は高速な BPE 系トークナイザーを提供するライブラリである。
- どちらもバイトレベルのトークナイズに関連するが、語彙やマージ規則が同じとは限らない。
③ WordPiece
代表例: BERT
論文名: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
URL: https://arxiv.org/abs/1810.04805
発表日: 2018-10-11
✅ ポイント
- BPE と同様に、単語をサブワードに分割するための方式。
- BPE が基本的に隣接ペアの出現頻度を基準にマージするのに対し、WordPiece はペアの有用性を評価するスコアを使う。
- スコアの考え方の一例として、ペアの出現頻度を個々のトークンの出現頻度で正規化する方法がある。
- BERT 系のトークナイザーで使われる。
-
##ingのように、単語の途中に現れるサブワードを##付きで表現することがある。
🧪 学習の疑似コード
# コーパスを読み込む
corpus = load_corpus()
# 初期語彙を作成
vocabulary = initialize_vocabulary(corpus)
while len(vocabulary) < target_vocab_size:
# 隣接ペアの統計を集計
pair_counts = count_adjacent_pairs(corpus)
# ペアのスコアを計算
scores = calculate_wordpiece_scores(
pair_counts,
vocabulary
)
# スコアに基づいて候補を選択
best_pair = select_best_pair(scores)
# ペアをマージして新しいトークンを追加
vocabulary.add(merge_pair(best_pair))
# コーパス内の該当するペアを置換
corpus = replace_pair(corpus, best_pair)
save_tokenizer(vocabulary)
これは、スコアに基づいて語彙を構築する考え方を示した疑似コードである。実際の WordPiece の学習アルゴリズムやスコアの定義は、実装によって異なる。
🔎 推論時の疑似コード
text = input_text()
# 学習済みの語彙を読み込む
vocabulary = load_vocabulary()
# テキストを単語などの単位に分割
words = preprocess(text)
token_ids = []
for word in words:
# 語彙を使って単語をサブワードに分割
subwords = wordpiece_tokenize(word, vocabulary)
# サブワードを ID に変換
token_ids.extend(convert_to_ids(subwords, vocabulary))
一般的な WordPiece の推論処理では、学習済みの語彙を使い、単語を表現できるサブワード列に分割する。
BERT 系の実装では、未知語を [UNK] として扱う場合もある。
🧮 WordPiece のスコア
ペアの有用性を説明するための近似的なスコアとして、次の式を考える。
$$
\operatorname{score}(x,y)=
\frac{f(xy)}{f(x)f(y)}
$$
ここで、
- (f(x)):トークン (x) の出現頻度
- (f(y)):トークン (y) の出現頻度
- (f(xy)):(x) と (y) が隣接して現れる頻度
このスコアでは、単にペアが頻繁に出現するかだけでなく、個々のトークンの出現頻度に対して、ペアがどれくらい強く結びついているかを評価できる。
例えば、次の2つのペアがあるとする。
| ペア | ペアの出現頻度 | 個々のトークンの頻度 | スコア |
|---|---|---|---|
a + b
|
100 | 1000、1000 | 0.0001 |
c + d
|
30 | 40、40 | 0.01875 |
この例では、a + b のほうが頻出するが、正規化したスコアでは c + d のほうが高くなる。
つまり、単純な出現頻度ではなく、個々のトークンの頻度に対する相対的な結びつきを評価するという考え方である。
なお、この式は WordPiece の考え方を説明するための近似例であり、すべての実装で使用される厳密なスコア式ではない。
🧮 ##ing の意味
WordPiece では、単語の先頭以外に現れるサブワードを ## 付きで表現することがある。
例えば、
playing
↓
play + ##ing
この場合、
-
play:単語の先頭に現れるサブワード -
##ing:単語の途中に続くサブワード
を表す。
## はトークンの表記上の記法であり、元のテキストに含まれる文字列ではない。
❓ よくある質問
Q. BPE との違いは?
- BPE は基本的に、隣接ペアの出現頻度に基づいてマージする。
- WordPiece は、ペアの有用性を表すスコアに基づいて語彙を構築する。
- ただし、両者ともサブワードを利用し、語彙を構築する方式である。
Q. なぜ ## を付けるの?
- 単語の先頭に現れるトークンと、単語の途中に現れるトークンを区別するため。
- 例えば
playと##ingを区別することで、単語の内部構造を表現できる。
Q. WordPiece は未知語に対応できるの?
- 単語を既知のサブワードに分割できれば、未知の単語にも対応できる。
- ただし、分割できない場合は
[UNK]などで扱う実装もある。
Q. WordPiece は BERT 専用なの?
- WordPiece はトークナイズの方式であり、BERT 以外でも利用できる。
- ただし、BERT が代表的な採用例としてよく知られている。
④ Unigram Language Model
代表例: SentencePiece の Unigram モデル
関連 URL: https://github.com/google/sentencepiece
✅ ポイント
- BPE のようにペアを繰り返しマージするのではなく、大きな候補語彙から不要なトークンを削除していく方式。
- テキストの分割方法を最初から1つに固定せず、複数の分割候補に確率を割り当てる。
- 各トークンの確率を学習し、コーパス全体をうまく説明できる語彙を構築する。
- トークンを削除した場合に、コーパスの説明能力がどの程度低下するかを評価する。
- 確率推定には EM アルゴリズムなどが利用される。
🧪 学習の疑似コード
# コーパスを読み込む
corpus = load_corpus()
# 大きめの候補語彙を作成
vocabulary = initialize_large_vocabulary(corpus)
# 初期確率を設定
probabilities = initialize_probabilities(vocabulary)
while len(vocabulary) > target_vocab_size:
# 現在の語彙で分割候補の確率を評価
segmentation_probs = evaluate_segmentations(
corpus,
vocabulary,
probabilities
)
# EM などを使ってトークンの確率を学習
probabilities = estimate_probabilities(
segmentation_probs
)
# 各トークンを削除した場合の影響を評価
removal_scores = evaluate_token_removal(
corpus,
vocabulary,
probabilities
)
# 説明能力への影響が小さいトークンを削除
candidates = select_removable_tokens(removal_scores)
vocabulary = remove_tokens(vocabulary, candidates)
# 残った語彙で確率を再学習
probabilities = reestimate_probabilities(
corpus,
vocabulary
)
save_tokenizer(vocabulary, probabilities)
実際のアルゴリズムでは、削除候補の選択数や確率の再推定方法などが定められている。このコードは、処理の流れを示す概念的な疑似コードである。
🔎 推論時の疑似コード
text = input_text()
# 学習済みの語彙と確率を読み込む
vocabulary, probabilities = load_tokenizer()
# 入力テキストに対する分割候補を生成
candidates = generate_segmentations(text, vocabulary)
# 各分割候補の確率を計算
scores = calculate_segmentation_scores(
candidates,
probabilities
)
# 最も確率の高い分割を選択
best_segmentation = select_best_segmentation(scores)
# トークン ID に変換
token_ids = convert_to_ids(
best_segmentation,
vocabulary
)
推論時には、学習済みの語彙と確率を使って、入力テキストを適切なトークン列に分割する。
実際の実装では、動的計画法などを利用して最適な分割を効率的に求める。
🧮 Unigram の確率モデル
テキストの分割候補を (S) とする。
各トークン (t) の確率を (p(t)) とすると、分割候補の確率は次のように表せる。
$$
P(S)=\prod_{t\in S}p(t)
$$
例えば、unhappiness に対して、次の分割候補があるとする。
候補 A:
un + happiness
候補 B:
un + happi + ness
それぞれの分割候補の確率は、
$$
P(A)=p(\text{un})p(\text{happiness})
$$
$$
P(B)=p(\text{un})p(\text{happi})p(\text{ness})
$$
となる。
Unigram では、このような複数の分割候補を確率モデルによって評価する。
実際の学習では、コーパス中の文字列に対して可能な分割を考慮し、全体の尤度が高くなるように語彙と確率を調整する。
🧮 EM アルゴリズムとの関係
Unigram では、EM アルゴリズムなどを使ってトークンの確率を学習する。
- E ステップ:現在のモデルに基づいて、各分割候補の確率や期待出現回数を評価する。
- M ステップ:その結果を使って、各トークンの確率を更新する。
この処理を繰り返すことで、コーパスを説明する確率モデルを学習する。
ただし、EM アルゴリズム自体が、尤度の低いトークンを直接削除するわけではない。
確率モデルの学習と、語彙からトークンを削除する処理は別のステップである。
🧮 語彙削減の考え方
Unigram では、あるトークンを削除した場合に、コーパスの説明能力がどの程度低下するかを評価する。
例えば、
候補語彙:
["un", "happiness", "happi", "ness", "happy"]
この中から happi を削除したとしても、別のトークンの組み合わせで同じ文字列を十分に表現できるなら、説明能力の低下は小さい可能性がある。
一方、削除によって多くの文字列をうまく分割できなくなる場合は、説明能力が大きく低下する可能性がある。
このように、トークン単体の出現頻度だけでなく、削除によるコーパス全体への影響を考慮することが重要である。
❓ よくある質問
Q. BPE との違いは?
- BPE は頻出ペアをマージして語彙を増やす。
- Unigram は大きな候補語彙からトークンを削除して語彙を小さくする。
- Unigram は、複数の分割候補を確率的に評価する。
Q. なぜ最初に大きな語彙を用意するの?
- 多くの分割候補を最初から用意しておくことで、コーパスを説明するための選択肢を確保するため。
- その後、不要な候補を削除することで、効率的な語彙を構築する。
Q. EM アルゴリズムは何をしているの?
- 主に、分割候補を考慮しながらトークンの確率を学習する。
- 語彙の削減は、トークンを削除した場合の説明能力の変化を評価して行う。
Q. Unigram は分割方法をランダムに決めるの?
- 確率モデルでは複数の分割候補を扱う。
- 推論時には、最も確率の高い分割を選ぶ方式や、確率に基づいて分割をサンプリングする方式などがある。
- したがって、学習時に複数の候補を扱うことと、推論時に必ずランダムに分割することは別である。