Huggingface Transformers 入門 (8) - トークナイザー
以下の記事を参考に書いてます。
前回
1. トークナイザー
「トークナイザー」は、「テキスト」を「トークン」に分割し、それを「ID」に変換する機能を提供します。「テキスト」はそのままではニューラルネットワークで処理できないため、IDに変換する必要があります。
2. トークン化の方法
テキストのトークン化は見た目以上に大変な作業で、トークン化の方法は複数あります。
・単語
・文字
・サブワード
2-1. 単語によるトークン化
◎ スペースによるトークン化
一番簡単なトークン化の方法は、「スペースによるトークン化」です。
"Don’t you love 🤗 Transformers? We sure do."
↓
["Don't", "you", "love", "🤗", "Transformers?", "We", "sure", "do."]
これは良い第1歩ですが、"Transformers? " や "do. " というトークンを見てみると、もっと良い方法があることがわかります。
◎ スペース / 句読点によるトークン化
スペースだけでなく、句読点も考慮に入れてトークン化してみましょう。
"Don’t you love 🤗 Transformers? We sure do."
↓
["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
良くなりました。ただ一つ気になるのは、"Don't" の扱い方です。"Don't" は do not の略なので、["Do", "n't"] のようにトークン化した方が良いでしょう。
ここから処理がより複雑になってきます。これがモデルの種類ごとに独自のトークン化クラスを持つ理由になります。テキストをトークン化するルールは複数あります。そしてもちろん、事前学習済みモデルを使う時は、事前学習時と同じルールのトークン化を適用しなければ、正しく動作しません。
【ノート】「Huggingface Transformers」のモデル紹介ページには、事前学習モデルでどのトークナイザーを使用しているかを知ることができます。例えば、「BERT」では、「WordPiece」ベースの「BertTokenizer」を使用していることがわかります。
◎ ルールベースによるトークン化
「spaCy」「Moses」というルールベースのトークナイザーを使うと、次のようにトークン化されます。
"Don’t you love 🤗 Transformers? We sure do."
↓
["Do", "n't", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
◎ 単語によるトークン化
上記の「スペース/句読点によるトークン化」や「ルールベースによるトークン化」は、テキストを単語に分割する「単語のトークン化」の例になります。これは最も直感的な方法ですが、膨大なコーパスがある場合には問題があります。例えば、「Transformer XL」は「スペース/句読点によるトークン化」を使用しており、語彙数は267,735にもなります。
巨大な語彙数は、巨大な埋め込み行列が必要なことを意味し、メモリ問題を引き起こします。「Transformer XL」は「Adaptive Embedding」と呼ばれる特殊な埋め込みを使用してこれに対処しますが、一般的には、Transformerモデルが50,000を超える語彙数を持つことはほとんどありません。
2-2. 文字によるトークン化
「単語によるトークン化」に満足できない場合は、「文字によるトークン化」を行うこともできます。これは非常にシンプルで、多くのメモリを節約できますが、「単語によるトークン化」のようにモデルが意味のあるテキスト表現を学習できず、パフォーマンスの低下につながります。
2-3. サブワードによるトークン化
全てのTransformerモデルは、「単語によるトークン化」と「文字によるトークン化」の両方の利点を得るため、「サブワードによるトークン化」を行います。一般的な単語はそのまま利用し、稀な単語はサブワードで分解します。例えば、"annoyingly" は稀な単語とみなされ、"annoying" と "ly" に分解されるかもしれません。
これにより、モデルは一般的な単語とサブワードを学習しながら、合理的な語彙数を維持することができます。さらに、未知語もサブワードに分解して処理できるようになります。
◎ BertTokenizerによるトークン化
以下は、「BertTokenizer」によるトークン化の例です。
"I have a new GPU!"
↓
['i', 'have', 'a', 'new', 'gp', '##u', '!']
>>> from transformers import BertTokenizer
>>> tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
>>> tokenizer.tokenize("I have a new GPU!")
['i', 'have', 'a', 'new', 'gp', '##u', '!']
「BertTokenizer」は、大文字小文字を区別しないので、テキストは小文字になります。そして、トークナイザーの語彙に "gpu" が存在していなかったので、"gp" と "##u" というサブワードに分割します。"##" は、前の単語(またはサブワード)にスペースなしで結合するという意味です。
◎ XLNetTokenizerによるトークン化
以下は、「XLNetTokenizer」によるトークン化の例です。
"Don’t you love 🤗 Transformers? We sure do."
↓
['▁Don', "'", 't', '▁you', '▁love', '▁', '🤗', '▁', 'Transform', 'ers', '?', '▁We', '▁sure', '▁do', '.']
>>> from transformers import XLNetTokenizer
>>> tokenizer = XLNetTokenizer.from_pretrained('xlnet-base-cased')
>>> tokenizer.tokenize("Don't you love 🤗 Transformers? We sure do.")
['▁Don', "'", 't', '▁you', '▁love', '▁', '🤗', '▁', 'Transform', 'ers', '?', '▁We', '▁sure', '▁do', '.']
"Transformers" が "Transform" と "ers" に分割されていることがわかります。"▁"の意味は、後ほど「SentencePiece」で説明します。
3. サブワードによるトークン化の方法
サブワードによるトークン化にも、複数の方法があります。
・BPE(Byte-Pair Encoding)
・byte-level BPE
・WordPiece
・Unigram
・SentencePiece
3-1. BPE(Byte-Pair Encoding)
「BPE」(Byte-Pair Encoding)は、はじめに「単語によるトークン化」を行ってから、「各単語の頻度のカウント」を行います。
その結果、次のような (単語 , 単語の頻度) があるとします。
('hug', 10), ('pug', 5), ('pun', 12), ('bun', 4), ('hugs', 5)
基本語彙は ['b', 'g', 'h', 'n', 'p', 's', 'u'] であり、すべての単語をまず文字ごとに分割します。
('h' 'u' 'g', 10), ('p' 'u' 'g', 5), ('p' 'u' 'n', 12), ('b' 'u' 'n', 4), ('h' 'u' 'g' 's', 5)
この中から、最も頻度の高いペアを選びます。最も頻度が高いのは 'ug'(10+5+5=20回)なので、トークン化機能が最初に学習するマージルールは、'ug' を語彙に追加することになります。
('h' 'ug', 10), ('p' 'ug', 5), ('p' 'u' 'n', 12), ('b' 'u' 'n', 4), ('h' 'ug' 's', 5)
次に頻度の高いペアは 'un'(16回)なので、'un' を語彙に追加します。次に頻度の高いペアは'hug'('h' + 'ug' : 15回)なので、'hug' を語彙に追加します。
この段階で、語彙は ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un', 'hug'] になり、コーパスは次のように表現されます。
('hug', 10), ('p' 'ug', 5), ('p' 'un', 12), ('b' 'un', 4), ('hug' 's', 5)
トークン化機能は、このように学習したルールを、新しい単語に適用することができます。例えば、'bug' は ['b', 'ug'] としてトークン化されますが、mug は ['<unk>', 'ug'] としてトークン化されます。これは、一般的な文字よりも、絵文字のような特殊文字で発生します。
語彙数(基本語彙数+マージ数)はハイパーパラメータです。例えば、「GPT」は478文字をベースにしているので、語彙数は40,478となり、40,000回のマージでトークナイザーの学習を止めることになります。
3-2. Byte-level BPE
「byte-level BPE」は、「BPE」の基本語彙が全ての基本文字を取得しなければならないという問題に対処するために、基本語彙としてバイトを使うという巧妙なトリックを使っています。
句読点を扱うためのいくつかの追加ルールを加えることで、未知のトークンを必要とせずに、全てのテキストをトークン化することができます。例えば、「GPT-2」は50,257の語彙数を持っていますが、基本語彙数(256)+特別な文末トークン(1)+マージ数(50,000) で対処しています。
3-3. WordPiece
「WordPiece」 は、「BERT」で使用されている方法です。「BPE」と同じく、基本語彙のグループから学習を開始しますが、マージルールが異なります。「BPE」が最も頻度の高いペアを選択するのに対し、「WordPiece」はマージされたコーパス上の尤度を最大化するペアを選択します。。
先ほどの例では、'ug' がある確率を 'u' がある確率で割ったものが 'g' である場合にのみ、'u' と 'g' をマージすることを意味しています。これは、2つの記号をマージすることで何を「失うか」を評価し、それが価値のあるものであることを確認するという意味で、「BPE」が行うこととは微妙に異なります。
3-4. Unigram
「Unigram」は、「BPE」や「WordPiece」のように、基本語彙のグループから学習を開始し、何らかのルールでマージするのではなく、大規模な語彙(例えば、事前トークン化された単語と一般的な部分文字列)から学習を開始し、それを段階的にトリミングしていきます。ライブラリ内の事前学習済みモデルでは、「SentencePiece」と組み合わせて使われています。
「BPE」「WordPiece」が、新しいテキストをトークン化するときに同じ順番で適用できるルールを特定の順番で作成しているのに対し、「Unigram」は新しいテキストをトークン化する方法をいくつか用意しています。
例えば、以下のような語彙があるとします。
['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un', 'hug']
以前は 'hugs' を ['hug', 's']、['h', 'ug', 's']、['h', 'u', 'g', 's'] のようにトークン化していました。では、どれを選べばいいのでしょうか?
訓練されたトークナイザーは、語彙を保存するだけでなく、コーパス内の各トークンの確率を保存します。そして、それぞれのトークン化に確率を与え、最も可能性の高いものを選ぶことができます。
これらの確率はトークン化を訓練するための損失を定義します。コーパスが𝑥_1,...,𝑥_𝑁 という単語で構成されていて、𝑥_𝑖 という単語について、 𝑆(𝑥_𝑖) が(現在の語彙で)𝑥_𝑖 のすべてのトークン化の可能性の集合であるとすると、損失は次のように定義されます。
3-5. SentencePiece
これまでに見てきた方法はすべて、何らかの形での「事前トークン化」を必要としていましたが、その際に問題となるのが、全ての言語がスペースを使って単語を区切っているわけではないということです。
「XLM」では言語(日本語、中国語、タイ語)毎に固有の「プレトークナイザー」を使用することで解決しています。この問題を解決するために、「SentencePiece」は、入力を生のストリームとして扱い、使用する文字のセットにスペースを含め、「BPE」や「Unigram」を使って適切な語彙を構築します。
そのため、「XLNetTokenizer」(SentencePieceを使用)を使用する前に見た例では、スペースを表す'▁'という文字がありました。トークン化されたテキストをデコードするのはとても簡単です。
ライブラリ内の「SentencePiece」を使用するすべてのTransformerモデルは、「Unigram」を使用しています。これを使用するモデルの例としては、「ALBERT」「XLNet」「Marian」があります。