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 )
みたいな感じでいいみたい。
もし、カスタムで学習したい場合はこのサイトとかを参考にするといいみたい。
Tokenizerのコンポーネントとうか、toknezerのPipelineのためのReferenceはここ。
でも、普通の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 の問題に適用したかというと、このアルファベットの代わりに各言語の文字で考えます。
- 単語に分割する。
- 各単語に end-of-word symbol ()を付け足す。
- も一つの文字と考えて、各文字のペアをカウントする。
- 最もよく出現するペアを一つにまとめて、新しい文字として考える。
- また、2から初めて文字のペアを数え直す。 新しく文字が追加されたので、ペアの数は減るが、ペアの種類は増える可能性も減る可能性もある!
- 文字の種類がある値になるまでこれを繰り返す。
要点としては、ペアの数、ペアの種類、文字の数、文字の種類の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が必要ないそうだ。
今度、じっくりと呼んでみよう。