🍣

日本語tokenizerを学習する

2023/09/30に公開
1

tokenizerを学習させてhuggingface hubにuploadするまでを行う

作成したtokenizerはここ
https://huggingface.co/if001/sentencepiece_ja

tokenizerはsentencepieceのunigramを使う

学習にはhuggingfaceのtokenizer ライブラリを使う
https://github.com/huggingface/tokenizers

sentencepieceのunigramを、transformerのtokenizerに変換する方法がなさそうだったので、
PreTrainedTokenizerを継承してhuggingface hubから使えるようにした。
スマートなやり方あれば教えてください。

学習

tokenizerを学習させるモチベーション

データセットはwikinewsのja/en、青空文庫として以下を使わせて頂く
https://huggingface.co/datasets/izumi-lab/wikinews-ja-20230728
https://huggingface.co/datasets/izumi-lab/wikinews-en-20230728
https://huggingface.co/datasets/if001/aozorabunko-clean-sin

学習はreadmeをそのまま
https://github.com/huggingface/tokenizers/tree/main/bindings/python

tokenizerをロードして、train_from_iteratorで学習する。
vocab_sizeは35000、後々更に大きいサイズを試してみたい

from tokenizers import Tokenizer, models, normalizers, pre_tokenizers, trainers

tokenizer = Tokenizer(models.Unigram())
tokenizer.normalizer = normalizers.NFKC()
tokenizer.pre_tokenizer = pre_tokenizers.UnicodeScripts()
tokenizer.decoder = decoders.ByteLevel()

trainer = trainers.UnigramTrainer(
        vocab_size=35000,
        show_progress=True,
        special_tokens=["<PAD>", "<BOS>", "<EOS>", "<UNK>", "<MASK>"],
        unk_token="<UNK>"
    )

def ds_yielder():
  // datasetのload

tokenizer.train_from_iterator(ds_yielder(), trainer=trainer)
tokenizer.save('./tokenizer.json')

学習コードのサンプルは以下
https://github.com/if001/spm_tokenizer_ja/blob/0.0.1/train_full.py

novelAIのtokenizerとの比較

https://huggingface.co/NovelAI/nerdstash-tokenizer-v1

token count: 20
['それは', '九月', '初', '旬', 'のある', '蒸', 'し', '暑い', '晩', 'のことであった', '。', '私は', '、', 'D', '坂の', '大', '通り', 'の中', '程', 'にある']

novelAI
token count: 22 
['それは', '九', '月', '初', '旬', 'のある', '蒸', 'し', '暑い', '晩', 'のこと', 'であった', '。', '私は', '、', 'D', '坂', 'の大', '通り', 'の中', '程', 'にある']
学習したもの
token count: 11
['九月', '、', '十月', '、', '十一月', 'は', '月', 'が', '綺麗', 'だ', '。']

novelAI
token count: 12
['九', '月', '、', '十', '月', '、', '十一', '月は', '月が', '綺麗', 'だ', '。']

['九月']と['九', '月']のように月が1つのトークンとして分割できている。

upload

AutoTokenizer.from_pretrainedから呼び出すことを想定

最初にhuggingfaceのUIからtokenizer用のrepositoryを作成しておく。

upload方法は色々あるが、ファイルサイズが小さいので今回はgitclientを使う
https://huggingface.co/docs/hub/repositories-getting-started

huggingface.tokenizertransfomer.AutoTokenizer.from_pretrainedから呼び出せるように、PreTrainedTokenizerを継承したclassとしてsentencepiece_ja.pyを作成する。

あとは、get_vocabvocab_size_tokenize_convert_token_to_id_convert_id_to_tokenのあたりを実装しておく。

class SentencePieceJA(PreTrainedTokenizer):
    def __init__(self, model_path = "./tokenizer.json", **kwargs):
        super().__init__(**kwargs)
        from tokenizers import Tokenizer
        self._tokenizer = Tokenizer.from_file(model_path)

https://huggingface.co/if001/sentencepiece_ja/blob/main/sentencepiece_ja.py

以下の用にsave_vocabularyを実装しておくと、tokenizer.save_pretrained('./sample')でuploadに必要なファイルが出力できる。

    def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> Tuple[str]:
        index = 0
        if os.path.isdir(save_directory):
            vocab_file = os.path.join(
                save_directory, (filename_prefix + "-" if filename_prefix else "") + 'vocab.txt'
            )
        else:
            vocab_file = (filename_prefix + "-" if filename_prefix else "") + save_directory        
        with open(vocab_file, "w", encoding="utf-8") as writer:
            for token, token_index in sorted(self.get_vocab().items(), key=lambda kv: kv[1]):
                if index != token_index:
                    index = token_index
                writer.write(token + "\n")
                index += 1
        return (vocab_file,)

tokenizer_config.jsonに以下を追加することで、tokenizerのload時にsentencepiece_ja.pyを使うようになる。

  "auto_map": {
    "AutoTokenizer": ["","sentencepiece_ja.SentencePieceJA"]
  }

auto_mapあたりの挙動はこの辺を確認
https://github.com/huggingface/transformers/blob/v4.30.0/src/transformers/models/auto/tokenization_auto.py#L648-L653

https://github.com/huggingface/transformers/blob/v4.30.0/src/transformers/models/auto/tokenization_auto.py#L671-L678

最終的にuploadするファイルは以下となる

  • tokenizer.json
  • special_tokens_map.json
  • tokenizer_config.json
  • vocab.txt
  • sentencepiece_ja.py

これらをgit push

Discussion

安岡孝一安岡孝一

tokenizer.jsonが出来上がっているのなら、PreTrainedTokenizerから組み上げるよりもDebertaV2TokenizerFastあたりを借りた方が、auto_mapも不要で楽だと思うのです。

from transformers import DebertaV2TokenizerFast
tkz = DebertaV2TokenizerFast(tokenizer_file="./tokenizer.json", split_by_punct=True, do_lower_case=False, keep_accents=True, vocab_file="/dev/null")
tkz.save_pretrained("model_dir")

よければお試しあれ。