FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

TensorFlow+Kerasに入門(6. keras2cppによるC++でのCIFAR10推論実行)

f:id:msyksphinz:20180701195704p:plain

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だ。

github.com

モデルの概要

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の実装と合っていない。

f:id:msyksphinz:20180705224005p:plain

  • 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
f:id:msyksphinz:20180705233234p:plain

モデルデバッグ時のテクニック

この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()