FPGAの部屋のmarseeさんの記事を見て、TensorFlow+Kerasに入門してみた。 というかmarseeさんの記事で掲載されているソースコードをほとんどCopy & Pasteして実行してみているだけだが...
TensorFlow+KerasでCifar10を学習するサンプルプログラムを実行して、そこから得られたモデルを使ってKeras2cppでモデルの変換を行ってみた。
最終的な目標は、Keras2cppを使ってC++のコードを出力し、それをネイティブC++環境で実行することだ。
前回のCIFAR10のConv2DとConvolution2Dについて、違いがあると思っていたのだがそんなことは無いらしい。 Kerasの実装を見てもそうなっていた。
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
というわけで、${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]; } } }