WonderPlanet Tech Blog

アドバンストテクノロジー部やCTO室のメンバーを中心に最新の技術情報を発信しています。

Kerasで複数のGPUを使い、学習を高速化してみた

こんにちは。アドバンストテクノロジー部のR&Dチーム所属岩原です。
今回はKerasで複数のGPUを使う方法を書きたいと思います。

Keras 2.0.9から簡単に複数GPUを使用した高速化が可能に。

Keras2.0.9からtraining_utilsというモジュールにmulti_gpu_modelという関数が追加されました。
コレを使うと、学習を複数のGPUで行わせることが可能になります。
inputをGPUの数だけ分割することによって、並列化を実現しているようです。
keras/training_utils.py at master · keras-team/keras

では、実際に試してみましょう。

環境

  • AWS EC2(p2.8xlarge) -> GPU8本
  • Deep Learning Base AMI (Ubuntu) Version 2.0 (ami-041db87c) -> CUDAやCuDNN、Python3.5などがセットアップ済み
  • Keras 2.1.2
  • tensorflow-gpu 1.4.1
  • Python 3.5.2

今回使用するコード

Kerasの例にあるcifar10_cnn.pyを複数GPUに対応させてみたいと思います。
keras/cifar10_cnn.py at master · keras-team/keras

まずはGPU1つのみの場合はどれくらいかかったのかを以下に示します。

~~~ 省略 ~~~
Epoch 90/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7667 - acc: 0.7457 - val_loss: 0.6644 - val_acc: 0.7838
Epoch 91/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7694 - acc: 0.7457 - val_loss: 0.6627 - val_acc: 0.7803
Epoch 92/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7692 - acc: 0.7449 - val_loss: 0.7553 - val_acc: 0.7680
Epoch 93/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7721 - acc: 0.7448 - val_loss: 0.7210 - val_acc: 0.7862
Epoch 94/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7751 - acc: 0.7436 - val_loss: 0.6743 - val_acc: 0.7811
Epoch 95/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7781 - acc: 0.7412 - val_loss: 0.7047 - val_acc: 0.7725
Epoch 96/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7781 - acc: 0.7427 - val_loss: 0.6371 - val_acc: 0.7909
Epoch 97/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7720 - acc: 0.7452 - val_loss: 0.6331 - val_acc: 0.7949
Epoch 98/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7917 - acc: 0.7399 - val_loss: 0.7105 - val_acc: 0.7699
Epoch 99/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7829 - acc: 0.7414 - val_loss: 0.6481 - val_acc: 0.7859
Epoch 100/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7868 - acc: 0.7404 - val_loss: 0.6266 - val_acc: 0.8005

ということで、学習に21 sec / 1epoch かかっていることになります。
では、コレを単純に8つのGPUで実行した場合にどのような結果になるのか試してみましょう。

単純に8つのGPUで実行した場合

importの追加

まずは必要なモジュールのimportを追加します。
モデルのビルドはCPUで行う必要があるため、Tensorflowをimportします。

import tensorflow as tf # add
from keras.utils.training_utils import multi_gpu_model # add

GPU数の定数を追加とbatch_sizeの変更

GPUの数を定数として定義しておきます。今回は8つ使用するので、8を指定します。
また、batch_sizeは並列で処理を行うために元々のbatch_sizeをGPUの数だけ掛けます。

gpu_count = 8 # add

batch_size = 32 * gpu_count # modify

モデルの構築はCPUで行う

モデルの構築はOOMエラー対策のため、CPUで明示的に行う必要があるので、
tf.deviceを使用します。

with tf.device("/cpu:0"): # add
    model = Sequential()
    model.add(Conv2D(32, (3, 3), padding='same',
                     input_shape=x_train.shape[1:]))
# 以下略

複数GPU対応する

modelをコンパイルする直前に、multi_gpu_model関数を呼び出すようにします。
引数gpusにはGPUの数を指定します。1を指定すると実行時エラーになるので注意して下さい。

model = multi_gpu_model(model, gpus=gpu_count) # add

コード全体

追記変更を含めたコード全体は以下のとおりです。

'''Train a simple deep CNN on the CIFAR10 small images dataset.

It gets to 75% validation accuracy in 25 epochs, and 79% after 50 epochs.
(it's still underfitting at that point, though).
'''

from __future__ import print_function
import keras
from keras.datasets import cifar10
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
import os
import tensorflow as tf # add
from keras.utils.training_utils import multi_gpu_model # add

gpu_count = 8 # add

batch_size = 32 * gpu_count # modify
num_classes = 10
epochs = 100
data_augmentation = True
num_predictions = 20
save_dir = os.path.join(os.getcwd(), 'saved_models')
model_name = 'keras_cifar10_trained_model.h5'


# The data, shuffled and split between train and test sets:
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')

# Convert class vectors to binary class matrices.
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
with tf.device("/cpu:0"): # add
    model = Sequential()
    model.add(Conv2D(32, (3, 3), padding='same',
                     input_shape=x_train.shape[1:]))
    model.add(Activation('relu'))
    model.add(Conv2D(32, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Conv2D(64, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(64, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Flatten())
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(num_classes))
    model.add(Activation('softmax'))
model = multi_gpu_model(model, gpus=gpu_count) # add
# initiate RMSprop optimizer
opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)

# Let's train the model using RMSprop
model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

if not data_augmentation:
    print('Not using data augmentation.')
    model.fit(x_train, y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_test, y_test),
              shuffle=True)
else:
    print('Using real-time data augmentation.')
    # This will do preprocessing and realtime data augmentation:
    datagen = ImageDataGenerator(
        featurewise_center=False,  # set input mean to 0 over the dataset
        samplewise_center=False,  # set each sample mean to 0
        featurewise_std_normalization=False,  # divide inputs by std of the dataset
        samplewise_std_normalization=False,  # divide each input by its std
        zca_whitening=False,  # apply ZCA whitening
        rotation_range=0,  # randomly rotate images in the range (degrees, 0 to 180)
        width_shift_range=0.1,  # randomly shift images horizontally (fraction of total width)
        height_shift_range=0.1,  # randomly shift images vertically (fraction of total height)
        horizontal_flip=True,  # randomly flip images
        vertical_flip=False)  # randomly flip images

    # Compute quantities required for feature-wise normalization
    # (std, mean, and principal components if ZCA whitening is applied).
    datagen.fit(x_train)

    # Fit the model on the batches generated by datagen.flow().
    model.fit_generator(datagen.flow(x_train, y_train,
                                     batch_size=batch_size),
                        epochs=epochs,
                        validation_data=(x_test, y_test),
                        workers=4)

# Save model and weights
if not os.path.isdir(save_dir):
    os.makedirs(save_dir)
model_path = os.path.join(save_dir, model_name)
model.save(model_path)
print('Saved trained model at %s ' % model_path)

# Score trained model.
scores = model.evaluate(x_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

結果

~~~ 省略 ~~~

Epoch 90/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7307 - acc: 0.7453 - val_loss: 0.6331 - val_acc: 0.7831
Epoch 91/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7301 - acc: 0.7457 - val_loss: 0.6280 - val_acc: 0.7817
Epoch 92/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7324 - acc: 0.7445 - val_loss: 0.6149 - val_acc: 0.7870
Epoch 93/100
196/196 [==============================] - 18s 91ms/step - loss: 0.7239 - acc: 0.7494 - val_loss: 0.6257 - val_acc: 0.7822
Epoch 94/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7247 - acc: 0.7472 - val_loss: 0.6101 - val_acc: 0.7879
Epoch 95/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7207 - acc: 0.7482 - val_loss: 0.6233 - val_acc: 0.7860
Epoch 96/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7191 - acc: 0.7489 - val_loss: 0.6349 - val_acc: 0.7798
Epoch 97/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7111 - acc: 0.7514 - val_loss: 0.6057 - val_acc: 0.7912
Epoch 98/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7118 - acc: 0.7524 - val_loss: 0.6084 - val_acc: 0.7894
Epoch 99/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7126 - acc: 0.7523 - val_loss: 0.6026 - val_acc: 0.7887
Epoch 100/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7035 - acc: 0.7567 - val_loss: 0.6052 - val_acc: 0.7932

21秒よりは少し早くなりましたが、思ってたよりは早くなっていません。

仮説

実際に学習をしている様子を眺めているとわかるのですが、進捗が頻繁に止まります。
これはImageDataGeneratorが画像を作る処理がボトルネックになっていそうです。
画像を生成する処理が追いついていないのではないか、という仮説に基づいて、対応してみましょう。
例のコードではfit_generatorを使うようになっているので、それをfit関数を使ってImageDataGeneratorが関与しないように変更してみましょう。

fit_generatorではなくfitを使うようにした場合

data_augmentation = True

data_augmentation = False

に変えるだけでOKです。

なお、サンプル数は変わらず50000になります。

Epoch 90/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.5075 - acc: 0.8223 - val_loss: 0.6516 - val_acc: 0.7786
Epoch 91/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4984 - acc: 0.8247 - val_loss: 0.6293 - val_acc: 0.7859
Epoch 92/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4987 - acc: 0.8237 - val_loss: 0.6290 - val_acc: 0.7860
Epoch 93/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4925 - acc: 0.8277 - val_loss: 0.6383 - val_acc: 0.7849
Epoch 94/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4922 - acc: 0.8274 - val_loss: 0.6283 - val_acc: 0.7839
Epoch 95/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4892 - acc: 0.8292 - val_loss: 0.6435 - val_acc: 0.7832
Epoch 96/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4850 - acc: 0.8298 - val_loss: 0.6449 - val_acc: 0.7820
Epoch 97/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4823 - acc: 0.8325 - val_loss: 0.6250 - val_acc: 0.7878
Epoch 98/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4741 - acc: 0.8337 - val_loss: 0.6227 - val_acc: 0.7902
Epoch 99/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4692 - acc: 0.8345 - val_loss: 0.6559 - val_acc: 0.7794
Epoch 100/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4684 - acc: 0.8354 - val_loss: 0.6374 - val_acc: 0.7840

今度は6秒になりました。やはりImageDataGeneratorがネックとなっていそうです。
メモリが潤沢にある場合やサンプル数が沢山用意できる環境であればよいのですが、
大抵はそのような恵まれた環境ではないかと思います。
ImageDataGenerator自体を並列化することで対応してみたいと思います。

ImageDataGeneratorのプロセス並列化

実は、ImageDataGeneratorのflowメソッドがSequenceを継承したIteratorオブジェクトを返すので、プロセス並列化出来たりします。

ImageDataGeneratorのプロセス並列化をした場合

    model.fit_generator(datagen.flow(x_train, y_train,
                                     batch_size=batch_size),
                        epochs=epochs,
                        validation_data=(x_test, y_test),
                        workers=4)

    model.fit_generator(datagen.flow(x_train, y_train,
                                     batch_size=batch_size),
                        epochs=epochs,
                        validation_data=(x_test, y_test),
                        workers=32,
                        max_queue_size=64,
                        use_multiprocessing=True)

に変え、

data_augmentation = False

data_augmentation = True

に戻します。

結果

Epoch 90/100
196/196 [==============================] - 7s 37ms/step - loss: 0.7232 - acc: 0.7481 - val_loss: 0.6056 - val_acc: 0.7893
Epoch 91/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7233 - acc: 0.7486 - val_loss: 0.5949 - val_acc: 0.7953
Epoch 92/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7175 - acc: 0.7488 - val_loss: 0.5963 - val_acc: 0.7908
Epoch 93/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7159 - acc: 0.7502 - val_loss: 0.6006 - val_acc: 0.7905
Epoch 94/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7119 - acc: 0.7528 - val_loss: 0.6063 - val_acc: 0.7882
Epoch 95/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7115 - acc: 0.7502 - val_loss: 0.5908 - val_acc: 0.7923
Epoch 96/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7057 - acc: 0.7556 - val_loss: 0.5935 - val_acc: 0.7925
Epoch 97/100
196/196 [==============================] - 7s 36ms/step - loss: 0.6995 - acc: 0.7586 - val_loss: 0.5920 - val_acc: 0.7910
Epoch 98/100
196/196 [==============================] - 7s 36ms/step - loss: 0.6939 - acc: 0.7586 - val_loss: 0.5929 - val_acc: 0.7967
Epoch 99/100
196/196 [==============================] - 7s 37ms/step - loss: 0.6946 - acc: 0.7573 - val_loss: 0.5752 - val_acc: 0.8007
Epoch 100/100
196/196 [==============================] - 7s 37ms/step - loss: 0.6966 - acc: 0.7566 - val_loss: 0.5837 - val_acc: 0.7966

7秒まで早くなりました。並列化することでボトルネックをある程度解消できた事になります。

まとめ

複数GPUによる高速化は可能です。
ただし、fit_generatorを使う場合、generator部がボトルネックにならないように気をつける必要があります。

注意

今回提示したソースコードだと、modelのsave時にエラーになります。
原因はmulti_gpu_modelのDocに書いてあります。

# On model saving
    To save the multi-gpu model, use `.save(fname)` or `.save_weights(fname)`
    with the template model (the argument you passed to `multi_gpu_model`),
    rather than the model returned by `multi_gpu_model`.

したがって、モデルをsaveしたい時は、multi_gpu_model返すmodelではなく、
multi_gpu_model渡したmodelをsaveするようにしてください。

今回は本質と関係ない部分なので、対応は省略しました。

ファインチューニングをやってみた

こんにちは。アドバンストテクノロジー部のR&Dチーム所属岩原です。
今回はファインチューニングについて色々と調査しました。

ファインチューニング(fine tuning)とは

既存のモデルの一部を再利用して、新しいモデルを構築する手法です。
優秀な汎用モデル(VGG16など)を使い、自分たち用のモデルを構築したり出来ます。
少ないデータ(といっても数十〜数百ぐらいは必要ですが)で、結構精度の良いモデルが構築できたりします。

全く違う方向性(写真画像系のモデルを元に、イラストの判定モデルを作るなど)だと余り効果が出てこないようですが、
元のモデルより更に詳細な特徴を抽出したい、などの用途だと効果が高いようです。

転移学習(transfer learning)という呼び方もされるみたいですが、使い分けとかどんな感じなんでしょうね?

実際にやってみた

環境

  • Ubuntu16.04(GTX1080Ti)
  • Keras 2.0.8
  • Tensorflow 1.3.0
  • nvidia-docker 1.0.1

AWSのGPU Computeインスタンス(p2やp3)でも使いまわせるようにDockerizeしています。

使用するデータセット

Food-101 -- Mining Discriminative Components with Random Forests
101ラベルの料理画像を計101000枚(1ラベル1000枚)用意しているデータセットです。
枚数としては心もとない気がしますが、ファインチューニングを試してみるにはちょうどよい枚数かと思います。

3つのデータパターン

データの数によってどこまで変わるか、という検証のため、パターンを3つ用意しました。

  • データ数を訓練データ100枚、検証データ25で行うパターン(パターン1)
    データ数を少なくし、ファインチューニングの効果を検証するパターンです。

  • パターン1のデータをImageDataGeneratorで水増ししたパターン(パターン2)
    限られたデータ数を水増しし、どこまで効果が出るのかを検証するパターンです。
    fit_generatorの引数steps_per_epochを2000(batch_sizeは32なので、2000 * 32で64000枚)、引数validation_stepsを500(同じく500 * 32で16000枚)に設定しました。

  • データセットの全てのデータ(訓練データ75750枚、検証データ25250枚)で学習を行うパターン(パターン3)
    データセット全てを学習&検証に回し、水増しとの違いを検証するパターンです。
    バッチサイズは32を設定しました。

3つのモデル

ファインチューニングの方法によってどこまで差が出るのかの検証のため、さらにモデルを3つ用意しました。

  • ピュアなVGG16(モデル1)
    重みを初期化したVGG16構造のモデルです。
    ファインチューニングしない場合の検証を行うパターンになります。

f:id:m_iwahara:20171212170350p:plain

Kerasを使用したコードはこんな感じになります。

def create_none_weight_vgg16_model(size):
    model_path = "./models/vgg16_none_weight.h5py"
    if not os.path.exists(model_path):
        input_tensor = Input(shape=(224,224,3))
        model = VGG16(weights=None, include_top=True, input_tensor=input_tensor, classes=size)
        model.save(model_path) # 毎回ダウンロードすると重いので、ダウンロードしたら保存する
    else:
        model = load_model(model_path) 
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    return model
  • VGG16の全結合層のみを取り替え(モデル2)
    全結合層を取っ払い、新たに全結合層をくっつけたモデルです。
    全結合層以外の層の重みは学習済みのパラメータをそのまま使用し、学習しないようにします。
    全結合層のみファインチューニングした場合の検証を行うパターンになります。
    ついでに、全結合層にDropoutを付けてみたりしています。
    なお、最適化関数は学習率を極端に抑えたSGDを使用しています。

f:id:m_iwahara:20171212170417p:plain

Kerasを使用したコードはこんな感じになります。

def get_vgg16_model():
    model_path = "./models/vgg16.h5py"
    if not os.path.exists(model_path):
        input_tensor = Input(shape=(224,224,3))
        # 出力層側の全結合層3つをモデルから省く
        model = VGG16(weights='imagenet', include_top=False, input_tensor=input_tensor)
        model.save(model_path) # 毎回ダウンロードすると重いので、ダウンロードしたら保存する
    else:
        model = load_model(model_path)
    return model

def create_fullconnected_fine_tuning(classes):
    # vgg16モデルを作る
    vgg16_model = get_vgg16_model()

    input_tensor = Input(shape=(224,224,3))

    for layer in vgg16_model.layers:
        layer.trainable = False

    x = vgg16_model.output
    x = Flatten()(x)
    x = Dense(2048, activation='relu')(x)
    x = Dropout(0.5)(x)
    x = Dense(1024, activation='relu')(x)
    predictions = Dense(classes, activation='softmax')(x)
    model = Model(inputs=vgg16_model.input, outputs=predictions)


    model.compile(loss='categorical_crossentropy',
                  optimizer=SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])
    return model
  • VGG16の最後の畳み込み層と全結合層を取り替え(モデル3)
    最後の畳み込み層の重みを初期化して学習するようにし、全結合層を取り替えたモデルです。
    上記以外の層は再学習をしないようにします。
    最後の畳み込み層と全結合層をファインチューニングした場合の検証を行うパターンになります。

f:id:m_iwahara:20171212170432p:plain

Kerasを使用したコードはこんな感じになります。

def create_last_conv2d_fine_tuning(classes):
    # vgg16モデルを作る
    vgg16_model = get_vgg16_model()

    input_tensor = Input(shape=(224,224,3))

    x = vgg16_model.output
    x = Flatten()(x)
    x = Dense(2048, activation='relu')(x)
    x = Dropout(0.5)(x)
    x = Dense(1024, activation='relu')(x)
    predictions = Dense(classes, activation='softmax')(x)
    model = Model(inputs=vgg16_model.input, outputs=predictions)
    # 最後の畳み込み層より前の層の再学習を防止
    for layer in model.layers[:15]: 
        layer.trainable = False

    model.compile(loss='categorical_crossentropy',
                  optimizer=SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])
    return model

その他

過学習に陥る前に学習を止めるEarlyStoppingを導入済みです。

結果

上記データパターンとモデルパターンの組み合わせを検証。

パターン1

モデル1

精度

f:id:m_iwahara:20171213091424p:plain

損失

f:id:m_iwahara:20171213091443p:plain

モデル2

精度

f:id:m_iwahara:20171213091528p:plain

損失

f:id:m_iwahara:20171213091541p:plain

モデル3

精度

f:id:m_iwahara:20171213091609p:plain

損失

f:id:m_iwahara:20171213091622p:plain

パターン2

モデル1

精度

f:id:m_iwahara:20171213091648p:plain

損失

f:id:m_iwahara:20171213091700p:plain

モデル2

精度

f:id:m_iwahara:20171213091714p:plain

損失

f:id:m_iwahara:20171213091725p:plain

モデル3

精度

f:id:m_iwahara:20171213091740p:plain

損失

f:id:m_iwahara:20171213091749p:plain

パターン3

モデル1

精度

f:id:m_iwahara:20171213091803p:plain

損失

f:id:m_iwahara:20171213091813p:plain

モデル2

精度

f:id:m_iwahara:20171213091825p:plain

損失

f:id:m_iwahara:20171213091837p:plain

モデル3

精度

f:id:m_iwahara:20171213091902p:plain

損失

f:id:m_iwahara:20171213091914p:plain

結果まとめ

「データ数は多いほうが良い。ファインチューニングはゼロから学習させるよりもかなり有効で、収束も早い。最後の畳み込み層からファインチューニングした方が精度が良い。」という感じですね。
枚数が足りないのか、過学習の傾向はどれも見られますが…。
なお、1 epoch辺りの学習時間は モデル1 > モデル3 > モデル2 といった結果になりました。
ファインチューニングを行うと学習時間も節約できるのでおすすめです。

参考

VGG16のFine-tuningによる犬猫認識 (2) - 人工知能に関する断創録

nvidia-docker2を使ってみる

こんにちは。アドバンストテクノロジー部のR&Dチーム所属岩原です。
今回は、nvidia-dockerをdocker-composeから使う - WonderPlanet Tech Blogの記事が、
nvidia-dockerのversion2.0の登場によって過去のものになってしまったので、対応した記事を新たに書きました。

検証環境

  • AWS(p2.xlarge)
  • Ubuntu 16.04 LTS

CUDAやドライバー,Dockerのインストールなど

nvidia-dockerをdocker-composeから使う - WonderPlanet Tech Blogの各項目を参照してください。
バージョンなどは上がっているかと思うので、そこは最新に合わせてください。
念のため、各項目のインストールページを載せておきます。

また、前回書き忘れていたのですが、dockerグループにユーザーを所属させると、
dockerの実行にsudoが必要なくなるので、やっておくと良いかと思います。
手順は以下のとおりです。

  • 現在のユーザーをdockerグループに所属させる
sudo gpasswd -a $USER docker

docker-compose対応

add support for runtime option in service definitions by cuckoohello · Pull Request #5405 · docker/composeによると、
docker-composeの正式対応(runtime指定)は1.19ぐらいになりそうです。
したがって、それまではdocker deamonのデフォルトのコンテナランタイムをnvidia-dockerに変える必要があります。

兎にも角にも、最新のdocker-composeを入れましょう。
前回と同じ手順ですが、バージョン変わってるのでついでにコマンドを載せておきます。
現在(2017/12/13)の最新安定版は1.17のようです。

sudo curl -L https://github.com/docker/compose/releases/download/1.17.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version

でバージョン情報が出力されればOKです。

nvidia-dockerのインストール

前準備として、GPGキーの登録やリポジトリの追加などを行います。

curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/ubuntu16.04/amd64/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update

上記が正常終了したら、次はnvidia-docker2のインストールです。

sudo apt-get install nvidia-docker2

インストールが完了したら、docker daemonの設定をリロードさせます。

sudo pkill -SIGHUP dockerd

問題なければ動作確認してみましょう。

docker run --runtime=nvidia --rm nvidia/cuda nvidia-smi

nvidia-smiの結果が出力されればOKです。

出力例(AWSのp2.xlargeの場合)

Wed Dec 13 05:28:55 2017
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 387.26                 Driver Version: 387.26                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla K80           Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   50C    P0    57W / 149W |      0MiB / 11439MiB |     99%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

docker-composeで使う(v1.19.0未満の場合)

dockerdのdefault runtimeをnvidiaに設定します。
nvidia-dockerのインストール直後は未設定なので、runcと言うものDockerデフォルトのものになっています。

sudo vi /etc/docker/daemon.json

でDocker daemonの設定ファイルを開き、"default-runtime": "nvidia",を追加します。
追加後のファイルの内容は以下の通りになるかと思います。

{
    "default-runtime": "nvidia", 
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}

これで、デフォルトで使われるdockerコンテナのランタイムがruncからnvidiaに変わります。
一旦、この状態で再起動かけると設定が適用されます。

この状態で、以下をdocker-compose.ymlとして保存し、

version: '3'
services:
  nvidia:
    image: nvidia/cuda
    command: nvidia-smi

docker-compose upしてnvidia-smiの結果が出力されればOKです。

出力例(AWS のp2.xlargeの場合)

Creating network "ubuntu_default" with the default driver
Creating ubuntu_nvidia_1 ...
Creating ubuntu_nvidia_1 ... done
Attaching to ubuntu_nvidia_1
nvidia_1  | Wed Dec 13 05:13:22 2017
nvidia_1  | +-----------------------------------------------------------------------------+
nvidia_1  | | NVIDIA-SMI 387.26                 Driver Version: 387.26                    |
nvidia_1  | |-------------------------------+----------------------+----------------------+
nvidia_1  | | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
nvidia_1  | | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
nvidia_1  | |===============================+======================+======================|
nvidia_1  | |   0  Tesla K80           Off  | 00000000:00:1E.0 Off |                    0 |
nvidia_1  | | N/A   49C    P0    57W / 149W |      0MiB / 11439MiB |     99%      Default |
nvidia_1  | +-------------------------------+----------------------+----------------------+
nvidia_1  |
nvidia_1  | +-----------------------------------------------------------------------------+
nvidia_1  | | Processes:                                                       GPU Memory |
nvidia_1  | |  GPU       PID   Type   Process name                             Usage      |
nvidia_1  | |=============================================================================|
nvidia_1  | |  No running processes found                                                 |
nvidia_1  | +-----------------------------------------------------------------------------+

volumesとかdevicesとかは必要なくなりましたが、Docker daemonの設定を弄らないといけないのは面倒です。
また、コンテナごとにコンテナランタイムを変えられないのはちょっと不便ですね。
そこら辺はdocker-composeのバージョンアップを待ちましょう。

docker-composeで使う(v1.19.0以上の場合)

まだリリースされていません(2017/12/13時点では1.18rc2が最新)が、前述のプルリクエストを見る限り、
以下のようにdocker-compose.ymlにruntime: nvidiaを追記するだけです。

version: '3'
services:
  nvidia:
    image: nvidia/cuda
    runtime: nvidia
    command: nvidia-smi

簡単ですね。 Docker daemonの設定を弄る必要はありません。
コンテナごとにruntimeを変えることも可能となるでしょう。

実践

nvidia-dockerをdocker-composeから使う - WonderPlanet Tech Blogでやったのと同じ、
Kerasのサンプルの1つである、mnist + CNNを動かすDockerイメージを作ります。
詳細は元記事を参照してください。

元記事と違う点として、docker-compose.ymlからvolume周りの設定が全て消えました。

version: "3"
services:
    tensorflow_keras:
        build:
            context: ./tensorflow_keras
            dockerfile: Dockerfile

上記以外は元記事と一緒です。

結果

tensorflow_keras_1  | 2017-12-13 05:55:43.084455: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0)

きちんとGPUが認識されているようです。

複数GPUは認識されるのか

インスタンスタイプをp2.8xlarge(8GPU)に上げてやってみました。 これで複数GPUが認識されるのかどうか確かめてみます。

tensorflow_keras_1  | 2017-12-13 06:09:22.743220: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:17.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743232: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:1) -> (device: 1, name: Tesla K80, pci bus id: 0000:00:18.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743238: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:2) -> (device: 2, name: Tesla K80, pci bus id: 0000:00:19.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743243: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:3) -> (device: 3, name: Tesla K80, pci bus id: 0000:00:1a.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743252: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:4) -> (device: 4, name: Tesla K80, pci bus id: 0000:00:1b.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743257: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:5) -> (device: 5, name: Tesla K80, pci bus id: 0000:00:1c.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743262: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:6) -> (device: 6, name: Tesla K80, pci bus id: 0000:00:1d.0)
tensorflow_keras_1  | 2017-12-13 06:09:22.743270: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:7) -> (device: 7, name: Tesla K80, pci bus id: 0000:00:1e.0)

全て認識されています。

使用するGPUを指定する方法

環境変数NVIDIA_VISIBLE_DEVICESを使います。 以下のように、NVIDIA_VISIBLE_DEVICESの値として0,1,2を指定すると、 0番目、1番目、2番目のGPUを指定したことになります。 他にもallnoneなども指定できます。 詳細はNVIDIA/nvidia-container-runtime: NVIDIA container runtimeで確認してください。

version: "3"
services:
    tensorflow_keras:
        build:
            context: ./tensorflow_keras
            dockerfile: Dockerfile
        environment:
            - NVIDIA_VISIBLE_DEVICES=0,1,2

実行してみると、

tensorflow_keras_1  | 2017-12-13 06:19:44.448613: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:17.0)
tensorflow_keras_1  | 2017-12-13 06:19:44.448624: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:1) -> (device: 1, name: Tesla K80, pci bus id: 0000:00:18.0)
tensorflow_keras_1  | 2017-12-13 06:19:44.448630: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1045] Creating TensorFlow device (/gpu:2) -> (device: 2, name: Tesla K80, pci bus id: 0000:00:19.0)

のように3つ使用されているのが確認できます。

結論

docker-composeは1.19が出るまで待つと良いです。
現状では、docker-composeを使用するとnvidiaのランタイムかDockerのランタイムどちらかしか使用できません。
ただし、volumeとか作る必要はなくなったので、そこは良くなった点ですね。

参考

Azure Custom VisionのAPIで画像分類 Swift版

ようやくAmazon Echo Dotをゲットして、毎日Alexaさんと楽しくおしゃべりしているアドバンストテクノロジー部の近藤です。

今回はMicrosoft Azure Custom Vision Serviceで作成した学習モデルをAPI経由で分類したい画像を入力して、出力をJSONで受け取る方法を紹介します。

Custom Vision Serviceについて

Microsoft Azureのサービスのひとつで、画像分類をする学習モデルを作成できるサービスです。
詳細はこちらの記事で紹介しています。 tech.wonderpla.net

Custom VisionのAPIについて

上記の記事を参考にCustom Visionで学習モデルを作成すると、API経由で分類するためのURLも発行されます。

「Performance」→「Prediction URL」のメニューを開くと、画面の右側にURLが記載されています。
今回は画像のファイルデータを送信して分類するので、「If you have an image file:」の方を利用します。 赤文字で書いてある必要なヘッダを設定してリクエストします。 f:id:HidehikoKondo:20171211163833p:plain

ドキュメント

ドキュメントはこちら
<Microsoft> developer portal

Swift版

公式サイトに各種言語でサンプルコードが公開されていますが、SwiftのサンプルコードがなかったのでSwiftで作ってみました。
パンダの画像をAPI経由で分類します。分類した結果はJSONで受け取ることが出来るので、それをそのままTextViewに表示しています。
APIのURLとPrediction-Keyは皆様のアカウントで発行されたものに差し替えてください。

Storyboardには結果の表示用のUITextVIewと分類する画像を表示したUIImageViewを配置します。
「@IBAction func prediction(_ sender: Any) 」にはStoryboardで配置したUIButtonに接続します。

import UIKit

class AzureViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate  {

    @IBOutlet weak var resultTextView: UITextView!
    @IBOutlet weak var animalPhoto: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func prediction(_ sender: Any) {
        // APIのURLをここにコピペする(スクショ参照)
        var path: NSString = "●●●●●●●"
        let image: UIImage = self.animalPhoto.image!
        let pngData: Data = UIImagePNGRepresentation(image)!
        let requestParams: NSArray = ["entities=true"]
        let parameter: String = requestParams.componentsJoined(by: "&")
        path = path.appendingFormat("?%@", parameter)

        //Httpリクエストの設定
        var request: URLRequest = URLRequest.init(url: URL.init(string: path as String)!)
        request.httpMethod = "POST"
        request.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")

        // Prediction-Keyをここにコピペする(スクショ参照)
        request.addValue("●●●●●●●", forHTTPHeaderField: "Prediction-key")
        request.httpBody = pngData

        // APIに対して画像を送って、分類した結果をJSONで受け取る
        let session: URLSession = URLSession.init(configuration: URLSessionConfiguration.default)
        let task = session.dataTask(with: request, completionHandler: {
            data, response, error in

            if let data = data, let response = response{
                print(response)
                do {
                    let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
                    DispatchQueue.main.async {
                        self.resultTextView.text = String.init(describing: json)
                    }
                } catch {
                    print("Serialize Error")
                }
            } else {
                print(error ?? "Error")
            }
        })
        task.resume()
    }
}

動物の画像判別

f:id:HidehikoKondo:20171204191123p:plain Custom Visionのコンソールで、「パンダ」「クマ」「コアラ」の顔を学習させたモデルを作成しました。
これをAPI経由でパンダの画像を判別させた結果がこちらです。
「Probability(確率)」が0.38.... で、「panda」であるという結果が帰ってきたのがわかります。

f:id:HidehikoKondo:20171201201431p:plain

注意事項

APIに送信して分類できる画像のサイズは4MBまでです。 4MB超えてた画像を送信すると、エラー(Bad Request)が返ってきます。

メリットとデメリット

メリット

  • 学習モデルを作成はCustom Visionのコンソール上で完結し、学習モデルを作るための環境を自分で用意する必要がありません。
  • Custom Visionで作成した学習モデルは、Webの画面上で再学習などの調整ができます。
  • Core MLに変換した学習モデルの場合は、アプリにインポートしてアプリをビルドし直す必要がありますが、API経由であれば新しい学習モデルに即切り替えることが可能となります。
  • クロスプラットフォームで利用できます!

デメリット

  • Core MLの学習モデルはローカルのみで完結するのに対し、API経由だとネットワークを使うためレスポンスは遅くなります。

まとめ

このようにCustom VisionのAPIを利用して、簡単に画像の分類をすることができます。
API経由であればスマホアプリでもWebアプリでも利用でき、いろんなプラットフォームで活用できると思います。
これだけ手軽に機械学習の機能を利用することができるようになると、いろんなサービスがこれから沢山出てくるんではないでしょうか?
機械学習界隈がこれからどんな風に盛り上がっていくか楽しみですね。

Amazon Echo (Alexa) 用に音声対話クイズのスキルを作成してみた

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
最近はAmazon Echo、Google Home、Clova WAVEと次々に新しいスマートスピーカーが出ていますね。
私も流行りに乗ってAmazon Echo用のAlexaスキルの作成を試してみたのでご紹介します。

作成するAlexaスキル

今回はクラッシュフィーバーのクイズを出題するスキルを試作します。
予め設定した問題を出題しユーザの回答(番号)によって正解か不正解かを返します。
流れとしては以下のような形です。

  1. Alexaから問題を出題
  2. ユーザが1番〜n番までの番号で回答
  3. Alexaから「正解」or「不正解」を返す
  4. 1に戻る(最後まで出題し終えたらAlexaから正解数を発表)

それでは早速始めます。


必要な事前登録

以下への登録が必要です。
developer.amazon.com aws.amazon.com


スキルの作成

開発者ポータルからAlexaのタブを開いて「Alexa Skills Kit」を始めます。
f:id:y-matsushita:20171128174147p:plain:w700

開いたら画面右上の「新しいスキルを追加する」から新規作成します。
f:id:y-matsushita:20171128125454p:plain:w200

スキル情報を入力して保存します。
f:id:y-matsushita:20171201125957j:plain:w700
今回は以下のように設定しました。
・スキルの種類:カスタム対話モデル
・言語:Japanese
・スキル名:クラフィクイズ
・呼び出し名:クラッシュフィーバー
・グローバルフィールド:全ていいえ

対話モデルの生成

次に対話モデルを生成します。
ユーザから受けた命令がどういった意図なのかAlexaが判断できるようにするための設定です。
今回はスキルビルダーから生成します。*1
以下のボタンからスキルビルダーを起動してください。
f:id:y-matsushita:20171128125823p:plain:w250

Intentの追加

Intentsタブの「ADD+」をクリックします。
f:id:y-matsushita:20171128130008j:plain:w700

追加できるIntentはユーザが独自で定義するCustom intentと予め用意されたBuilt-in libraryの2種類があります。
今回は以下のIntentを追加します。

  • Custom intent
     - QuizIntent
     (ユーザがクイズに回答する際に呼び出し)

  • Built-in library
     - AMAZON.RepeatIntent
     (ユーザがクイズの問題を聞き直したい場合に呼び出し)
     - AMAZON.StartOverIntent
     (クイズをスタートする場合に呼び出し)

まずはCustom intentのQuizIntentから追加します。
テキストボックスに「QuizIntent」と入力しCreate intentを押下します。 f:id:y-matsushita:20171128142412p:plain:w700

次にユーザが話す想定の文章を入力します。
ここでは以下のような文章を入力しました。

  • {Answer} 番です
  • {Answer} 番
  • 正解は {Answer} 番

f:id:y-matsushita:20171128144513p:plain:w700
ユーザには回答を番号で答えてもらう想定で、情報を取得したい箇所を{Answer}としています。
注意点として{Answer}の前後には半角スペースが入っていないと後のビルド時にエラーとなってしまうようです。
編集箇所が多くなった後でエラーが発生すると、どこが該当箇所か分からなくなるので注意しましょう。
また、Intentに{Answer}が入った文章を追加するとIntentSlotsに自動でAnswerが追加されます。
Slotには{Answer}で取得できる情報の例を追加します。今回は定義済みのAMAZON.NUMBER(数値情報)がありますので、それを設定してCustom intent の設定は完了です。

あとはBuilt-in libraryのAMAZON.RepeatIntentとAMAZON.StartOverIntentも同様に追加します。
Built-in library のIntentは何も例文を入れなくてもある程度認識してくれますが、ひとまず絶対に認識してほしい文章を入れておきます。今回はAMAZON.RepeatIntentに「もう一回教えて」などのようにクイズの問題を聞き直す文章を入れ、AMAZON.StartOverIntentに「ゲームスタート」とクイズを開始する言葉を入れました。
AMAZON.RepeatIntentとAMAZON.StartOverIntentからは取得したい情報が無いためSlotは設定しません。
まとめると今回は以下のような形で登録しました。

IntentSlot
QuizIntent ・{Answer} 番です
・{Answer} 番
・正解は {Answer} 番
{Answer}
・AMAZON.NUMBER
AMAZON.RepeatIntent もう一回教えて
もう一度教えて
無し
AMAZON.StartOverIntent ゲームスタート 無し

最後まで完了すると以下のようにIntentsとSlot Typesが並びます。
f:id:y-matsushita:20171128150846j:plain:w300

ビルド

Intentの登録が完了したらモデルをセーブして「Build Model」を押下します。
f:id:y-matsushita:20171128145837p:plain:w200
問題なく完了すれば以下のような表示がされます。
f:id:y-matsushita:20171128145928p:plain:w600

ビルドで失敗してしまった場合は以下が原因でエラーが出ていることが多かったので見直してみてください。

エラー内容確認する箇所
Error building interaction model
A sample utterance for a slot in the interaction model is invalid
設定した{Answer}の前後に半角スペースがあるかどうか
Error building interaction model Bad request {Answer}にAMAZON.NUMBERのSlotTypeを追加しているかどうか

ビルドが完了したら画面右上の「Configuration」から設定画面へ戻ります。
f:id:y-matsushita:20171128151132p:plain:w100


AWS Lambda

ここから先はAWS Lambdaで編集します。
AWS LambdaでAlexaから受け取ったIntentをもとにユーザに返すべき発言をAlexaを通して返します。

Alexa用のLambda関数を作成

Lambdaの関数の作成から「alexa-skill-kit-sdk-factskill」のテンプレートを選びます。
f:id:y-matsushita:20171128151842p:plain:w700

適当な関数名とロールを入力して作成します。
f:id:y-matsushita:20171128151944p:plain:w700

トリガーにAlexa Skils Kitを追加します。
f:id:y-matsushita:20171128152022p:plain:w700
追加すると以下のようになります。
f:id:y-matsushita:20171128152220j:plain:w400

コードを書き換え

デフォルトのコードを以下に書き換え保存します。

"use strict";
const Alexa = require('alexa-sdk');
// ステートの定義
const states = {
  QUIZ: '_QUIZMODE',
  START: "_STARTMODE"
};

// クイズ内容の定義
const questions = [
  { 'q' : 'クラッシュフィーバーの略称は? 1.クラフィ、2.クラッシュ',  'a' : '1' },
  { 'q' : 'ワンダープラネットの略称は? 1.ダープラ、2.ワンプラ、3.ワンダー',  'a' : '2'},
  { 'q' : 'クラッシュフィーバーの通貨単位は次のうちどれ? 1.ゼニー、2.ゴールド、3.ビット',  'a' : '3'},
  { 'q' : '存在しないタイプはどれ? 1.魔法、2.体力、3.バランス',  'a' : '1'},
  { 'q' : '果実を使うと上がるのはどれ? 1.レベル、2.バグ、3.ステータス',  'a' : '3'},
  { 'q' : 'クエストに参加するのに必要なものは次のうちどれ? 1.スタミナ、2.ビット、3.エナジー',  'a' : '3'},
];
var languageString = {
    "ja-JP": {
        "translation": {
            "WELCOME_MESSAGE": "クラフィクイズへようこそ。 ",
            "HELP_MESSAGE": "正解だと思う番号を回答してください。",
            "START_MESSAGE": "ゲームを始める場合は「ゲームスタート」と言ってください。 ",
            "ANSWER_CORRECT_MESSAGE": "正解。 ",
            "ANSWER_WRONG_MESSAGE": "残念、不正解。 ",
            "TELL_QUESTION_MESSAGE": "第%s問。 ",
            "GAME_OVER_MESSAGE": "全ての問題が終わりました。あなたの点数は%s点でした。遊んでくれてありがとう。 ",
            "UNHANDLED_MESSAGE": "すみません、よく聞きとれませんでした。"
        }
    }
};

exports.handler = function(event, context, callback) {
  var alexa = Alexa.handler(event, context);
  alexa.resources = languageString;
  alexa.registerHandlers(handlers, startStateHandlers, quizHandlers);
  alexa.execute();
};

var handlers = {
  'LaunchRequest': function () {
    this.handler.state = states.START;
    this.emitWithState("StartGame");
  },
  "AMAZON.StartOverIntent": function() {
      this.handler.state = states.START;
      this.emitWithState("StartGame");
  },
  'AMAZON.HelpIntent': function () {
    this.emit(':ask', this.t("HELP_MESSAGE") + this.t("START_MESSAGE"));
  },
  'Unhandled': function () {
    var speechOutput = this.t("UNHANDLED_MESSAGE") + this.t("START_MESSAGE");
    this.emit(":ask", speechOutput, speechOutput);
  }
};


// ゲーム開始ステート
var startStateHandlers = Alexa.CreateStateHandler(states.START, {
    "StartGame": function () {
      this.handler.state = states.QUIZ; // クイズ回答ステートをセット
      this.attributes['advance'] = 1;   // 進行状況をセッションアトリビュートにセット
      this.attributes['correct'] = 0;   // 正解数を初期化
      var message = this.t("WELCOME_MESSAGE") + this.t("HELP_MESSAGE") + this.t("TELL_QUESTION_MESSAGE", "1") + questions[0].q;
      var reprompt = this.t("TELL_QUESTION_MESSAGE") + questions[0].q;
      this.emit(':ask', message, reprompt); // 相手の回答を待つ
      console.log(message);
    }
});


// クイズ回答ステート
var quizHandlers = Alexa.CreateStateHandler(states.QUIZ, {
  'QuizIntent': function() {

    // スロットから回答を参照
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    if(!usersAnswer){
      this.emitWithState("Unhandled");
    }

    var resultMessage;
    if(questions[this.attributes['advance']-1].a == usersAnswer){
        resultMessage = this.t("ANSWER_CORRECT_MESSAGE")     //正解
        this.attributes['correct'] ++;
    }else{
        resultMessage = this.t("ANSWER_WRONG_MESSAGE")     //不正解
    }

    if(this.attributes['advance'] < questions.length){
        // まだ問題が残っている場合
        var nextMessage = this.t("TELL_QUESTION_MESSAGE", this.attributes['advance']+1) + questions[this.attributes['advance']].q;
        this.attributes['advance'] ++;
        this.emit(':ask', resultMessage+nextMessage, nextMessage);
    }else{
        // 全ての問題が終了した場合
        var endMessage = this.t("GAME_OVER_MESSAGE", this.attributes['correct'])
        // スキルを初期状態に戻すためステートをリセット
        this.handler.state = '';
        this.attributes['STATE'] = undefined;
        this.attributes['advance'] = 0;
        this.emit(':tell', resultMessage + endMessage, endMessage);
    }
  },
  "AMAZON.RepeatIntent": function() {
    var nextMessage = this.t("TELL_QUESTION_MESSAGE", this.attributes['advance']) + questions[this.attributes['advance']-1].q;
    this.emit(':ask', nextMessage, nextMessage);
  },
  'Unhandled': function() {
    var reprompt = this.t("UNHANDLED_MESSAGE") + this.t("HELP_MESSAGE");
    this.emit(':ask', reprompt, reprompt);
  }
});

このスキルでは主にstartStateHandlersとquizHandlersの2つのステートで状態を管理しています。
まずスキル起動直後はAlexaからAMAZON.StartOverIntentが来るのを待ちます。ユーザが「ゲームスタート」と言うとAMAZON.StartOverIntentが発生するのでstartStateHandlersで初期化を行い、ステートをquizHandlersへ移します。AMAZON.StartOverIntent以外(ユーザが「ゲームスタート」と言っていない)の場合は、誘導用のメッセージを返します。

quizHandlersではユーザの発言した回答の番号がthis.event.request.intent.slots.Answer.valueで取得できるので、questionsのaと比較し正否の判定を行います。またAMAZON.RepeatIntentが呼ばれている場合は再度問題文を読み上げます。もし何にも引っかからない場合はUnhandledが呼ばれ、ヘルプ用のメッセージを表示してユーザを誘導します。
正否判定の後には問題数と進行状況を比較し、クイズを続けるか、終えるかを判断しています。クイズが続く場合はthis.emitを:askにして再度Alexaから問いかけを行い、終了する場合はthis.emitを:tellにしてクイズの正解数を読み上げて対話を終了します。

ここまででコードの保存が完了したらLambdaの画面右上のARNをコピーします。
以上でLambda側の操作は完了です。
f:id:y-matsushita:20171128152558p:plain:w700

ARNの設定

Alexaの開発者コンソールに戻り、先ほどコピーしたARNを入力します。
他にも設定項目がいくつかありますが、今回はデフォルトのままでOKです。
f:id:y-matsushita:20171128173451p:plain:w700

入力後「次へ」を押して問題なければ作成したスキルでテストが可能になります。

シミュレータでのテスト

画面をスクロールするとサービスシミュレータという項目があります。
色々入力してどういう結果が返ってくるかWEB上でテストしてみましょう。
問題なければ「ゲームスタート」と送るとクイズが始まるはずです。
f:id:y-matsushita:20171128173712p:plain:w700
何かおかしな挙動がみつかれば対話モデルやLambdaのプログラムを修正していく流れになります。
サービスリクエストに使われたJSONはそのままLambdaに渡すとLambda側のみでテストすることも可能です。

実機でのテスト

シミュレータでのテストで問題が無ければ、いよいよ実機でテストを行います。
Alexaの開発者アカウントと実機テストをする端末のアカウントが同じであれば、すでに実機テストが可能です。
また「Skills Beta Testing」を使ってテストユーザとして招待する形で実機テストも可能です。
f:id:y-matsushita:20171201141213p:plain:w200
「Skills Beta Testing」を有効化するには、「公開情報」と「プライバシーとコンプライアンス」を記入していきます。
この作業をしても申請をしなければ一般公開されないのでご安心ください。

公開情報を入力

公開情報のタブを開き、必要事項を記入していきます。
全て入力する必要がありますが、申請しないのであれば適当に入力してもテストは可能です。 f:id:y-matsushita:20171201152450p:plain:w700
今回は以下のように設定しました。
・カテゴリー:Games, Trivia & Accessories
・サブカテゴリー:Games
・テストの手順:任意の文書
・国と地域:国と地域を選択する→Japan
・スキルの簡単な説明:任意の文書
・スキルの詳細な説明:任意の文書
・サンプルフレーズ:任意の文書(ウェイクワードなどを記載)
・キーワード:任意のキーワード (省略可)
・アイコン:108x108と512x512の画像を設定

プライバシーとコンプライアンスを入力

プライバシーとコンプライアンスのタブを開き、該当する項目にチェックを入れます。
f:id:y-matsushita:20171201153810p:plain:w700

「公開情報」と「プライバシーとコンプライアンス」を入力して問題なければ、
以下のボタンが押せるようになり、実機でのベータテストが可能になります。
f:id:y-matsushita:20171201154801p:plain:w200

テストユーザを登録

スキルを動かしたいユーザのAlexaアカウントのメールアドレスを入力します。
f:id:y-matsushita:20171201160501j:plain:w700

登録したメールアドレスの受信ボックスを確認すると以下のような文面のメールが確認できます。
f:id:y-matsushita:20171201160928j:plain:w500
リンクが2つあるのが確認できますが、下の「JP customers」のリンクをクリックしてください。
Alexaのスキル管理画面は英語版の「.com」と日本語版の「.co.jp」があります。
上のリンクをクリックすると英語版として認識されるため日本語のAlexaスキルが実行できない恐れがあります。

リンク先ではスキルの設定ができるので「有効にする」をクリックしましょう。
f:id:y-matsushita:20171201161639p:plain:w300

有効化すると遂にスキル一覧に作成したスキルが出てきます!
f:id:y-matsushita:20171201162043p:plain:w700

Alexaに呼びかけてみよう!

以上の手順を終えて呼びかけると、他の公開されているスキルと同様に反応してくれるはずです。 *2
手順が長くなりましたが、なんとかAlexaに独自のスキルを追加することができました。
実機のAlexaに話しかけて反応を楽しみましょう!
f:id:y-matsushita:20171201124024j:plain:w700

参考

*1:2017/11/27時点でβ版のため、今後のアップデートで設定内容が大きく変わる恐れがあります。

*2:Alexaに「すみません、なんだかうまくいかないみたいです。」と言われてしまう場合は、ウェイクワードを変えてみましょう。
ウェイクワードによっては起動しづらいことがあるようです。今回作成したスキルもウェイクワードを「クラフィクイズ」にしていた際、認識が上手くされないことがありました。