BERTでテキストを分類する

TensorFlow.orgで表示 GoogleColabで実行 GitHubで表示ノートブックをダウンロードするTFハブモデルを参照してください

このチュートリアルには、BERTを微調整して、プレーンテキストのIMDB映画レビューのデータセットに対して感情分析を実行するための完全なコードが含まれています。モデルのトレーニングに加えて、テキストを適切な形式に前処理する方法を学習します。

このノートブックでは、次のことを行います。

  • IMDBデータセットをロードする
  • TensorFlowハブからBERTモデルをロードします
  • BERTと分類器を組み合わせて独自のモデルを構築する
  • 独自のモデルをトレーニングし、その一環としてBERTを微調整します
  • モデルを保存し、それを使用して文を分類します

IMDBデータセットを初めて使用する場合は、詳細について「基本的なテキスト分類」を参照してください。

BERTについて

BERTおよびその他のTransformerエンコーダアーキテクチャは、NLP(自然言語処理)のさまざまなタスクで大成功を収めています。これらは、深層学習モデルでの使用に適した自然言語のベクトル空間表現を計算します。 BERTファミリーのモデルは、Transformerエンコーダーアーキテクチャを使用して、入力テキストの各トークンを前後のすべてのトークンの完全なコンテキストで処理します。そのため、名前はTransformersからの双方向エンコーダー表現です。

BERTモデルは通常、大量のテキストで事前にトレーニングされてから、特定のタスクに合わせて微調整されます。

セットアップ

# A dependency of the preprocessing for BERT inputs
pip install -q -U tensorflow-text

tensorflow / modelsのAdamWオプティマイザーを使用します

pip install -q tf-models-official
import os
import shutil

import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_text as text
from official.nlp import optimization  # to create AdamW optimizer

import matplotlib.pyplot as plt

tf.get_logger().setLevel('ERROR')

感情分析

このノートブックは、レビューのテキストに基づいて、映画レビューをポジティブまたはネガティブに分類するための感情分析モデルをトレーニングします。

インターネット映画データベースからの50,000本の映画レビューのテキストを含む大規模な映画レビューデータセットを使用します。

IMDBデータセットをダウンロードする

データセットをダウンロードして抽出してから、ディレクトリ構造を調べてみましょう。

url = 'https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz'

dataset = tf.keras.utils.get_file('aclImdb_v1.tar.gz', url,
                                  untar=True, cache_dir='.',
                                  cache_subdir='')

dataset_dir = os.path.join(os.path.dirname(dataset), 'aclImdb')

train_dir = os.path.join(dataset_dir, 'train')

# remove unused folders to make it easier to load the data
remove_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(remove_dir)
Downloading data from https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
84131840/84125825 [==============================] - 7s 0us/step

次に、 text_dataset_from_directoryユーティリティを使用して、ラベル付きのtf.data.Datasetを作成します。

IMDBデータセットはすでにトレーニングとテストに分割されていますが、検証セットがありません。以下のvalidation_split引数を使用して、トレーニングデータの80:20分割を使用して検証セットを作成しましょう。

AUTOTUNE = tf.data.AUTOTUNE
batch_size = 32
seed = 42

raw_train_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'aclImdb/train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='training',
    seed=seed)

class_names = raw_train_ds.class_names
train_ds = raw_train_ds.cache().prefetch(buffer_size=AUTOTUNE)

val_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'aclImdb/train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='validation',
    seed=seed)

val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

test_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'aclImdb/test',
    batch_size=batch_size)

test_ds = test_ds.cache().prefetch(buffer_size=AUTOTUNE)
Found 25000 files belonging to 2 classes.
Using 20000 files for training.
Found 25000 files belonging to 2 classes.
Using 5000 files for validation.
Found 25000 files belonging to 2 classes.

いくつかのレビューを見てみましょう。

for text_batch, label_batch in train_ds.take(1):
  for i in range(3):
    print(f'Review: {text_batch.numpy()[i]}')
    label = label_batch.numpy()[i]
    print(f'Label : {label} ({class_names[label]})')
Review: b'"Pandemonium" is a horror movie spoof that comes off more stupid than funny. Believe me when I tell you, I love comedies. Especially comedy spoofs. "Airplane", "The Naked Gun" trilogy, "Blazing Saddles", "High Anxiety", and "Spaceballs" are some of my favorite comedies that spoof a particular genre. "Pandemonium" is not up there with those films. Most of the scenes in this movie had me sitting there in stunned silence because the movie wasn\'t all that funny. There are a few laughs in the film, but when you watch a comedy, you expect to laugh a lot more than a few times and that\'s all this film has going for it. Geez, "Scream" had more laughs than this film and that was more of a horror film. How bizarre is that?<br /><br />*1/2 (out of four)'
Label : 0 (neg)
Review: b"David Mamet is a very interesting and a very un-equal director. His first movie 'House of Games' was the one I liked best, and it set a series of films with characters whose perspective of life changes as they get into complicated situations, and so does the perspective of the viewer.<br /><br />So is 'Homicide' which from the title tries to set the mind of the viewer to the usual crime drama. The principal characters are two cops, one Jewish and one Irish who deal with a racially charged area. The murder of an old Jewish shop owner who proves to be an ancient veteran of the Israeli Independence war triggers the Jewish identity in the mind and heart of the Jewish detective.<br /><br />This is were the flaws of the film are the more obvious. The process of awakening is theatrical and hard to believe, the group of Jewish militants is operatic, and the way the detective eventually walks to the final violent confrontation is pathetic. The end of the film itself is Mamet-like smart, but disappoints from a human emotional perspective.<br /><br />Joe Mantegna and William Macy give strong performances, but the flaws of the story are too evident to be easily compensated."
Label : 0 (neg)
Review: b'Great documentary about the lives of NY firefighters during the worst terrorist attack of all time.. That reason alone is why this should be a must see collectors item.. What shocked me was not only the attacks, but the"High Fat Diet" and physical appearance of some of these firefighters. I think a lot of Doctors would agree with me that,in the physical shape they were in, some of these firefighters would NOT of made it to the 79th floor carrying over 60 lbs of gear. Having said that i now have a greater respect for firefighters and i realize becoming a firefighter is a life altering job. The French have a history of making great documentary\'s and that is what this is, a Great Documentary.....'
Label : 1 (pos)

TensorFlowハブからモデルを読み込んでいます

ここで、TensorFlowハブからロードするBERTモデルを選択し、微調整できます。利用可能な複数のBERTモデルがあります。

  • BERT- ベースケースなし、および元のBERT作成者によってリリースされたトレーニング済みウェイトを備えた7つのモデル
  • 小さなBERTの一般的なアーキテクチャは同じですが、Transformerブロックが少ないか小さいため、速度、サイズ、品質の間のトレードオフを調べることができます。
  • ALBERT :レイヤー間でパラメーターを共有することでモデルサイズを(計算時間ではなく)削減する4つの異なるサイズの「ALiteBERT」。
  • BERTエキスパート:すべてBERTベースのアーキテクチャを備えているが、ターゲットタスクとより緊密に連携するために、さまざまな事前トレーニングドメインから選択できる8つのモデル。
  • ElectraはBERTと同じアーキテクチャ(3つの異なるサイズ)を持っていますが、Generative Adversarial Network(GAN)に似たセットアップで弁別子として事前にトレーニングされています。
  • トーキング・ヘッズ・アテンションとゲート付きGELU [ ベースラージ]を備えたBERTには、Transformerアーキテクチャーのコアに2つの改良が加えられています。

TensorFlow Hubのモデルドキュメントには、詳細と研究文献への参照があります。上記のリンクをたどるか、次のセルの実行後にtfhub.devされるtfhub.devクリックします。

微調整が速いので、小さいBERT(パラメーターが少ない)から始めることをお勧めします。小さいモデルが好きで、精度が高い場合は、ALBERTが次の選択肢になる可能性があります。さらに精度を高めたい場合は、クラシックなBERTサイズ、またはElectra、Talking Heads、BERTExpertなどの最近の改良版のいずれかを選択してください。

以下で利用可能なモデルとは別に、より大きく、さらに高い精度をもたらすことができるモデルの複数のバージョンがありますが、それらは大きすぎて単一のGPUで微調整することはできません。 TPUコラボでBERTを使用して、SolveGLUEタスクでそれを行うことができます。

以下のコードでは、tfhub.dev URLを切り替えるだけで、これらのモデルを試すことができます。これは、モデル間のすべての違いがTFハブのSavedModelsにカプセル化されているためです。

BERTモデルを選択して微調整します

BERT model selected           : https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1
Preprocess model auto-selected: https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3

前処理モデル

テキスト入力は、BERTに入力する前に、数値トークンIDに変換し、いくつかのテンソルに配置する必要があります。 TensorFlow Hubは、上記の各BERTモデルに一致する前処理モデルを提供します。これは、TF.textライブラリのTFopsを使用してこの変換を実装します。テキストを前処理するために、TensorFlowモデルの外部で純粋なPythonコードを実行する必要はありません。

前処理モデルは、上に印刷されたURLで読むことができるBERTモデルのドキュメントで参照されているモデルである必要があります。上のドロップダウンからのBERTモデルの場合、前処理モデルが自動的に選択されます。

bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess)

いくつかのテキストで前処理モデルを試して、出力を見てみましょう。

text_test = ['this is such an amazing movie!']
text_preprocessed = bert_preprocess_model(text_test)

print(f'Keys       : {list(text_preprocessed.keys())}')
print(f'Shape      : {text_preprocessed["input_word_ids"].shape}')
print(f'Word Ids   : {text_preprocessed["input_word_ids"][0, :12]}')
print(f'Input Mask : {text_preprocessed["input_mask"][0, :12]}')
print(f'Type Ids   : {text_preprocessed["input_type_ids"][0, :12]}')
Keys       : ['input_type_ids', 'input_mask', 'input_word_ids']
Shape      : (1, 128)
Word Ids   : [ 101 2023 2003 2107 2019 6429 3185  999  102    0    0    0]
Input Mask : [1 1 1 1 1 1 1 1 1 0 0 0]
Type Ids   : [0 0 0 0 0 0 0 0 0 0 0 0]

ご覧のとおり、BERTモデルが使用する前処理からの3つの出力( input_words_idinput_maskinput_type_ids )があります。

その他の重要なポイント:

  • 入力は128トークンに切り捨てられます。トークンの数はカスタマイズでき、 TPUコラボでBERTを使用してGLUEを解決するタスクの詳細を確認できます。
  • input_type_idsは、単一文の入力であるため、値(0)は1つだけです。複数文の入力の場合、入力ごとに1つの番号があります。

このテキストプリプロセッサはTensorFlowモデルであるため、モデルに直接含めることができます。

BERTモデルの使用

BERTを独自のモデルに組み込む前に、その出力を見てみましょう。 TF Hubからロードして、戻り値を確認します。

bert_model = hub.KerasLayer(tfhub_handle_encoder)
bert_results = bert_model(text_preprocessed)

print(f'Loaded BERT: {tfhub_handle_encoder}')
print(f'Pooled Outputs Shape:{bert_results["pooled_output"].shape}')
print(f'Pooled Outputs Values:{bert_results["pooled_output"][0, :12]}')
print(f'Sequence Outputs Shape:{bert_results["sequence_output"].shape}')
print(f'Sequence Outputs Values:{bert_results["sequence_output"][0, :12]}')
Loaded BERT: https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1
Pooled Outputs Shape:(1, 512)
Pooled Outputs Values:[ 0.76262873  0.99280983 -0.1861186   0.36673835  0.15233682  0.65504444
  0.9681154  -0.9486272   0.00216158 -0.9877732   0.0684272  -0.9763061 ]
Sequence Outputs Shape:(1, 128, 512)
Sequence Outputs Values:[[-0.28946388  0.3432126   0.33231565 ...  0.21300787  0.7102078
  -0.05771166]
 [-0.28742015  0.31981024 -0.2301858  ...  0.58455074 -0.21329722
   0.7269209 ]
 [-0.66157013  0.6887685  -0.87432927 ...  0.10877253 -0.26173282
   0.47855264]
 ...
 [-0.2256118  -0.28925604 -0.07064401 ...  0.4756601   0.8327715
   0.40025353]
 [-0.29824278 -0.27473143 -0.05450511 ...  0.48849759  1.0955356
   0.18163344]
 [-0.44378197  0.00930723  0.07223766 ...  0.1729009   1.1833246
   0.07897988]]

BERTモデルは、 pooled_outputsequence_outputencoder_outputs 3つの重要なキーを持つマップを返します。

  • pooled_outputは、各入力シーケンス全体を表します。形状は[batch_size, H]です。これは、映画レビュー全体の埋め込みと考えることができます。
  • sequence_outputは、コンテキスト内の各入力トークンを表します。形状は[batch_size, seq_length, H]です。これは、映画レビューのすべてのトークンのコンテキスト埋め込みと考えることができます。
  • encoder_outputsの中間のアクティベーションされているLトランスブロック。 outputs["encoder_outputs"][i]は、 0 <= i < Lの場合、i番目のTransformerブロックの出力を持つ形状[batch_size, seq_length, 1024]テンソルです。リストの最後の値はsequence_outputと同じです。

微調整には、 pooled_output配列を使用します。

モデルを定義する

前処理モデル、選択したBERTモデル、1つの高密度レイヤー、およびドロップアウトレイヤーを使用して、非常に単純な微調整モデルを作成します。

def build_classifier_model():
  text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
  preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
  encoder_inputs = preprocessing_layer(text_input)
  encoder = hub.KerasLayer(tfhub_handle_encoder, trainable=True, name='BERT_encoder')
  outputs = encoder(encoder_inputs)
  net = outputs['pooled_output']
  net = tf.keras.layers.Dropout(0.1)(net)
  net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
  return tf.keras.Model(text_input, net)

モデルが前処理モデルの出力で実行されることを確認しましょう。

classifier_model = build_classifier_model()
bert_raw_result = classifier_model(tf.constant(text_test))
print(tf.sigmoid(bert_raw_result))
tf.Tensor([[0.50131935]], shape=(1, 1), dtype=float32)

もちろん、モデルがまだトレーニングされていないため、出力は無意味です。

モデルの構造を見てみましょう。

tf.keras.utils.plot_model(classifier_model)

png

モデルトレーニング

これで、前処理モジュール、BERTエンコーダー、データ、分類器など、モデルをトレーニングするためのすべての要素が揃いました。

損失関数

これはバイナリ分類の問題であり、モデルは確率(単一ユニット層)を出力するため、 losses.BinaryCrossentropy損失関数を使用します。

loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
metrics = tf.metrics.BinaryAccuracy()

オプティマイザ

微調整のために、BERTが最初にトレーニングされたのと同じオプティマイザー「AdaptiveMoments」(Adam)を使用しましょう。このオプティマイザーは、予測損失を最小限に抑え、 AdamWとしても知られている重みの減衰(モーメントを使用しない)による正則化を行います。

学習率( init_lr )には、BERT事前トレーニングと同じスケジュールを使用します。つまり、概念的な初期学習率の線形減衰であり、トレーニングステップの最初の10%( num_warmup_steps )で線形ウォームアップフェーズが接頭辞として付けられます。 BERTの論文と一致して、初期学習率は微調整の方が小さくなっています(5e-5、3e-5、2e-5のベスト)。

epochs = 5
steps_per_epoch = tf.data.experimental.cardinality(train_ds).numpy()
num_train_steps = steps_per_epoch * epochs
num_warmup_steps = int(0.1*num_train_steps)

init_lr = 3e-5
optimizer = optimization.create_optimizer(init_lr=init_lr,
                                          num_train_steps=num_train_steps,
                                          num_warmup_steps=num_warmup_steps,
                                          optimizer_type='adamw')

BERTモデルの読み込みとトレーニング

以前に作成したclassifier_modelを使用して、損失、メトリック、およびオプティマイザーを使用してモデルをコンパイルできます。

classifier_model.compile(optimizer=optimizer,
                         loss=loss,
                         metrics=metrics)
print(f'Training model with {tfhub_handle_encoder}')
history = classifier_model.fit(x=train_ds,
                               validation_data=val_ds,
                               epochs=epochs)
Training model with https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1
Epoch 1/5
625/625 [==============================] - 83s 124ms/step - loss: 0.4881 - binary_accuracy: 0.7403 - val_loss: 0.3917 - val_binary_accuracy: 0.8340
Epoch 2/5
625/625 [==============================] - 77s 124ms/step - loss: 0.3296 - binary_accuracy: 0.8518 - val_loss: 0.3714 - val_binary_accuracy: 0.8450
Epoch 3/5
625/625 [==============================] - 78s 124ms/step - loss: 0.2530 - binary_accuracy: 0.8939 - val_loss: 0.4036 - val_binary_accuracy: 0.8486
Epoch 4/5
625/625 [==============================] - 78s 124ms/step - loss: 0.1968 - binary_accuracy: 0.9226 - val_loss: 0.4468 - val_binary_accuracy: 0.8502
Epoch 5/5
625/625 [==============================] - 78s 124ms/step - loss: 0.1604 - binary_accuracy: 0.9392 - val_loss: 0.4716 - val_binary_accuracy: 0.8498

モデルを評価する

モデルがどのように機能するかを見てみましょう。 2つの値が返されます。損失(エラーを表す数値、値が小さいほど良い)、および精度。

loss, accuracy = classifier_model.evaluate(test_ds)

print(f'Loss: {loss}')
print(f'Accuracy: {accuracy}')
782/782 [==============================] - 53s 67ms/step - loss: 0.4476 - binary_accuracy: 0.8554
Loss: 0.44761356711387634
Accuracy: 0.8554400205612183

時間の経過に伴う精度と損失をプロットします

model.fit()によって返されるHistoryオブジェクトに基づきます。比較のためにトレーニングと検証の損失、およびトレーニングと検証の精度をプロットできます。

history_dict = history.history
print(history_dict.keys())

acc = history_dict['binary_accuracy']
val_acc = history_dict['val_binary_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']

epochs = range(1, len(acc) + 1)
fig = plt.figure(figsize=(10, 6))
fig.tight_layout()

plt.subplot(2, 1, 1)
# "bo" is for "blue dot"
plt.plot(epochs, loss, 'r', label='Training loss')
# b is for "solid blue line"
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
# plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(epochs, acc, 'r', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
dict_keys(['loss', 'binary_accuracy', 'val_loss', 'val_binary_accuracy'])
<matplotlib.legend.Legend at 0x7f542fffc590>

png

このプロットでは、赤い線はトレーニングの損失と精度を表し、青い線は検証の損失と精度を表しています。

推論のためにエクスポート

これで、後で使用するために微調整したモデルを保存するだけです。

dataset_name = 'imdb'
saved_model_path = './{}_bert'.format(dataset_name.replace('/', '_'))

classifier_model.save(saved_model_path, include_optimizer=False)
WARNING:absl:Found untraced functions such as restored_function_body, restored_function_body, restored_function_body, restored_function_body, restored_function_body while saving (showing 5 of 310). These functions will not be directly callable after loading.

モデルをリロードして、まだメモリに残っているモデルと並べて試してみましょう。

reloaded_model = tf.saved_model.load(saved_model_path)

ここでは、以下のexamples変数に追加するだけで、任意の文でモデルをテストできます。

def print_my_examples(inputs, results):
  result_for_printing = \
    [f'input: {inputs[i]:<30} : score: {results[i][0]:.6f}'
                         for i in range(len(inputs))]
  print(*result_for_printing, sep='\n')
  print()


examples = [
    'this is such an amazing movie!',  # this is the same sentence tried earlier
    'The movie was great!',
    'The movie was meh.',
    'The movie was okish.',
    'The movie was terrible...'
]

reloaded_results = tf.sigmoid(reloaded_model(tf.constant(examples)))
original_results = tf.sigmoid(classifier_model(tf.constant(examples)))

print('Results from the saved model:')
print_my_examples(examples, reloaded_results)
print('Results from the model in memory:')
print_my_examples(examples, original_results)
Results from the saved model:
input: this is such an amazing movie! : score: 0.998905
input: The movie was great!           : score: 0.994330
input: The movie was meh.             : score: 0.968163
input: The movie was okish.           : score: 0.069656
input: The movie was terrible...      : score: 0.000776

Results from the model in memory:
input: this is such an amazing movie! : score: 0.998905
input: The movie was great!           : score: 0.994330
input: The movie was meh.             : score: 0.968163
input: The movie was okish.           : score: 0.069656
input: The movie was terrible...      : score: 0.000776

TF Servingでモデルを使用する場合は、名前付き署名の1つを介してSavedModelが呼び出されることに注意してください。 Pythonでは、次のようにテストできます。

serving_results = reloaded_model \
            .signatures['serving_default'](tf.constant(examples))

serving_results = tf.sigmoid(serving_results['classifier'])

print_my_examples(examples, serving_results)
input: this is such an amazing movie! : score: 0.998905
input: The movie was great!           : score: 0.994330
input: The movie was meh.             : score: 0.968163
input: The movie was okish.           : score: 0.069656
input: The movie was terrible...      : score: 0.000776

次のステップ

次のステップとして、 TPUで実行され、複数の入力を操作する方法を示すTPUチュートリアルBERTを使用してGLUEタスクを解決することを試すことができます。