トークナイザ
トークナイザはNLPパイプラインの重要な構成要素の1つです。トークナイザの目的は1つで、テキストをモデルが処理できるデータに変換することです。モデルが処理できるのは数値のみなので、トークナイザは入力されたテキストを数値データに変換する必要があります。このセクションでは、トークン化パイプラインで何が起きているのかを具体的に説明します。
NLPのタスクにおいて、一般的に処理されるデータは生文で、以下はその例です。
Jim Henson was a puppeteer (Jim Hensonは人形師でした)
しかしながらモデルが処理できるのは数値のみなので、生文を数値に変換する方法を考える必要があります。トークナイザはまさにこの役割を担っているものであり、変換にはさまざまな方法があります。目的はモデルにとって最も意味のある表現を見つけることです。そして可能な限り、コンパクトな表現を見つけることも目的としています。
ここではトークン化アルゴリズムの例をいくつか見ながら、トークン化に関する疑問を解消していきます。
単語ベース
最初に思い浮かぶトークナイズ方法は、単語ベース のものです。一般に、いくつかのルールを設定するだけで非常に簡単に使用でき、そして多くの場合において適切な結果を得ることができます。例えば、以下の画像のように生のテキストを単語に分割し、それぞれの数値表現を見つけることが目的です。
テキストの分け方にはさまざまな種類があります。例えば、Pythonの split()
関数を適用して、テキストを空白で区切ることで単語に分割することができます。
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
また、単語トークン化には句読点に関する特別なルールを持つものもあります。この種のトークナイザを使用すると、かなり大きな「語彙」が作成されることになります。語彙は、コーパスに含まれるトークンの総数で定義されます。
各単語には個別のID(0〜語彙のサイズの数値)が割り当てられます。モデルはこれらのIDを使用して各単語を識別します。
単語ベースのトークナイザで言語を完全にカバーしようとすると、その言語の各単語に対応する識別子(ID)が必要になり、膨大な量のトークンが生成されることになります。例えば、英語には50万語以上の単語があるので、各単語から入力IDへのマップ(対応表)を作るには、それだけのIDを記録しておく必要があります。また、「dog」のような単語と「dogs」のような単語は表現が異なるため、モデルは初め “dog” と “dogs” が似ていることを知ることができず、無関係な単語として認識してしまいます。また、“run” と “running” のような類似した単語についても同様で、モデルは初期状態では類似しているとは認識できません。
最後に、語彙にない単語 (未知語)を表すためのトークンが必要です。これは “unknown” トークンと呼ばれ、”[UNK]” や ”<unk>” として表されます。トークナイザが多くの unknown トークンを生成している場合、単語の適切な表現を取得できず、情報が失われていると解釈できます。語彙を作成する際の目標は、unknownトークンにトークン化されてしまう単語(未知語)がより少なくなるようにすることです。
unknown トークンの総数を減らす方法の1つは、文字ベース のトークナイザを使用することです。
文字ベース
文字ベース トークナイザはテキストを単語単位ではなく文字単位で分割します。これには2つの主な利点があります。
- 語彙サイズがはるかに小さくなります
- すべての単語は文字で構成されるため、語彙外のトークン(未知語)がはるかに少なくなります
しかし、ここでも空白と句読点に関する問題が発生します。
このアプローチも先と同様、完璧なものではありません。ここでは、表現が単語ではなく文字に基づいているので、直感的にはテキストの意味をうまく汲み取れないとも考えられます。各文字は単独ではあまり意味を持たないのに対し、単語はそのようなことはありません。しかし、言語によってはここでも違いがあります。例えば中国語の各文字は、ラテン語の文字よりも情報を持っています。(漢字1文字とアルファベット1文字では、表現している情報量が異なる場合がありますね。)
考慮すべきもう1つの点としては、モデルが処理する必要があるトークンの数が非常に多くなってしまうことです。単語ベースのトークナイザでは、単語は1つのトークンになりますが、文字ベースのトークナイザでは、単語は10個以上のトークンに変換される可能性があります。
両者のいいとこ取りをするために、これらのアプローチを組み合わせた第3の手法を使用することができます。それが サブワードトークン化 です。
サブワードトークン化
サーブワードトークン化アルゴリズムは、「出現頻度の高い単語は小さなサブワードに分割されるべきではないが、出現頻度の低い単語は、意味を持ったサブワードに分割されるべきである」という原理に基づいています。
例えば “annoyingly” は出現頻度の低い単語として扱われ、“annoying” と “ly” に分割されることがあります。これら2つのサブワードは、それぞれ単独で頻繁に出現する可能性がありますが、一方で “annoyingly” は稀な単語なので、その意味を “annoying” と “ly” の合成語として表現しようという考え方になります。
それではここで、サブワードトークン化アルゴリズムが “Let’s do tokenization!” という系列をトークン化する様子を見てみましょう。
これらのサブワードは最終的に、うまく意味を表現したものとして機能します。例えば上の例では “tokenization” は “token” と “ization” に分割されていましたが、これら2つのトークンは、空間効率が良く(2つのトークンだけで長い単語を表現できている)、意味論的にも有意なものとなっています。これにより、比較的小さな語彙で多くの単語をカバーすることができ、未知語がほとんど出ないようになります。
このアプローチはトルコ語などの膠着語(機能語が自立語にくっついて文が構成される言語)において特に有効です。トルコ語では、サブワードを繋げることで(ほぼ)任意の長さの合成語を作ることができます。
さらなるトークン化手法!
実は他にも多くのトークン化手法が存在し、例えば以下のようなものがあります。
- Byte-level BPE: GPT-2で使用される手法
- WordPiece: BERTで使用される手法
- SentencePiece もしくは Unigram: いくつかの多言語モデルで使用される手法
ここまで読めば、APIを使ってトークナイザを使い始めるために必要な知識は十分に身についていると思います!
読み込みと保存
トークナイザの読み込みと保存は、モデルと同様に簡単です。実際、これらは同じ2つのメソッド from_pretrained()
と save_pretrained()
に基づいています。これらのメソッドは、トークナイザが使用するアルゴリズム(モデルでいう アーキテクチャ)と、語彙(モデルでいう 重み)を読み込むか保存するかを決定します。
BERTと同じチェックポイントで学習されたBERTトークナイザを読み込む方法は、モデルでの読み込み方法と同じです。ただし、BertTokenizer
クラスを使う点だけが異なります。
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
AutoModel
と同様に、AutoTokenizer
クラスはチェックポイント名に基づいてライブラリ内の適切なトークナイザクラスを取得し、任意のチェックポイントを直接使用することができます。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
そして、前のセクションで見たようにトークナイザを使用することができます。
tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
トークナイザの保存は、モデルの保存と同じ方法でできます。
tokenizer.save_pretrained("directory_on_my_computer")
token_type_ids
についてはChapter3で詳しく説明し、attention_mask
についても後ほど説明します。まずは input_ids
がどのように生成されるかを見てみましょう。
エンコーディング
テキストを数値に変換することを エンコード と呼びます。エンコードはトークン化とその後の入力IDへの変換の2段階のプロセスで行われます。
ここまで見てきたように、最初のステップはテキストをトークン(単語や単語の一部、句読点など)に分割することです。このプロセスを管理するためのルールがいくつか存在します。まずは、モデルの名前を使ってトークナイザをインスタンス化する必要があります。これにより、モデルが事前学習されたときに使用されたものと同じルールを使用することができます。
2番目のステップはトークンを数値に変換することです。これにより、テンソルを構築し、モデルに入力することができます。これを行うために、トークナイザは 語彙 を有しています。これは、from_pretrained()
メソッドでインスタンス化するときにダウンロードされる部分です。繰り返しになりますが、モデルの事前学習で使用された語彙と同じものを使用する必要があることに注意してください。
この2つの理解を深めるために、それぞれのステップを別々に見ていきます。ステップの中間結果を表示するために、トークン化パイプラインの一部を別々に実行するメソッドを使用しますが、実際には(セクション2で見たように)入力に対して直接トークナイザを呼び出す必要があります。
トークン化
トークン化のプロセスは tokenize()
メソッドによって行われます。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
このメソッドの出力はトークンもしくは文字のリストです。
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
ここではサブワードトークナイザを使用しているので、単語を語彙に含まれるトークンになるまで分割していきます。具体的には transformer
が transform
と ##er
に分割されているのがわかります。
トークンからIDへの変換
トークンからIDへの変換は convert_tokens_to_ids()
のトークナイザメソッドによって行われます。
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
これらの出力は、適切なフレームワークのテンソルに変換された後、前述のようにモデルの入力として使用できます。
✏️ 試してみよう! 最後の2つのステップ(トークン化と入力IDへの変換)を、セクション2で使った入力文(“I’ve been waiting for a HuggingFace course my whole life.” と “I hate this so much!“)に対して再現してみましょう。先ほどと同じ入力IDが得られるかどうかを確認してみてください。
デコーディング
デコーディング はエンコーディングとは逆の処理になります。decode()
メソッドを使うことで、語彙のインデックスから文字列を取得することができます。
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'
decode
メソッドは語彙のインデックスをトークンに戻すだけでなく、同じ単語の一部であったトークンをまとめて、読みやすい文章に変換するところも担っています。この挙動は、プロンプトから生成されたテキストや、翻訳や要約などの系列から系列への変換などの問題を解くモデルを使うときに非常に役に立ちます。
ここまでで、トークナイザでできる基本的な処理(トークン化、IDへの変換、IDから文字列への変換)を理解できたのではないでしょうか。しかし、これは氷山の一角に過ぎません。次のセクションでは、これらの処理を限界まで拡張していき、その限界を超える方法を見ていきましょう。