BPE 向け pretokenizer のメモ(特に qwen2)
LLM の BPE(Byte Pair Encoding)による tokenizer では, BPE で処理しやすくするため(?)に, まず pretokenize(?)で入力文字列を分割(split)する.
たとえば "world123" は "world", "123" などと letter, digit に分割するなど.
このとき分割のパターンは regex(正規表現)で指定される(gpt2 から? そうなっているようでだいたい後続の tokenizer はそれを継承している)
gpt2
's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+
しかし regex は基本遅いし, unicode 考慮し robust に動作する regex はめんどい(一応 RE2 あるけど, G 社製でコード品質がビミューなのでつかいたくない, absl 依存でつらい, など)ので regex 利用は避けたいときがある.
実際にはシンプルなルールで処理できる.
ただし unicode 文字の分類(?)の判定処理が必要となる(letter, digit, etc).
実装としては llama.cpp を参考にするとよいでしょう.
Qwen2
行頭にスペースがあるかどうかでエンコードが変わるとある.
(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n ]+|\\s+(?!\\S)|\\s+
ぱっと見ようわからんが, llama3 と似ている感じか.
ChatGPT クンに聞いたらいい感じに正規表現解説してくれるよ.
(?i:'s|'t|'re|'ve|'m|'ll|'d)
: case sensitive でマッチ. (?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])
と同等. "He's going." -> "'s"
[^\\r\\n\\p{L}\\p{N}]?\\p{L}+
: 改行, Unicode letter or number 以外にマッチ + letter が続く. スペースを含む letter を許可. example: @World
-> World
, 123World
-> World
. ‗World
-> ‗World
\\p{N}
: number 1 文字. つまり number は先頭にも後方にもスペースは取らない. また一文字ごと. " 123" -> "1", "2", "3"
[^\\s\\p{L}\\p{N}]+[\\r\\n]*
: スペース, letter or number 以外にマッチ, それに 0 or 複数の改行がつづく. example: "Hello!!!\n\n" は !!!\n\n
にマッチ
?[^\\s\\p{L}\\p{N}]+[\\r\\n]*
: 先頭に 1 個以上のスペースを含み, スペース, letter or number 以外にマッチ, それに 0 or 複数の改行がつづく. 基本的にはスペース + 記号関係にマッチする. e.g. @!$
-> @!$
\\s*[\\r\\n ]+
: 前方と後方にスペースを含むのを許す改行にマッチ
\\s+(?!\\S)
: スペース + non スペースか文字列の終わりまでスペースが続くもの: e.g. "World " なら最後の 2 whitespaces にマッチ.
\\s+
: 1 以上のスペース
まとめると以下となるだろうか. マッチの順もだいたいリスト順となると思われる.
-
He's
など短縮形にマッチ('s
) - 前方にスペース含んだ letter にマッチ
‗Hello World
->‗Hello
,‗World
- 前方にスペース含まない letter にマッチ
Hello
- 数値(1 文字)にマッチ
123
-> '1' '2' '3' - 前方にスペース含むのを許す記号にマッチ. 改行含む
@?
,@@
, ' @\n' - スペース含む改行にマッチ
- スペースにマッチ
llama3
qwen2 とほぼ同じ(qwen2 が llama3 をベースにしたのかもであるが)であるが,
数値だけ, 3 文字まで一つにするの違いがある. e.g. "123" -> "123"
日本語
unicode で判定しているので, たとえば全角の3でも数値 \p{N}
として判定される.
Discussion