Edited at

Microsoft Cognitive Toolkit : Image Caption Part2 - Neural Image Caption System


目標

Microsoft Cognitive Toolkit (CNTK) を用いた画像キャプションの続きです。

Part2 では、Part1 で準備した訓練データを用いて CNTK による画像キャプションを行います。

CNTK と NVIDIA GPU CUDA がインストールされていることを前提としています。


導入

Microsoft Cognitive Toolkit : Image Caption Part1 - STAIR Captions では、Microsoft Common Object in Contexts (COCO) [1] と STAIR Captions [2] から画像特徴量と日本語キャプションテキストを準備しました。

Part2 では、再帰的ニューラルネットワーク (RNN) を使った画像キャプションモデルを作成して訓練します。


再帰的ニューラルネットワーク

再帰的ニューラルネットワークの構成要素として、Long Short-Term Memory (LSTM) [3] を採用します。LSTM はゲートと呼ばれる機構を用いて、RNN における長期記憶の保持と勾配問題を改善した層の1つです。

全体的な構造を下の図に示します。ニューラル画像キャプションの論文 [4] を参考に LSTM を 3層積み重ねたモデルにしました。色付きの層のパラメータを訓練します。同じ色は重みを共有していることを表しています。

nics.png

画像を CNN に入力する部分は Part1 で済ませているので、画像特徴量を全結合層に入力するところから訓練を開始します。全結合層の出力を LSTM0 に入力する段階が $t=-1$ になります。全結合層の後には Layer Normalization [5] を適用します。

一方、入力単語は Embedding 層を使って密なベクトル表現に変換してから 3層の LSTM に順に入力していき、次の単語を予測します。Embedding 層では Word2Vec [6] 同様、単語の分散表現を獲得します。

入力単語に適用する Embedding 層の直後と各 LSTM の出力に Layer Normalization を適用します。そしてすべての LSTM の隠れ層に Self-Stabilizer [7] を適用します。


訓練における諸設定

各パラメータの初期値は CNTK のデフォルト設定を使用しています。多くの場合、Glorot の初期値 [8] になっています。

次の単語を予測する分類問題なので損失関数を Cross Entropy Error として、最適化アルゴリズムには Adam [9] を採用しました。Adam のハイパーパラメータ $\beta_1$ は 0.9、$\beta_2$ は CNTK のデフォルト値に設定しました。

学習率は、Cyclical Learning Rate (CLR) [10] を使って、最大学習率は 0.01、ベース学習率は 0.0001、ステップサイズはエポック数の 10倍、方策は triangular2 に設定しました。

過学習対策として、L2 正則化の値は 0.0005 に設定しました。

モデルの訓練はミニバッチ学習によって 100 Epoch を実行しました。


実装


実行環境


ハードウェア

・CPU Intel(R) Core(TM) i7-6700K 4.00GHz

・GPU NVIDIA GeForce GTX 1060 6GB


ソフトウェア

・Windows 10 Pro 1903

・CUDA 10.0

・cuDNN 7.6

・Python 3.6.6

・cntk-gpu 2.7

・cntkx 0.1.13

・nltk 3.4.5

・pandas 0.25.0


実行するプログラム


nics_training.py

import cntk as C

import os
import pickle

from cntk.layers import Dense, Embedding, LayerNormalization, LSTM, Recurrence, RecurrenceFrom
from cntkx.learner import CyclicalLearingRate
from pandas import DataFrame

num_feature = 1024
num_word = 12212
num_hidden = 512

epoch_size = 100
minibatch_size = 1024
num_samples = 413915

sample_size = 64
step_size = int(num_samples / sample_size * 10)
weight_decay = 0.0005

def create_reader(path, train):
return C.io.MinibatchSource(C.io.CTFDeserializer(path, C.io.StreamDefs(
words=C.io.StreamDef(field="word", shape=num_word, is_sparse=True),
targets=C.io.StreamDef(field="target", shape=num_word, is_sparse=True),
features=C.io.StreamDef(field="feature", shape=num_feature, is_sparse=False))),
randomize=train, max_sweeps=C.io.INFINITELY_REPEAT if train else 1)

def neural_image_caption_system(features, words):
with C.layers.default_options(enable_self_stabilization=True):
h0 = Dense(num_hidden, bias=False)(features)
h0 = LayerNormalization()(h0)

h = Embedding(num_hidden)(words)
h = LayerNormalization()(h)

h1, c1 = Recurrence(LSTM(num_hidden), return_full_state=True)(h0).outputs
h2, c2 = Recurrence(LSTM(num_hidden), return_full_state=True)(LayerNormalization()(h1)).outputs
h3, c3 = Recurrence(LSTM(num_hidden), return_full_state=True)(LayerNormalization()(h2)).outputs

h = RecurrenceFrom(LSTM(num_hidden))(h1, c1, h)
h = LayerNormalization()(h)
h = RecurrenceFrom(LSTM(num_hidden))(h2, c2, h)
h = LayerNormalization()(h)
h = RecurrenceFrom(LSTM(num_hidden))(h3, c3, h)
h = LayerNormalization()(h)

h = Dense(num_word)(h)

return h

if __name__ == "__main__":
#
# built-in reader
#
train_reader = create_reader("./train2014_nics_captions.txt", True)

#
# features, words, and targets
#
features = C.sequence.input_variable(shape=(num_feature,), is_sparse=False, sequence_axis=C.Axis("I"))
words = C.sequence.input_variable(shape=(num_word,))
targets = C.sequence.input_variable(shape=(num_word,))

input_map = {features: train_reader.streams.features, words: train_reader.streams.words,
targets: train_reader.streams.targets}

#
# model, loss function, and error metrics
#
model = neural_image_caption_system(features, words)

loss = C.cross_entropy_with_softmax(model, targets)
errs = C.classification_error(model, targets)

#
# optimizer
#
learner = C.adam(model.parameters, lr=0.01, momentum=0.9, gradient_clipping_threshold_per_sample=sample_size,
gradient_clipping_with_truncation=True, l2_regularization_weight=weight_decay)
clr = CyclicalLearningRate(learner, base_lrs=1e-4, max_lrs=0.01, minibatch_size=sample_size, step_size=step_size)
progress_printer = C.logging.ProgressPrinter(tag="Training")

trainer = C.Trainer(model, (loss, errs), [learner], [progress_printer])

C.logging.log_number_of_parameters(model)

#
# train model
#
logging = {"epoch": [], "loss": [], "error": []}
for epoch in range(epoch_size):
sample_count = 0
epoch_loss = 0
epoch_metric = 0
while sample_count < num_samples:
data = train_reader.next_minibatch(min(minibatch_size, num_samples - sample_count), input_map=input_map)

trainer.train_minibatch(data)

clr.batch_step()

minibatch_count = data[features].num_samples
sample_count += minibatch_count
epoch_loss += trainer.previous_minibatch_loss_average * minibatch_count
epoch_metric += trainer.previous_minibatch_evaluation_average * minibatch_count

#
# loss and error logging
#
logging["epoch"].append(epoch + 1)
logging["loss"].append(epoch_loss / num_samples)
logging["error"].append(epoch_loss / num_samples)

trainer.summarize_training_progress()

#
# save model and logging
#
model.save("./nics.model")
print("Saved model.")

df = DataFrame(logging)
df.to_csv("./nics.csv", index=False)
print("Saved logging.")



解説

実行するプログラムのいくつかの箇所を抜き出して補足しておきます。CNTK の実装や訓練テクニックに関する理解の役に立てば幸いです。


CTFDeserializer

CNTK が提供するテキストファイル形式のビルトインリーダー CNTK Text Format Deserializer について捕捉します。


create_reader.py

def create_reader(path, train):

return C.io.MinibatchSource(C.io.CTFDeserializer(path, C.io.StreamDefs(
words=C.io.StreamDef(field="word", shape=num_word, is_sparse=True),
targets=C.io.StreamDef(field="target", shape=num_word, is_sparse=True),
features=C.io.StreamDef(field="feature", shape=num_feature, is_sparse=False))),
randomize=train, max_sweeps=C.io.INFINITELY_REPEAT if train else 1)

CNTK のビルトインリーダーの1つである CTFDeserializer は可変長のデータを読み込んでくれます。

今回訓練時のミニバッチサイズを 1024 に設定していますが、1024個のデータが供給されるのではなく、実際には訓練サンプル数と単語数の積が 1024 以下になる個数が供給されます。したがって訓練時の実際のミニバッチサイズはおおよそ 60~70前後になります。


Recurrence, RecurrenceFrom

CNTK で RNN を実装する際に肝となるのが、Recurrence 関数と RecurrenceFrom 関数です。

この名前のよく似た2つの関数は、初期状態が何らかの定数ならば Recurrence 関数を、初期状態に dynamic axis が含まれているならば RecurrenceFrom 関数を使うというところに違いがあります。

最初の LSTM は初期状態が 0 から始まるので Recurrence 関数を使っています。


neural_image_caption_system

h1, c1 = Recurrence(LSTM(num_hidden), return_full_state=True)(h0).outputs

h2, c2 = Recurrence(LSTM(num_hidden), return_full_state=True)(LayerNormalization()(h1)).outputs
h3, c3 = Recurrence(LSTM(num_hidden), return_full_state=True)(LayerNormalization()(h2)).outputs

入力単語を受け取って次の単語を予測する LSTM は初期状態が dynamic axis を含む Recurrence 関数の出力から始まるので RecurrenceFrom 関数を使っています。


neural_image_caption_system

h = RecurrenceFrom(LSTM(num_hidden))(h1, c1, h)

...
h = RecurrenceFrom(LSTM(num_hidden))(h2, c2, h)
...
h = RecurrenceFrom(LSTM(num_hidden))(h3, c3, h)
...

CNTK で RNN を作成する際はこの2つの関数を使い分ける必要があります。


Self-Stabilizer

あまり有名ではないですが、Self-Stabilizer はニューラルネットワークの訓練時における学習率のスケジューリングに関するテクニックの1つです。[7]

ここで、ある層の入力を $x$, 重みを $W$, バイアスを $b$, 活性化関数を $\phi$ とすると、出力を $y$ は以下の式で表せます。

y = \phi(Wx + b)

Self-Stabilizer ではスカラーパラメータ $\beta$ を1つ加えます。追加したスカラーパラメータ $\beta$ も逆伝播を使って訓練します。

y = \phi(\beta \times Wx + b)

追加されたスカラーパラメータ $\beta$ は、各層の入力が損失関数の勾配とどのように関係しているかを保持することができるようになり、入力に対して勾配が大きく改善されると $\beta$ も大きくなり、改善が小さい場合は $\beta$ も小さくなります。

CNTK の Self-Stabilizer では、負の値になることと指数関数に起因する不安定性を避けるため、パラメータ $\beta$ に softplus 関数を適用しています。


結果

訓練時の損失関数と誤認識率のログを可視化したものが下の図です。左のグラフが損失関数、右のグラフが誤認識率になっており、横軸はエポック数、縦軸はそれぞれ損失関数の値と誤認識率を表しています。

nics300x300_logging.png

これで画像キャプション生成モデルが訓練できたので検証用データで性能評価をしてみました。

今回の性能評価には Bilingual Evaluation Understudy (BLEU) [11] を算出しました。BLEU の算出には nltk を使って、NIST による平滑化を行いました。検証データとして val2014 を使用すると以下のような結果になりました。ハイフンの後の数字は n-gram を表しています。

BLEU-4 Score 25.55

BLEU-1 Score 66.17

訓練したモデルの画像キャプションの例をいくつか示します。


成功している例

nics300x300_better.png


失敗している例

nics300x300_failed.png


参考

Microsoft COCO Common Objects in Context

STAIR Captions

Microsoft Cognitive Toolkit : Image Caption Part1 - STAIR Captions


  1. Tsung-Yi Lin, Michael Maire, Serge Belongie, Lubomir Bourdev, Ross Girshick, James Hays, Pietro Perona, Deva Ramanan, C. Lawrence Zitnick, and Piotr Dollár. "Microsoft COCO: Common Objects in Context", European Conference on Computer Vision. 2014, pp 740-755.

  2. Yuya Yoshikawa, Yutaro Shigeto, and Akikazu Takeuchi. "STAIR Captions: Constructing a Large-Scale Japanese Image Caption Dataset", arXiv preprint arXiv:1705.00823 (2017).

  3. Sepp Hochreiter, and Jürgen Schmidhuber. "Long Short-Term Memory", Neural Computation. 1997, pp 1735-1780.

  4. Oriol Vinyals, Alexander Toshev, Samy Bengio, and Dumitru Erhan. "Show and Tell: A Neural Image Caption Generator", The IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2015, pp 3156-3164.

  5. Jimmy Lei Ba, Jamie Ryan Kiros, and Geoffrey E. Hinton. "Layer Normalization", arXiv preprint arXiv:1607.06450 (2016).

  6. Tomas Mikolov, Ilya Sutskever, Kai Chen, and Greg Corrado. "Distributed Representations of Words and Phrases and their Compositionality", In Advances in Neural Information Processing Systems (NIPS). 2013, pp 3111-3119.

  7. Pegah Ghahremani, and Jasha Droppo. "Self-Stabilized Deep Neural Network", 2016 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). 2016, pp 5450-5454.

  8. Xaiver Glorot and Yoshua Bengio. "Understanding the difficulty of training deep feedforward neural networks", Proceedings of the Thirteenth International Conference on Artificial Intelligence and Statistics. 2010, pp 249-256.

  9. Diederik P. Kingma and Jimmy Lei Ba. "Adam: A method for stochastic optimization", arXiv preprint arXiv:1412.6980 (2014).

  10. Leslie N. Smith. "Cyclical Learning Rates for Training Neural Networks", 2017 IEEE Winter Conference on Applications of Computer Vision. 2017, pp 464-472.

  11. Kishore Papineni, Salim Roukos, Todd Ward, and Wei-Jing Zhu. "BLEU: a Method for Automatic Evaluation of Machine Translation", Proceedings of the 40-th Annual Meeting of the Association for Computational Linguistics (ACL). 2002, pp 311-318.