huggingface/transformersのAutoTokenizerから学習済みSentencePiece Tokenizerを呼び出す

#はじめに
[huggingface/transformers](https://github.com/huggingface/transformers)の日本語BERTモデルには、`BertJapaneseTokenizer`が用意されています。これは[MeCab](https://taku910.github.io/mecab/)でpre tokenizeし、wordpieceかcharacter単位にtokenizeします。
しかし、`BertJapaneseTokenizer`は[SentencePiece](https://github.com/google/sentencepiece)に対応していません。SentencePieceを使いたい場合はどうすれば良いのでしょうか。
本記事では2種類の方法を説明します。
1つ目は、SentencePieceに対応している`AlbertTokenizer`を使用する方法です。非常に簡単なのでこちらの方法がおすすめです。
2つ目は、`PreTrainedTokenizerFast`から呼び出せるように学習済みのSentencePieceモデルを変換する方法です。手間が掛かるのでおすすめしません。

筆者が未熟なため、記事内容の誤りや記事内容より優れた方法があるかと思います。そのような箇所を見つけた際にはコメントでご指摘いただけますと幸いです。

#参考記事
1.[huggingface / transformersを使って日本語BERTの事前学習を実施してオリジナルな言語モデルを作ってみる](https://qiita.com/m__k/items/6f71ab3eca64d98ec4fc#2-tokenizer%E3%82%92%E5%AD%A6%E7%BF%92%E3%81%99%E3%82%8B)
2.[SentencePieceでの日本語分かち書きをTransformersのパイプラインに組み込む](https://radiology-nlp.hatenablog.com/entry/2021/06/08/013549)

#方法1：`AlbertTokenizer`を使用する
非常にシンプルです。`config.json`で`tokenizer_class`を`AlbertTokenizer`に指定し、`tokenizer_config.json`を下記のようにします。
※ `config.json`で`pad_token_id`が正しく設定されているか確認してください
※ special tokenがデフォルト値で問題ないか確認し、適宜変更してください

```json:tokenizer_config.json
{
    "do_lower_case": false,
    "vocab_file":"/model_dir/spiece.model",
    "unk_token":"<unk>",
    "bos_token":"
",
    "eos_token":"
",
    "pad_token":"[PAD]"
}
```
これを`AutoTokenizer.from_pretraind'で読み込みます。

```python:code1
from transformers import AutoTokenizer

# 適当な例文
text = '吾輩は猫である。名前はまだない'

tokenizer = AutoTokenizer.from_pretrained('bert_dir')
output = tokenizer(text)
print(tokenizer.convert_ids_to_tokens(output['input_ids']))
```

```python:result1
['[CLS]', '▁', '吾', '輩', 'は', '猫', 'である', '。', '名前', 'はまだ', 'ない', '[SEP]']
```

tokenizeできています。

#方法2：学習済みのSentencePieceモデルを変換する
##`PreTrainedTokenizerFast`に学習済みのTokenizerを読み込む
`PreTrainedTokenizerFast`の`__init__`引数に`tokenizer_file`があります。これに、[huggingface/tokenizers](https://github.com/huggingface/tokenizers)で作成されたTokenizerファイル(json形式)を渡すと学習済みのTokenizerを読み込めます。
つまり、SentencePieceモデルをTokenizerファイルに変換できれば良さそうです。

##学習済みSentenPieceモデルの変換
huggingface/tokenizersには`save`メソッドが実装されているため、一度huggingface/tokenizersに学習済みSentenPieceモデルを読み込み`save`メソッドを実行すれば変換できます。
huggingface/tokenizersに学習済みSentenPieceモデルを読み込むのは、参考記事2の通りに実装すれば良いので簡単です。
※ 私の環境では[sentencepiece_model_pb2.py](https://github.com/google/sentencepiece/blob/master/python/src/sentencepiece/sentencepiece_model_pb2.py)のダウンロードを要求されました
※ 追記：変換用の[公式スクリプト](https://github.com/huggingface/tokenizers/blob/master/bindings/python/scripts/convert.py)がありました。ただし、BERT用に変換するものはないため、公式スクリプトを参考にして書き直したほうが良さそうです。

```python:convert_sentencepiece_to_tokenizer.py
import argparse

from tokenizers.implementations import SentencePieceUnigramTokenizer
from tokenizers.processors import BertProcessing

def convert_sentencepiece_to_tokenizer(model_path, save_dir):
    tokenizer = SentencePieceUnigramTokenizer.from_spm(model_path)

    # BERTで使用するため、BERT用の後処理を加えます
    tokenizer.post_processor = BertProcessing(
        cls=("[CLS]", tokenizer.token_to_id('[CLS]')),
        sep=("[SEP]", tokenizer.token_to_id('[SEP]'))
    )

    tokenizer.save(save_dir+'tokenizer.json')


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--model_path', help='model path', type=str)
    parser.add_argument('--save_dir', help='save directory path', type=str)
    args = parser.parse_args()
    convert_sentencepiece_to_tokenizer(args.model_path, args.save_dir)
```
これで作成した`tokenizer.json`を、`PreTrainedTokenizerFast`の`tokenizer_file`に引き渡せば、学習済みのSentencePieceモデルを呼び出せます。
実際にできるか試してみます。

```python:code2
from transformers import PreTrainedTokenizerFast

# 適当な例文
text = '吾輩は猫である。名前はまだない'

tokenizer = PreTrainedTokenizerFast(
    tokenizer_file='/save_dir/tokenizer.json',
    unk_token='<unk>',
    bos_token='
',
    eos_token='
',
    cls_token='[CLS]',
    sep_token = '[SEP]',
    pad_token='[PAD]',
    mask_token='[MASK]'
)

output = tokenizer(text)
print(tokenizer.convert_ids_to_tokens(output['input_ids']))
```

```python:result2
['[CLS]', '▁', '吾', '輩', 'は', '猫', 'である', '。', '名前', 'はまだ', 'ない', '[SEP]']
```

SentencePieceでtokenizeできています。
後は、`config.json`で`tokenizer_class`を`PreTrainedTokenizerFast`に指定し、`tokenizer_config.json`を下記のようにします。
※ `config.json`で`pad_token_id`が正しく設定されているか確認してください

```json:tokenizer_config.json
{
    "do_lower_case": false,
    "tokenizer_file":"/save_dir/tokenizer.json",
    "unk_token":"<unk>",
    "bos_token":"
",
    "eos_token":"
",
    "cls_token":"[CLS]",
    "sep_token":"[SEP]",
    "pad_token":"[PAD]",
    "mask_token":"[MASK]"
}
```
これで`code1`を実行してみます。

```python:result3
['[CLS]', '▁', '吾', '輩', 'は', '猫', 'である', '。', '名前', 'はまだ', 'ない', '[SEP]']
```

こちらでもtokenizeできています。

#おわりに
本記事では2種類の方法を説明しましたが、方法1でやるべきだと思います。
方法2は、SentencePieceのalgorithmがUnigramでないと実行できません。各algorithmに合わせて変換コードを実装する必要があるため、更に手間がかかります。(XLNetやALBERT用であれば、公式のスクリプトを使えば問題ありません)対して方法1はalgorithmの影響もないため、非常にシンプルです。
~~方法2の利点？は、うまく拡張できれば[Juman++](https://nlp.ist.i.kyoto-u.ac.jp/?JUMAN%2B%2B)などにも対応できる**かもしれない**ことです。そこまでちゃんと調べていないため、機会があれば試してみようと思います。~~(できなさそうです)

(余談ですが、2種類の方法があるのは自分で方法2を考えた後に方法1を知ったためです。ドキュメントの読み込みとBERT関連の知識不足を思い知りました。)