🤗

Hugging Face Courseで学ぶ自然言語処理とTransformer 【part6】

2021/08/22に公開

はじめに

前回part5から期間が開いてしまいましたが、
Hugging Face CourseのHandling multiple sequences~Putting all togetherあたりの内容をベースに、Chapter2のまとめを行なっていきます。

モデルへの入力はバッチ処理の形式を想定

テキストをトークンに分割して数値データに変換するまでは下記のようにしてできます。

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

checkpoint = 'cl-tohoku/bert-base-japanese-whole-word-masking'

# checkpointからtokenizerとmodelをロード
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

# サンプルテキスト
sequence = "今日は天気が良いので外に出かけたいな。"

tokens = tokenizer.tokenize(sequence) # テキストをトークンに分割
ids = tokenizer.convert_tokens_to_ids(tokens) # token=>idに変換
input_ids = torch.tensor(ids) # torch.tensorに変換

これをモデルに入力しますが、以下のようにinput_idsをそのまま渡すだけではエラーになってしまいます。

model(input_ids)

デフォルトではモデルへは複数の入力が渡されるバッチ処理の形式が想定されており、例えば入力データがn個あった場合は(n, 1)という形状になっている必要があります。
そのため入力データが1個だけの場合でも、(1, 1)の形状であることが必要です。

上記の場合だとidsをリストに一回入れてからtorch.tensorに変換してやれば良いです。

input_ids = torch.tensor([ids]) # torch.tensorに変換

print(input_ids)
print(model(input_ids).logits)
tensor([[ 3246,     9, 11385,    14,  3614,   947,   393,     7, 16841,  1549,
            18,     8]])
tensor([[0.0032, 0.1043]], grad_fn=<AddmmBackward>)

実際はいちいちここまでせずとも以下のようにtokenizerでいい具合にやってくれます。

input_ids = tokenizer(sequence, return_tensors='pt').input_ids
input_ids
tensor([[    2,  3246,     9, 11385,    14,  3614,   947,   393,     7, 16841,
          1549,    18,     8,     3]])

ちゃんとBERTの[CLS]トークンや[SEP]トークンの分も合わせて出してくれます。

入力データのPadding

バッチ処理形式で入力データを複数まとめてモデルに渡せるのは便利ですが、入力データそれぞれの長さが異なる場合はPaddingという処理によって長さを揃えてやる必要があります。

tokenizer.pad_token_idでtokenizerで使用されているPadding用のIDを取得して埋めてやれば良いですが、これも以下のようにすれば自動でやってくれます。

# サンプルデータ
sequences = ['今日は天気が良いので外に出かけたいな。', '今日は暑い。']

sequence1_ids = tokenizer(sequences[0], return_tensors='pt').input_ids
sequence2_ids = tokenizer(sequences[1], return_tensors='pt').input_ids

print("1文目のtokenize結果: ", sequence1_ids) # 1文目のtokenize結果
print("2文目のtokenize結果: ", sequence2_ids) # 2文目のtokenize結果

# 入力をpaddingしてバッチにする
batched_ids = tokenizer(sequences, padding=True, return_tensors='pt').input_ids # padding=Trueで自動的にPaddingしてくれる

print("入力バッチのtokenize結果", batched_ids)
1文目のtokenize結果:  tensor([[    2,  3246,     9, 11385,    14,  3614,   947,   393,     7, 16841,
          1549,    18,     8,     3]])
2文目のtokenize結果:  tensor([[    2,  3246,     9, 16860, 28457,     8,     3]])
入力バッチのtokenize結果 tensor([[    2,  3246,     9, 11385,    14,  3614,   947,   393,     7, 16841,
          1549,    18,     8,     3],
        [    2,  3246,     9, 16860, 28457,     8,     3,     0,     0,     0,
             0,     0,     0,     0]])

短い方の入力はPadding用のトークンIDである0で補完され、入力データの長さがバッチ全体で揃えられていることがわかります。

Attention Mask

先程の例で作ったデータをモデルに入力してみると、以下のようになります。

print("1文目の推論結果: ", model(sequence1_ids).logits)
print("2文目の推論結果: ", model(sequence2_ids).logits)
print("入力バッチの推論結果: ", model(batched_ids).logits)
1文目の推論結果:  tensor([[ 0.3572, -0.1252]], grad_fn=<AddmmBackward>)
2文目の推論結果:  tensor([[ 0.0875, -0.1589]], grad_fn=<AddmmBackward>)
padding後の入力バッチの推論結果:  tensor([[ 0.3572, -0.1252],
        [ 0.4989, -0.0792]], grad_fn=<AddmmBackward>)

それぞれ単体で入力した場合とバッチで入力した場合を比較すると、
1文目の推論結果は同じですが、2文目の推論結果が異なっています。
これは何故かと言うと、モデルのAttention層がpaddingによって補完された部分にも注目しているためです。
実際にはpaddingされた部分は文の意味とは関係ないため、モデルにはこの部分は無視して推論処理を欲しいですよね。

そこでAttention Maskを使ってモデルにどの部分がpaddingされたものかを判別させます。
Attention Maskは入力(input_ids)と同じサイズで、推論に使って欲しいトークンの部分は1
paddingトークンのように無視して欲しい部分は0で構成されています。

# attention_maskを取得
batched_masks = tokenizer(sequences, padding=True, return_tensors='pt').attention_mask
print("Attention Mask:", batched_masks)
# モデルに入力バッチとattention_maskを渡す
output_batched = model(batched_ids, attention_mask=batched_masks).logits
print("入力バッチの推論結果: ", output_batched)
Attention Mask: tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]])
入力バッチの推論結果:  tensor([[ 0.3572, -0.1252],
        [ 0.0875, -0.1589]], grad_fn=<AddmmBackward>)

これで2文目の推論結果も等しくなりました。

長い入力データの対処(Truncation)

Transformerモデルへの入力サイズには上限があり、ほとんどのモデルは512トークンもしくは1024トークンまでとなっています。
これよりも長くなるような入力データを扱いたい場合は以下の2通りの対処法があります。

  • 長い入力サイズに対応したモデルを使う。(Longformerなど)
  • 入力データを切り取る(truncate)

大抵の場合は入力データの切り取りを行なって対処します。
モデルの入力サイズの上限はmodel.config.max_position_embeddingsといったパラメータから確認&取得することができます。

model.config.max_position_embeddings

色々なPadding&Truncation

PaddingやTruncationは以下のように色々と条件を組み合わせて行うことができます。

# サンプルデータ
sequences = ['今日は天気が良いので外に出かけたいな。', '今日は暑い。']

# 入力データに含まれるもののうち最大のサイズのデータの長さに合わせてpaddingする
model_inputs = tokenizer(sequences, padding="longest", return_tensors='pt').input_ids

print("入力データに含まれるもののうち最大のサイズのデータの長さに合わせてpadding")
print(model_inputs)

# モデルの最大入力サイズに合わせてpaddingする
model_inputs = tokenizer(sequences, padding="max_length", return_tensors='pt').input_ids

print("モデルの最大入力サイズに合わせてpadding")
print(model_inputs)

# paddingのサイズを手動で指定する
model_inputs = tokenizer(sequences, padding="max_length", max_length=16, return_tensors='pt').input_ids

print("paddingのサイズを手動で指定")
print(model_inputs)


# truncationのサイズを指定する
model_inputs = tokenizer(sequences, truncation=True, max_length=5, return_tensors='pt').input_ids

print("truncationのサイズを手動で指定")
print(model_inputs)

# padding&truncation
model_inputs = tokenizer(sequences, padding=True, truncation=True, max_length=10, return_tensors='pt').input_ids

print("padding&truncation")
print(model_inputs)

出力結果

入力データに含まれるもののうち最大のサイズのデータの長さに合わせてpadding
tensor([[    2,  3246,     9, 11385,    14,  3614,   947,   393,     7, 16841,
          1549,    18,     8,     3],
        [    2,  3246,     9, 16860, 28457,     8,     3,     0,     0,     0,
             0,     0,     0,     0]])
==============================
モデルの最大入力サイズに合わせてpadding
tensor([[   2, 3246,    9,  ...,    0,    0,    0],
        [   2, 3246,    9,  ...,    0,    0,    0]])
==============================
paddingのサイズを手動で指定
tensor([[    2,  3246,     9, 11385,    14,  3614,   947,   393,     7, 16841,
          1549,    18,     8,     3,     0,     0],
        [    2,  3246,     9, 16860, 28457,     8,     3,     0,     0,     0,
             0,     0,     0,     0,     0,     0]])
==============================
truncationのサイズを手動で指定
tensor([[    2,  3246,     9, 11385,     3],
        [    2,  3246,     9, 16860,     3]])
==============================
padding&truncation
tensor([[    2,  3246,     9, 11385,    14,  3614,   947,   393,     7,     3],
        [    2,  3246,     9, 16860, 28457,     8,     3,     0,     0,     0]])
==============================

その他

tokenizerの出力タイプ

これまではPyTorchベースでずっとやってきましたが、return_tensorsを指定して
使用するモデルのフレームワークによってtokenizerの出力データタイプを変えることもできます。

# Returns PyTorch tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt").input_ids
print(type(model_inputs))

# Returns TensorFlow tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf").input_ids
print(type(model_inputs))

# Returns NumPy arrays
model_inputs = tokenizer(sequences, padding=True, return_tensors="np").input_ids
print(type(model_inputs))

出力結果

<class 'torch.Tensor'>
<class 'tensorflow.python.framework.ops.EagerTensor'>
<class 'numpy.ndarray'>

Special Tokens

Special Tokensはモデルによって異なるものが使われていたり、一切使われていなかったりと様々です。
モデルがどういったSpecial Tokensを使っているかは、対応するtokenizerに含まれている情報から確認できます。

Special Tokenの情報の確認

# 各Special TokenとそのID
print(tokenizer.all_special_tokens)
print(tokenizer.all_special_ids)
[1, 3, 0, 2, 4]
['[UNK]', '[SEP]', '[PAD]', '[CLS]', '[MASK]']

まとめ

色々と細かく見てきましたが、入力データをトークン化してモデルに入力して推論結果を出力するまでの基本的な流れは以下のようになります。

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

# 日本語BERTのチェックポイントを指定
checkpoint = 'cl-tohoku/bert-base-japanese-whole-word-masking'

# checkpointからtokenizerとmodelをロード
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

# サンプルデータ
sequences = ['今日は天気が良いので外に出かけたいな。', '今日は暑い。']

# トークン化(padding, truncation)
tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")

# モデルへ入力
output = model(**tokens)

Chapter2全体を通しては、

  • TransformerモデルとPipelineの構成要素 ⇨ part3
  • Transformerモデルの扱い方 ⇨ part4
  • Tokenizerの扱い方 ⇨ part5
  • トークン化~モデル入力までに必要な処理(padding, truncation, attention mask) ⇨ 本記事

といった流れで見てきました。
まだ基本的な使い方に留まった内容ではありますが、Transformersライブラリを使った自然言語処理モデリングにちょっとは馴染めてきた気がします・・・!

次回からは学習済みのモデルをFine-tuningして個別のタスク向けに活用するためのより実用的な方法を見ていきます。

Discussion