RNN を使ったテキスト分類

View on TensorFlow.org Run in Google Colab View source on GitHub Download notebook

このテキスト分類チュートリアルでは、感情分析のために IMDB 映画レビュー大型データセット を使って リカレントニューラルネットワーク を訓練します。

設定

!pip install -q tf-nightly
import tensorflow_datasets as tfds
import tensorflow as tf

matplotlib をインポートしグラフを描画するためのヘルパー関数を作成します。

import matplotlib.pyplot as plt

def plot_graphs(history, metric):
  plt.plot(history.history[metric])
  plt.plot(history.history['val_'+metric], '')
  plt.xlabel("Epochs")
  plt.ylabel(metric)
  plt.legend([metric, 'val_'+metric])
  plt.show()

入力パイプラインの設定

IMDB 映画レビュー大型データセットは二値分類データセットです。すべてのレビューは、好意的(positive) または 非好意的(negative) のいずれかの感情を含んでいます。

TFDS を使ってこのデータセットをダウンロードします。

dataset, info = tfds.load('imdb_reviews/subwords8k', with_info=True,
                          as_supervised=True)
train_examples, test_examples = dataset['train'], dataset['test']
WARNING:absl:TFDS datasets with text encoding are deprecated and will be removed in a future version. Instead, you should use the plain text version and tokenize the text using `tensorflow_text` (See: https://www.tensorflow.org/tutorials/tensorflow_text/intro#tfdata_example)

Downloading and preparing dataset imdb_reviews/subwords8k/1.0.0 (download: 80.23 MiB, generated: Unknown size, total: 80.23 MiB) to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0...
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incompleteZKAXRS/imdb_reviews-train.tfrecord
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incompleteZKAXRS/imdb_reviews-test.tfrecord
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incompleteZKAXRS/imdb_reviews-unsupervised.tfrecord
Dataset imdb_reviews downloaded and prepared to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0. Subsequent calls will reuse this data.

このデータセットの info には、エンコーダー(tfds.features.text.SubwordTextEncoder) が含まれています。

encoder = info.features['text'].encoder
print('Vocabulary size: {}'.format(encoder.vocab_size))
Vocabulary size: 8185

このテキストエンコーダーは、任意の文字列を可逆的にエンコードします。必要であればバイトエンコーディングにフォールバックします。

sample_string = 'Hello TensorFlow.'

encoded_string = encoder.encode(sample_string)
print('Encoded string is {}'.format(encoded_string))

original_string = encoder.decode(encoded_string)
print('The original string: "{}"'.format(original_string))
Encoded string is [4025, 222, 6307, 2327, 4043, 2120, 7975]
The original string: "Hello TensorFlow."

assert original_string == sample_string
for index in encoded_string:
  print('{} ----> {}'.format(index, encoder.decode([index])))
4025 ----> Hell
222 ----> o 
6307 ----> Ten
2327 ----> sor
4043 ----> Fl
2120 ----> ow
7975 ----> .

訓練用データの準備

次に、これらのエンコード済み文字列をバッチ化します。padded_batch メソッドを使ってバッチ中の一番長い文字列の長さにゼロパディングを行います。

BUFFER_SIZE = 10000
BATCH_SIZE = 64
train_dataset = (train_examples
                 .shuffle(BUFFER_SIZE)
                 .padded_batch(BATCH_SIZE, padded_shapes=([None],[])))

test_dataset = (test_examples
                .padded_batch(BATCH_SIZE,  padded_shapes=([None],[])))
train_dataset = (train_examples
                 .shuffle(BUFFER_SIZE)
                 .padded_batch(BATCH_SIZE))

test_dataset = (test_examples
                .padded_batch(BATCH_SIZE))

モデルの作成

tf.keras.Sequential モデルを構築しましょう。最初に Embedding レイヤーから始めます。Embedding レイヤーは単語一つに対して一つのベクトルを収容します。呼び出しを受けると、Embedding レイヤーは単語のインデックスのシーケンスを、ベクトルのシーケンスに変換します。これらのベクトルは訓練可能です。(十分なデータで)訓練されたあとは、おなじような意味をもつ単語は、しばしばおなじようなベクトルになります。

このインデックス参照は、ワンホットベクトルを tf.keras.layers.Dense レイヤーを使って行うおなじような演算に比べてずっと効率的です。

リカレントニューラルネットワーク(RNN)は、シーケンスの入力を要素を一つずつ扱うことで処理します。RNN は、あるタイムステップでの出力を次のタイムステップの入力へと、次々に渡していきます。

RNN レイヤーとともに、tf.keras.layers.Bidirectional ラッパーを使用することができます。このラッパーは、入力を RNN 層の順方向と逆方向に伝え、その後出力を結合します。これにより、RNN は長期的な依存関係を学習できます。

model = tf.keras.Sequential([
    tf.keras.layers.Embedding(encoder.vocab_size, 64),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(1)
])

訓練プロセスを定義するため、Keras モデルをコンパイルします。

model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(1e-4),
              metrics=['accuracy'])

モデルの訓練

history = model.fit(train_dataset, epochs=10,
                    validation_data=test_dataset, 
                    validation_steps=30)
Epoch 1/10
391/391 [==============================] - 44s 112ms/step - loss: 0.6841 - accuracy: 0.5080 - val_loss: 0.5014 - val_accuracy: 0.7854
Epoch 2/10
391/391 [==============================] - 40s 103ms/step - loss: 0.3863 - accuracy: 0.8439 - val_loss: 0.3742 - val_accuracy: 0.8130
Epoch 3/10
391/391 [==============================] - 40s 103ms/step - loss: 0.2670 - accuracy: 0.8956 - val_loss: 0.3443 - val_accuracy: 0.8677
Epoch 4/10
391/391 [==============================] - 41s 104ms/step - loss: 0.2088 - accuracy: 0.9249 - val_loss: 0.3572 - val_accuracy: 0.8667
Epoch 5/10
391/391 [==============================] - 40s 103ms/step - loss: 0.1817 - accuracy: 0.9343 - val_loss: 0.3592 - val_accuracy: 0.8677
Epoch 6/10
391/391 [==============================] - 40s 103ms/step - loss: 0.1713 - accuracy: 0.9401 - val_loss: 0.3932 - val_accuracy: 0.8672
Epoch 7/10
391/391 [==============================] - 41s 104ms/step - loss: 0.1424 - accuracy: 0.9523 - val_loss: 0.3967 - val_accuracy: 0.8646
Epoch 8/10
391/391 [==============================] - 40s 102ms/step - loss: 0.1407 - accuracy: 0.9525 - val_loss: 0.4044 - val_accuracy: 0.8656
Epoch 9/10
391/391 [==============================] - 40s 103ms/step - loss: 0.1218 - accuracy: 0.9605 - val_loss: 0.3897 - val_accuracy: 0.8568
Epoch 10/10
391/391 [==============================] - 40s 103ms/step - loss: 0.1076 - accuracy: 0.9659 - val_loss: 0.4564 - val_accuracy: 0.8609

test_loss, test_acc = model.evaluate(test_dataset)

print('Test Loss: {}'.format(test_loss))
print('Test Accuracy: {}'.format(test_acc))
391/391 [==============================] - 15s 39ms/step - loss: 0.4525 - accuracy: 0.8580
Test Loss: 0.4524710178375244
Test Accuracy: 0.8580399751663208

上記のモデルはシーケンスに適用されたパディングをマスクしていません。パディングされたシーケンスで訓練を行い、パディングをしていないシーケンスでテストするとすれば、このことが結果を歪める可能性があります。理想的にはこれを避けるために、 マスキングを使うべきですが、下記のように出力への影響は小さいものでしかありません。

予測値が 0.5 以上であればポジティブ、それ以外はネガティブです。

def pad_to_size(vec, size):
  zeros = [0] * (size - len(vec))
  vec.extend(zeros)
  return vec
def sample_predict(sample_pred_text, pad):
  encoded_sample_pred_text = encoder.encode(sample_pred_text)

  if pad:
    encoded_sample_pred_text = pad_to_size(encoded_sample_pred_text, 64)
  encoded_sample_pred_text = tf.cast(encoded_sample_pred_text, tf.float32)
  predictions = model.predict(tf.expand_dims(encoded_sample_pred_text, 0))

  return (predictions)
# パディングなしのサンプルテキストの推論

sample_pred_text = ('The movie was cool. The animation and the graphics '
                    'were out of this world. I would recommend this movie.')
predictions = sample_predict(sample_pred_text, pad=False)
print(predictions)
[[0.13266508]]

# パディングありのサンプルテキストの推論

sample_pred_text = ('The movie was cool. The animation and the graphics '
                    'were out of this world. I would recommend this movie.')
predictions = sample_predict(sample_pred_text, pad=True)
print(predictions)
[[0.35219505]]

plot_graphs(history, 'accuracy')

png

plot_graphs(history, 'loss')

png

2つ以上の LSTM レイヤーを重ねる

Keras のリカレントレイヤーには、コンストラクタの return_sequences 引数でコントロールされる2つのモードがあります。

  • それぞれのタイムステップの連続した出力のシーケンス全体(shape が (batch_size, timesteps, output_features) の3階テンソル)を返す。
  • それぞれの入力シーケンスの最後の出力だけ(shape が (batch_size, output_features) の2階テンソル)を返す。
model = tf.keras.Sequential([
    tf.keras.layers.Embedding(encoder.vocab_size, 64),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64,  return_sequences=True)),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32)),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(1)
])
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(1e-4),
              metrics=['accuracy'])
history = model.fit(train_dataset, epochs=10,
                    validation_data=test_dataset,
                    validation_steps=30)
Epoch 1/10
391/391 [==============================] - 77s 198ms/step - loss: 0.6881 - accuracy: 0.5056 - val_loss: 0.5110 - val_accuracy: 0.7635
Epoch 2/10
391/391 [==============================] - 74s 188ms/step - loss: 0.4654 - accuracy: 0.7977 - val_loss: 0.3917 - val_accuracy: 0.8266
Epoch 3/10
391/391 [==============================] - 74s 190ms/step - loss: 0.3158 - accuracy: 0.8815 - val_loss: 0.3495 - val_accuracy: 0.8531
Epoch 4/10
391/391 [==============================] - 74s 190ms/step - loss: 0.2492 - accuracy: 0.9111 - val_loss: 0.3503 - val_accuracy: 0.8682
Epoch 5/10
391/391 [==============================] - 74s 190ms/step - loss: 0.2045 - accuracy: 0.9338 - val_loss: 0.3744 - val_accuracy: 0.8651
Epoch 6/10
391/391 [==============================] - 74s 190ms/step - loss: 0.1813 - accuracy: 0.9430 - val_loss: 0.3882 - val_accuracy: 0.8661
Epoch 7/10
391/391 [==============================] - 74s 190ms/step - loss: 0.1495 - accuracy: 0.9574 - val_loss: 0.4249 - val_accuracy: 0.8589
Epoch 8/10
391/391 [==============================] - 74s 190ms/step - loss: 0.1419 - accuracy: 0.9595 - val_loss: 0.4415 - val_accuracy: 0.8568
Epoch 9/10
391/391 [==============================] - 74s 188ms/step - loss: 0.1202 - accuracy: 0.9710 - val_loss: 0.4609 - val_accuracy: 0.8573
Epoch 10/10
391/391 [==============================] - 74s 189ms/step - loss: 0.1104 - accuracy: 0.9740 - val_loss: 0.4995 - val_accuracy: 0.8349

test_loss, test_acc = model.evaluate(test_dataset)

print('Test Loss: {}'.format(test_loss))
print('Test Accuracy: {}'.format(test_acc))
391/391 [==============================] - 29s 74ms/step - loss: 0.5008 - accuracy: 0.8322
Test Loss: 0.5008367300033569
Test Accuracy: 0.8321999907493591

# パディングなしのサンプルテキストの推論

sample_pred_text = ('The movie was not good. The animation and the graphics '
                    'were terrible. I would not recommend this movie.')
predictions = sample_predict(sample_pred_text, pad=False)
print(predictions)
[[-1.995122]]

# パディングありのサンプルテキストの推論

sample_pred_text = ('The movie was not good. The animation and the graphics '
                    'were terrible. I would not recommend this movie.')
predictions = sample_predict(sample_pred_text, pad=True)
print(predictions)
[[-1.4606018]]

plot_graphs(history, 'accuracy')

png

plot_graphs(history, 'loss')

png

GRU レイヤーなど既存のほかのレイヤーを調べてみましょう。

カスタム RNN の構築に興味があるのであれば、Keras RNN ガイド を参照してください。