colab でプッシュ通知

colab で2-3時間の training を動かしているときにプッシュ通知があると便利。 training 終了や例外がでたときにスマホにプッシュ通知が来るようにできる。
https://www.pushbullet.com/ を設定・アプリインストール。API Key を発行。こんなに簡単にプッシュ通知が実現。API Key は秘密にすべきなので Google Drive におくのが良いでしょう。

!pip install pushbullet.py
from pushbullet import Pushbullet
pb = Pushbullet('api_key')
push = pb.push_note("This is the title", "This is the body")

2018年読んだ本を記録する

Minimum Seq2Seq implementation using Tensorflow 1.4/1.5 API

This is minimum Seq2Seq implementation using Tensorflow 1.4/1.5 API with some comments, which supports Attention and Beam Search and is based on tensorflow/nmt/README.md.
I'd recommend you run this in https://colab.research.google.com/.

This code is also available on gist.

import numpy as np
import tensorflow as tf
from tensorflow.python.layers import core as layers_core

hparams = tf.contrib.training.HParams(
    batch_size=3,
    encoder_length=4,
    decoder_length=5,
    num_units=6,
    src_vocab_size=7,
    embedding_size=8,
    tgt_vocab_size=9,
    learning_rate = 0.01,
    max_gradient_norm = 5.0,
    beam_width =9,
    use_attention = False,
)

# Symbol for start decode process.
tgt_sos_id = 7

# Symbol for end of decode process.
tgt_eos_id = 8

# For debug purpose.
tf.reset_default_graph()

# Encoder
#   encoder_inputs: [encoder_length, batch_size]
#   This is time major where encoder_length comes first instead of batch_size.
encoder_inputs = tf.placeholder(tf.int32, shape=(hparams.encoder_length, hparams.batch_size), name="encoder_inputs")

# Embedding
#   Matrix for embedding: [src_vocab_size, embedding_size]
embedding_encoder = tf.get_variable(
    "embedding_encoder", [hparams.src_vocab_size, hparams.embedding_size])

# Look up embedding:
#   encoder_inputs: [encoder_length, batch_size]
#   encoder_emb_inputs: [encoder_length, batch_size, embedding_size]
encoder_emb_inputs = tf.nn.embedding_lookup(embedding_encoder, encoder_inputs)

# LSTM cell.
encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(hparams.num_units)

# Run Dynamic RNN
#   encoder_outputs: [encoder_length, batch_size, num_units]
#   encoder_state: [batch_size, num_units], this is final state of the cell for each batch.
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
    encoder_cell, encoder_emb_inputs, time_major=True, dtype=tf.float32)

# Decoder input
#   decoder_inputs: [decoder_length, batch_size]
#   decoder_lengths: [batch_size]
#   This is grand truth target inputs for training.
decoder_inputs = tf.placeholder(tf.int32, shape=(hparams.decoder_length, hparams.batch_size), name="decoder_inputs")
decoder_lengths = tf.placeholder(tf.int32, shape=(hparams.batch_size), name="decoer_length")

# EmbeddingDecoder:
#    Embedding for decoder.
#    This is used to convert encode training target texts to list of ids.
embedding_decoder = tf.get_variable(
    "embedding_decoder", [hparams.tgt_vocab_size, hparams.embedding_size])


# Look up embedding:
#   decoder_inputs: [decoder_length, batch_size]
#   decoder_emb_inp: [decoder_length, batch_size, embedding_size]
decoder_emb_inputs = tf.nn.embedding_lookup(embedding_decoder, decoder_inputs)

# https://stackoverflow.com/questions/39573188/output-projection-in-seq2seq-model-tensorflow
# Internally, a neural network operates on dense vectors of some size,
# often 256, 512 or 1024 floats (let's say 512 for here).
# But at the end it needs to predict a word from the vocabulary which is often much larger,
# e.g., 40000 words. Output projection is the final linear layer that converts (projects) from the internal representation to the larger one.
# So, for example, it can consist of a 512 x 40000 parameter matrix and a 40000 parameter for the bias vector.
projection_layer = layers_core.Dense(
            hparams.tgt_vocab_size, use_bias=False)

helper = tf.contrib.seq2seq.TrainingHelper(decoder_emb_inputs, decoder_lengths, time_major=True)

# Decoder with helper:
#   decoder_emb_inputs: [decoder_length, batch_size, embedding_size]
#   decoder_length: [batch_size] vector, which represents each target sequence length.
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(hparams.num_units)

if hparams.use_attention:
  # Attention
  # attention_states: [batch_size, max_time, num_units]
  attention_states = tf.transpose(encoder_outputs, [1, 0, 2])

  # Create an attention mechanism
  attention_mechanism = tf.contrib.seq2seq.LuongAttention(
      hparams.num_units, attention_states,
      memory_sequence_length=None)

  decoder_cell = tf.contrib.seq2seq.AttentionWrapper(
      decoder_cell, attention_mechanism,
      attention_layer_size=hparams.num_units)

  initial_state = decoder_cell.zero_state(hparams.batch_size, tf.float32).clone(cell_state=encoder_state)
else:
  initial_state = encoder_state

# Decoder and decode
decoder = tf.contrib.seq2seq.BasicDecoder(
    decoder_cell, helper, initial_state,
    output_layer=projection_layer)

# Dynamic decoding
#   final_outputs.rnn_output: [batch_size, decoder_length, tgt_vocab_size], list of RNN state.
#   final_outputs.sample_id: [batch_size, decoder_length], list of argmax of rnn_output.
#   final_state: [batch_size, num_units], list of final state of RNN on decode process.
#   final_sequence_lengths: [batch_size], list of each decoded sequence.

final_outputs, _final_state, _final_sequence_lengths = tf.contrib.seq2seq.dynamic_decode(decoder)

print("rnn_output.shape=", final_outputs.rnn_output.shape)
print("sample_id.shape=", final_outputs.sample_id.shape)
print("final_state=", _final_state)
print("final_sequence_lengths.shape=", _final_sequence_lengths.shape)

logits = final_outputs.rnn_output

# Target labels
#   As described in doc for sparse_softmax_cross_entropy_with_logits,
#   labels should be [batch_size, decoder_lengths] instead of [batch_size, decoder_lengths, tgt_vocab_size].
#   So labels should have indices instead of tgt_vocab_size classes.
target_labels = tf.placeholder(tf.int32, shape=(hparams.batch_size, hparams.decoder_length))

# Loss
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
    labels=target_labels, logits=logits)

# Train
global_step = tf.Variable(0, name='global_step', trainable=False)

# Calculate and clip gradients
params = tf.trainable_variables()
gradients = tf.gradients(loss, params)
clipped_gradients, _ = tf.clip_by_global_norm(
    gradients, hparams.max_gradient_norm)

# Optimization
optimizer = tf.train.AdamOptimizer(hparams.learning_rate)
train_op = optimizer.apply_gradients(
    zip(clipped_gradients, params), global_step=global_step)

#optimizer = tf.train.GradientDescentOptimizer(hparams.learning_rate)
#train_op = optimizer.minimize(loss, global_step=global_step)

sess = tf.Session()
sess.run(tf.global_variables_initializer())

# Training data.

# Tweet
tweet1 = np.array([1, 2, 3, 4])
tweet2 = np.array([0, 5, 6, 3])

# Make batch data.
train_encoder_inputs = np.empty((hparams.encoder_length, hparams.batch_size))
train_encoder_inputs[:, 0] = tweet1
train_encoder_inputs[:, 1] = tweet2
train_encoder_inputs[:, 2] = tweet1
print("Tweets")
print(train_encoder_inputs)

# Reply
training_decoder_input1 = [tgt_sos_id, 2, 3, 4, 5]
training_decoder_input2 = [tgt_sos_id, 5, 6, 4, 3]

training_target_label1 = [2, 3, 4, 5, tgt_eos_id]
training_target_label2 = [5, 6, 4, 3, tgt_eos_id]

training_target_labels = np.empty((hparams.batch_size, hparams.decoder_length))
training_target_labels[0] = training_target_label1
training_target_labels[1] = training_target_label2
training_target_labels[2] = training_target_label1
print("Replies")
print(training_target_labels)

training_decoder_inputs = np.empty((hparams.decoder_length, hparams.batch_size))
training_decoder_inputs[:, 0] = training_decoder_input1
training_decoder_inputs[:, 1] = training_decoder_input2
training_decoder_inputs[:, 2] = training_decoder_input1
print(training_decoder_inputs)

feed_dict = {
    encoder_inputs: train_encoder_inputs,
    target_labels: training_target_labels,
    decoder_inputs: training_decoder_inputs,
    decoder_lengths: np.ones((hparams.batch_size), dtype=int) * hparams.decoder_length
}

# Train
for i in range(100):
  _, loss_value = sess.run([train_op, loss], feed_dict=feed_dict)

# Inference
inference_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(
    embedding_decoder,
    tf.fill([hparams.batch_size], tgt_sos_id), tgt_eos_id)

# Inference Decoder
inference_decoder = tf.contrib.seq2seq.BasicDecoder(
    decoder_cell, inference_helper, initial_state,
    output_layer=projection_layer)


# We should specify maximum_iterations, it can't stop otherwise.
source_sequence_length = hparams.encoder_length
maximum_iterations = tf.round(tf.reduce_max(source_sequence_length) * 2)

# Dynamic decoding
outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(
    inference_decoder, maximum_iterations=maximum_iterations)
translations = outputs.sample_id

# Input tweets
inference_encoder_inputs = np.empty((hparams.encoder_length, hparams.batch_size))
inference_encoder_inputs[:, 0] = tweet1
inference_encoder_inputs[:, 1] = tweet2
inference_encoder_inputs[:, 2] = tweet1

feed_dict = {
    encoder_inputs: inference_encoder_inputs,
}

replies = sess.run([translations], feed_dict=feed_dict)
print(replies)

# Beam Search
# Replicate encoder infos beam_width times
decoder_initial_state = tf.contrib.seq2seq.tile_batch(
    initial_state, multiplier=hparams.beam_width)

# Define a beam-search decoder
inference_decoder = tf.contrib.seq2seq.BeamSearchDecoder(
        cell=decoder_cell,
        embedding=embedding_decoder,
        start_tokens=tf.fill([hparams.batch_size], tgt_sos_id),
        end_token=tgt_eos_id,
        initial_state=decoder_initial_state,
        beam_width=hparams.beam_width,
        output_layer=projection_layer,
        length_penalty_weight=0.0)

# Dynamic decoding
outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(
    inference_decoder, maximum_iterations=maximum_iterations)
translations = outputs.predicted_ids

replies = sess.run([translations], feed_dict=feed_dict)
print(replies)

Seq2Seq まとめ

以前作った Seq2Seq を利用した chatbot はゆるやかに改良中なのだが、進捗はあまり良くない。学習の待ち時間は長く暇だし、コード自体も拡張性が低い。そういうわけで最新の Tensorflow のバージョンで書き直そうと思って作業を始めた。しかし深掘りしていくと Seq2Seq の詳細を分かっていなかったことが発覚したのでここにまとめる。間違いを見つけたらコメントか @higepon まで。

Seq2Seq のすべてを解説するのではなく、Tensoflow/nmt/README.mdチュートリアルをベースにする。読んだだけでは、理解できなかった部分を補っていく形で進める。

必要とされる前提知識

  • DNN の基礎。構造、training、 loss とかそういう話。back prop は別に理解できなくても可。
  • RNN の基礎。RNN が時系列の扱いに向いているとか。RNN の構造を unfold した遷移図が理解できていればよい(ここを読むと良い)
  • Word Embedding。単語をベクトルとして表現みたいな話。

Seq2Seq のおおよその構造と罠

以下の図は Tensoflow/nmtREADME.md から持ってきた。翻訳で使われている Seq2Seq の図。

I am a student を Encoder に通すと、その文を言語に依存しないベクトル表現にしてくれる(図の真ん中)。それを Decoder に渡すと Je suis étudiant という特定の言語に翻訳してくれると説明されることが多い。Training 時には Encoder と Decoder のパラメータを学習していくことになる。Seq2Seq ライブラリを使う立場のプログラマから見ると、翻訳のペアの学習データさえ用意すれば Seq2Seq はブラックボックスとして使える。


はずなのだが、そんなにうまくいかない。


なぜかというと以下のような詳細質問に答えられず。実装やチューニングが難しいのだ。

  • RNN の亜種たちのうちどれを使うのだろう? LSTM? GRU。違いはなんだろうか。
  • hyper parameters は具体的に何を意味するのだろう。RNN の hidden state。Embedding のサイズ。Vocabulary 数?。
  • Attention が良いらしいが何が違うの?
  • Beam Search はパフォーマンスが良いらしい。それはどこのレイヤの話?
  • 具体的な loss は何と何を比較してるの?
  • 強化学習を導入するときに Rewards はどのように計算して、どのように与えるのだろうか?
  • bucketing って何?

Seq2Seq の詳細


Seq2Seq の詳細を上の図から理解していこう。ここでは筆者の「理解できたふり」を防ぐために各 input / output などの shape をごまかさずに書いていく。またデータは batch size は省いて 1 つのデータのみを扱う。また を扱うので、どういう順番で処理されていくのか番号を振っていく。


さて Training 済の Seq2Seq モデルがあるとして “I am a student” を入力して翻訳結果を得るフローを見ていこう。

Embedding layer への入力

図の (1) の矢印の部分。単語 “I” を学習済みのモデルの Embedding lookup でベクトル表現に変換する。つまり “I” => [0.1, -0.0045, 0.79, …, 0.45] のように変換する。変換後のベクトルを emb_input とする。shape は [emb_size] である。embedding の空間上では意味が近いと距離も近い。例えば “I” と “We” は近いけど “I” と “Apple” は遠いみたいなイメージ。

hidden layer 1 への入力

図の (2) の部分。青い四角形は RNN の state だと思うと分かりやすい。つまり emb_input を入力されると layer 1 の enc_state[1] が S11 に更新される。enc_state[1] の shape はRNN初期化時に指定したパラメータ。tensorflow なら num_units を使って [num_units] 。この enc_state[1] は “I” という単語が入力されたよと覚えておくイメージ。

hidden layer 2 への入力

ほぼ同じ。図の (3) の部分。enc_state[1] (=S11) の値は入力して enc_state[2] が S21 に更新される。これも同じく “I” という単語が入力されたよと覚えておくものだけど、きっともっと抽象度の高い何かを覚えている気がする。(要確認)。

ここまで起きたこと

Encoder に “I” を入力したら layer 1, 2 の enc_state が変わった。Encoderが “I” を受け取ったことを覚えたイメージ。ここまでが入力の1ステップ。

"am" を入力

次のサイクルでは “am” を Encoder に入力する。ポイントは Encoder は以前に “I” を受け取ったことを覚えていることだ。これを実現しているのが緑の (5') と (6') 部分。順番に見ていこう。

  • (4) で “am” をベクトル表現に変換
  • (5) で “am” のベクトル表現を渡す
  • (5') で enc_state[1] = S11 を入力として RNN に渡す
  • (5) と (5') を利用して enc_state[l] = S12 に更新。S12 は以前 “I” を受け取って次に “am” を受け取ったと記憶しているイメージ。
  • (6) と (6’)も同様に state[2] を S22 に更新する。
“a” と “student” も同様

“am" と全く同じ方法で “a” と “student” を入力する。入力後 Encoder の状態は enc_state[1] = S14, enc_state[2] = S24 となっているはずである。この enc_state[1] と enc_state[2] が以前出てきた、言語に依存しない “I am a student” のベクトル表現である。

decoder プロセスの開始

まず Decoderに <s> (decode を開始せよという意味)の入力 (A) をすると (B) が出力される。(B) の shape は [emb_size] である。この emb_input と Encoder の enc_state[1] つまり (B’) も RNN に入力されて、decoder の dec_state[1] が S_dec11 に更新される。ここで左側の Encoder の RNN と右側の Decoder は独立していることを思い出そう。なので S_dec11 のように layer 1 の t=1 での state のような表記をしている。同様に (C) と (C’) つまりS_dec11と S24 から dec_state[2] が S_dec12 に更新される。

projection layer

(C) と (C’) の入力により dec_state[2] が S_dec12 となった。これを (D) として projection layer に入力すると、ようやく一文字目の翻訳結果 “Je” (E) がでてくる。projection layer は RNN の state (=S_dec12) を project して vocabulary 空間の logits に変換する。より具体的には [vocab_size] のベクトルに変換する。そのベクトルに argmax すれば “Je” が出てくる。

次の翻訳結果を得る

(E) で得た "Je “ を今度は、また Decoder に入力 (F) する。そして全く同様に Decoder の state を更新しつつ projection layer から “suis" がでてくる。これをまた Decoder に入力する。というのを繰り返して projection layer から </s> (= decode 終わり)の合図が出たらおしまいである。

Training

さてここまでは Training 済みのモデルを利用しての翻訳の流れだったが Training はどうするのだろう。トレーニング時は “I am a student” <=> "Je suis étudiant" というペアが学習データとしてある。"I am a student" を順々に encoder に入力後、 decoderに <s> を入力して projection layer で出力されたものを “Je” と loss を計算。
このあと2つやり方があって

  • Je を入力して出てきたものを “suis” と loss 計算
  • もしくは
  • projection layer で出力されたものを入力して出てきたものを “suis” と loss 計算

の2つがあるみたい。

nmt 解説

長かったけどこれを踏まえて、tensoflow/nmt の基礎コードを解読していく。

placeholders
  • encoder_inputs [max_encoder_time, batch_size]: source input words.
    • これは encoder_input の batch 。 encoder_input は [max_encoder_time] でボキャブラリ辞書の index 列として表現されている
  • decoder_inputs [max_decoder_time, batch_size]: target input words.
    • Decoder に<s> からはじまる入力を入れるための placeholder
  • decoder_outputs [max_decoder_time, batch_size]: target output words, these are decoder_inputs shifted to the left by one time step with an end-of-sentence tag appended on the right.
    • 英語でも書いてあるが decoder_input を左にシフトして </s> が含まれるもの
Embedding
# Embedding
embedding_encoder = variable_scope.get_variable(
    "embedding_encoder", [src_vocab_size, embedding_size], ...)
# Look up embedding:
#   encoder_inputs: [max_time, batch_size]
#   encoder_emb_inp: [max_time, batch_size, embedding_size]
encoder_emb_inp = embedding_ops.embedding_lookup(
    embedding_encoder, encoder_inputs)

embedding_encoder は encoder の embedding_layer をパラメータを学習するもの。encoder_emp_inp は図の embedding layer から hidden layer1 に渡される embedding vector の列。 RNN には1つずつ渡されるがここではまとまって扱われている。

Encoder
# Build RNN cell
# encoder cell つくるよ
encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)

# Run Dynamic RNN
#   encoder_outputs: [max_time, batch_size, num_units]
#   encoder_state: [batch_size, num_units]
# 実際に encoder する
# encoder_state は最終 state
# encoder_outputs は各入力に対する state の列
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
    encoder_cell, encoder_emb_inp,
    sequence_length=source_sequence_length, time_major=True)
Decoder
# Build RNN cell
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Helper
# この Helper を入れ替えることで Beam search とかにできる
# decoder_lengths は target の長さ
# TrainingHelper は上記の「Je を入力して出てきたものを “suis” と loss 計算」をするみたい
helper = tf.contrib.seq2seq.TrainingHelper(
    decoder_emb_inp, decoder_lengths, time_major=True)
# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
    decoder_cell, helper, encoder_state,
    output_layer=projection_layer)
# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)
logits = outputs.rnn_output
Projection Layer
## ただの bias の fully connected layer
projection_layer = layers_core.Dense(
    tgt_vocab_size, use_bias=False)
Loss

ここがちょっとわかりづらいのだけど logits がいまの model から出てきた翻訳結果で、decoder_outputs が正解。

crossent = tf.nn.sparse_softmax_cross_entropy_with_logits(
    labels=decoder_outputs, logits=logits)
train_loss = (tf.reduce_sum(crossent * target_weights) /
    batch_size)

おまけ

Beam Search

Beam Search は prediction 時に projection layer から出てきた logits の argmax を取ってつなぐのではなくて確率の積が最大化するものを選ぶ仕組み。

Attention

Attention は RNN の改良。いま対象の time=t を処理しているときにどこに attention を持つべきかを指示できる。翻訳の例だと I と Je のように関連のある単語に decoder 側の RNN が attention を向けられるようになるイメージ。(要確認)。

bucketing

実際の学習データは長さにばらつきがあるため、長さのグループ(bucket) に分けて、グループごとに学習する仕組み。

強化学習

Projection layer で生成されたものに対して rewards を計算して logits と掛け合わせたものを最大化するようにすれば良いと思う。