Definition:Byte Pair Encoding
BPEとは、出現頻度の高い隣接するバイト(または文字)のペアを繰り返し1つの新しいトークンに統合していくことで、語彙を構築するサブワード分割アルゴリズムである。もとは1994年にPhilip Gageが考案したデータ圧縮アルゴリズムであり、2016年にSennrichらが自然言語処理のトークナイゼーション手法として転用したことで広く普及した。
BPEによる語彙構築は、次の手順で行われる。
例えば low、lower、lowest という単語が繰り返し出現するコーパスでは、まず l と o が統合されて lo となり、次に lo と w が統合されて low となる、というように段階的に長い単位が語彙に加わっていく。
GPT-2以降のモデルで採用されている「バイトレベルBPE」は、文字ではなくUTF-8のバイト列を最小単位として統合を行う。これにより、絵文字や任意の言語の文字を含め、理論上あらゆる文字列を256種類のバイトの組み合わせで表現できるため、未知語(OOV: Out-of-Vocabulary)の問題が原理的に発生しない。また単語境界を保持するため、語頭の空白を Ġ のような特殊マーカーとしてトークンに含める実装が一般的である。
BPEは、頻出語を少数のトークンで、希少語や未知語をより細かいサブワードの組み合わせで表現できるため、固定語彙でありながら任意の文字列を扱える点が利点である。また分かち書きを行わない言語にも(前処理なしで)適用できる。
一方で、統合は各段階での局所的な頻度に基づく貪欲法であり、大域的に最適な分割が得られる保証はない。また数字を不規則な単位で分割してしまうことが多く、大規模言語モデルの算術性能低下の一因として指摘されることがある。日本語や中国語のように単語境界が明示されない言語では、形態素解析など別の前処理と組み合わせて使われることも多い。
import re
from collections import Counter
def get_pair_frequencies(vocab):
"""語彙中の全隣接シンボル対の出現頻度を集計する"""
pair_freqs = Counter()
for word, freq in vocab.items():
symbols = word.split()
for pair in zip(symbols, symbols[1:]):
pair_freqs[pair] += freq
return pair_freqs
def apply_merge(vocab, pair):
"""指定したペアを1つのシンボルに統合した新しい語彙を返す"""
bigram = " ".join(pair)
replacement = "".join(pair)
return {word.replace(bigram, replacement): freq for word, freq in vocab.items()}
def build_corpus_from_text(text):
"""生のテキストを単語に分割し、出現頻度を数える"""
words = re.findall(r"\w+", text.lower())
return Counter(words)
def train_word_level_bpe(text, num_merges):
"""テキストから単語頻度を数え、BPEマージ規則を学習し、学習後の語彙とマージ履歴を返す"""
corpus = build_corpus_from_text(text)
# 各単語を文字単位に分割し、末尾に語尾マーカーを付与
vocab = {" ".join(list(word)) + " ": freq for word, freq in corpus.items()}
merge_history = []
for _ in range(num_merges):
pair_freqs = get_pair_frequencies(vocab)
if not pair_freqs:
break
best_pair = max(pair_freqs, key=pair_freqs.get)
vocab = apply_merge(vocab, best_pair)
merge_history.append((best_pair, pair_freqs[best_pair]))
return vocab, merge_history
# 使用例: 実際の英文コーパスに近いテキスト
text = """
Neural networks learn useful representations from raw data through repeated
training. A tokenizer converts raw text into a sequence of tokens before it
is fed into a neural network. Byte pair encoding is one of the most common
tokenization algorithms used in modern neural network based language models.
Training a tokenizer requires a large amount of text, and the resulting
vocabulary reflects the statistical structure of that text.
"""
final_vocab, merges = train_word_level_bpe(text, num_merges=30)
print("学習されたマージ規則:")
for pair, freq in merges:
print(f" {pair} → 頻度{freq}")
print("\n最終的な語彙(サブワード分割後):")
for word, freq in final_vocab.items():
print(f" {word} (頻度: {freq})")
出力結果:
学習されたマージ規則:
('s', '') → 頻度10
('e', '') → 頻度10
('i', 'n') → 頻度9
('r', 'a') → 頻度7
('r', 'e') → 頻度7
('e', 'n') → 頻度7
('t', '') → 頻度7
('n', 'e') → 頻度6
('a', '') → 頻度6
('t', 'h') → 頻度6
('t', 'o') → 頻度6
('l', '') → 頻度5
('a', 't') → 頻度5
('d', '') → 頻度5
('m', 'o') → 頻度5
('o', 'r') → 頻度4
('n', '') → 頻度4
('s', 'e') → 頻度4
('in', 'g') → 頻度4
('ing', '') → 頻度4
('to', 'k') → 頻度4
('tok', 'en') → 頻度4
('e', 'r') → 頻度4
('o', 'f') → 頻度4
('of', '') → 頻度4
('ne', 'u') → 頻度3
('neu', 'ra') → 頻度3
('neura', 'l') → 頻度3
('ne', 't') → 頻度3
('net', 'w') → 頻度3
最終的な語彙(サブワード分割後):
neural (頻度: 3)
netw or k s (頻度: 1)
l e a r n (頻度: 1)
u se f u l (頻度: 1)
re p re sen t at i o n s (頻度: 1)
f r o m (頻度: 1)
ra w (頻度: 2)
d at a (頻度: 1)
th r o u g h (頻度: 1)
re p e at e d (頻度: 1)
t ra in ing (頻度: 2)
a (頻度: 5)
token i z er (頻度: 2)
c o n v er t s (頻度: 1)
t e x t (頻度: 3)
in to (頻度: 2)
se q u en c e (頻度: 1)
of (頻度: 4)
token s (頻度: 1)
b e f ore (頻度: 1)
i t (頻度: 1)
i s (頻度: 2)
f e d (頻度: 1)
netw or k (頻度: 2)
b y t e (頻度: 1)
p a i r (頻度: 1)
en c o d ing (頻度: 1)
o ne (頻度: 1)
th e (頻度: 3)
mo s t (頻度: 1)
c o m mo n (頻度: 1)
token i z at i o n (頻度: 1)
a l g or i th m s (頻度: 1)
u se d (頻度: 1)
in (頻度: 1)
mo d er n (頻度: 1)
b a se d (頻度: 1)
l a n g u a g e (頻度: 1)
mo d e l s (頻度: 1)
re q u i re s (頻度: 1)
l a r g e (頻度: 1)
a mo u n t (頻度: 1)
a n d (頻度: 1)
re s u l t ing (頻度: 1)
v o c a b u l a r y (頻度: 1)
re f l e c t s (頻度: 1)
s t at i s t i c a l (頻度: 1)
s t r u c t u re (頻度: 1)
th at (頻度: 1)
import numpy as np
# カリンの属性(Query)
# [甘さ, 酸味, 色]
Q = np.array([7, 4, 2])
# 既存果物の属性(Key)
# [甘さ, 酸味, 色]
K = np.array([
[8, 3, 2], # りんご
[6, 8, 3], # みかん
[3, 9, 1], # レモン
])
# 既存果物の評価(Value)
V = np.array([
4.5, # りんご
4.2, # みかん
4.0 # レモン
])
# カリンQと各果物Kの類似度(内積)
scores = K @ Q
# Softmaxで重みに変換
weights = np.exp(scores) / np.exp(scores).sum()
# 評価を重み付きで合成
output = weights @ V
print("類似度:", scores)
print("重み:", weights)
print("カリンの予測評価:", output)
出力結果は、
類似度: [72 80 59]
重み: [3.35350130e-04 9.99664649e-01 7.58001761e-10]
カリンの予測評価: 4.2001006048874645
なお、@ は、Pythonにおける行列積(matrix multiplication)演算子。
Mathematics is the language with which God has written the universe.