CS231n/課題3/RNNを使った画像説明文生成

今回は、Stanford University/CS231n/Assignment3/Image Captioning with RNNs (recurrent neural networks:再帰型ニューラルネットワーク)をやる。

このエクササイズでは、再帰型ニューラルネットワークを実装して、それらを使って画像用の新しい説明文を生成するモデルを訓練する。

# 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
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

Microsoft COCO

このエクササイズで2014年にリリースされた、画像キャプションの標準テストベッドとなっているMicrosoft COCOデータセットを使用する。このデータセットは、8万の訓練画像と4万の検証画像から成り、各画像はAmazon Mechanical Turkの人々によって書かれた5つの説明文で注釈付られている。データはこのエクササイズ用に既に前処理されている。全画像に対して、ImageNetで事前学習済みVGG-16ネットワークのfc7層から特徴を抽出しており、これらの特徴は、train2014_vgg16_fc7.h5とval2014_vgg16_fc7.h5にそれぞれ保存されている。処理時間とメモリ要件を削減するために、特徴次元は4096から512に減らした。これらの特徴は、train2014_vgg16_fc7_pca.h5とval2014_vgg16_fc7_pca.h5から入手可能。

原画像は容量がでかい(ほぼ20GB)ので今回は除外したが、全画像はFlickrから入手されており、訓練/検証画像のURLはtrain2014_urls.txtとval2014_urls.txtにそれぞれ保存されている。こうすることで画像を即座にダンロードできる。

文字列を扱うのは非効率なので、エンコード版説明文で作業する。各文字は整数列で説明文を構成するように整数値IDを割り当てられている。整数値IDと文字間マッピングはcoco2014_vocab.jsonの中にあり、整数値IDのnumpyアレイを文字列に再変換するのにはcs231n/coco_utils.pyの関数decode_captionsを利用できる。

単語集には一対の特別なトークンを付け足してある。各説明文の初めにトークンを終わりにトークンをそれぞれ追加している。レアな単語はトークンと置き換えた。さらに、異なる長さの説明文を含むミニバッチで訓練したいので、短いキャプションのトークンの後にトークンを挿入し、 tokenの損失や勾配は計算しない。それらを実装するのは大変な作業なので、これらのトークンは予め実装済み。

全MS-COCOデータ(説明文、URL、単語集)は、cs231n/coco_utils.pyのload_coco_data関数を使ってロードできる。下のセルを実行して全データをロードする。

# 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

Look at the data

cs231n/coco_utils.pyのsample_coco_minibatch関数を使って、load_coco_dataから返されたデータ構造のデータのミニバッチをサンプリングできる。下を実行して学習データの小ミニバッチをサンプリングして画像と説明文を見る。何度か実行してデータセットの内容をそれとなく理解する。

decode_captions関数を使って説明文をデコードし、画像はFlickr URLを使用してその場でダウンロードするので、画像を見るにはインターネットに接続している必要があることに留意する。

plt.rcParams['figure.figsize'] = 15, 10
plt.rcParams["font.size"] = "16"
# Sample a minibatch and show the images and captions
batch_size = 3

captions, features, urls = sample_coco_minibatch(data, batch_size=batch_size)
for i, (caption, url) in enumerate(zip(captions, urls)):
    plt.imshow(image_from_url(url))
    plt.axis('off')
    caption_str = decode_captions(caption, data['idx_to_word'])
    plt.title(caption_str)
    plt.show()

Recurrent Neural Networks

講義の中で言ったように、画像説明文付けには再帰ニューラルネットワーク(RNN)言語モデルを用いる。cs231n/rnn_layers.pyには、再帰型ニューラルネットに必要なさまざまな層型が実装されており、cs231n/classifiers/rnn.pyは、これらの層を使用して画像説明文生成モデルを実装する。

先ず、cs231n/rnn_layers.異なる種類のRNN層を実装する。

Vanilla RNN: step forward

cs231n/rnn_layers.pyを開く。このファイルは、RNNで広く使われている様々な層タイプのフォワード/バックワードパスを実装する。

最初に、function rnn_step_forwardを実装して、vanilla RNNの単一時間ステップ用のフォワードパスを実装する。終わったら、下記を実行して実装をチェク。エラーは1e-8未満になるはず。

N, D, H = 3, 10, 4

x = np.linspace(-0.4, 0.7, num=N*D).reshape(N, D)
prev_h = np.linspace(-0.2, 0.5, num=N*H).reshape(N, H)
Wx = np.linspace(-0.1, 0.9, num=D*H).reshape(D, H)
Wh = np.linspace(-0.3, 0.7, num=H*H).reshape(H, H)
b = np.linspace(-0.2, 0.4, num=H)

next_h, _ = rnn_step_forward(x, prev_h, Wx, Wh, b)
expected_next_h = np.asarray([
  [-0.58172089, -0.50182032, -0.41232771, -0.31410098],
  [ 0.66854692,  0.79562378,  0.87755553,  0.92795967],
  [ 0.97934501,  0.99144213,  0.99646691,  0.99854353]])

print('next_h error: ', rel_error(expected_next_h, next_h))
next_h error:  6.292421426471037e-09

Vanilla RNN: step backward

cs231n/rnn_layers.pyにrnn_step_backward関数を実装する。終わったら下記を実行して実装を数値的に勾配確認する。エラーは1e-8未満になるはず。

from cs231n.rnn_layers import rnn_step_forward, rnn_step_backward
np.random.seed(231)
N, D, H = 4, 5, 6
x = np.random.randn(N, D)
h = np.random.randn(N, H)
Wx = np.random.randn(D, H)
Wh = np.random.randn(H, H)
b = np.random.randn(H)

out, cache = rnn_step_forward(x, h, Wx, Wh, b)
dnext_h = np.random.randn(*out.shape)

fx = lambda x: rnn_step_forward(x, h, Wx, Wh, b)[0]
fh = lambda prev_h: rnn_step_forward(x, h, Wx, Wh, b)[0]
fWx = lambda Wx: rnn_step_forward(x, h, Wx, Wh, b)[0]
fWh = lambda Wh: rnn_step_forward(x, h, Wx, Wh, b)[0]
fb = lambda b: rnn_step_forward(x, h, Wx, Wh, b)[0]

dx_num = eval_numerical_gradient_array(fx, x, dnext_h)
dprev_h_num = eval_numerical_gradient_array(fh, h, dnext_h)
dWx_num = eval_numerical_gradient_array(fWx, Wx, dnext_h)
dWh_num = eval_numerical_gradient_array(fWh, Wh, dnext_h)
db_num = eval_numerical_gradient_array(fb, b, dnext_h)
dx, dprev_h, dWx, dWh, db = rnn_step_backward(dnext_h, cache)

print('dx error: ', rel_error(dx_num, dx))
print('dprev_h error: ', rel_error(dprev_h_num, dprev_h))
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:  4.0192769090159184e-10
dprev_h error:  2.5632975303201374e-10
dWx error:  8.820222259148609e-10
dWh error:  4.703287554560559e-10
db error:  7.30162216654e-11

Vanilla RNN: forward

vanilla RNNのsingle timestep(単一時間ステップ)用フォワード/バックワードパスの実装が終わったので、これらピースを組み合わせて、データの全数列を処理するRNNを実装する。

cs231n/rnn_layers.pyにrnn_forward関数を実装する。これは上で定義したrnn_step_forward関数を使って実装する必要がある。終わったら、下記を実行して実装を確認する。エラーは1e-7未満になるはず。

N, T, D, H = 2, 3, 4, 5

x = np.linspace(-0.1, 0.3, num=N*T*D).reshape(N, T, D)
h0 = np.linspace(-0.3, 0.1, num=N*H).reshape(N, H)
Wx = np.linspace(-0.2, 0.4, num=D*H).reshape(D, H)
Wh = np.linspace(-0.4, 0.1, num=H*H).reshape(H, H)
b = np.linspace(-0.7, 0.1, num=H)

h, _ = rnn_forward(x, h0, Wx, Wh, b)
expected_h = np.asarray([
  [
    [-0.42070749, -0.27279261, -0.11074945,  0.05740409,  0.22236251],
    [-0.39525808, -0.22554661, -0.0409454,   0.14649412,  0.32397316],
    [-0.42305111, -0.24223728, -0.04287027,  0.15997045,  0.35014525],
  ],
  [
    [-0.55857474, -0.39065825, -0.19198182,  0.02378408,  0.23735671],
    [-0.27150199, -0.07088804,  0.13562939,  0.33099728,  0.50158768],
    [-0.51014825, -0.30524429, -0.06755202,  0.17806392,  0.40333043]]])
print('h error: ', rel_error(expected_h, h))
h error:  7.728466158305164e-08

Vanilla RNN: backward

cs231n/rnn_layers.pyの関数rnn_backwardにvanilla RNN用のバックワードパスを実装する。これは、全数列に対しback-propagation(誤差逆伝播法)を実行し、上で定義したrnn_step_backward関数へコールする。エラーは5e-7未満になるはず。

np.random.seed(231)

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

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

out, cache = rnn_forward(x, h0, Wx, Wh, b)
dout = np.random.randn(*out.shape)
dx, dh0, dWx, dWh, db = rnn_backward(dout, cache)

fx = lambda x: rnn_forward(x, h0, Wx, Wh, b)[0]
fh0 = lambda h0: rnn_forward(x, h0, Wx, Wh, b)[0]
fWx = lambda Wx: rnn_forward(x, h0, Wx, Wh, b)[0]
fWh = lambda Wh: rnn_forward(x, h0, Wx, Wh, b)[0]
fb = lambda b: rnn_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:  1.5382468491701097e-09
dh0 error:  3.3839681556240896e-09
dWx error:  7.150535245339328e-09
dWh error:  1.297338408201546e-07
db error:  1.4889022954777414e-10

Word embedding: forward

深層学習システムでは、ベクトルを用いて単語を表現することが多い。語彙集の各単語はベクトルと関連付けられ、このベクトルは他のシステムと共に学習される。

cs231n/rnn_layers.pyにword_embedding_forward関数を実装して、(整数で形成されている)単語をベクトルに変換する。下記を実行して実装をチェックする。エラーは大体1e-8程度になる必要がある。

N, T, V, D = 2, 4, 5, 3

x = np.asarray([[0, 3, 1, 2], [2, 1, 0, 3]])
W = np.linspace(0, 1, num=V*D).reshape(V, D)

out, _ = word_embedding_forward(x, W)
expected_out = np.asarray([
 [[ 0.,          0.07142857,  0.14285714],
  [ 0.64285714,  0.71428571,  0.78571429],
  [ 0.21428571,  0.28571429,  0.35714286],
  [ 0.42857143,  0.5,         0.57142857]],
 [[ 0.42857143,  0.5,         0.57142857],
  [ 0.21428571,  0.28571429,  0.35714286],
  [ 0.,          0.07142857,  0.14285714],
  [ 0.64285714,  0.71428571,  0.78571429]]])

print('out error: ', rel_error(expected_out, out))
out error:  1.0000000094736443e-08

Word embedding: backward

関数word_embedding_backwardにword embedding関数用のバックワードパスを実装する。終わったら下を実行して、実装を数値的に勾配チェックする。エラーは1e-11未満。

np.random.seed(231)

N, T, V, D = 50, 3, 5, 6
x = np.random.randint(V, size=(N, T))
W = np.random.randn(V, D)

out, cache = word_embedding_forward(x, W)
dout = np.random.randn(*out.shape)
dW = word_embedding_backward(dout, cache)

f = lambda W: word_embedding_forward(x, W)[0]
dW_num = eval_numerical_gradient_array(f, W, dout)

print('dW error: ', rel_error(dW, dW_num))
dW error:  3.2774595693100364e-12

Temporal Affine layer

タイムステップ毎に、そのタイムステップでのRNN隠れベクトルを語彙集の各単語用スコアに変換するためにアフィン関数を使う。これは課題2で実装したアフィン層に非常によく似ているので、こちらでcs231n/rnn_layers.pyのtemporal_affine_forward / temporal_affine_backward関数にこの関数を実装済み。下を実行して実装の数値勾配確認をする。エラーは1e-9未満になるはず。

np.random.seed(231)

# Gradient check for temporal affine layer
N, T, D, M = 2, 3, 4, 5
x = np.random.randn(N, T, D)
w = np.random.randn(D, M)
b = np.random.randn(M)

out, cache = temporal_affine_forward(x, w, b)
dout = np.random.randn(*out.shape)

fx = lambda x: temporal_affine_forward(x, w, b)[0]
fw = lambda w: temporal_affine_forward(x, w, b)[0]
fb = lambda b: temporal_affine_forward(x, w, b)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
dw_num = eval_numerical_gradient_array(fw, w, dout)
db_num = eval_numerical_gradient_array(fb, b, dout)

dx, dw, db = temporal_affine_backward(dout, cache)

print('dx error: ', rel_error(dx_num, dx))
print('dw error: ', rel_error(dw_num, dw))
print('db error: ', rel_error(db_num, db))
dx error:  2.9215945034030545e-10
dw error:  1.5772088618663602e-10
db error:  3.252200556967514e-11

Temporal Softmax loss

RNN言語モデルではタイムステップ毎に、語彙集の各単語用スコアを生成する。各タイムステップでのground-truth(正解)単語は分かっているので、時間ステップ毎の損失と勾配を算出するのにsoftmax損失関数を使う。経時的に損失を合計し、ミニバッチに対してそれらを平均化する。

しかしながら、ミニバッチに対する演算では説明文の長さはまちまちという一つの欠点が存在するので、各説明分の終わりにトークンを付け加えて全ての説明文が同じ長さになるようにする。この tokensは、損失と勾配にカウントされたくないので、スコアと正解ラベルに加えて、損失関数は、スコアのどの要素が損失に加算されるのかを伝えるマスクアレイも受け入れる。

これは宿題1で実装したソフトマックス損失関数に酷似しているので、この損失関数はcs231n/rnn_layers.pyのtemporal_softmax_loss関数として実装済み。

下を走らせて損失のサニティーチェックと関数の数値勾配チェックをする。dxに対するエラーは1e-7未満のはず。

# Sanity check for temporal softmax loss
from cs231n.rnn_layers import temporal_softmax_loss

N, T, V = 100, 1, 10

def check_loss(N, T, V, p):
    x = 0.001 * np.random.randn(N, T, V)
    y = np.random.randint(V, size=(N, T))
    mask = np.random.rand(N, T) <= p
    print(temporal_softmax_loss(x, y, mask)[0])

check_loss(100, 1, 10, 1.0)   # Should be about 2.3
check_loss(100, 10, 10, 1.0)  # Should be about 23
check_loss(5000, 10, 10, 0.1) # Should be about 2.3

# Gradient check for temporal softmax loss
N, T, V = 7, 8, 9

x = np.random.randn(N, T, V)
y = np.random.randint(V, size=(N, T))
mask = (np.random.rand(N, T) > 0.5)

loss, dx = temporal_softmax_loss(x, y, mask, verbose=False)
dx_num = eval_numerical_gradient(lambda x: temporal_softmax_loss(x, y, mask)[0], x, verbose=False)

print('dx error: ', rel_error(dx, dx_num))
2.3027781774290146
23.025985953127226
2.2643611790293394
dx error:  2.583585303524283e-08

RNN for image captioning

必要な層の実装が済んだので、それらの層を組み合わせて画像説明文付けモデルをビルドする。cs231n/classifiers/rnn.pyを開いてCaptioningRNN classに注目する。

損失関数にモデルのフォワード/バックワードパスを実装する。今はvanialla RNNsに対してcell_type=’rnn’であるケースを実装するだけでいい。後でLSTMケースを実装することになる。終わったら、下を実行して小テストケースを使用してフォワードパスをチェックする。エラーは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='rnn',
          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(-1.5, 0.3, num=(N * D)).reshape(N, D)
captions = (np.arange(N * T) % V).reshape(N, T)

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

print('loss: ', loss)
print('expected loss: ', expected_loss)
print('difference: ', abs(loss - expected_loss))
loss:  9.832355910027387
expected loss:  9.83235591003
difference:  2.6130209107577684e-12

下のセルを実行してCaptioningRNNクラスのnumeric gradient checkingを行う。エラーは大体5e-6以下になるはず。

np.random.seed(231)

batch_size = 2
timesteps = 3
input_dim = 4
wordvec_dim = 5
hidden_dim = 6
word_to_idx = {'<NULL>': 0, 'cat': 2, 'dog': 3}
vocab_size = len(word_to_idx)

captions = np.random.randint(vocab_size, size=(batch_size, timesteps))
features = np.random.randn(batch_size, input_dim)

model = CaptioningRNN(word_to_idx,
          input_dim=input_dim,
          wordvec_dim=wordvec_dim,
          hidden_dim=hidden_dim,
          cell_type='rnn',
          dtype=np.float64,
        )

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

for param_name in sorted(grads):
    f = lambda _: model.loss(features, captions)[0]
    param_grad_num = eval_numerical_gradient(f, model.params[param_name], verbose=False, h=1e-6)
    e = rel_error(param_grad_num, grads[param_name])
    print('%s relative error: %e' % (param_name, e))
W_embed relative error: 2.331070e-09
W_proj relative error: 1.112417e-08
W_vocab relative error: 4.274379e-09
Wh relative error: 5.858117e-09
Wx relative error: 1.590657e-06
b relative error: 9.727211e-10
b_proj relative error: 1.934807e-08
b_vocab relative error: 7.087097e-11

Overfit small data

今回の課題で、過去の課題で画像分類モデルを訓練するのに使用したSolverクラスに似たaptioningSolverクラスを使用して画像キャプションシステムを訓練する。

cs231n/captioning_solver.pyのCaptioningSolverクラスに目を通す。この懐かしいAPIに慣れ親しんだら下を実行して、モデルが100標本の小サンプルを過学習するかを確認する。損失は0.1未満になる必要がある。

np.random.seed(231)

small_data = load_coco_data(max_train=50)

small_rnn_model = CaptioningRNN(
          cell_type='rnn',
          word_to_idx=data['word_to_idx'],
          input_dim=data['train_features'].shape[1],
          hidden_dim=512,
          wordvec_dim=256,
        )

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

small_rnn_solver.train()

# Plot the training losses
plt.rcParams['figure.figsize'] = 12, 8
plt.rcParams["font.size"] = "16"
plt.plot(small_rnn_solver.loss_history)
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Training loss history')
plt.show()
(Iteration 1 / 100) loss: 76.913487
(Iteration 11 / 100) loss: 21.063558
(Iteration 21 / 100) loss: 4.016274
(Iteration 31 / 100) loss: 0.566926
(Iteration 41 / 100) loss: 0.239486
(Iteration 51 / 100) loss: 0.162021
(Iteration 61 / 100) loss: 0.111554
(Iteration 71 / 100) loss: 0.097593
(Iteration 81 / 100) loss: 0.099118
(Iteration 91 / 100) loss: 0.073982

Test-time sampling

分類モデルと違い、画像説明付けモデルは、訓練時とテスト時に全く異なる振る舞いをする。訓練時には正解キャプションにアクセスできるので、タイムステップ毎にRNNへの入力として正解単語をフィードする。テスト時は、タイムステップ毎に語彙集からサンプリングし、次のタイムステップにRNNへの入力としてそのサンプルをフィードする。

cs231n/classifiers/rnn.pyにtest-time sampling用のサンプルメソッドを実装する。終わったら下のコードを実行して、訓練/検証両データによる過学習モデルからサンプリング(見本抽出)する。訓練データの見本は非常に出来が良く、検証データの見本は意味不明になるはずだ。

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_rnn_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"] = "16"
        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/