🤗

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

2021/07/03に公開

はじめに

この記事ではHugging Face CourseのChapter2: Using 🤗 Transformers~のIntroduction~Behind the pipeline内容をベースにした内容をまとめています。

一個前の記事はこちら

コードの実行は今回もGoogle Colabで行う例になります。

Intro

前回までのChapter1: Transformer modelsでは、Transformerモデルについてライブラリのpipeline APIを使って学習済みモデルをさくっと使ってみたり、基本的な仕組みや構成などについて見てきました。

今回からはライブラリの詳しい使い方について見ていき、Transformerの実践的な活用に向けた第一歩として、pipelineの流れをTokenizerとModelに分けてみていきます。

使用フレームワークについて

Transformersライブラリは深層学習フレームワークとしてPyTorchTensorFlowの両方に対応しています。
Hugging Face Courseでは使用するフレームワーク別に内容を切り替えられるように構成されており、ページの上部のボタンで好きな方を選んで進めることができます。

本記事では特に触れない限りはPyTorchベースの内容で進めていきたいと思っています。

Transformersライブラリについて

改めてTransformersライブラリの特徴を挙げると、

  • SOTAのNLPモデルが数行のコードを書くだけで使える。
  • PyTorchのnn.Module[1]TensorFlowのtf.keras.Model[2]といった主要なDeepLearningフレームワークベースで実装されているので、多くのDeepLearning開発環境に柔軟に対応できる。
  • "All in one file"というのがコンセプトとしてあり、準伝播の処理などが共通の一つのファイルで用意されていて、コード理解の負担を減らし扱いやすくている。

といった点が挙げられます。

Pipelineの挙動

pipelineでは以下の図のようなステップでそれぞれの処理が実行されます。

Image from https://huggingface.co/course/chapter2/2

Tokenizer

生のテキストデータのまま直接モデルに入力することはできないので、まずはテキストを数値データへ変換する必要があり、Tokenizerがその役割を担当します。

Tokenizerは入力テキストに対して以下の処理を実行する役割があります。

  • テキストを単語、サブワード、記号などのモデルにとって意味をなすであろう最小の単位(トークンと呼ばれる)に区切る。
  • ぞれぞれのトークンにIDを振る。
  • モデルの入力に必要な情報となるスペシャルトークンを入力テキストに追加する。
    例えばBERTでは文の始まりを示す<CLS>や文の区切りを示す<EOS>といったトークンが必要になります。

Tokenizerは使用する学習済みモデルごとに作成されたものがあります。
そのためモデル学習時に使用されたTokenizerと同じものを使う必要があります。

transformersライブラリではAutoTokenizerクラスのfrom_pretrainedメソッドを使用することで指定したモデルのTokenizerを使うことができます。

以下、日本語BERTのtokenizerを使う例です。

  1. 必要なライブラリのインストール
!pip install fugashi
!pip install ipadic
  1. tokenizerの使用
from transformers import AutoTokenizer

# 日本語BERTのTokenizerを読み込む
tokenizer_jp = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')

# サンプルデータ
raw_inputs = [
    "今日は雨が凄いので外出したくないな。",
    "今日は天気が良いので外に出かけたいな。",
]

# サンプルデータを入力してTokenizerによる変換を行う
tokenized_inputs = tokenizer_jp(raw_inputs, padding=True, truncation=True, return_tensors='pt')

print(tokenized_inputs)
  • tokenizerについてはBERTの日本語学習済みモデルで使用しているものを読み込んでいます。
  • Transformerモデルの入力としてはテンソル形式に変換する必要があります。
    Tokenizerでは使用するモデルのフレームワークに柔軟に対応できるよう、返り値のテンソルのタイプを選ぶことができます。ここではPyTorchのテンソル型で返してくれるよう、return_tensors='pt'としています。

出力結果は下記のようになります。

{'input_ids': tensor([[    2,  3246,     9,  3741,    14, 14930, 28457,   947, 21828,    15,    5034,    80,    18,     8,     3],
        [    2,  3246,     9, 11385,    14,  3614,   947,   393,     7,    16841,    1549,    18,     8,     3,     0]]), 
        'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 
        'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]])}

返り値の各項目については以下の情報が含まれています。

  • input_ids: 入力テキストをトークンに分け、それぞれをIDに置き換えたもの。
  • attention_mask: 入力トークンについて、attentionの対象となるかを示すもの。
  • token_type_ids: 一つの入力が複数文になっている場合などに、各トークンについて、それぞれがどの文のものかを示す。モデルによってはこの情報が必要。

tokenizerで使われているトークンとIDがどのような感じになっているかも気になるので見てみます。

print(tokenizer.get_vocab())

出力結果

{'[PAD]': 0,
 '[UNK]': 1,
 '[CLS]': 2,
 '[SEP]': 3,
 '[MASK]': 4,
 'の': 5,
 '、': 6,
 'に': 7,
 '。': 8,
 'は': 9,
 ...(途中略)
  '経済': 994,
 '風': 995,
 '##re': 996,
 '産': 997,
 '退': 998,
 '赤': 999,
 ...}

トークンとIDの対応は辞書形式で取得できるようになっており、最初の方にはスペシャルトークンがあって、残りが単語や記号などのトークンという形になっていますね。
中には"##"を含むトークンがありますが、##の部分はワイルドカードになっていて、"##re"だと「"re"で終わる単語」というトークンを表していることになります。

トークンに分割されたものを上の辞書通りに対応させてテキストに戻すとどうなるでしょうか。
ID→トークンの変換は上で取得した辞書を元に作っても良いですが、tokenizer.convert_ids_to_tokens()を使えば

converted_tokenized_inputs = [*map(lambda x: tokenizer.convert_ids_to_tokens(x), tokenized_inputs.input_ids)] # IDをトークンに変換

# つないでテキストにしてみる。
for inputs in converted_tokenized_inputs:
    print(''.join(inputs))

出力結果

[CLS]今日は雨が凄##いので外出したくないな。[SEP]
[CLS]今日は天気が良いので外に出かけたいな。[SEP][PAD]

tokenizerによって追加された[CLS]、[SEP]、[PAD]といったスペシャルトークンが含まれているのが確認できますね。
また、1文目の"凄い"のところは、"凄"と"##い"という分け方になっているのが分かります。

tokenizer.decode()を使えばワイルドカード部分も綺麗にしてテキストに戻してくれるようです。

for input_ids in tokenized_inputs.input_ids:
    print(tokenizer.decode(input_ids).replace(' ', '')) # トークン間に半角スペースが含まれるため、除去
[CLS]今日は雨が凄いので外出したくないな。[SEP]
[CLS]今日は天気が良いので外に出かけたいな。[SEP][PAD]

Tokenizerの役割についてみてきましたが、いったんこの辺にしておきます。
詳細はまた別の記事でもまとめていきます。

Model

transformersライブラリではAutoModelクラスのfrom_pretrainedメソッドを使用することでTokenizerと同様に学習済みモデルをダウンロードして使うことができます。

以下、日本語版学習済みBERTを使う例です。

from transformers import AutoModel

checkpoint = 'cl-tohoku/bert-base-japanese-whole-word-masking'
model = AutoModel.from_pretrained(checkpoint)

このように使用するモデルを指定することで、その重みパラメータがダウンロードされます。

TransformerモデルはTokenizerによって変換された入力系列データを受け取り、各トークンに対し文脈を考慮した意味を表現した高次元のベクトルを出力します
要は入力データに対する特徴抽出をしてくれるということですが、実際どういう挙動になるのか見てみます。

Transformerモデルが出力する高次元ベクトル

出力されるベクトルは以下の3次元からなるのが一般的です。

  • Batch Size:一度にモデルに処理される入力系列データの数(バッチの大きさ)
  • Sequence Length:一個の入力系列データの長さ(含まれるトークンの数)
  • Hidden Size:各トークンの意味を表現するためのベクトルの次元

Transformerの出力が高次元ベクトルだと言っているのは、3番目のHidden Sizeが大きい所以です。

上のTokenizerの使用例で作成したデータをモデルに入力してみます。

outputs = model(**tokenized_inputs)
print(outputs.last_hidden_state.shape)

出力結果

torch.Size([2, 15, 768])

出力はnamedtupleまたはdictionary形式になっているため、取得したい属性を指定することでその値を取得することができます。この例ではlast_hidden_stateに含まれる値を取得していますが、これが入力に対するTransformerの出力全体になっています。
属性を指定しなくてもoutputs[0]といった感じでindex指定でも取り出せるようです。

出力ベクトルのサイズは[入力データの数, 入力データの長さ、Hidden Size]となっており、このモデルでは各トークンの特徴表現として768次元の情報を使っているということが分かります。

Model Heads

Headと呼ばれるのはTransformerによって出力された高次元ベクトルを受け取り、最終的な出力へ繋げるニューラルネットのレイヤーを指します。
下図の一番右のoutputより一個前にあるのがHeadですね。
例えば文書分類タスクをやる場合、入力データ→特徴抽出(Transformer)→分類器(Head)→分類結果 みたいな流れになります。

Image from Hugging Face Course
ここまでで試したものだと入力データに対する生の高次元特徴表現ベクトルが出力されるまででしたが、TransformersライブラリにはタスクごとのHead込みで扱えるものもいくつか用意されています。

以下分類モデル向けのAutoModelForSequenceClassificationを使った、日本語BERTによるネガポジ判定を行うモデルの使用例です。

from transformers import AutoModelForSequenceClassification

# 分類モデル向けに用意されているチェックポイント
checkpoint = 'daigo/bert-base-japanese-sentiment'

# Tokenizerを読み込む
tokenizer = BertJapaneseTokenizer.from_pretrained(checkpoint)

# サンプルデータ
raw_inputs = [
    "今日は雨が凄いので外出したくないな。",
    "今日は天気が良いので外に出かけたいな。",
]

# トークン化
tokenized_inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors='pt')

# モデルの読み込み
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

# トークン化したデータを入力して出力を受け取る
outputs = model(**tokenized_inputs)

Post-Processing

part1の記事でも同じようなことをやっていましたが、今回はpipelineを使わずにやるので、出力結果の後処理なども自前で用意します。

上で得たモデルからの生の出力結果は以下のようになっています。

SequenceClassifierOutput([('logits', tensor([[ 0.8353, -0.2080],
                                   [ 2.2407, -1.8656]], grad_fn=<AddmmBackward>))])

logitsという部分に出力結果が入っているようです。
このlogitsというのは、Headの出力値をそのまま反映しているものです。
分類結果として活用するためには、Softmaxを使ってこれを正規化し、確率に変換する必要があります。

import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)

出力結果

tensor([[0.7395, 0.2605],
        [0.9838, 0.0162]], grad_fn=<SoftmaxBackward>)

logitsの値が確率に変換されていることが確認できました。

また、ラベル情報もモデルから読み込むことができます。

model.config.id2label

出力結果

{0: 'ポジティブ', 1: 'ネガティブ'}

ラベル情報を用いて結果を分かりやすく出力する場合、例えば下記のようにすることができます。
ここでは単純に確率値が大きい方を分類結果とすることにします。

import numpy as np

label_dict = model.config.id2label
predictions_list = predictions.detach().numpy()

for i in range(len(raw_inputs)):
    print('入力テキスト:', raw_inputs[i])
    print('分類結果:', label_dict[np.argmax(predictions_list[i])])
    print('確率:', np.max(predictions_list[i]))

出力結果

入力テキスト: 今日は雨が凄いので外出したくないな。
分類結果: ポジティブ
確率: 0.7394907
入力テキスト: 今日は天気が良いので外に出かけたいな。
分類結果: ポジティブ
確率: 0.9837974

一文目の結果はネガティブになって欲しかったですが、ポジティブと判定されています。
この状態だともう少し工夫をしないと精度面では良くなさそうですが、いったんpipelineと同様の処理を実現することができました。

まとめ

pipelineの各ステップごとにフォーカスして何をやっているかを見てきました。

  • Tokenizerは入力テキストをトークンに分割し、IDに変換後テンソル型の系列データとして返す。
  • Transformer ModelはTokenizerによって変換された入力を受け取り、データの特徴を表す高次元ベクトルを出力する。
  • 最終的に欲しい出力の次元になるようHeadとしてレイヤーを付け足す。
  • モデル全体の出力結果は後処理によって人が見て意味のある形に変換する。

このように各ステップごとに順を追って処理を確認すると何をやっているかのイメージが少しずつ明確になっていきますね。
次回はModels~の内容をベースにModelやTokenizerについてもう少し踏み込んで見ていきます。

脚注
  1. https://pytorch.org/docs/stable/generated/torch.nn.Module.html ↩︎

  2. https://www.tensorflow.org/api_docs/python/tf/keras/Model ↩︎

Discussion