RNN を使ったテキスト分類

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

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

設定

import tensorflow as tf
import tensorflow_datasets as tfds

ERROR: tensorflow 2.1.0 has requirement gast==0.2.2, but you'll have gast 0.3.3 which is incompatible.

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

import matplotlib.pyplot as plt

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

入力パイプラインの設定

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

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

dataset, info = tfds.load('imdb_reviews/subwords8k', with_info=True,
                          as_supervised=True)
train_dataset, test_dataset = dataset['train'], dataset['test']

このデータセットの 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_dataset.shuffle(BUFFER_SIZE)
train_dataset = train_dataset.padded_batch(BATCH_SIZE)

test_dataset = test_dataset.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, activation='sigmoid')
])

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

model.compile(loss='binary_crossentropy',
              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 [==============================] - 47s 120ms/step - loss: 0.6505 - accuracy: 0.6032 - val_loss: 0.5692 - val_accuracy: 0.7042
Epoch 2/10
391/391 [==============================] - 42s 108ms/step - loss: 0.3409 - accuracy: 0.8627 - val_loss: 0.3547 - val_accuracy: 0.8542
Epoch 3/10
391/391 [==============================] - 42s 108ms/step - loss: 0.2572 - accuracy: 0.9030 - val_loss: 0.3324 - val_accuracy: 0.8724
Epoch 4/10
391/391 [==============================] - 42s 107ms/step - loss: 0.2092 - accuracy: 0.9259 - val_loss: 0.3228 - val_accuracy: 0.8734
Epoch 5/10
391/391 [==============================] - 42s 108ms/step - loss: 0.1914 - accuracy: 0.9332 - val_loss: 0.3479 - val_accuracy: 0.8750
Epoch 6/10
391/391 [==============================] - 42s 108ms/step - loss: 0.1620 - accuracy: 0.9446 - val_loss: 0.3617 - val_accuracy: 0.8687
Epoch 7/10
391/391 [==============================] - 43s 109ms/step - loss: 0.1441 - accuracy: 0.9532 - val_loss: 0.3888 - val_accuracy: 0.8661
Epoch 8/10
391/391 [==============================] - 43s 109ms/step - loss: 0.1446 - accuracy: 0.9507 - val_loss: 0.3781 - val_accuracy: 0.8641
Epoch 9/10
391/391 [==============================] - 43s 110ms/step - loss: 0.1280 - accuracy: 0.9587 - val_loss: 0.4171 - val_accuracy: 0.8630
Epoch 10/10
391/391 [==============================] - 43s 109ms/step - loss: 0.1128 - accuracy: 0.9643 - val_loss: 0.4377 - val_accuracy: 0.8620
test_loss, test_acc = model.evaluate(test_dataset)

print('Test Loss: {}'.format(test_loss))
print('Test Accuracy: {}'.format(test_acc))
    391/Unknown - 17s 44ms/step - loss: 0.4410 - accuracy: 0.8585Test Loss: 0.44097715334209336
Test Accuracy: 0.8584799766540527

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

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

def pad_to_size(vec, size):
  zeros = [0] * (size - len(vec))
  vec.extend(zeros)
  return vec
def sample_predict(sentence, 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.5525528]]
# パディングありのサンプルテキストの推論

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.49454296]]
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, activation='sigmoid')
])
model.compile(loss='binary_crossentropy',
              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 [==============================] - 81s 206ms/step - loss: 0.6421 - accuracy: 0.5968 - val_loss: 0.4979 - val_accuracy: 0.7599
Epoch 2/10
391/391 [==============================] - 77s 197ms/step - loss: 0.3574 - accuracy: 0.8583 - val_loss: 0.3481 - val_accuracy: 0.8573
Epoch 3/10
391/391 [==============================] - 76s 195ms/step - loss: 0.2572 - accuracy: 0.9082 - val_loss: 0.3314 - val_accuracy: 0.8641
Epoch 4/10
391/391 [==============================] - 76s 194ms/step - loss: 0.2159 - accuracy: 0.9263 - val_loss: 0.3620 - val_accuracy: 0.8620
Epoch 5/10
391/391 [==============================] - 76s 194ms/step - loss: 0.1820 - accuracy: 0.9408 - val_loss: 0.3603 - val_accuracy: 0.8625
Epoch 6/10
391/391 [==============================] - 77s 196ms/step - loss: 0.1580 - accuracy: 0.9527 - val_loss: 0.3957 - val_accuracy: 0.8641
Epoch 7/10
391/391 [==============================] - 76s 194ms/step - loss: 0.1452 - accuracy: 0.9564 - val_loss: 0.4118 - val_accuracy: 0.8562
Epoch 8/10
391/391 [==============================] - 77s 197ms/step - loss: 0.1242 - accuracy: 0.9658 - val_loss: 0.4608 - val_accuracy: 0.8609
Epoch 9/10
391/391 [==============================] - 76s 194ms/step - loss: 0.1057 - accuracy: 0.9715 - val_loss: 0.4801 - val_accuracy: 0.8490
Epoch 10/10
391/391 [==============================] - 78s 200ms/step - loss: 0.1136 - accuracy: 0.9677 - val_loss: 0.4897 - val_accuracy: 0.8615
test_loss, test_acc = model.evaluate(test_dataset)

print('Test Loss: {}'.format(test_loss))
print('Test Accuracy: {}'.format(test_acc))
    391/Unknown - 32s 82ms/step - loss: 0.5015 - accuracy: 0.8481Test Loss: 0.501451134414929
Test Accuracy: 0.8480799794197083
# パディングなしのサンプルテキストの推論

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)
[[0.0741732]]
# パディングありのサンプルテキストの推論

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)
[[0.01727706]]
plot_graphs(history, 'accuracy')

png

plot_graphs(history, 'loss')

png

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

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