6本目!Neural Machine Translation of Rare Words with Subword Units

では、今回はいろんな論文に登場するBPEについて見ていきたいと思います!

こいつはTokenzierとしてよく使うsentence pieceの元となった論文ですな。

ちなみにsentece pieceとbpeの違いは、sentence pieceはユニグラム言語モデルっていうやつを使って、subwordを構築したらしい。このおかげで日本語や中国語みたいに、空白文字列のない言語モデルでもきれいに分割ができるようになったみたい。

つまり、日本語を使うならsentece pieceを使いましょうってことらしい。

まあ、Huggin FaceのTokenizerは基本的にsentence piece っぽいし。 日本語でmecab ベースの toknenizerを使うなら、

tokenizer = BertTokenizer.from_pretrained("cl-tohoku/bert-large-japanese")

でいいし、sentence piece ベースのtokenizerを使うなら、rinnaのgithubからgoogle_sp.modelをDLしてきて、

    tokenizer = T5Tokenizer(
        vocab_file="google_sp.model",
        bos_token="<s>",
        eos_token="</s>",
        unk_token="<unk>",
        pad_token="[PAD]",
        cls_token="[CLS]",
        sep_token="[SEP]",
        mask_token="[MASK]",
        extra_ids=0,
        additional_special_tokens=(),
        do_lower_case=True
    )

みたいな感じでいいみたい。

もし、カスタムで学習したい場合はこのサイトとかを参考にするといいみたい。

tech.mntsq.co.jp

Tokenizerのコンポーネントとうか、toknezerのPipelineのためのReferenceはここ。

huggingface.co

でも、普通のTokenizerとBert Tokenizerの違いが判らん。

実装を見てみると、

        self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab, unk_token=self.unk_token)

    def _tokenize(self, text):
        split_tokens = []
        if self.do_basic_tokenize:
            for token in self.basic_tokenizer.tokenize(text, never_split=self.all_special_tokens):

                # If the token is part of the never_split set
                if token in self.basic_tokenizer.never_split:
                    split_tokens.append(token)
                else:
                    split_tokens += self.wordpiece_tokenizer.tokenize(token)
        else:
            split_tokens = self.wordpiece_tokenizer.tokenize(text)
        return split_tokens

ってなっている。つまり、内部でBertを使っているとかじゃなくて、ただBertの論文に会わせて、WordpieceをつかうTokenizerとして実装しているみたい。

もし、T5-tokenizerのほうのモデルを改良したかったら、sentence pieceのライブラリから作るみたい。

    import sentencepiece
    sentencepiece.SentencePieceTrainer.Train(r'--input=ja.txt, --model_prefix=test, --vocab_size=32000', --character_coverage=0.9995)
    tokenizer = T5Tokenizer('test.model')

ただし、1文が4192文字以内で、空行を除いたファイルを作らないといけないから、ちょい面倒かも?

まあ、Tokenizerの実装についてはこのへんでおいておいて実際の論文の中身に入っていきたいと思います。

Byte Pair Encoding

そもそも、Byte Pair Encdingとは文字列の圧縮方法として提案された手法です。

例えば、「AAAABBBBBAAABBB」みたいな単語の塊が見られます。

これを「BB」を「Z」に置き換えると「AAAAZZBAAAZB」、それだけで文字数が3文字圧縮できます。さらにこれを「AA」を「Y」に置き換えると「YYZZBYAZB」となり、さらに3文字圧縮できます。これから「YY」を「X」、「ZZ」を「U」に変換すれば圧縮の完了で、「XUBYAZB」となり、結果的に8文字も圧縮でき、約46%の圧縮率となります。

解凍時にははこの変換ルールを逆変換すればいいので、シンプルながらも強力な圧縮方法です。

で、こいつをどう NLP の問題に適用したかというと、このアルファベットの代わりに各言語の文字で考えます。

  1. 単語に分割する。
  2. 各単語に end-of-word symbol ()を付け足す。
  3. も一つの文字と考えて、各文字のペアをカウントする。
  4. 最もよく出現するペアを一つにまとめて、新しい文字として考える。
  5. また、2から初めて文字のペアを数え直す。 新しく文字が追加されたので、ペアの数は減るが、ペアの種類は増える可能性も減る可能性もある!
  6. 文字の種類がある値になるまでこれを繰り返す。

要点としては、ペアの数、ペアの種類、文字の数、文字の種類の4つをきちんと区別して考えましょうってこと。

目的は文字の種類の最小化。

そのためにペアの種類の最大値を用いるってこと。

Algorythmとしてはこんな感じで実装はこう。

import re, collections

def get_stats(vocab):
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols) -1):
            pairs[symbols[i], symbols[i+1]] += freq
    return pairs


def merge_vocab(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

def main():
    vocab = {'t e s t </w>': 5, 't r i p </w>': 8, 'w e s t </w>': 4, 't r i m </w>': 2}
    num_merges = 10

    for i in range(num_merges):
        pairs = get_stats(vocab)
        print(pairs)
        best = max(pairs, key=pairs.get)
        print(best)
        vocab = merge_vocab(best, vocab)
        print(vocab)

    print('End ', vocab)

if __name__=='__main__':
    main()

で、なんでend-of-word symbolが必要かって言うと、「trip」と「ripping」みたいな単語があったとして、「rip」の部分について、別物として考えるためらしい。

まあ、日本語でも「おはよう」と「ようこそ」の「よう」はおんなじと考えてほしくないしね。

Unicode Regulation

いろんな文字コードを一定に変換する方法らしい。

日本語はNFKCっっぽい。sentence pieceではDefaultでこのNFKCが使われているみたい。

じゃあ、単語の区切り方はどうやってんのかな?

Sentence Pieceの論文のAbstractを除いてみると、

While existing subword segmentation tools assume that the input is pre-tokenized into word sequences, SentencePiece can train subword models directly from raw sentences, which allows us to make a purely end-to-end and language independent system

ってある。つまり、この論文のように pre-tokenizationが必要ないそうだ。

今度、じっくりと呼んでみよう。