bert-japaneseの学習済みsentencepieceモデルを眺める

bert-japaneseでは日本語のテキストのトークン化にsentencepieceが使われる。

日本語版wikipedia(リポジトリのconfig.iniによると20181220のダンプ)で学習されたsentencepieceのモデルが作者のサイトgoogle driveで公開されている。

今回はこのsentencepieceモデルをながめてみる。sentencepieceをあまりちゃんと使ったことがないので、使い方も含めて見ておく。

モデルに関連するファイル

モデルに関連するファイルはwiki-ja.model, wiki-ja.vocabの2つだ。

wiki-ja.model

sentencepieceの学習済みモデルのそのもので、これをライブラリに読み込ませてテキストを分割する。

wiki-ja.vocab

sentencepieceの分割処理自体には不要だが、分割された文字列をBERTに与える時にidに変換するための辞書の役割を果たしている。wiki-ja.vocabは1行ごとに文字列とスコアがタブ区切りで記された単純なファイルだ。

Sentencepieceを使っている部分

sentencepieceを呼び出してトークン化するクラスは次のように書かれていた。

bert-japaneseのtokenization_sentencepiece.pyから

class SentencePieceTokenizer(object):
    """Runs SentencePiece tokenization (from raw text to tokens list)"""

    def __init__(self, model_file=None, do_lower_case=True):
        """Constructs a SentencePieceTokenizer."""
        self.tokenizer = sp.SentencePieceProcessor()
        if self.tokenizer.Load(model_file):
            print("Loaded a trained SentencePiece model.")
        else:
            print("You have to give a path of trained SentencePiece model.")
            sys.exit(1)
        self.do_lower_case = do_lower_case

    def tokenize(self, text):
        """Tokenizes a piece of text."""
        text = convert_to_unicode(text)
        if self.do_lower_case:
            text = text.lower()
        output_tokens = self.tokenizer.EncodeAsPieces(text)
        return output_tokens

loadで学習済みモデルを読み込んでおき、EncodeAsPiecesを日本語文字列を引数として呼び出すと、分割された文字列がリストとして返却されるようだ。

Sentencepieceのリポジトリを見ると、文字列を分割する関数は何種類かある。

関数 機能
EncodeAsPieces(text) textを最もスコアが高い方法で分割してリストを返却
NBestEncodeAsPieces(text, nbest_size) textの分割のうちスコアがnbest_size番目までを返却
SampleEncodeAsPieces(text, nbest_size, alpha) nbest_size, alphaを満たすtextの分割からランダムに1つを返却

それぞれ次のような動きをする:

from pprint import pprint
import sentencepiece as sp
tokenizer = sp.SentencePieceProcessor()
tokenizer.load('model/wiki-ja.model')
# True
text = 'Sentencepieceって何?'

tokenizer.EncodeAsPieces(text)
# ['▁', 'S', 'ent', 'ence', 'pie', 'ce', 'って', '何', '?']

pprint(tokenizer.NBestEncodeAsPieces(text, 5))
# [['▁', 'S', 'ent', 'ence', 'pie', 'ce', 'って', '何', '?'],
#  ['▁', 'S', 'ent', 'ence', 'pie', 'c', 'e', 'って', '何', '?'],
#  ['▁', 'S', 'ent', 'ence', 'p', 'ie', 'ce', 'って', '何', '?'],
#  ['▁', 'S', 'ent', 'ence', 'pi', 'e', 'ce', 'って', '何', '?'],
#  ['▁', 'S', 'en', 't', 'ence', 'pie', 'ce', 'って', '何', '?']]

# 同一条件で5回サンプルしてみる
pprint([tokenizer.SampleEncodeAsPieces(text, -1, 0.1) for _ in range(5)])
# [['▁', 'S', 'ent', 'en', 'ce', 'p', 'ie', 'c', 'e', 'って', '何', '?'],
#  ['▁', 'S', 'ent', 'en', 'ce', 'p', 'ie', 'c', 'e', 'って', '何', '?'],
#  ['▁', 'S', 'ent', 'ence', 'pi', 'e', 'c', 'e', 'っ', 'て', '何', '?'],
#  ['▁', 'S', 'ent', 'en', 'ce', 'p', 'ie', 'ce', 'っ', 'て', '何', '?'],
#  ['▁', 'S', 'e', 'nt', 'e', 'nc', 'ep', 'ie', 'ce', 'って', '何', '?']]

SampleEncodeAsPiecesはちょっと変わった関数だが、そもそもSentencepieceには、(統計的に出現しやすい)色々な分割を出せれば、画像学習の時のようなデータの水増しが可能となるのでは?という発想が背景にあるらしく、それを実現するために存在している。

なお、出力にしばしば出ている「▁」(LOWER ONE EIGHTH BLOCK, '\u2581')はsentencepieceでスペースのかわりに使われる文字で、適宜スペースに置換する必要がある。先頭に必ず「▁」が付くのは仕様らしい。

2019-04-10変更: add_dummy_prefixフラグをTrueにすると、先頭に「▁」が付く。

(先頭の「▁」は独立でつくこともあるし、「▁aaa」が語彙にあれば非独立に出てくることもある)

モデルの語彙

Sentencepieceに少し慣れたところで、wiki-jaモデルの語彙を見る。作業内容はjupyter notebookの通り(途中の出力が長いので注意)。

まず、これは.vocabを見なくても分ることだが、語彙数は32000。

そのうち、アルファベットだけで出来ている語彙が1490あり、日本語版でもアルファベットがそこそこ含まれていることがわかる。

ソートして眺めていると、sentencepieceは「を続けている」、「が必要」、「契約を」、「存在しなかった」など、形態素的には複合した分割も学習している。

そこで、語彙の先頭が機能語等で始まっているものと逆に語尾が機能語等で終わっているものを集計してみる。

語彙の表層
429 155
817 470
688 659
1360 105
437 126
とは 14 4
こと 114 46
として 56 46
124 374
121 100
から 28 66
147 291
する 73 293
115 488
され 44 171
という 41 16
59 441
まで 12 34
41 3
36 1369
37 111

「を」で終わる語彙より始まる語彙のほうが多い、「が」は始まりにつく場合が多いが「は」は終わりにつく場合が多い、「の」は両方同程度ついているなど語ごとの偏りがみられる。 様々な機能語について複合語彙が学習されていて、形態素との関係を対応付けるのは容易ではなさそうだ。

この他気になったのは、「▁」や「、」で始まる語彙が少なからず出てくる点。

「▁」で始まる語彙は781、「、」を含む語彙は106あった。

「、」を含む語彙は「)、「」など括弧の間に「、」が入っているものと「、1990」のような西暦の前に「、」が入っているものだけだった。

一方、「▁」を先頭に持つ語彙は「▁」の後に普通の日本語や英単語がくっついているものが多かった。さらに「▁」を取り除いた部分が語彙に含まれない語彙が175あった。これには「よって」、「こうして」、「なぜなら」のような文の役割の手掛かりとなりうる語彙がふくまれていた。

感想

実はこの調査の目的には、先頭につく'▁'を機械的に取り除いてしまっても問題ないかを確かめることがあった(入力と文字数が変わってしまうと文字の位置が重要なタスク(SQuADなど)に影響する)。

先頭につく'▁'を機械的に取り除いてしまうと、175の語彙は漏れてしまうことが分かった。

32000中の175なのでそこまで影響はないだろうが、もったいない気もする。