今回は、Stanford University/CS231n/Assignment3/Image Captioning with RNNs (recurrent neural networks:再帰型ニューラルネットワーク)をやる。
Image Captioning with RNNs¶
# 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
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を利用できる。
単語集には一対の特別なトークンを付け足してある。各説明文の初めに
全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))
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))
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))
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))
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))
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))
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))
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))
Temporal Softmax loss¶
RNN言語モデルではタイムステップ毎に、語彙集の各単語用スコアを生成する。各タイムステップでのground-truth(正解)単語は分かっているので、時間ステップ毎の損失と勾配を算出するのにsoftmax損失関数を使う。経時的に損失を合計し、ミニバッチに対してそれらを平均化する。
しかしながら、ミニバッチに対する演算では説明文の長さはまちまちという一つの欠点が存在するので、各説明分の終わりに
これは宿題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))
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))
下のセルを実行して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))
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()
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()