TensorFlow : コード解説 : RNN – リカレント・ニューラルネットワーク & LSTM
* TensorFlow : Tutorials : リカレント・ニューラルネットワーク に、数式排除/コード重視の方針で詳細な解説を加筆したものです。
LSTM ネットワーク
RNN – リカレント・ニューラルネットワーク、特に LSTM への入門としては Understanding LSTM Networks を参照のこと。要点は LSTM ネットワークの理解 (翻訳/要約) に簡単にまとめておきました。
言語モデリング
このチュートリアルではリカレント・ニューラルネットワークをどのように訓練するかを示します。言語モデリングの挑戦的なタスクです。ゴールは文に確率を割り当てる確率モデルを最適化することです。前の単語の履歴を与えられたテキストで次の単語を予測することで達成します。
言語モデリングは、音声認識、機械翻訳あるいは画像キャプションのような多くの興味ある問題へのキーで将来有望です – こちら をご覧ください。
このチュートリアルでは、PTB データセット上で非常に良い結果を達成してる、Zaremba et al., 2014 (pdf) からの結果を再現します。
Penn Tree Bank (PTB) を使用します。これは訓練するに小さくて比較的速く、これらのモデルのクオリティを計測するための人気のあるベンチマークです。
チュートリアル・ファイル
このチュートリアルではコード: models/rnn/ptb から次のファイルを参照します :
ファイル | 目的 |
---|---|
ptb_word_lm.py | PTB データセット上で言語モデルを訓練するコード。 |
reader.py | データセットを読むコード。 |
データのダウンロードと準備
このチュートリアルに必要なデータは Tomas Mikolov の web ページからの
PTB データセットの data/ ディレクトリにあります :
http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
データセットは既に前処理されていて、文の最後のマーカーと珍しい単語のための特殊なシンボル (<unk>) を含む、全体で 10000 の異なる単語を含みます。
ニューラルネットワークが処理しやすいようにこれら全部を reader.py で一意の整数識別子に変換します。
ptb_word_lm.py#main は、最初に reader.py#ptb_raw_data を呼び出してデータセットを取得します。
def main(unused_args): if not FLAGS.data_path: raise ValueError("Must set --data_path to PTB data directory") raw_data = reader.ptb_raw_data(FLAGS.data_path) train_data, valid_data, test_data, _ = raw_data
def ptb_raw_data(data_path=None): """ PTB 生データをデータディレクトリ "data_path" からロードする。 PTB テキストファイルを読み込み、文字列を整数値 id に変換、 入力のミニ・バッチを遂行します。 The PTB dataset comes from Tomas Mikolov's webpage: http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz Args: data_path: string path to the directory where simple-examples.tgz has been extracted. Returns: tuple (train_data, valid_data, test_data, vocabulary) where each of the data objects can be passed to PTBIterator. """ train_path = os.path.join(data_path, "ptb.train.txt") valid_path = os.path.join(data_path, "ptb.valid.txt") test_path = os.path.join(data_path, "ptb.test.txt") word_to_id = _build_vocab(train_path) train_data = _file_to_word_ids(train_path, word_to_id) valid_data = _file_to_word_ids(valid_path, word_to_id) test_data = _file_to_word_ids(test_path, word_to_id) vocabulary = len(word_to_id) return train_data, valid_data, test_data, vocabulary
モデル
LSTM
モデルの核は LSTM セルから構成されます。このセルは、一度に一つの単語を処理し文の(可能性のある)継続性の確率を計算します。ネットワークのメモリ状態は 0 のベクタで初期化されて各単語を読んだ後で更新されます。また、計算上の理由で、サイズ batch_size のミニ・バッチでデータを処理します。
基本的な擬似コードは次のようなものです :
lstm = rnn_cell.BasicLSTMCell(lstm_size) # LSTM メモリの初期状態。 state = tf.zeros([batch_size, lstm.state_size]) loss = 0.0 for current_batch_of_words in words_in_dataset: # 状態の値は単語の各バッチ処理の後で更新されます。 output, state = lstm(current_batch_of_words, state) # LSTM 出力は次の単語予測をするために使用できます。 logits = tf.matmul(output, softmax_w) + softmax_b probabilities = tf.nn.softmax(logits) loss += loss_function(probabilities, target_words)
PTBModel コンストラクタ
モデルの実装は PTBModel クラスで、コンストラクタで構築されます。
class PTBModel(object): def __init__(self, is_training, config): self.batch_size = batch_size = config.batch_size self.num_steps = num_steps = config.num_steps size = config.hidden_size vocab_size = config.vocab_size # プレースホルダー self._input_data = tf.placeholder(tf.int32, [batch_size, num_steps]) self._targets = tf.placeholder(tf.int32, [batch_size, num_steps]) # 1 で初期化された忘却ゲート・バイアスによれば幾分良い結果が得られますが、 # モデルのハイパーパラメータは論文での報告とは異なるものである必要があります。 lstm_cell = rnn_cell.BasicLSTMCell(size, forget_bias=0.0) if is_training and config.keep_prob < 1: lstm_cell = rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=config.keep_prob) cell = rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers) self._initial_state = cell.zero_state(batch_size, tf.float32) with tf.device("/cpu:0"): embedding = tf.get_variable("embedding", [vocab_size, size]) inputs = tf.nn.embedding_lookup(embedding, self._input_data) if is_training and config.keep_prob < 1: inputs = tf.nn.dropout(inputs, config.keep_prob) # tensorflow.models.rnn.rnn.py の rnn() の簡易版。 # チュートリアル目的のみで unrolled LSTM を構築します。 # 一般的には、rnn.py の rnn() か state_saving_rnn() を使用します。 # # The alternative version of the code below is: # # from tensorflow.models.rnn import rnn # inputs = [tf.squeeze(input_, [1]) # for input_ in tf.split(1, num_steps, inputs)] # outputs, states = rnn.rnn(cell, inputs, initial_state=self._initial_state) outputs = [] states = [] state = self._initial_state with tf.variable_scope("RNN"): for time_step in range(num_steps): if time_step > 0: tf.get_variable_scope().reuse_variables() (cell_output, state) = cell(inputs[:, time_step, :], state) outputs.append(cell_output) states.append(state) output = tf.reshape(tf.concat(1, outputs), [-1, size]) softmax_w = tf.get_variable("softmax_w", [size, vocab_size]) softmax_b = tf.get_variable("softmax_b", [vocab_size]) logits = tf.matmul(output, softmax_w) + softmax_b loss = seq2seq.sequence_loss_by_example([logits], [tf.reshape(self._targets, [-1])], [tf.ones([batch_size * num_steps])], vocab_size) self._cost = cost = tf.reduce_sum(loss) / batch_size self._final_state = states[-1] if not is_training: return self._lr = tf.Variable(0.0, trainable=False) tvars = tf.trainable_variables() grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), config.max_grad_norm) optimizer = tf.train.GradientDescentOptimizer(self.lr) self._train_op = optimizer.apply_gradients(zip(grads, tvars))
Truncated バックプロパゲーション
学習プロセスを扱いやすくするために、バックプロパゲーションのための勾配を展開されたステップの固定数 (num_steps) で打ち切る (truncate) ことは一般的に実践されます。これは、一度に num_steps の長さの入力を供給して各反復後に後方へパスを行なうことにより実装は容易です。
truncated バックプロパゲーションのためのグラフ作成のためのコードの簡易版です :
# 与えられた反復における入力のためのプレースホルダー。 words = tf.placeholder(tf.int32, [batch_size, num_steps]) lstm = rnn_cell.BasicLSTMCell(lstm_size) # LSTM メモリの初期状態。 initial_state = state = tf.zeros([batch_size, lstm.state_size]) for i in range(len(num_steps)): # 状態値は単語のバッチ毎処理後に更新されます。 output, state = lstm(words[:, i], state) # 残りのコード。 # ... final_state = state
そしてこれがデータセット全体に渡る反復の実装です。
# 単語の各バッチ後の LSTM の状態を保持する numpy 配列。 numpy_state = initial_state.eval() total_loss = 0.0 for current_batch_of_words in words_in_dataset: numpy_state, current_loss = session.run([final_state, loss], # 以前の反復からの LSTM 状態の初期化。 feed_dict={initial_state: numpy_state, words: current_batch_of_words}) total_loss += current_loss
入力
単語 ID は LSTM に供給される前に密な表現(単語のベクタ表現チュートリアル 参照)に埋め込まれます。これはモデルに、特定の単語についての知識を効率的に表現することを可能にします。コードを書くのもまた簡単です:
# embedding_matrix は形状 [vocabulary_size, embedding size] のテンソルです。 word_embeddings = tf.nn.embedding_lookup(embedding_matrix, word_ids)
埋め込み行列はランダムに初期化されてモデルはデータを検索するだけで単語の意味を識別できるようにするために学習します。
損失関数
ターゲット単語の確率の対数の負の平均を最小化したいです :
\[ \text{loss} = -\frac{1}{N}\sum_{i=1}^{N} \ln p_{\text{target}_i} \]
関数 sequence_loss_by_example
が利用可能です。
output = tf.reshape(tf.concat(1, outputs), [-1, size]) softmax_w = tf.get_variable("softmax_w", [size, vocab_size]) softmax_b = tf.get_variable("softmax_b", [vocab_size]) logits = tf.matmul(output, softmax_w) + softmax_b loss = seq2seq.sequence_loss_by_example([logits], [tf.reshape(self._targets, [-1])], [tf.ones([batch_size * num_steps])], vocab_size) self._cost = cost = tf.reduce_sum(loss) / batch_size
論文で報告されている典型的な尺度は平均単語毎 (per-word) perplexity (単に perplexity とも)で、これは以下に等しいです
\[e^{-\frac{1}{N}\sum_{i=1}^{N} \ln p_{\text{target}_i}} = e^{\text{loss}} \]
そして私たちは訓練プロセスを通じてこの値をモニタします。
複数 LSTM のスタック
モデルにより表現力を与えるために、LSTM の複数層を追加できます。
第1層の出力は第2層の入力、等々になります。
MultiRNNCell と呼ばれるクラスを持ち、これは実装をシームレスにします :
lstm = rnn_cell.BasicLSTMCell(lstm_size) stacked_lstm = rnn_cell.MultiRNNCell([lstm] * number_of_layers) initial_state = state = stacked_lstm.zero_state(batch_size, tf.float32) for i in range(len(num_steps)): # 単語の各バッチの処理後、状態値は更新されます。 output, state = stacked_lstm(words[:, i], state) # 残りのコード。 # ... final_state = state
実際のコード :
lstm_cell = rnn_cell.BasicLSTMCell(size, forget_bias=0.0) if is_training and config.keep_prob < 1: lstm_cell = rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=config.keep_prob) cell = rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers)
コードを実行する
pip パッケージのインストールを行ない、tensorflow git レポジトリをクローンし、そして git ツリーのルートにいることを仮定しています。(もしソースからビルドしているならば、bazel でターゲット tensorflow/models/rnn/ptb:ptb_word_lm をビルドします。)
次に: cd tensorflow/models/rnn/ptb python ptb_word_lm --data_path=/tmp/simple-examples/data/ --model small
チュートリアル・コードには3つのサポートされたモデル構成があります: "small", "medium" そして "large" です。これらの違いは LSTM のサイズと訓練に使われるハイパーパラメータのセットにあります。
より大きなモデルは、より良い結果を得られるでしょう。small モデルはテストセット上で 120 以下の perplexity に達することができ、large モデルは 80 以下です、訓練に数時間かかるかもしれませんが。
What Next?
モデルを良くするための私たちが言及していない幾つかのトリックあります、これは次を含みます:
- 学習率を減少するスケジュール
- LSTM 層間のドロップアウト
コードを研究してモデルを更に改善するように修正しましょう。
以上