CS231n/課題3/LSTM(長短期記憶)を使った画像説明付け

今回は、Stanford University/CS231n/Assignment3/Image Captioning with LSTMs(Long short-term memory:長・短期記憶)をやる。

前回のexerciseではvanilla RNNを実装してそれを画像キャプションに使用した。このエクササイズではLSTM更新ルールを実装してそれを画像キャプションに使う。

# As usual, a bit of setup
from __future__ import print_function
import time, os, json
import numpy as np
import matplotlib.pyplot as plt
from cs231n.gradient_check import eval_numerical_gradient, eval_numerical_gradient_array
from cs231n.rnn_layers import *
from cs231n.captioning_solver import CaptioningSolver
from cs231n.classifiers.rnn import CaptioningRNN
from cs231n.coco_utils import load_coco_data, sample_coco_minibatch, decode_captions
from cs231n.image_utils import image_from_url

plt.rcParams['figure.figsize'] = 10, 8 # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

def rel_error(x, y):
    """ returns relative error """
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

# for auto-reloading external modules
# see http://stackoverflow.com/questions/1907993/autoreload-of-modules-in-ipython
%load_ext autoreload
%autoreload 2
%matplotlib inline

Load MS-COCO data

前回同様、キャプションにMicrosoft COCO datasetを使う。

# Load COCO data from disk; this returns a dictionary
# We'll work with dimensionality-reduced features for this notebook, but feel
# free to experiment with the original features by changing the flag below.
data = load_coco_data(pca_features=True)

# Print out all the keys and values from the data dictionary
for k, v in data.items():
    if type(v) == np.ndarray:
        print(k, type(v), v.shape, v.dtype)
    else:
        print(k, type(v), len(v))
train_captions <class 'numpy.ndarray'> (400135, 17) int32
train_image_idxs <class 'numpy.ndarray'> (400135,) int32
val_captions <class 'numpy.ndarray'> (195954, 17) int32
val_image_idxs <class 'numpy.ndarray'> (195954,) int32
train_features <class 'numpy.ndarray'> (82783, 512) float32
val_features <class 'numpy.ndarray'> (40504, 512) float32
idx_to_word <class 'list'> 1004
word_to_idx <class 'dict'> 1004
train_urls <class 'numpy.ndarray'> (82783,) <U63
val_urls <class 'numpy.ndarray'> (40504,) <U63

LSTM

Long-Short Term Memory (LSTM) RNNsと呼ばれるvanialla RNNの改良が、昨今、多くの人々によって使われている。Vanilla RNNsで長い数列を訓練するのは、反復マトリクス乗算に起因する勾配の消失と爆発のせいで難しくなっている。LSTMはこの問題をvanilla RNNの単純な更新ルールを以下のようにゲート機構と置き換えることで解決している。

vanilla RNN同様、各タイムステップで、入力$x_t\in\mathbb{R}^D$と元の隠れ状態 $h_{t-1}\in\mathbb{R}^H$を受け取る。LSTMは$H$-次元セル状態も保持するので元のセル状態$c_{t-1}\in\mathbb{R}^H$も受け取る。LSTMの学習可能パラメーターはinput-to-hidden matrix $W_x\in\mathbb{R}^{4H\times D}$、hidden-to-hidden matrix $W_h\in\mathbb{R}^{4H\times H}$、bias vector $b\in\mathbb{R}^{4H}$である。

時間ステップ毎に、先ず、activation vector(活性化ベクトル)$a\in\mathbb{R}^{4H}$を$a=W_xx_t + W_hh_{t-1}+b$として計算する。次に、これを、$a_i$が$a$の最初の$H$成分から成り、$a_f$が$a$の次の$H$成分である等の4つのベクトル$a_i,a_f,a_o,a_g\in\mathbb{R}^H$に分割する。その次に、入力ゲート$g\in\mathbb{R}^H$、忘却ゲート$f\in\mathbb{R}^H$、出力ゲート$o\in\mathbb{R}^H$、を計算し、ブロック入力$g\in\mathbb{R}^H$を、共にelementwiseの$\sigma$がシグモイド関数、$\tanh$が双曲線正接である、$$ \begin{align*} i = \sigma(a_i) \hspace{2pc} f = \sigma(a_f) \hspace{2pc} o = \sigma(a_o) \hspace{2pc} g = \tanh(a_g) \end{align*} $$として計算する。

最後に、次のセル状態$c_t$と次の隠れ状態$h_t$を、$\odot$がベクトルの要素ごとの積である$$ c_{t} = f\odot c_{t-1} + i\odot g \hspace{4pc} h_t = o\odot\tanh(c_t) $$として計算する。

今後は、LSTM更新ルールを実装して、それを画像説明付けタスクに使用する。

コード内のデータは、$X_t \in \mathbb{R}^{N\times D}$になるようにバッチ中に保存されることが想定されていて、活性化 $A \in \mathbb{R}^{N\times 4H}$が、$A = X_t W_x + H_{t-1} W_h$として効率的に計算できるようにパラメーター$W_x \in \mathbb{R}^{D \times 4H}$, $W_h \in \mathbb{R}^{H\times 4H}$の転置版を使って作業する。

LSTM: step forward

cs231n/rnn_layers.pyのlstm_step_forward関数にLSTMの単一時間ステップ用フォワードパスを実装する。これは、上で実装したrnn_step_forward関数に似るはずだが、代わりにLSTM update ruleを使用している。

終わったら下を実行して実装の簡単なテストを行う。エラーは大体1e-8未満。

N, D, H = 3, 4, 5
x = np.linspace(-0.4, 1.2, num=N*D).reshape(N, D)
prev_h = np.linspace(-0.3, 0.7, num=N*H).reshape(N, H)
prev_c = np.linspace(-0.4, 0.9, num=N*H).reshape(N, H)
Wx = np.linspace(-2.1, 1.3, num=4*D*H).reshape(D, 4 * H)
Wh = np.linspace(-0.7, 2.2, num=4*H*H).reshape(H, 4 * H)
b = np.linspace(0.3, 0.7, num=4*H)

next_h, next_c, cache = lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)

expected_next_h = np.asarray([
    [ 0.24635157,  0.28610883,  0.32240467,  0.35525807,  0.38474904],
    [ 0.49223563,  0.55611431,  0.61507696,  0.66844003,  0.7159181 ],
    [ 0.56735664,  0.66310127,  0.74419266,  0.80889665,  0.858299  ]])
expected_next_c = np.asarray([
    [ 0.32986176,  0.39145139,  0.451556,    0.51014116,  0.56717407],
    [ 0.66382255,  0.76674007,  0.87195994,  0.97902709,  1.08751345],
    [ 0.74192008,  0.90592151,  1.07717006,  1.25120233,  1.42395676]])

print('next_h error: ', rel_error(expected_next_h, next_h))
print('next_c error: ', rel_error(expected_next_c, next_c))
next_h error:  5.7054131967097955e-09
next_c error:  5.8143123088804145e-09

LSTM: step backward

cs231n/rnn_layers.pyのlstm_step_backward関数に単一LSTMタイムステップ用バックワードパスを実装する。終わったら、下を実行して実装の数値勾配チェックをする。エラーは大体1e-6以下になるはず。

np.random.seed(231)

N, D, H = 4, 5, 6
x = np.random.randn(N, D)
prev_h = np.random.randn(N, H)
prev_c = np.random.randn(N, H)
Wx = np.random.randn(D, 4 * H)
Wh = np.random.randn(H, 4 * H)
b = np.random.randn(4 * H)

next_h, next_c, cache = lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)

dnext_h = np.random.randn(*next_h.shape)
dnext_c = np.random.randn(*next_c.shape)

fx_h = lambda x: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[0]
fh_h = lambda h: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[0]
fc_h = lambda c: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[0]
fWx_h = lambda Wx: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[0]
fWh_h = lambda Wh: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[0]
fb_h = lambda b: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[0]

fx_c = lambda x: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[1]
fh_c = lambda h: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[1]
fc_c = lambda c: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[1]
fWx_c = lambda Wx: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[1]
fWh_c = lambda Wh: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[1]
fb_c = lambda b: lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b)[1]

num_grad = eval_numerical_gradient_array

dx_num = num_grad(fx_h, x, dnext_h) + num_grad(fx_c, x, dnext_c)
dh_num = num_grad(fh_h, prev_h, dnext_h) + num_grad(fh_c, prev_h, dnext_c)
dc_num = num_grad(fc_h, prev_c, dnext_h) + num_grad(fc_c, prev_c, dnext_c)
dWx_num = num_grad(fWx_h, Wx, dnext_h) + num_grad(fWx_c, Wx, dnext_c)
dWh_num = num_grad(fWh_h, Wh, dnext_h) + num_grad(fWh_c, Wh, dnext_c)
db_num = num_grad(fb_h, b, dnext_h) + num_grad(fb_c, b, dnext_c)

dx, dh, dc, dWx, dWh, db = lstm_step_backward(dnext_h, dnext_c, cache)

print('dx error: ', rel_error(dx_num, dx))
print('dh error: ', rel_error(dh_num, dh))
print('dc error: ', rel_error(dc_num, dc))
print('dWx error: ', rel_error(dWx_num, dWx))
print('dWh error: ', rel_error(dWh_num, dWh))
print('db error: ', rel_error(db_num, db))
dx error:  6.335032254429549e-10
dh error:  3.3963774090592634e-10
dc error:  1.5221723979041107e-10
dWx error:  2.1010960934639614e-09
dWh error:  9.712296109943072e-08
db error:  2.491522041931035e-10

LSTM: forward

cs231n/rnn_layers.pyの関数lstm_forwardに、データの全時系列でLSTM forwardを実行するためにlstm_forward関数を実装する。

終わったら、実装チェックに下を走らせる。エラーは大体1e-7に収まるはず。

N, D, H, T = 2, 5, 4, 3
x = np.linspace(-0.4, 0.6, num=N*T*D).reshape(N, T, D)
h0 = np.linspace(-0.4, 0.8, num=N*H).reshape(N, H)
Wx = np.linspace(-0.2, 0.9, num=4*D*H).reshape(D, 4 * H)
Wh = np.linspace(-0.3, 0.6, num=4*H*H).reshape(H, 4 * H)
b = np.linspace(0.2, 0.7, num=4*H)

h, cache = lstm_forward(x, h0, Wx, Wh, b)

expected_h = np.asarray([
 [[ 0.01764008,  0.01823233,  0.01882671,  0.0194232 ],
  [ 0.11287491,  0.12146228,  0.13018446,  0.13902939],
  [ 0.31358768,  0.33338627,  0.35304453,  0.37250975]],
 [[ 0.45767879,  0.4761092,   0.4936887,   0.51041945],
  [ 0.6704845,   0.69350089,  0.71486014,  0.7346449 ],
  [ 0.81733511,  0.83677871,  0.85403753,  0.86935314]]])

print('h error: ', rel_error(expected_h, h))
h error:  8.610537452106624e-08

LSTM: backward

cs231n/rnn_layers.pyの関数lstm_backwardに、データの全時系列に対するLSTM用バックワードパスを実装する。実装後、下を実行して実装の数値勾配チェックをする。エラーは大体1e-7以下のはず。

from cs231n.rnn_layers import lstm_forward, lstm_backward
np.random.seed(231)

N, D, T, H = 2, 3, 10, 6

x = np.random.randn(N, T, D)
h0 = np.random.randn(N, H)
Wx = np.random.randn(D, 4 * H)
Wh = np.random.randn(H, 4 * H)
b = np.random.randn(4 * H)

out, cache = lstm_forward(x, h0, Wx, Wh, b)

dout = np.random.randn(*out.shape)

dx, dh0, dWx, dWh, db = lstm_backward(dout, cache)

fx = lambda x: lstm_forward(x, h0, Wx, Wh, b)[0]
fh0 = lambda h0: lstm_forward(x, h0, Wx, Wh, b)[0]
fWx = lambda Wx: lstm_forward(x, h0, Wx, Wh, b)[0]
fWh = lambda Wh: lstm_forward(x, h0, Wx, Wh, b)[0]
fb = lambda b: lstm_forward(x, h0, Wx, Wh, b)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
dh0_num = eval_numerical_gradient_array(fh0, h0, dout)
dWx_num = eval_numerical_gradient_array(fWx, Wx, dout)
dWh_num = eval_numerical_gradient_array(fWh, Wh, dout)
db_num = eval_numerical_gradient_array(fb, b, dout)

print('dx error: ', rel_error(dx_num, dx))
print('dh0 error: ', rel_error(dh0_num, dh0))
print('dWx error: ', rel_error(dWx_num, dWx))
print('dWh error: ', rel_error(dWh_num, dWh))
print('db error: ', rel_error(db_num, db))
dx error:  6.9939005453315376e-09
dh0 error:  1.5042746972106784e-09
dWx error:  3.2262956411424662e-09
dWh error:  2.6984652580094597e-06
db error:  8.236633698313836e-10

LSTM captioning model

LSTMを実装したので、cs231n/classifiers/rnn.pyのCaptioningRNNクラスの損失メソッド実装を、self.cell_typeがlstmであるケースに対応するために更新する。これは10行未満のコードを付け足すだけで済むはず。

終わったら、下を実行して実装をチェック。差異は1e-10未満になるはず。

N, D, W, H = 10, 20, 30, 40
word_to_idx = {'<NULL>': 0, 'cat': 2, 'dog': 3}
V = len(word_to_idx)
T = 13

model = CaptioningRNN(word_to_idx,
          input_dim=D,
          wordvec_dim=W,
          hidden_dim=H,
          cell_type='lstm',
          dtype=np.float64)

# Set all model parameters to fixed values
for k, v in model.params.items():
  model.params[k] = np.linspace(-1.4, 1.3, num=v.size).reshape(*v.shape)

features = np.linspace(-0.5, 1.7, num=N*D).reshape(N, D)
captions = (np.arange(N * T) % V).reshape(N, T)

loss, grads = model.loss(features, captions)
expected_loss = 9.82445935443

print('loss: ', loss)
print('expected loss: ', expected_loss)
print('difference: ', abs(loss - expected_loss))
loss:  9.824459354432264
expected loss:  9.82445935443
difference:  2.2648549702353193e-12

Overfit LSTM captioning model

下を実行して、前回RNN用に使用したのと同じ小データセットでLSTMキャプションモデルを過学習させる。エラーは0.5未満になるはず。

np.random.seed(231)

small_data = load_coco_data(max_train=50)

small_lstm_model = CaptioningRNN(
          cell_type='lstm',
          word_to_idx=data['word_to_idx'],
          input_dim=data['train_features'].shape[1],
          hidden_dim=512,
          wordvec_dim=256,
          dtype=np.float32,
        )

small_lstm_solver = CaptioningSolver(small_lstm_model, small_data,
           update_rule='adam',
           num_epochs=50,
           batch_size=25,
           optim_config={
             'learning_rate': 5e-3,
           },
           lr_decay=0.995,
           verbose=True, print_every=10,
         )

small_lstm_solver.train()

# Plot the training losses
plt.rcParams['figure.figsize'] = 15, 10
plt.rcParams["font.size"] = "17"
plt.plot(small_lstm_solver.loss_history)
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Training loss history')
plt.show()
(Iteration 1 / 100) loss: 79.551150
(Iteration 11 / 100) loss: 43.829095
(Iteration 21 / 100) loss: 30.062555
(Iteration 31 / 100) loss: 14.019290
(Iteration 41 / 100) loss: 5.994535
(Iteration 51 / 100) loss: 1.843790
(Iteration 61 / 100) loss: 0.656726
(Iteration 71 / 100) loss: 0.284520
(Iteration 81 / 100) loss: 0.253928
(Iteration 91 / 100) loss: 0.155888

LSTM test-time sampling

self.cell_typeがlstmであるケースに対応するために、CaptioningRNNクラスの見本メソッドを修正する。これには10行未満のコードで事が足りるはず。

終わったら下を実行して、いくつかの訓練/検証セットサンプルによる過学習LSTMモデルからサンプリングする。

for split in ['train', 'val']:
    minibatch = sample_coco_minibatch(small_data, split=split, batch_size=2)
    gt_captions, features, urls = minibatch
    gt_captions = decode_captions(gt_captions, data['idx_to_word'])

    sample_captions = small_lstm_model.sample(features)
    sample_captions = decode_captions(sample_captions, data['idx_to_word'])

    for gt_caption, sample_caption, url in zip(gt_captions, sample_captions, urls):
        plt.rcParams['figure.figsize'] = 16, 10
        plt.rcParams["font.size"] = "17"
        plt.imshow(image_from_url(url))
        plt.title('%s\n%s\nGT:%s' % (split, sample_caption, gt_caption))
        plt.axis('off')
        plt.show()
参考サイトhttps://github.com/