keras2cppのコードを解析していき、いくつかkeras2cppのコードがKeras2に合っていない部分を発見した。
前提条件
今回はkeras2cppの実装にある程度合わせるため、input_image_formatをchannel_first
に設定している。
これは${HOME}/.keras/keras.json
で設定して、全体で適用されるようにしている。
${HOME}/.keras/keras.json
{ "floatx": "float32", "epsilon": 1e-07, "backend": "tensorflow", "image_data_format": "channels_last" }
ターゲットにしたのは、KerasのCIFAR10モデルであるcifar10_cnn.py
だ。
モデルの概要
model.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d_1 (Conv2D) (None, 32, 32, 32) 896 _________________________________________________________________ activation_1 (Activation) (None, 32, 32, 32) 0 _________________________________________________________________ conv2d_2 (Conv2D) (None, 32, 30, 30) 9248 _________________________________________________________________ activation_2 (Activation) (None, 32, 30, 30) 0 _________________________________________________________________ max_pooling2d_1 (MaxPooling2 (None, 32, 15, 15) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 32, 15, 15) 0 _________________________________________________________________ conv2d_3 (Conv2D) (None, 64, 15, 15) 18496 _________________________________________________________________ activation_3 (Activation) (None, 64, 15, 15) 0 _________________________________________________________________ conv2d_4 (Conv2D) (None, 64, 13, 13) 36928 _________________________________________________________________ activation_4 (Activation) (None, 64, 13, 13) 0 _________________________________________________________________ max_pooling2d_2 (MaxPooling2 (None, 64, 6, 6) 0 _________________________________________________________________ dropout_2 (Dropout) (None, 64, 6, 6) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 2304) 0 _________________________________________________________________ dense_1 (Dense) (None, 512) 1180160 _________________________________________________________________ activation_5 (Activation) (None, 512) 0 _________________________________________________________________ dropout_3 (Dropout) (None, 512) 0 _________________________________________________________________ dense_2 (Dense) (None, 10) 5130 _________________________________________________________________ activation_6 (Activation) (None, 10) 0 ================================================================= Total params: 1,250,858 Trainable params: 1,250,858 Non-trainable params: 0 _________________________________________________________________
keras2cppの修正
まず、Conv2Dの実装の部分だが、フィルタを実装する際のインデックスの向きがKerasの実装と合っていない。
- keras2cpp/keras_model.cc
@@ -250,7 +255,8 @@ std::vector< std::vector<float> > keras::conv_single_depth_valid( //const float * k_data = k[k1_size-k1-1].data(); //const float * im_data = im[i-st_x+k1].data(); for(unsigned int k2 = 0; k2 < k[0].size(); ++k2) { - sum += k[k1_size-k1-1][k2_size-k2-1] * im[i-st_x+k1][j-st_y+k2]; + sum += k[k1][k2] * im[i-st_x+k1][j-st_y+k2]; } } y[i-st_x][j-st_y] = sum; @@ -263,7 +269,8 @@ std::vector< std::vector<float> > keras::conv_single_depth_valid( // with border mode = same std::vector< std::vector<float> > keras::conv_single_depth_same( std::vector< std::vector<float> > const & im, - std::vector< std::vector<float> > const & k) + std::vector< std::vector<float> > const & k, + bool debug) { size_t k1_size = k.size(), k2_size = k[0].size(); unsigned int st_x = (k1_size - 1) >> 1; @@ -285,7 +292,14 @@ std::vector< std::vector<float> > keras::conv_single_depth_same( if(j-st_y+k2 < 0) continue; if(j-st_y+k2 > max_imr) continue; - sum += k[k1_size-k1-1][k2_size-k2-1] * im[i-st_x+k1][j-st_y+k2]; + sum += k[k1][k2] * im[i-st_x+k1][j-st_y+k2]; } } y[i][j] = sum;
さらに、Flattenの実装が合っていない。入力値が3次元に対して、Flattenする順番は(y, x, ch)
なので、ループの順番を入れ替える。
- keras2cpp/keras_model.cc
diff --git a/machine_learning/tensorflow/keras/keras_model/keras_model.cc b/machine_learning/tensorflow/keras/keras_model/keras_model.cc index 6c65550..8c3707e 100644 --- a/machine_learning/tensorflow/keras/keras_model/keras_model.cc +++ b/machine_learning/tensorflow/keras/keras_model/keras_model.cc @@ -125,15 +125,17 @@ keras::DataChunk* keras::LayerFlatten::compute_output(keras::DataChunk* dc) { size_t size = im.size() * csize * rsize; keras::DataChunkFlat *out = new DataChunkFlat(size); float * y_ret = out->get_1d_rw().data(); - for(size_t i = 0, dst = 0; i < im.size(); ++i) { - for(size_t j = 0; j < csize; ++j) { - float * row = im[i][j].data(); - for(size_t k = 0; k < rsize; ++k) { - y_ret[dst++] = row[k]; + size_t dst = 0; + for(size_t j = 0; j < csize; ++j) { + for(size_t k = 0; k < rsize; ++k) { + for(size_t i = 0; i < im.size(); ++i) { + y_ret[dst++] = im[i][j][k]; } } }
というわけで実行した結果、Kerasで生成したモデルを変換してC++に読み込ませ、生成したバイナリでCIFAR10を実行すると、以下のようになった。
最後の10個の数値が、10種類の分類になっている。最も大きいのは0.379694で、3番目の分類の画像であることを示している。 正しく計算できていることが確認できた。
$ g++ -g -Wall -O0 -std=c++11 keras_model.cc example_main.cc -o run_cifar10 This is simple example with Keras neural network model loading into C++. Keras model will be used in C++ for prediction only. 3 Reading model from ./dumped.nnet Layers 18 Layer 0 Conv2D Layer 1 Activation Layer 2 Conv2D Layer 3 Activation Layer 4 MaxPooling2D Layer 5 Dropout Layer 6 Conv2D Layer 7 Activation Layer 8 Conv2D Layer 9 Activation Layer 10 MaxPooling2D Layer 11 Dropout Layer 12 Flatten Layer 13 Dense Layer 14 Activation Layer 15 Dropout Layer 16 Dense Layer 17 Activation DataChunk2D 3x32x32 DataChunkFlat values: 0.031574 0.034804 0.039662 0.379694 0.024345 0.132760 0.240743 0.016436 0.053178 0.046803
モデルデバッグ時のテクニック
このkeras2cppの解析をするにあたり、kerasの動作を解析する必要がある。 いろんなテクニックを調べて実装した。
ある入力データに対して、各レイヤの出力結果をダンプする
Kerasの重み情報付き学習モデルを使って、ある入力値での各レイヤの出力値をダンプしたいときは、Kerasの各レイヤのモデル関数の実行結果をダンプしていく。
下記の例は、学習したKerasのモデルに対して、x_test[0]
の画像を入力し、その時の各レイヤの出力情報をダンプする。
ダンプした各レイヤの計算結果は、layerNN_output.txt
として保存される。これを見ながら、keras2cppの計算結果と突き合わせて行った。
- write_layer_output.py
from keras.models import load_model from keras.utils import np_utils from keras.datasets import cifar10 import numpy as np from keras import backend as K import StringIO def dump_mdarray(fp, mdarray): if len(mdarray.shape) == 1: for elem in mdarray: fp.write("%f " % elem) fp.write("\n") else: for sub_array in mdarray: dump_mdarray(fp, sub_array) (x_train, y_train), (x_test, y_test) = cifar10.load_data() y_test = np_utils.to_categorical(y_test, 10) x_train = x_train.astype('float32') x_test = x_test.astype('float32') x_train /= 255 x_test /= 255 model = load_model('saved_models/keras_cifar10_trained_model.h5') layer_output = [np.expand_dims(x_test[0], axis=0),] layer_idx = 0 for layer in model.layers: get_layer_output = K.function([layer.input], [layer.output]) layer_output = get_layer_output(layer_output) fp = open("layer" + str(layer_idx) + "_output.txt", "w") dump_mdarray(fp, layer_output[0]) layer_idx = layer_idx + 1 fp.close()