GPT-2のBPEトークナイザに某ゲームの用語が紛れている？【謎トークン降臨！】

「[つくりながら学ぶ！LLM自作入門](https://www.amazon.co.jp/dp/4839987807)」を読んでいてBPEモデルについて調べていたら見つけました、

# TL; DR

- GPT-2のBPEモデルに以下の日本語トークン

```
23614: 覚醒
24731: ドラゴン
33454: 龍喚士
34473: ヘラ
39821: 龍契士
43361: ゼウス
```

# はじめに

LLMでは、入力された文字列を以下のような流れで数値計算可能なベクトルへ変換します。
(例の数字は適当)

```
文章
"This is a pen."

↓(1)トークン（単語に相当）の配列に分割

["This", " ", "is", " ", "a", " ", "pen", "."]
(実際はトークンに対応する整数値(ID)で表現)
[105, 1, 10, 1, 29, 1, 1001, 5]

↓(2)トークンを連続量のベクトルに変換

[[0.52, 0.21, -0.11], [0.14, 0.03, -0.26], ...]
```

(1) のトークン分割処理（トークナイザ）では単純に空白で分割して単語ごとに区切ることもできます（英語の場合）が、GPT等のOpenAIのモデルではBPE（バイトペアエンコーディング）というより効率的なトークナイザを使用しています。

https://en.wikipedia.org/wiki/Byte-pair_encoding

Pythonでは「tiktoken」というライブラリで利用可能です。

https://github.com/openai/tiktoken

BPEには、「登録されていない単語であってもトークンに区切れる」という特徴があります。イメージとしては、複合語「scarecrow」のトークンが無くても、分割した「scare」と「crow」の2トークンで表現するといった具合です。
（※実際には、言語の形態素にそって分割されるとは限りません）

以下のような長い単語も、短く分割してトークン化することができます。

```python
>>> tokenizer.encode("pneumonoultramicroscopicsilicovolcanoconiosis")
[79, 25668, 261, 25955, 859, 2500, 1416, 404, 873, 41896, 709, 349, 5171, 36221, 42960]
>>> tokenizer.decode([79])
'p'
>>> tokenizer.decode([25668])
'neum'
>>> tokenizer.decode([261])
'on'
```

また、分割する際のトークン数が少なくなるように、BPEでは実際の文字列データからトークン一覧を作成しています。

# 紛れ込んだ日本語トークン

前置きはこのあたりにして、記事の本題です。GPT-2で使用しているトークナイザのトークン一覧はほぼ英語ですが、数％日本語のトークンも紛れています。

まずは、英語以外のトークンを抽出します。

```python:英語以外のトークンを抽出
import tiktoken
import re

tokenizer = tiktoken.get_encoding("gpt2")

tokens = {}
for i in range(0, 50257): # 全トークンのIDでループ
  # 非ascii文字を含む、かつ印字可能なトークンのみ表示
	s = tokenizer.decode([i])
	if re.match(r'[^\x00-\x7F�\b]', s) and [c.isprintable() for c in s]:
		tokens[i] = s

for k, v in tokens.items():
	print(f"{k}: {v}")

print(len(tokens))
```

```:結果（形式は｛トークンID： トークン｝）
960: —
1399: …
1849:  
1906: –
2634: é
3581: •
3907: ­
4500: ——
4603:   
4707: ・
```

ここから日本語のみを拾い集めたのが以下の一覧です。160個くらいあり長いので折りたたんでいます。~~目視なので抜け漏れあったらすみません~~

<details><summary>日本語トークン一覧（クリックで開く）</summary>

```
5641: の
6312: ー
6527: ン
8943: ス
9202: ル
9263: ラ
11482: イ
11839: ア
11885: 龍
12675: リ
13298: ト
13765: ド
14099: ク
14777: ッ
15351: 神
15661: シ
16165: ウ
16646: ィ
17681: フ
17933: ゴ
18566: い
18803: 士
19073: ドラ
20115: マ
20513: オ
20804: 魔
21091: ジ
21410: 的
21689: 人
21763: カ
21959: デ
22174: ん
22180: し
22997: ゴン
23131: ャ
23363: ヘ
23376: タ
23544: エ
23614: 覚醒
23728: ガ
24001: ブ
24186: レ
24336: テ
24440: ュ
24679: コ
24731: ドラゴン
24806: ェ
25053: ノ
25084: キ
25224: た
25362: ァ
25465: 天
25581: 王
25589: ワ
25748: る
25795: ム
26095: グ
26229: ナ
26503: サ
26945: な
26998: メ
27370: か
27542: ミ
27852: ダ
28134: て
28255: り
28618: に
29557: う
29659: バ
29752: ヴ
30159: ま
30165: ニ
30201: と
30432: ゼ
30640: で
30965: プ
31090: チ
31676: は
31708: ーン
31758: を
31817: 】
31854: 【
31917: く
32014: 大
32546: パ
33180: っ
33454: 龍喚士
33623: す
33778: き
34103: ウス
34473: ヘラ
34633: の魔
35318: 装
35572: 田
35585: が
35604: ベ
35702: ック
35799: ット
36310: 子
36704: 戦
36853: ら
36922: ビ
37412: ハ
37426: ズ
37662: ォ
37858: ヤ
37955: 生
38519: 者
38834: 不
39258: れ
39467: シャ
39821: 龍契士
40235: 姫
40361: モ
40493: 『
40549: 』
40629: ディ
40792: 中
40948: あ
41115: ツ
41468: 上
41658: ケ
41939: ファ
42234: 闘
42396: イト
42468: 是
42637: 女
42869: ーク
42983: ワン
43095: 方
43266: も
43291: 作
43302: スト
43353: ール
43357: さ
43361: ゼウス
43899: ギ
44112: 黒
44326: ーテ
44431: ティ
44444: ヴァ
44686: ーティ
44916: ネ
45298: 之
45435: ッド
45635: 使
45823: ンジ
46036: こ
46268: 光
46777: だ
46948: エル
47271: セ
47559: ソ
47794: アル
47987: 代
48204: ラン
48304: 版
48457: フォ
48458: ザ
49011: 三
49390: 五
49476: 武
49546: 将
49960: 機
```
</details>

# 日本語トークンから感じる某ソシャゲ

トークンを見てみると、目を引く単語がちらほら見つかります。

```
23614: 覚醒
24731: ドラゴン
33454: 龍喚士
34473: ヘラ
39821: 龍契士
43361: ゼウス
```

およそ日常会話では使わない単語 ~~、というかパズルゲームをするときに使うような単語~~です。


単語の分割も見てみましょう。まずは比較として「日本語」という単語を見てみます。

```python
>>> tokenizer.encode("日本語")
[33768, 98, 17312, 105, 45739, 252]
>>> tokenizer.decode([33768])
'�'
>>> tokenizer.decode([98])
'�'
>>> tokenizer.decode([17312])
'�'
>>> tokenizer.decode([105])
'�'
>>> tokenizer.decode([45739])
'�'
>>> tokenizer.decode([252])
'�'
```

3文字の単語が6トークンに分割されてしまいました。日本語の複合語としては認識されていなさそうです。

一方こちらはどうでしょう？

```python
>>> tokenizer.encode("パズドラ")
[32546, 37426, 19073]
>>> tokenizer.decode([32546])
'パ'
>>> tokenizer.decode([37426])
'ズ'
>>> tokenizer.decode([19073])
'ドラ'
```

```python
>>> tokenizer.encode("パールヴァティー")
[32546, 43353, 44444, 44431, 6312]
>>> tokenizer.decode([32546])
'パ'
>>> tokenizer.decode([43353])
'ール'
>>> tokenizer.decode([44444])
'ヴァ'
>>> tokenizer.decode([44431])
'ティ'
>>> tokenizer.decode([6312])
'ー'
```

```python
>>> tokenizer.encode("サーティワン")
[26503, 44686, 42983]
>>> tokenizer.decode([26503])
'サ'
>>> tokenizer.decode([44686])
'ーティ'
>>> tokenizer.decode([42983])
'ワン'
```

~~バステトかわいい~~

`日本語` のときとは打って変わって、1トークンで1~2文字とかなり効率的に表現できています。

## Why?
調べても確かなことは出てきませんでした。

推測になってしまいますが、データセットの説明に「Redditから辿れるリンクをスクレイピングした」とあるため、攻略スレ等が元データの中に含まれていて日本語データが偏った可能性があります。

https://paperswithcode.com/dataset/webtext

ただし、GPT-2のBPEトークン作成に使われた元データは非公開のため、原典にあたることは難しそうです。

# おわりに
以上、BPEトークナイザに含まれる日本語トークンについての紹介でした。正直不完全燃焼ではありますが、元データが非公開なのでここで力尽きてしまいました。
有識者が見ればまだ特徴的な単語が隠れているかもしれません。~~私はヘラ・イースくらいの知識で止まっているので詰~~

より詳しい情報をお持ちの方がいましたら記事投稿お待ちしております（他力本願）