😱

新しく日本語BERTのトークナイザを学習するときは limit_alphabet に気をつけよう

2022/05/27に公開

huggingface/tokenizers を使って日本語BERTのトークナイザを新しく作りたい場合、色々な実装方法が考えられるが、BERT 向けにカスタマイズされた実装を持つクラスである BertWordPieceTokenizer を使うのが一番楽な実装である。例えば、以下の記事はとても参考になる。

https://tech.mntsq.co.jp/entry/2021/02/26/120013

コードにすると、以下のような感じになるだろう。

from tokenizers import BertWordPieceTokenizer
from tokenizers.pre_tokenizers import BertPreTokenizer
from tokenizers.processors import BertProcessing

tokenizer = BertWordPieceTokenizer(
    handle_chinese_chars=False,
    strip_accents=False,
    lowercase=False
)
# MecabPreTokenizer() は MeCab による分かち書きを行うクラス(上の記事を参照)
tokenizer._tokenizer.pre_tokenizer = PreTokenizer.custom(MecabPreTokenizer())

text = []
with open("訓練用のテキスト.txt", "r") as f:
    for line in f:
        cur_line = line.strip()
        if cur_line:
            text.append(cur_line)

tokenizer.train_from_iterator(
    text,
    limit_alphabet=30000,
    min_frequency=1,
    vocab_size=30000
)

# Python で実装された MecabPreTokenizer は保存することができないので,
# ダミーの Rust で書かれた BertPreTokenizer を注入しておく(上の記事を参照)
tokenizer._tokenizer.pre_tokenizer = BertPreTokenizer()

tokenizer.post_processor = \
    BertProcessing(
        sep=('[SEP]', tokenizer.token_to_id('[SEP]')),
        cls=('[CLS]', tokenizer.token_to_id('[CLS]')),
    )
tokenizer.save("保存先のパス")

さて、ここで train_from_iterator() の中で指定した limit_alphabet というオプションが非常に重要である。値は別に 30000 ちょうどである必要はないのだが、何も指定しないのではダメである。この件について説明しよう。


そもそも limit_alphabet とは何かというと、トークナイザが使える文字の種類の上限である。
BERT の WordPiece というアルゴリズムは、まずはじめに全ての単語を文字単位に一度バラバラにしたものを初期トークンとし、その後、塊として現れやすいトークンを結合して新しいトークンに追加することを繰り返す(参考記事)。この最初の文字単位にバラバラにするフェーズにおいて、低頻度の文字まで語彙に含まれるのが困るという考えから、使える文字の種類の上限として limit_alphabet が設定されているのである。

さて、BERT のオリジナル実装の対象言語は英語である。英語圏において、アルファベットはたった 26 文字しかなく、その他記号類と合わせても大した数ではない。というわけで、limit_alphabet のデフォルトの値は 1,000 に設定されている。

・・・もうお分かりいただけただろうか。膨大な種類の漢字を使う日本語では、1,000種類しか文字が使えないのではダメなのである!
例えば、2022年5月現在、常用漢字の個数は2,136字もある。よって、limit_alphabet をきちんと設定しないと、常用漢字の半分が [UNK] トークンになってしまう。
これを考えると、低めに見積もっても limit_alphabet は 3,000 くらいには設定した方がよいと思われる(個人的には、語彙数と同じ値を設定して全部の文字を使うのでも良い気がするが...)


なお、これは BertWordPieceTokenizer というBERT向けにカスタマイズされたクラスを使った場合に生じる問題であり、自分で WordPiece クラスから訓練する場合は問題がない(ただし、この場合には BERT向けに特殊トークンを追加したり、サブワードの前に ## の prefix を付けたりする設定が必要になるなど、実装に手間がかかる)。
その際には、以下の BertWordPieceTokenizer の中身を参考にしつつ、WordPieceTrainer の引数の limit_alphabet の部分は値を入れずに None のままにしておくのがよいだろう。

https://github.com/huggingface/tokenizers/blob/main/bindings/python/py_src/tokenizers/implementations/bert_wordpiece.py


なお、この件について、tokenizers ライブラリの作者の一人である @Narsil さんとの議論がとても参考になったので、合わせて掲載しておく。

https://github.com/huggingface/tokenizers/issues/1004

Discussion