自分用のメモ/学習を兼ねて、TensorFlow の RNN のチュートリアルを一行ずつみながらやってみる。
LSTM とはなんぞや、とか、そもそもの TensorFlow の使い方とかは、チュートリアル中にあるので割愛。
チュートリアルで用いているコードの内容の解説だけをおこなう。
コードのインデントが2で気持ち悪いとか、この関数 deprecated
なんだけど💢といった苦情は Google 社へお願い致します。
対象としているチュートリアルは、 Language and Sequemce Processing の下にある、Recurrent Neural Networks というページ。
データ準備
まずはチュートリアル中で使用するデータ/コードのDLから。
コード
TensorFlow 本体にはチュートリアルのコードが一部しか含まれていないため、別途DLが必要。
TensorFlow 公式の models というレポジトリにコードが含まれているので、それをクローンする
git clone https://github.com/tensorflow/models.git
クローンしたレポジトリの、 models/tutorials/rnn/ptb
の下に、 ptb_word_lm.py
と reader.py
というファイルがあるはず。
データ
チュートリアルに従って DL & 展開
wget http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
tar xvfz simple-examples.tgz
展開したディレクトリ中の、data/
以下が今回使用するデータ。
データの形式
展開したデータを見てみる。
$ head ptb.train.txt
aer banknote berlitz calloway centrust cluett fromstein gitano guterman hydro-quebec ipo kia memotec mlx nahb punts rake regatta rubens sim snack-food ssangyong swapo wachter
pierre <unk> N years old will join the board as a nonexecutive director nov. N
mr. <unk> is chairman of <unk> n.v. the dutch publishing group
rudolph <unk> N years old and former chairman of consolidated gold fields plc was named a nonexecutive director of this british industrial conglomerate
a form of asbestos once used to make kent cigarette filters has caused a high percentage of cancer deaths among a group of workers exposed to it more than N years ago researchers reported
the asbestos fiber <unk> is unusually <unk> once it enters the <unk> with even brief exposures to it causing symptoms that show up decades later researchers said
<unk> inc. the unit of new york-based <unk> corp. that makes kent cigarettes stopped using <unk> in its <unk> cigarette filters in N
although preliminary findings were reported more than a year ago the latest results appear in today 's new england journal of medicine a forum likely to bring new attention to the problem
a <unk> <unk> said this is an old story
we 're talking about years ago before anyone heard of asbestos having any questionable properties
事前に未知語を <unk>
という特殊記号で置き換え済みの英文。
検証用データ ptb.valid.txt
、テスト用データ ptb.test.txt
も同様の形式。
チュートリアル開始
DLしたチュートリアルコードは python ptb_word_lm.py --data_path=/tmp/simple-examples/data/ --model small
(data_path
は適宜読み替えてください) で走らせることができるようになっているが、一行ずつ解説するのには都合が悪いため、main
関数の中身をバラバラにして少しずつ実行していくことにする。
ここからは、チュートリアルのコードを一行ずつ実行するために、処理内容は変えずにコードを一部改変していることに注意してください。
データの読み込み
まず行われるのはデータの読み込み処理。 main
関数の一番最初の部分は次のようになっている
raw_data = reader.ptb_raw_data(FLAGS.data_path)
train_data, valid_data, test_data, _ = raw_data
では、 reader.ptb_raw_data()
関数の中身を見ていこう。
def ptb_raw_data(data_path=None):
"""Load PTB raw data from data directory "data_path".
Reads PTB text files, converts strings to integer ids,
and performs mini-batching of the inputs.
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
ptb_raw_data()
は、まず訓練データから語彙 (word_to_id
) を構築 (_build_vocab()
) し、それを用いて訓練/検証/テストデータをインデックス化 (_file_to_word_ids()
) している。
_build_vocab()
は次の通り:
def _read_words(filename):
with tf.gfile.GFile(filename, "r") as f:
return f.read().decode("utf-8").replace("\n", "<eos>").split()
def _build_vocab(filename):
data = _read_words(filename) # データを読み込み、スペースで分割する (つまり単語列を得る)
counter = collections.Counter(data) # 単語を数え上げる
count_pairs = sorted(counter.items(), key=lambda x: (-x[1], x[0])) # 登場回数の大きい順にソート
words, _ = list(zip(*count_pairs)) # 単語を登場回数が大きな順に取り出して
word_to_id = dict(zip(words, range(len(words)))) # インデックス化する
return word_to_id
処理内容はコード中に日本語コメントで注釈しました。
最終的に帰ってくる word_to_id
は、各単語をインデックスに変換する辞書です ({'the': 0, 'a': 1, ..., 'hoge': 133, ...}
)。
次に _file_to_word_ids()
:
def _file_to_word_ids(filename, word_to_id):
data = _read_words(filename)
return [word_to_id[word] for word in data if word in word_to_id]
これは非常に単純で、読み込んだ単語列をインデックス化したリストを返しているだけです。
ただし、訓練データから構築した語彙 word_to_id
中に含まれない単語は無視しています。
結局、欲しかった train_data, valid_data, test_data
はそれぞれ最終的に [102, 14, 24, 32, 752, 381, ...]
形式のリストとなります。元データの単語を、単にそのインデックスで置き換えたリストです。
ネットワーク構築
main
関数の続き:
with tf.name_scope("Train"):
train_input = PTBInput(config=config, data=train_data, name="TrainInput")
with tf.variable_scope("Model", reuse=None, initializer=initializer):
m = PTBModel(is_training=True, config=config, input_=train_input)
tf.contrib.deprecated.scalar_summary("Training Loss", m.cost)
tf.contrib.deprecated.scalar_summary("Learning Rate", m.lr)
with tf.name_scope("Valid"):
valid_input = PTBInput(config=config, data=valid_data, name="ValidInput")
with tf.variable_scope("Model", reuse=True, initializer=initializer):
mvalid = PTBModel(is_training=False, config=config, input_=valid_input)
tf.contrib.deprecated.scalar_summary("Validation Loss", mvalid.cost)
with tf.name_scope("Test"):
test_input = PTBInput(config=eval_config, data=test_data, name="TestInput")
with tf.variable_scope("Model", reuse=True, initializer=initializer):
mtest = PTBModel(is_training=False, config=eval_config,
input_=test_input)
Train/Valid/Test でそれぞれ同じようなことをおこなっています。
まず PTBInput
(これは単なる構造体です) にデータを詰めて、 PTBModel
(ネットワーク本体) に渡し、サマリを出力します。
順番に見ていきます。
PTBInput
class PTBInput(object):
"""The input data."""
def __init__(self, config, data, name=None):
self.batch_size = batch_size = config.batch_size # 「1エポックあたりに使うデータの数」
self.num_steps = num_steps = config.num_steps # 「LSTM の unroll の長さ」
self.epoch_size = ((len(data) // batch_size) - 1) // num_steps # 「batch_size x num_steps を何回使えるか」=「データが一回りするまでの回転数」=「エポックあたりの標本数」
self.input_data, self.targets = reader.ptb_producer(
data, batch_size, num_steps, name=name)
「LSTM の unroll」とは、LSTM の時系列を数ステップぶん明示的に書き下すことで、時間方向への依存をなくして「普通の誤差逆伝播法」を使えるようにすることを言います。つまり、訓練時には num_steps
の長さの単語をひとつの「標本」として、 batch_size
ぶんの標本を一度に RNN に入力して誤差を計算することになります。
さて、 最後の部分で reader.ptb_producer()
が何をやっているのかを追跡しないとなりません。
def ptb_producer(raw_data, batch_size, num_steps, name=None):
"""Iterate on the raw PTB data.
This chunks up raw_data into batches of examples and returns Tensors that
are drawn from these batches.
Args:
raw_data: one of the raw data outputs from ptb_raw_data.
batch_size: int, the batch size.
num_steps: int, the number of unrolls.
name: the name of this operation (optional).
Returns:
A pair of Tensors, each shaped [batch_size, num_steps]. The second element
of the tuple is the same data time-shifted to the right by one.
Raises:
tf.errors.InvalidArgumentError: if batch_size or num_steps are too high.
"""
with tf.name_scope(name, "PTBProducer", [raw_data, batch_size, num_steps]):
raw_data = tf.convert_to_tensor(raw_data, name="raw_data", dtype=tf.int32) # 扱いやすいようにビルトインの List から Tensor に変換
data_len = tf.size(raw_data)
batch_len = data_len // batch_size # 行列の列数
data = tf.reshape(raw_data[0 : batch_size * batch_len],
[batch_size, batch_len]) # 1次元リストだった raw_data を、batch_size x batch_len の行列に整形
epoch_size = (batch_len - 1) // num_steps # 1エポック (データの一回り) の大きさ
assertion = tf.assert_positive(
epoch_size,
message="epoch_size == 0, decrease batch_size or num_steps")
with tf.control_dependencies([assertion]):
epoch_size = tf.identity(epoch_size, name="epoch_size")
i = tf.train.range_input_producer(epoch_size, shuffle=False).dequeue() # [0, 1, .., epoch_size-1] という整数を順ぐりに無限生成するイテレータ
x = tf.strided_slice(data, [0, i * num_steps],
[batch_size, (i + 1) * num_steps]) # この使われ方の strided_slice は、data[0:batch_size, i*num_steps:(i+1)*num_steps] だと思って良い
x.set_shape([batch_size, num_steps])
y = tf.strided_slice(data, [0, i * num_steps + 1],
[batch_size, (i + 1) * num_steps + 1]) # 正解 y は x の次に来る単語なので、1を足してスライスを右に一つずらす
y.set_shape([batch_size, num_steps])
return x, y
注釈はコード中に日本語で入れましたが、おそらく上の図を見たほうが、ここで何をしたいのかは明らかだと思います。
PTBModel
main
関数に戻ります。作成したデータをモデルに投入します。PTBModel
は以下のとおりです:
class PTBModel(object):
"""The PTB model."""
def __init__(self, is_training, config, input_):
self._input = input_
batch_size = input_.batch_size
num_steps = input_.num_steps
size = config.hidden_size
vocab_size = config.vocab_size
# Slightly better results can be obtained with forget gate biases
# initialized to 1 but the hyperparameters of the model would need to be
# different than reported in the paper.
lstm_cell = tf.contrib.rnn.BasicLSTMCell(
size, forget_bias=0.0, state_is_tuple=True) # LSTM Cell を構築
if is_training and config.keep_prob < 1:
lstm_cell = tf.contrib.rnn.DropoutWrapper(
lstm_cell, output_keep_prob=config.keep_prob) # 訓練時は Dropout を設定
cell = tf.contrib.rnn.MultiRNNCell(
[lstm_cell] * config.num_layers, state_is_tuple=True) # LSTM を積み上げる
self._initial_state = cell.zero_state(batch_size, data_type())
with tf.device("/cpu:0"):
embedding = tf.get_variable(
"embedding", [vocab_size, size], dtype=data_type()) # LSTM本体に渡す前に、インデックス化された単語を embedding しておく
inputs = tf.nn.embedding_lookup(embedding, input_.input_data)
if is_training and config.keep_prob < 1:
inputs = tf.nn.dropout(inputs, config.keep_prob)
# Simplified version of models/tutorials/rnn/rnn.py's rnn().
# This builds an unrolled LSTM for tutorial purposes only.
# In general, use the rnn() or state_saving_rnn() from rnn.py.
#
# The alternative version of the code below is:
#
# inputs = tf.unstack(inputs, num=num_steps, axis=1)
# outputs, state = tf.nn.rnn(cell, inputs,
# initial_state=self._initial_state)
# (意訳):このチュートリアルでは LSTM を自力で unroll するけど、
# 実用上は tf.unstack() -> tf.nn.rnn() を使えばいいよ。
outputs = []
state = self._initial_state
with tf.variable_scope("RNN"):
for time_step in range(num_steps):
# time_step==0 のときだけ変数を作成して、それ以外では使いまわす。
# unroll するからパラメータが増えたようにみえるけど、本当は同じパラメータなのだから当然同じものである。
if time_step > 0: tf.get_variable_scope().reuse_variables()
(cell_output, state) = cell(inputs[:, time_step, :], state) # 積み上げた LSTM から時系列のデータを順番に取り出す (unrolling)
outputs.append(cell_output)
output = tf.reshape(tf.concat_v2(outputs, 1), [-1, size]) # 取り出したデータを合体して整形
softmax_w = tf.get_variable(
"softmax_w", [size, vocab_size], dtype=data_type())
softmax_b = tf.get_variable("softmax_b", [vocab_size], dtype=data_type())
logits = tf.matmul(output, softmax_w) + softmax_b # 取り出したデータでソフトマックスする
loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example(
[logits],
[tf.reshape(input_.targets, [-1])],
[tf.ones([batch_size * num_steps], dtype=data_type())])
self._cost = cost = tf.reduce_sum(loss) / batch_size # batch_size ぶん一度に計算したので割る
self._final_state = state # 時系列の最後の内部状態は、次のバッチの初期状態となるので取っておく
if not is_training: # 検証/テスト時はここでおしまい 以下は訓練時のみ
return
self._lr = tf.Variable(0.0, trainable=False) # lr: learning rate
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),
config.max_grad_norm) # 勾配をクリップしないとうまく学習できないことがわかっている
optimizer = tf.train.GradientDescentOptimizer(self._lr) # SGD で最適化
self._train_op = optimizer.apply_gradients(
zip(grads, tvars),
global_step=tf.contrib.framework.get_or_create_global_step())
self._new_lr = tf.placeholder(
tf.float32, shape=[], name="new_learning_rate") # 学習率を外から更新できるようにしておく
self._lr_update = tf.assign(self._lr, self._new_lr)
長いですね。めげずに読んでいきましょう。
この PTBModel
が構築できれば、あとは実際にパラメータ (重み) を計算をするだけです。
パラメータ (重み) 計算
例のごとく main
関数に戻ると、ようやく計算フェイズです。
sv = tf.train.Supervisor(logdir=FLAGS.save_path)
with sv.managed_session() as session:
for i in range(config.max_max_epoch):
lr_decay = config.lr_decay ** max(i + 1 - config.max_epoch, 0.0)
m.assign_lr(session, config.learning_rate * lr_decay) # 学習率は指数的に減らしていく
print("Epoch: %d Learning rate: %.3f" % (i + 1, session.run(m.lr)))
train_perplexity = run_epoch(session, m, eval_op=m.train_op,
verbose=True) # 計算の本体はこいつ
print("Epoch: %d Train Perplexity: %.3f" % (i + 1, train_perplexity))
valid_perplexity = run_epoch(session, mvalid)
print("Epoch: %d Valid Perplexity: %.3f" % (i + 1, valid_perplexity))
test_perplexity = run_epoch(session, mtest)
print("Test Perplexity: %.3f" % test_perplexity)
if FLAGS.save_path:
print("Saving model to %s." % FLAGS.save_path)
sv.saver.save(session, FLAGS.save_path, global_step=sv.global_step)
さて、計算の本体である run_epoch()
を読む必要があります。そんなに長くないです:
def run_epoch(session, model, eval_op=None, verbose=False):
"""Runs the model on the given data."""
start_time = time.time()
costs = 0.0
iters = 0
state = session.run(model.initial_state) # state はまず初期化
fetches = {
"cost": model.cost,
"final_state": model.final_state,
}
if eval_op is not None:
fetches["eval_op"] = eval_op
for step in range(model.input.epoch_size):
feed_dict = {}
for i, (c, h) in enumerate(model.initial_state):
feed_dict[c] = state[i].c # 前回の最終状態を初期状態とする
feed_dict[h] = state[i].h
vals = session.run(fetches, feed_dict) # ほしいのは、fetches に入っている cost と final_state
cost = vals["cost"]
state = vals["final_state"] # 次の初期状態は、今の最終状態
costs += cost # cost は積算
iters += model.input.num_steps # 繰り返し回数も積算
if verbose and step % (model.input.epoch_size // 10) == 10:
print("%.3f perplexity: %.3f speed: %.0f wps" %
(step * 1.0 / model.input.epoch_size, np.exp(costs / iters),
iters * model.input.batch_size / (time.time() - start_time)))
return np.exp(costs / iters)
以上です。
回してみる
コードの読解は長々とかかりましたが、実際に動かすのはコマンド一発です。
python ptb_word_lm.py --data_path=/tmp/simple-examples/data/ --model small
GTX960 の場合、small
モデルでは perplexity (エラー率だと思って良い 1) が (訓練、検証、テスト)=(40.725, 119.601, 114.755)、かかった時間は30分ほどでした。
チュートリアルファイル中の説明によると、small
モデルでは perplexity は (37.99, 121.39, 115.91) らしいので、外れていてはなさそうです。
medium
はなんとか回りそうですが、large
は (メモリ的にも時間的にも) 厳しそうです。
-
perplexity は、誤解を恐れずに言えば「予測が『何個の候補からランダムに選ぶのと同じ』性能に到達したのか、その候補数」です。参考: http://www.slideshare.net/hoxo_m/perplexity ↩