複数系列の処理
前のセクションでは、最も単純な使用例である、単一の短い系列(テキスト)に対して推論を行う方法を見てきました。しかし、これについて以下のような疑問をお持ちの方もいるかもしれません。
- 複数の系列をどのように処理するのか?
- 長さの異なる複数の系列をどのように処理するのか?
- モデルがうまく機能するためには、単語のインデックスだけが入力として必要なのか?
- 系列が長すぎてしまうということはあるのか?
これらの疑問について、実際はどのような問題があるのか、そして🤗 Transformers APIを使ってどのように解決できるのかを見ていきましょう。
モデルへのバッチ入力
以前のエクササイズで、系列が数値のリストに変換される方法を見てきました。この数値列をテンソルに変換し、モデルに入力してみましょう。
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = "I've been waiting for a HuggingFace course my whole life."
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor(ids)
# This line will fail.
model(input_ids)
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
おっと!「セクション2のパイプラインの手順に従ったのに、なぜ失敗したのか?」と思われるかもしれません。
この問題はモデルに単一の系列を入力しようとしたために発生しました。🤗 Transformersモデルは、デフォルトでは複数の系列を入力として受け付けます。ここでは、sequence
に対してトークナイザを適用したときに、トークナイザがその背後で行ったすべての処理を行おうとしました。しかし、もう少し詳しく見てみると、トークナイザは入力IDのリストをテンソルに変換するだけでなく、それに対して次元を追加していることがわかります。
tokenized_inputs = tokenizer(sequence, return_tensors="pt")
print(tokenized_inputs["input_ids"])
tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
2607, 2026, 2878, 2166, 1012, 102]])
それでは次元を追加して再度試してみましょう。
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = "I've been waiting for a HuggingFace course my whole life."
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor([ids])
print("Input IDs:", input_ids)
output = model(input_ids)
print("Logits:", output.logits)
ここで入力IDと結果のロジット(モデルの出力)を見てみましょう。
Input IDs: [[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]]
Logits: [[-2.7276, 2.8789]]
バッチ処理とは、複数の系列をまとめてモデルに入力することです。系列が1つしかない場合でも、バッチを構築することができます。
batched_ids = [ids, ids]
これは2つの同じ系列からなるバッチとなっています。
✏️ 試してみよう! この batch_ids
をテンソルに変換し、モデルに入力してみましょう。前と同じロジット(モデル出力)が得られることを確認してください(ただし、二重になっていることに注意してください)。
バッチ処理により、複数の系列をモデルに入力できるようになります。単一の系列でバッチを構築するのと同じように、簡単に複数の系列を使用することができます。ただし、ここで1つ問題があります。2つ以上の系列をバッチ処理する場合、系列の長さがそれぞれ異なる場合があります。これまでテンソルを扱ったことがある場合は、テンソルの形状は長方形である必要があることをご存知なのではないでしょうか。従って、異なる長さの系列の入力IDリストを直接テンソルに変換することはできません。この問題を回避するための方法として、入力をパディングすることが一般的です。
入力のパディング
以下の二重のリストはテンソルには変換できません。
batched_ids = [
[200, 200, 200],
[200, 200]
]
この問題を回避するために、パディングを使用して、テンソルの形状を長方形にしてみましょう。パディングは、パディングトークンと呼ばれる特別な単語を短い系列に対して追加することで、すべての系列の長さを同じにします。例えば、10語の系列が10個、20語の系列が1個ある場合、パディングにより、すべての系列の長さが20語になります。上記の例では、結果として得られるテンソルは次のようになります。
padding_id = 100
batched_ids = [
[200, 200, 200],
[200, 200, padding_id],
]
パティングトークンのIDは tokenizer.pad_token_id
で見つけることができます。それでは、これを使って2つの系列を個別にモデルに入力する場合と、バッチ処理した場合の結果を比較してみましょう。
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence1_ids = [[200, 200, 200]]
sequence2_ids = [[200, 200]]
batched_ids = [
[200, 200, 200],
[200, 200, tokenizer.pad_token_id],
]
print(model(torch.tensor(sequence1_ids)).logits)
print(model(torch.tensor(sequence2_ids)).logits)
print(model(torch.tensor(batched_ids)).logits)
tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
tensor([[ 1.5694, -1.3895],
[ 1.3373, -1.2163]], grad_fn=<AddmmBackward>)
バッチ処理した予測のロジットについて何か違いがあるようです。2行目は2つ目の系列のロジットと同じであるべきですが、完全に異なる値となってしまっています!
これは、Transformerモデルの代表的な特徴であるアテンション層が、それぞれのトークンに対してコンテクスト化を行っていることに起因します。アテンション層は、系列のすべてのトークンに注意(アテンション)を向けるため、パディングトークンも考慮の対象として扱います。異なる長さの系列を個別にモデルに入力する場合と、同じ系列をバッチ処理した場合の両方で同じ結果を得るためには、アテンション層にパディングトークンを無視するように指示する必要があります。これは、アテンションマスクを使用することで実現できます。
アテンションマスク
アテンションマスクとは入力IDのテンソルと全く同じ形をしたテンソルのことで、0と1で構成されています。1は対応するトークンに注意を向けることを示し、0は対応するトークンに注意を向けないこと(つまり、アテンション層に無視されること)を示します。
前の例に対して、アテンションマスクを追加してみましょう。
batched_ids = [
[200, 200, 200],
[200, 200, tokenizer.pad_token_id],
]
attention_mask = [
[1, 1, 1],
[1, 1, 0],
]
outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
print(outputs.logits)
tensor([[ 1.5694, -1.3895],
[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
これで、バッチ内の2つ目の系列について同じロジットが得られました。
2つ目の系列の最後の値がパディングIDであることに注目してください。これは、アテンションマスクの0の値となっています。
✏️ 試してみよう! セクション2で使用した2つの文 (“I’ve been waiting for a HuggingFace course my whole life.” と “I hate this so much!“) を手動でトークン化してみましょう。そしてこれらをモデルに入力し、セクション2で得られたロジットと同じ結果となることを確認してみましょう。次に、パディングトークンを使用してこれらをバッチ処理し、適切なアテンションマスクを作成してみましょう。また同様にモデルに入力した際、セクション2で得られた結果と同じものになることを確認してみましょう。
より長い系列
トランスフォーマーモデルでは、モデルに入力できる系列の長さに制限があります。ほとんどのモデルは512トークンまたは1024トークンの系列を処理できますが、これより長い系列を処理しようとするとクラッシュしてしまいます。この問題に対しては、2つの解決策があります。
- 長い系列を処理できるモデルを使用する
- 系列を途中で区切って短くする
処理できる系列長はモデルによって異なり、非常に長い系列の処理に特化したモデルも存在します。Longformer はその一例です。また、LED も長い系列を処理できるモデルです。非常に長い系列を処理する必要があるタスクに取り組んでいる場合は、これらのモデルを見てみて下さい。
もう1つの手法として、max_sequence_length
パラメータを指定して系列を途中で区切ることをお勧めします。
sequence = sequence[:max_sequence_length]