FPGA開発日記

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

TensorFlow+Kerasに入門(5. keras2cppによる推論の試行→失敗の解析)

f:id:msyksphinz:20180701195704p:plain

FPGAの部屋のmarseeさんの記事を見て、TensorFlow+Kerasに入門してみた。 というかmarseeさんの記事で掲載されているソースコードをほとんどCopy & Pasteして実行してみているだけだが...

TensorFlow+KerasでCifar10を学習するサンプルプログラムを実行して、そこから得られたモデルを使ってKeras2cppでモデルの変換を行ってみた。

最終的な目標は、Keras2cppを使ってC++のコードを出力し、それをネイティブC++環境で実行することだ。

前回のCIFAR10のConv2DとConvolution2Dについて、違いがあると思っていたのだがそんなことは無いらしい。 Kerasの実装を見てもそうなっていた。

github.com

Convolution1D = Conv1D
Convolution2D = Conv2D
Convolution3D = Conv3D

ということは、keras2cppが正しくモデルをロードできていないことになる。 まず、本当にKerasが正しいモデルを出力できているか確認するために、再度cifar10_cnn.pyを確認し、モデルをロードし直して正しく動作するかを確認してみた。

  • cifar10_cnn.py (一部抜粋)
...
# The data, 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')
...
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

なるほど、トレーニングデータとテストデータに関しては、正規化をして最小値0.0、最大値1.0になるように調整が行われている。 Evalutationすることもできるので確認してみる。

  • load_cifar10_and_run.py
from keras.models import load_model
from keras.utils import np_utils
from keras.datasets import cifar10
import numpy as np

model = load_model('saved_models/keras_cifar10_trained_model.h5')

(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

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

model.summary()
print (model.layers[0].get_weights()[0].shape)
print (model.layers[0].get_weights()[1].shape)

実行結果は以下のようになる。なるほど、正しく動作しているように見える。

_________________________________________________________________
1/1 [==============================] - 0s 68ms/step
[[0.0045613  0.00613373 0.01441358 0.70682645 0.00228056 0.18099114
  0.0682691  0.00159967 0.01358497 0.00133945]]
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
1/1 [==============================] - 0s 4ms/step
[[2.8684381e-05 2.5516627e-03 3.8286013e-10 6.8825792e-11 4.9396240e-13
  2.2506797e-14 2.8216110e-11 6.6930409e-14 9.9741900e-01 5.9493942e-07]]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
1/1 [==============================] - 0s 3ms/step
[[3.6343671e-03 1.3918772e-03 2.3248680e-07 2.6948282e-08 5.4430562e-09
  3.0102656e-10 2.0767525e-09 2.4880802e-09 9.9492198e-01 5.1519131e-05]]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
1/1 [==============================] - 0s 2ms/step
[[1.4720474e-01 3.0774474e-02 1.9176827e-03 2.3141543e-04 6.2325860e-05
  4.9663004e-06 2.8075752e-05 3.2286775e-05 8.1751990e-01 2.2241862e-03]]
[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
1/1 [==============================] - 0s 9ms/step
[[5.8011246e-13 2.9352740e-12 1.1264588e-05 4.1041901e-07 1.9398519e-06
  3.1077538e-10 9.9998653e-01 2.1929313e-13 7.8279492e-12 2.1002187e-13]]
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
1/1 [==============================] - 0s 6ms/step
[[4.3796723e-07 4.0958039e-07 1.4267879e-04 1.9871062e-03 4.9955817e-04
  2.7933982e-04 9.9708611e-01 3.4963859e-06 3.0437428e-07 5.1951002e-07]]
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
1/1 [==============================] - 0s 6ms/step
[[9.5340777e-03 6.8315786e-01 1.0422353e-03 6.7459568e-03 2.3364674e-04
  7.3021576e-03 1.4611247e-02 6.7516724e-03 5.7323242e-04 2.7004796e-01]]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
1/1 [==============================] - 0s 5ms/step
[[2.0090256e-04 7.4372560e-06 1.6379679e-02 3.8548892e-03 8.8955453e-03
  5.3594634e-04 9.7006273e-01 2.2731041e-05 3.1498908e-05 8.5860156e-06]]
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
1/1 [==============================] - 0s 2ms/step
[[2.5151833e-03 1.5344517e-04 7.6048978e-02 4.9335814e-01 8.1626080e-02
  6.4011998e-02 2.7596030e-01 4.6143876e-03 1.3146200e-03 3.9684062e-04]]
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
1/1 [==============================] - 0s 3ms/step
[[8.5118888e-03 9.6004045e-01 5.3663384e-03 5.3788273e-04 4.1483255e-04
  1.8853426e-04 3.8111003e-03 2.3319628e-04 5.7239388e-03 1.5171825e-02]]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]

このモデルデータを変換してkeras2cppで読み込んでも、どうしても計算が合わずに失敗してしまう。 重みデータの形が違うのかと思い、いろいろ修正してみたが駄目であった。

という試行錯誤を繰り返しているうちに、どうもConv2Dの生成する重みの行列の順番が違うのではないかという結論に達した。

どうもKerasが吐き出す重みは、以下のような順番になっている。

  • (バッチ数, 横幅, 縦幅, チャネル数) ところが、keras2cppは以下のような重みのフォーマットを想定して計算式が書いてあるように見える。
  • (バッチ数, チャネル数, 横幅, 縦幅)

いろいろと調べると、Kerasの設定によっては、下側重みの並びになるようにKerasを調整することができるらしい。keras2cppが公開されたときは、このオプションが存在しなかったのかな。

Convolutionalレイヤー - Keras Documentation

f:id:msyksphinz:20180704020604p:plain

というわけで、${HOME}/.keras/keras.jsonを書き換えて再度学習のやり直しだ...一日損した...

  • ${HOME}/.keras/keras.json
{
    "epsilon": 1e-07,
    "floatx": "float32",
    "image_data_format": "channels_first",
    "backend": "tensorflow"
}

ちなみに、なぜkeras2cppが(バッチ数, チャネル数, 横幅, 縦幅)しか受け入れないと思ったかというと、内部の計算ループがそのような形にしかなっていなかったから。 forループの順番が、カーネルサイズ、チャネルサイズ、横幅、縦幅っておかしいなあ?と思った次第だった。

  • keras_model.cc
keras::DataChunk* keras::LayerConv2D::compute_output(keras::DataChunk* dc) {
...
  for(unsigned int j = 0; j < m_kernels.size(); ++j) { // loop over kernels
    for(unsigned int m = 0; m < im.size(); ++m) { // loope over image depth

      vector<vector<float> > tmp_w = (m_border_mode == "valid")?
                        keras::conv_single_depth_valid(im[m], m_kernels[j][m]) :
                        keras::conv_single_depth_same(im[m], m_kernels[j][m]);

      for(unsigned int x = 0; x < tmp_w.size(); ++x) {
        for(unsigned int y = 0; y < tmp_w[0].size(); ++y) {
          y_ret[j][x][y] += tmp_w[x][y];
        }
      }
    }
f:id:msyksphinz:20180704021141p:plain