tokenizerに少数のtokenを追加してtoken長を減らすには？

tokenizerは多言語モデルであれば日本語のvocabは限られた単語や漢字しか含んでいないので"りんご"、"みかん"、"ぶどう"と言った割と一般的な名詞すら含んでいない。
このためtokenizerではこれらの単語をひらがな一文字ごとに区切り3tokenかかる。
tokenizerは"apple","orange","grape"は含んでおり、これらの単語は1tokenに変換できる。
辞書型の置換を使い、
「りんご」→「apple \<jp1_token\>」
「リンゴ」→「apple \<jp2_token\>」
「アップル」→「apple \<jp3_token\>」
「みかん」→「orange \<jp1_token\>」
「ミカン」→「orange \<jp2_token\>」
「オレンジ」→「orange \<jp3_token\>」
「ぶどう」→「grape \<jp1_token\>」
「ブドウ」→「grape \<jp2_token\>」
「グレープ」→「grape \<jp3_token\>」

下記に示すように英語と日本語の辞書を用意して``["<jp1_token>", "<jp2_token>", "<jp3_token>"]``を追加するだけでtokenizerに含まれるすべての英単語の日本語やカタカナ呼びのvocab水増し出来る。
また、token長も16→13となっている。

```python
from transformers import AutoTokenizer

dict_jp_to_en = {
    "りんご": " apple<jp1_token>",
    "リンゴ": " apple<jp2_token>",
    "アップル": " apple<jp3_token>",
    "みかん": " orange<jp1_token>",
    "ミカン": " orange<jp2_token>",
    "オレンジ": " orange<jp3_token>",
    "ぶどう": " grape<jp1_token>",
    "ブドウ": " grape<jp2_token>",
    "グレープ": " grape<jp3_token>",
    # 他の単語も追加
}

def convert(text, dict_jp_to_en):
    for key in dict_jp_to_en.keys():
        text = text.replace(key, dict_jp_to_en[key])
    return text

text = "りんごとオレンジとグレープを買いました。"
model_list = ["nvidia/Llama3-ChatQA-1.5-70B"]


print(text)
for model_name in model_list:
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    input_ids = tokenizer(text)['input_ids']
    print(tokenizer.name_or_path, len(input_ids), input_ids, tokenizer.vocab_size)
    #print(tokenizer.convert_ids_to_tokens(input_ids))
    s_list = []
    for input_id in input_ids:
        s_list.append(tokenizer.decode([input_id]))
    print(s_list)

    tokenizer.add_tokens(["<jp1_token>", "<jp2_token>", "<jp3_token>"])
    text2 = convert(text, dic_jp_to_en)
    input_ids2 = tokenizer(text2)['input_ids']
    print(tokenizer.name_or_path, len(input_ids2), input_ids2, tokenizer.vocab_size)
    #print(tokenizer.convert_ids_to_tokens(input_ids2))
    s_list = []
    for input_id in input_ids2:
        s_list.append(tokenizer.decode([input_id]))
    print(s_list)
}
-----------------------------------------------------------
りんごとオレンジとグレープを買いました。
nvidia/Llama3-ChatQA-1.5-70B 16 [31431, 25827, 48154, 19732, 90962, 111999, 76428, 19732, 41758, 44971, 107610, 30512, 107691, 16995, 79721, 1811] 128000
['り', 'ん', 'ご', 'と', 'オ', 'レン', 'ジ', 'と', 'グ', 'レ', 'ープ', 'を', '買', 'い', 'ました', '。']
nvidia/Llama3-ChatQA-1.5-70B 13 [24149, 128256, 19732, 19087, 128258, 19732, 52252, 128258, 30512, 107691, 16995, 79721, 1811] 128000
[' apple', '<jp1_token>', 'と', ' orange', '<jp3_token>', 'と', ' grape', '<jp3_token>', 'を', '買', 'い', 'ました', '。']
```
ただし、削減に意味があるためには日本語で3token以上に分割される単語でかつ英語で1tokenに変換される単語でないと意味がない。日本語で2tokenに分割される単語に対しては英語で1token+追加tokenでは削減効果がない。

従って主にカタカナで書く外来語に対しては効果がありそうなものの漢字二文字の熟語に対してはあまり効果がなさそうである。
また、日本語の「アルミカン」とかの学習データから英語の「aluminum can」に「orange」の意味が付与される恐れがある。
また同音異義語に対しては「デザート」や「ライト」や「ロード」や「ナイト」を上手く変換できないかもしれない。

# 実際にテストした時の問題
実際に「パーソナルコンピュータ」のwikiページ(の途中まで)に対してこれをやって削減できるか試す。
tokenizerで文章を分割してこれを見ながら辞書に単語を追加していく。この時の問題点を挙げる。

https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%BC%E3%82%BD%E3%83%8A%E3%83%AB%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF

## ①：逆変換が一意にならない
例えば以下の辞書があった場合、「マイクロコンピュータ」→["micro", "\<jp3_token\>", "computer", "\<jp3_token\>"]となりこの逆変換は「マイクロコンピュータ」と戻せる。この逆変換は一意である。

一方で、「マイコン」→["micro", "computer", "\<jp3_token\>"]の逆変換は「microコンピュータ」か「マイコン」か分からない。（辞書の順番による）


```python
dict_jp_to_en = {
    "マイクロ": " micro<jp3_token>",
    "コンピュータ": " computer<jp3_token>",
    "マイコン": " micro computer<jp3_token>",
}
```

## ②：文字の区切りが正しくない。
一例あげると「ブロードバンドルーター」において辞書にある「バンドル」という単語で置換され、['ブロ', 'ード', ' bundle', '\<jp3_token\>', 'ータ']と変換された。
この逆変換は一意であるもの変換の意味的には正しくない。これが発生するかは辞書の順番による。

## ③：日本語の分割自体がばらつく
日本語の分割が句読点や単語の前後関係に依存する。
例えばアメリカという単語はページの中で異なっており、
['197', '1', '年に', 'アメリカ', 'の']と['使', 'わ', 'れて', 'おり', '、ア', 'メリカ', 'では',]と必ずしも同じ分割ではない。おそらくtokenizerに'、ア'が含まれているので変になるのだと思われる。

## ④：効率が悪い
例えば「パーソナルコンピュータ」は[' personal', '\<jp3_token\>', ' computer', '\<jp3_token\>']と変換できるが、'\<jp3_token\>'自体は複数回繰り返し使う意味が薄い。さらに'\<jp4_token_x2\>'を追加して、[' personal', ' computer', '\<jp4_token_x2\>']を[' personal', '\<jp3_token\>', ' computer', '\<jp3_token\>']と同じとみなす変換があれば効率的だろうか


## ⑤：英語にない概念
「お好み焼き」とか「うどん」とか「羅生門」とか。
このような単語は前述の方法では変換できない。


# 結果
以下の様にadd_tokensで語彙を3つ追加し、日本語token長＞(英語token+追加token)の辞書を作る（これは使用tokenizerに依る）。この時、10512→9536と10%程度token長は短くなった。辞書には400程度の単語を追加をした(パーソナルコンピュータのwikipediaページ内の200単語、基礎英語a~d200単語程度)。実際には上手く動いてない単語も多数ある。割合でいうと基礎単語1000単語くらい追加すればカタカナの多いページは2～5%程度削減できるのではないか。カタカナの外来語が多いページは現状tokenizerの分割に無駄が多く削減の可能性はある。一方で羅生門なんかは今回の方針では1tokenも削減できなかった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/d17b9e49-9ec5-3281-a4ff-022fb0545cee.png)

```python
for model_name in model_list:
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    tokenizer.add_tokens(["<jp1_token>", "<jp2_token>", "<jp3_token>"])
    dict_jp_to_en2 = dict_jp_to_en.copy()
    for key in dict_jp_to_en.keys():
        input_ids1 = tokenizer(key)['input_ids']
        input_ids2 = tokenizer(dict_jp_to_en[key])['input_ids']
        if len(input_ids1) <= len(input_ids2):
             #print(key, dict_jp_to_en[key], input_ids1, input_ids2)
             dict_jp_to_en2.pop(key)
        else:
             print(key, dict_jp_to_en[key], input_ids1, input_ids2)
    dict_jp_to_en = dict_jp_to_en2

    input_ids3 = tokenizer(text)['input_ids']
    print(tokenizer.name_or_path, len(input_ids3), tokenizer.vocab_size)

    text2 = convert(text, dict_jp_to_en)
    input_ids4 = tokenizer(text2)['input_ids']
    print(tokenizer.name_or_path, len(input_ids4), tokenizer.vocab_size)
------------------------------------------
りんご  apple<jp1_token> [31431, 25827, 48154] [24149, 128256]
みかん  orange<jp1_token> [64121, 32149, 25827] [19087, 128256]
ミカン  orange<jp2_token> [101198, 71493, 16073] [19087, 128257]
オレンジ  orange<jp3_token> [90962, 111999, 76428] [19087, 128258]
ブドウ  grape<jp2_token> [52884, 45923, 65299] [52252, 128257]
グレープ  grape<jp3_token> [41758, 44971, 107610] [52252, 128258]
...
# wiki:パーソナルコンピュータより
パーソナル   personal<jp3_token> [80805, 38248, 121, 115307] [220, 4443, 128258]
シェアリング  sharing<jp3_token> [122908, 39880, 122835] [11821, 128258]
マイクロ  micro<jp3_token> [68759, 25197, 110305] [8162, 128258]
プロセッサ  processor<jp3_token> [107420, 64810, 26269, 60868] [18121, 128258]
ラップトップ  laptop<jp3_token> [32131, 103830, 20251, 103830] [21288, 128258]
デスクトップ  desktop<jp3_token> [68408, 22398, 110548, 103830] [17963, 128258]
タブレット  tablet<jp1_token> [47307, 52884, 123167] [21354, 128256]
...
# 基礎英単語a～d
宛先  address<jp2_token> [8676, 249, 61826] [2686, 128257]
アドレス  address<jp3_token> [39880, 45923, 108744] [2686, 128258]
アダルト  adult<jp3_token> [39880, 99959, 108958] [6822, 128258]
アドバイス  advice<jp3_token> [39880, 45923, 66953, 112900] [9650, 128258]
アフタヌーン  afternoon<jp3_token> [39880, 52591, 47307, 125864, 107060] [13658, 128258]
年齢  age<jp1_token> [8107, 106036, 95] [4325, 128256]
エイジ  age<jp3_token> [76739, 25197, 76428] [4325, 128258]
飛行機  airplane<jp1_token> [107850, 23039, 101513] [44024, 128256]
エアプレーン  airplane<jp3_token> [76739, 39880, 118192, 107060] [44024, 128258]
飛行場  airport<jp2_token> [107850, 23039, 75267] [17149, 128257]
...
値下げ  discount<jp2_token> [112603, 17297, 102639] [11336, 128257]
ディスカウント  discount<jp3_token> [103857, 122407, 65299, 52414] [11336, 128258]
一品料理  dish<jp1_token> [15120, 25446, 122735] [12269, 128256]
ドクター  doctor<jp3_token> [45923, 29220, 102514] [10896, 128258]
ドリーム  dream<jp3_token> [45923, 37823, 103202] [8063, 128258]
飲み物  drink<jp1_token> [115320, 64121, 53953] [7172, 128256]

nvidia/Llama3-ChatQA-1.5-70B 10512 128000
nvidia/Llama3-ChatQA-1.5-70B 9536 128000
```

# まとめ：
カタカナの名詞に対するtokenizerの分割が効率的でないように感じたのでtokenizerの英単語vocabを流用して分割してみた。変換辞書を追加することで効率的になる可能性はあるものの逆変換が一意にならないなどの課題もある。