TensorFlow.org で実行 | Google Colabで実行 | GitHubでソースを表示 | ノートブックをダウンロード |
はじめに
再帰型ニューラルネットワーク(RNN)は、時系列や自然言語などのシーケンスデータのモデリングを強力に行うニューラルネットワークのクラスです。
概略的には、RNN レイヤーは for
ループを使用して、それまでに確認した時間ステップに関する情報をエンコードする内部状態を維持しながらシーケンスの時間ステップをイテレートします。
Keras RNN API は、次に焦点を当てて設計されています。
使いやすさ:
keras.layers.RNN
、keras.layers.LSTM
、keras.layers.GRU
レイヤーがビルトインされているため、難しい構成選択を行わずに、再帰型モデルを素早く構築できます。カスタマイズしやすさ: カスタムビヘイビアを使って独自の RNN セルレイヤーを構築し(
for
ループの内部)、一般的なkeras.layers.RNN
レイヤー(for
ループ自体)で使用することもできます。このため、異なるリサーチアイデアを最小限のコードで柔軟に素早くプロトタイプすることができます。
セットアップ
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
2022-12-14 21:38:01.496796: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory 2022-12-14 21:38:01.496895: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory 2022-12-14 21:38:01.496905: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
ビルトイン RNN レイヤー: 単純な例
Keras には、次の 3 つのビルトイン RNN レイヤーがあります。
keras.layers.SimpleRNN
: 前の時間ステップの出力が次の時間ステップにフィードされる、完全に連結された RNN です。keras.layers.GRU
: Cho et al., 2014 で初めて提案されたレイヤー。keras.layers.LSTM
: Hochreiter & Schmidhuber, 1997 で初めて提案されたレイヤー。
2015 年始めに、Keras に、LSTM および GRU の再利用可能なオープンソース Python 実装が導入されました。
整数のシーケンスを処理し、そのような整数を 64 次元ベクトルに埋め込み、LSTM
レイヤーを使用してベクトルのシーケンスを処理する Sequential
モデルの単純な例を次に示しています。
model = keras.Sequential()
# Add an Embedding layer expecting input vocab of size 1000, and
# output embedding dimension of size 64.
model.add(layers.Embedding(input_dim=1000, output_dim=64))
# Add a LSTM layer with 128 internal units.
model.add(layers.LSTM(128))
# Add a Dense layer with 10 units.
model.add(layers.Dense(10))
model.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, None, 64) 64000 lstm (LSTM) (None, 128) 98816 dense (Dense) (None, 10) 1290 ================================================================= Total params: 164,106 Trainable params: 164,106 Non-trainable params: 0 _________________________________________________________________
ビルトイン RNN は、多数の有益な特徴をサポートしています。
dropout
およびrecurrent_dropout
引数を介した再帰ドロップアウトgo_backwards
引数を介して、入力シーケンスを逆順に処理する能力unroll
引数を介したループ展開(CPU で短いシーケンスを処理する際に大幅な高速化が得られる)- など。
詳細については、「RNN API ドキュメント」を参照してください。
出力と状態
デフォルトでは、RNN レイヤーの出力には、サンプル当たり 1 つのベクトルが含まれます。このベクトルは、最後の時間ステップに対応する RNN セル出力で、入力シーケンス全体の情報が含まれます。この出力の形状は (batch_size, units)
で、units
はレイヤーのコンストラクタに渡される units
引数に対応します。
RNN レイヤーは、return_sequences=True
に設定した場合、各サンプルに対する出力のシーケンス全体(各サンプルの時間ステップごとに 1 ベクトル)を返すこともできます。この出力の形状は (batch_size, timesteps, units)
です。
model = keras.Sequential()
model.add(layers.Embedding(input_dim=1000, output_dim=64))
# The output of GRU will be a 3D tensor of shape (batch_size, timesteps, 256)
model.add(layers.GRU(256, return_sequences=True))
# The output of SimpleRNN will be a 2D tensor of shape (batch_size, 128)
model.add(layers.SimpleRNN(128))
model.add(layers.Dense(10))
model.summary()
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, None, 64) 64000 gru (GRU) (None, None, 256) 247296 simple_rnn (SimpleRNN) (None, 128) 49280 dense_1 (Dense) (None, 10) 1290 ================================================================= Total params: 361,866 Trainable params: 361,866 Non-trainable params: 0 _________________________________________________________________
さらに、RNN レイヤーはその最終内部状態を返すことができます。返された状態は、後で RNN 実行を再開する際に使用するか、別の RNN を初期化するために使用できます。この設定は通常、エンコーダ・デコーダ方式の Sequence-to-Sequence モデルで使用され、エンコーダの最終状態がデコーダの初期状態として使用されます。
内部状態を返すように RNN レイヤーを構成するには、レイヤーを作成する際に、return_state
パラメータを True
に設定します。LSTM
には状態テンソルが 2 つあるのに対し、GRU
には 1 つしかないことに注意してください。
レイヤーの初期状態を構成するには、追加のキーワード引数 initial_state
を使ってレイヤーを呼び出します。次の例に示すように、状態の形状は、レイヤーのユニットサイズに一致する必要があることに注意してください。
encoder_vocab = 1000
decoder_vocab = 2000
encoder_input = layers.Input(shape=(None,))
encoder_embedded = layers.Embedding(input_dim=encoder_vocab, output_dim=64)(
encoder_input
)
# Return states in addition to output
output, state_h, state_c = layers.LSTM(64, return_state=True, name="encoder")(
encoder_embedded
)
encoder_state = [state_h, state_c]
decoder_input = layers.Input(shape=(None,))
decoder_embedded = layers.Embedding(input_dim=decoder_vocab, output_dim=64)(
decoder_input
)
# Pass the 2 states to a new LSTM layer, as initial state
decoder_output = layers.LSTM(64, name="decoder")(
decoder_embedded, initial_state=encoder_state
)
output = layers.Dense(10)(decoder_output)
model = keras.Model([encoder_input, decoder_input], output)
model.summary()
Model: "model" __________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== input_1 (InputLayer) [(None, None)] 0 [] input_2 (InputLayer) [(None, None)] 0 [] embedding_2 (Embedding) (None, None, 64) 64000 ['input_1[0][0]'] embedding_3 (Embedding) (None, None, 64) 128000 ['input_2[0][0]'] encoder (LSTM) [(None, 64), 33024 ['embedding_2[0][0]'] (None, 64), (None, 64)] decoder (LSTM) (None, 64) 33024 ['embedding_3[0][0]', 'encoder[0][1]', 'encoder[0][2]'] dense_2 (Dense) (None, 10) 650 ['decoder[0][0]'] ================================================================================================== Total params: 258,698 Trainable params: 258,698 Non-trainable params: 0 __________________________________________________________________________________________________
RNN レイヤーと RNN セル
ビルトイン RNN レイヤーのほかに、RNN API は、セルレベルの API も提供しています。入力シーケンスの全バッチを処理する RNN レイヤーとは異なり、RNN セルは単一の時間ステップのみを処理します。
セルは、RNN レイヤーの for
ループ内にあります。keras.layers.RNN
レイヤー内のセルをラップすることで、シーケンスのバッチを処理できるレイヤー(RNN(LSTMCell(10))
など)を得られます。
数学的には、RNN(LSTMCell(10))
は LSTM(10)
と同じ結果を出します。実際、TF v1.x でのこのレイヤーの実装は、対応する RNN セルを作成し、それを RNN レイヤーにラップするだけでした。ただし、ビルトインの GRU
と LSTM
レイヤーを使用すれば、CuDNN が使用できるようになり、パフォーマンスの改善を確認できることがあります。
ビルトイン RNN セルには 3 つあり、それぞれ、それに一致する RNN レイヤーに対応しています。
keras.layers.SimpleRNNCell
はSimpleRNN
レイヤーに対応します。keras.layers.GRUCell
はGRU
レイヤーに対応します。keras.layers.LSTMCell
はLSTM
レイヤーに対応します。
セルの抽象化とジェネリックな keras.layers.RNN
クラスを合わせることで、リサーチ用のカスタム RNN アーキテクチャの実装を簡単に行えるようになります。
バッチ間のステートフルネス
非常に長い(無限の可能性のある)シーケンスを処理する場合は、バッチ間ステートフルネスのパターンを使用するとよいでしょう。
通常、RNN レイヤーの内部状態は、新しいバッチが確認されるたびにリセットされます(レイヤーが確認する各サンプルは、過去のサンプルとは無関係だと考えられます)。レイヤーは、あるサンプルを処理する間のみ状態を維持します。
ただし、非常に長いシーケンスがある場合、より短いシーケンスに分割し、レイヤーの状態をリセットせずにそれらの短いシーケンスを順次、RNN レイヤーにフィードすることができます。こうすると、レイヤーはサブシーケンスごとに確認していても、シーケンス全体の情報を維持することができます。
これは、コンストラクタに stateful=True
を設定して行います。
シーケンス s = [t0, t1, ... t1546, t1547]
があるとした場合、これを次のように分割します。
s1 = [t0, t1, ... t100]
s2 = [t101, ... t201]
...
s16 = [t1501, ... t1547]
そして、次のようにして処理します。
lstm_layer = layers.LSTM(64, stateful=True)
for s in sub_sequences:
output = lstm_layer(s)
状態をクリアする場合は、layer.reset_states()
を使用できます。
注意: このセットアップでは、あるバッチのサンプル
i
は前のバッチのサンプルi
の続きであることを前提としています。つまり、すべてのバッチには同じ数のサンプル(バッチサイズ)が含まれることになります。たとえば、バッチに[sequence_A_from_t0_to_t100, sequence_B_from_t0_to_t100]
が含まれるとした場合、次のバッチには、[sequence_A_from_t101_to_t200, sequence_B_from_t101_to_t200]
が含まれます。
完全な例を次に示します。
paragraph1 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph2 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph3 = np.random.random((20, 10, 50)).astype(np.float32)
lstm_layer = layers.LSTM(64, stateful=True)
output = lstm_layer(paragraph1)
output = lstm_layer(paragraph2)
output = lstm_layer(paragraph3)
# reset_states() will reset the cached state to the original initial_state.
# If no initial_state was provided, zero-states will be used by default.
lstm_layer.reset_states()
RNN 状態の再利用
RNN の記録済みの状態は、layer.weights()
には含まれません。RNN レイヤーの状態を再利用する場合は、layer.states
によって状態の値を取得し、new_layer(inputs, initial_state=layer.states)
などの Keras Functional API またはモデルのサブクラス化を通じて新しいレイヤーの初期状態として使用することができます。
この場合には、単一の入力と出力を持つレイヤーのみをサポートする Sequential モデルを使用できない可能性があることにも注意してください。このモデルでは追加入力としての初期状態を使用することができません。
paragraph1 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph2 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph3 = np.random.random((20, 10, 50)).astype(np.float32)
lstm_layer = layers.LSTM(64, stateful=True)
output = lstm_layer(paragraph1)
output = lstm_layer(paragraph2)
existing_state = lstm_layer.states
new_lstm_layer = layers.LSTM(64)
new_output = new_lstm_layer(paragraph3, initial_state=existing_state)
双方向性 RNN
時系列以外のシーケンスについては(テキストなど)、開始から終了までのシーケンスを処理だけでなく、逆順に処理する場合、RNN モデルの方がパフォーマンスに優れていることがほとんどです。たとえば、ある文で次に出現する単語を予測するには、その単語の前に出現した複数の単語だけでなく、その単語に関する文脈があると役立ちます。
Keras は、そのような双方向性のある RNN を構築するために、keras.layers.Bidirectional
ラッパーという簡単な API を提供しています。
model = keras.Sequential()
model.add(
layers.Bidirectional(layers.LSTM(64, return_sequences=True), input_shape=(5, 10))
)
model.add(layers.Bidirectional(layers.LSTM(32)))
model.add(layers.Dense(10))
model.summary()
Model: "sequential_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= bidirectional (Bidirectiona (None, 5, 128) 38400 l) bidirectional_1 (Bidirectio (None, 64) 41216 nal) dense_3 (Dense) (None, 10) 650 ================================================================= Total params: 80,266 Trainable params: 80,266 Non-trainable params: 0 _________________________________________________________________
内部的には、Bidirectional
は渡された RNN レイヤーをコピーし、新たにコピーされたレイヤーの go_backwards
フィールドを転換して、入力が逆順に処理されるようにします。
Bidirectional
RNN の出力は、デフォルトで、フォワードレイヤー出力とバックワードレイヤー出力の総和となります。これとは異なるマージ動作が必要な場合は(連結など)、Bidirectional
ラッパーコンストラクタの merge_mode
パラメータを変更します。Bidirectional
の詳細については、API ドキュメントをご覧ください。
パフォーマンス最適化と CuDNN カーネル
TensorFlow 2.0 では、ビルトインの LSTM と GRU レイヤーは、GPU が利用できる場合にデフォルトで CuDNN カーネルを活用するように更新されています。この変更により、以前の keras.layers.CuDNNLSTM/CuDNNGRU
レイヤーは使用廃止となったため、実行するハードウェアを気にせずにモデルを構築することができます。
CuDNN カーネルは、特定の前提を以って構築されており、レイヤーはビルトイン LSTM または GRU レイヤーのデフォルト値を変更しない場合は CuDNN カーネルを使用できません。これらには次のような例があります。
activation
関数をtanh
からほかのものに変更する。recurrent_activation
関数をsigmoid
からほかのものに変更する。recurrent_dropout
> 0 を使用する。unroll
を True に設定する。LSTM/GRU によって内部tf.while_loop
は展開済みfor
ループに分解されます。use_bias
を False に設定する。- 入力データが厳密に右詰でない場合にマスキングを使用する(マスクが厳密に右詰データに対応している場合でも、CuDNN は使用されます。これは最も一般的な事例です)。
制約の詳細については、LSTM および GRU レイヤーのドキュメントを参照してください。
利用できる場合に CuDNN カーネルを使用する
パフォーマンスの違いを確認するために、単純な LSTM モデルを構築してみましょう。
入力シーケンスとして、MNIST 番号の行のシーケンスを使用し(ピクセルの各行を時間ステップとして扱います)、番号のラベルを予測します。
batch_size = 64
# Each MNIST image batch is a tensor of shape (batch_size, 28, 28).
# Each input sequence will be of size (28, 28) (height is treated like time).
input_dim = 28
units = 64
output_size = 10 # labels are from 0 to 9
# Build the RNN model
def build_model(allow_cudnn_kernel=True):
# CuDNN is only available at the layer level, and not at the cell level.
# This means `LSTM(units)` will use the CuDNN kernel,
# while RNN(LSTMCell(units)) will run on non-CuDNN kernel.
if allow_cudnn_kernel:
# The LSTM layer with default options uses CuDNN.
lstm_layer = keras.layers.LSTM(units, input_shape=(None, input_dim))
else:
# Wrapping a LSTMCell in a RNN layer will not use CuDNN.
lstm_layer = keras.layers.RNN(
keras.layers.LSTMCell(units), input_shape=(None, input_dim)
)
model = keras.models.Sequential(
[
lstm_layer,
keras.layers.BatchNormalization(),
keras.layers.Dense(output_size),
]
)
return model
MNIST データセットを読み込みましょう。
mnist = keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
sample, sample_label = x_train[0], y_train[0]
モデルのインスタンスを作成してトレーニングしましょう。
sparse_categorical_crossentropy
をモデルの損失関数として選択します。モデルの出力形状は [batch_size, 10]
です。モデルのターゲットは整数ベクトルで、各整数は 0 から 9 の範囲内にあります。
model = build_model(allow_cudnn_kernel=True)
model.compile(
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer="sgd",
metrics=["accuracy"],
)
model.fit(
x_train, y_train, validation_data=(x_test, y_test), batch_size=batch_size, epochs=1
)
938/938 [==============================] - 7s 6ms/step - loss: 0.9826 - accuracy: 0.6904 - val_loss: 0.5383 - val_accuracy: 0.8268 <keras.callbacks.History at 0x7f549468ac40>
では、CuDNN カーネルを使用しないモデルと比較してみましょう。
noncudnn_model = build_model(allow_cudnn_kernel=False)
noncudnn_model.set_weights(model.get_weights())
noncudnn_model.compile(
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer="sgd",
metrics=["accuracy"],
)
noncudnn_model.fit(
x_train, y_train, validation_data=(x_test, y_test), batch_size=batch_size, epochs=1
)
938/938 [==============================] - 25s 26ms/step - loss: 0.4071 - accuracy: 0.8774 - val_loss: 0.2736 - val_accuracy: 0.9183 <keras.callbacks.History at 0x7f549467d460>
NVIDIA GPU と CuDNN がインストールされたマシンで実行すると、CuDNN で構築されたモデルの方が、通常の TensorFlow カーネルを使用するモデルに比べて非常に高速に実行されます。
CPU のみの環境で推論を実行する場合でも、同じ CuDNN 対応モデルを使用できます。次の tf.device
注釈は単にデバイスの交換を強制しています。GPU が利用できないな場合は、デフォルトで CPU で実行されます。
実行するハードウェアを気にする必要がなくなったのです。素晴らしいと思いませんか?
import matplotlib.pyplot as plt
with tf.device("CPU:0"):
cpu_model = build_model(allow_cudnn_kernel=True)
cpu_model.set_weights(model.get_weights())
result = tf.argmax(cpu_model.predict_on_batch(tf.expand_dims(sample, 0)), axis=1)
print(
"Predicted result is: %s, target result is: %s" % (result.numpy(), sample_label)
)
plt.imshow(sample, cmap=plt.get_cmap("gray"))
Predicted result is: [3], target result is: 5
リスト/ディクショナリ入力、またはネストされた入力を使う RNN
ネスト構造の場合、インプルメンターは単一の時間ステップにより多くの情報を含めることができます。たとえば、動画のフレームに、音声と動画の入力を同時に含めることができます。この場合のデータ形状は、次のようになります。
[batch, timestep, {"video": [height, width, channel], "audio": [frequency]}]
別の例では、手書きのデータに、現在のペンの位置を示す座標 x と y のほか、筆圧情報も含めることができます。データは次のように表現できます。
[batch, timestep, {"location": [x, y], "pressure": [force]}]
次のコードは、このような構造化された入力を受け入れるカスタム RNN セルの構築方法を例に示しています。
ネストされた入力/出力をサポートするカスタムセルを定義する
独自レイヤーの記述に関する詳細は、「サブクラス化による新規レイヤーとモデルの作成」を参照してください。
class NestedCell(keras.layers.Layer):
def __init__(self, unit_1, unit_2, unit_3, **kwargs):
self.unit_1 = unit_1
self.unit_2 = unit_2
self.unit_3 = unit_3
self.state_size = [tf.TensorShape([unit_1]), tf.TensorShape([unit_2, unit_3])]
self.output_size = [tf.TensorShape([unit_1]), tf.TensorShape([unit_2, unit_3])]
super(NestedCell, self).__init__(**kwargs)
def build(self, input_shapes):
# expect input_shape to contain 2 items, [(batch, i1), (batch, i2, i3)]
i1 = input_shapes[0][1]
i2 = input_shapes[1][1]
i3 = input_shapes[1][2]
self.kernel_1 = self.add_weight(
shape=(i1, self.unit_1), initializer="uniform", name="kernel_1"
)
self.kernel_2_3 = self.add_weight(
shape=(i2, i3, self.unit_2, self.unit_3),
initializer="uniform",
name="kernel_2_3",
)
def call(self, inputs, states):
# inputs should be in [(batch, input_1), (batch, input_2, input_3)]
# state should be in shape [(batch, unit_1), (batch, unit_2, unit_3)]
input_1, input_2 = tf.nest.flatten(inputs)
s1, s2 = states
output_1 = tf.matmul(input_1, self.kernel_1)
output_2_3 = tf.einsum("bij,ijkl->bkl", input_2, self.kernel_2_3)
state_1 = s1 + output_1
state_2_3 = s2 + output_2_3
output = (output_1, output_2_3)
new_states = (state_1, state_2_3)
return output, new_states
def get_config(self):
return {"unit_1": self.unit_1, "unit_2": unit_2, "unit_3": self.unit_3}
ネストされた入力/出力で RNN モデルを構築する
上記で定義した keras.layers.RNN
レイヤーとカスタムセルを使用する Keras モデルを構築しましょう。
unit_1 = 10
unit_2 = 20
unit_3 = 30
i1 = 32
i2 = 64
i3 = 32
batch_size = 64
num_batches = 10
timestep = 50
cell = NestedCell(unit_1, unit_2, unit_3)
rnn = keras.layers.RNN(cell)
input_1 = keras.Input((None, i1))
input_2 = keras.Input((None, i2, i3))
outputs = rnn((input_1, input_2))
model = keras.models.Model([input_1, input_2], outputs)
model.compile(optimizer="adam", loss="mse", metrics=["accuracy"])
ランダムに生成されたデータでモデルをトレーニングする
このモデルに適した候補データセットを持ち合わせていないため、ランダムな Numpy データを使って実演することにします。
input_1_data = np.random.random((batch_size * num_batches, timestep, i1))
input_2_data = np.random.random((batch_size * num_batches, timestep, i2, i3))
target_1_data = np.random.random((batch_size * num_batches, unit_1))
target_2_data = np.random.random((batch_size * num_batches, unit_2, unit_3))
input_data = [input_1_data, input_2_data]
target_data = [target_1_data, target_2_data]
model.fit(input_data, target_data, batch_size=batch_size)
10/10 [==============================] - 1s 20ms/step - loss: 0.7515 - rnn_1_loss: 0.2690 - rnn_1_1_loss: 0.4825 - rnn_1_accuracy: 0.1094 - rnn_1_1_accuracy: 0.0330 <keras.callbacks.History at 0x7f540437b5b0>
Keras keras.layers.RNN
レイヤーでは、シーケンス内の個別のステップの数学ロジックを定義することだけが期待されています。シーケンスのイテレーションは、keras.layers.RNN
レイヤーによって処理されます。新しいタイプの RNN(LSTM など) を素早くプロトタイプ化する上で、非常に強力な方法です。
詳細については、API ドキュメントを参照してください。