post Image
[Python]KerasをTensorFlowから,TensorFlowをc++から叩いて実行速度を上げる


はじめに

深層学習は学習自体に時間がかかるのはもちろんのこと,訓練済み学習モデルを走らせる際にもそれなりに時間がかかります.しかし,SSD(Single Shot MultiBox Detector)等でリアルタイム物体認識をさせたい!とか,DQN(DeepQNetwork)等で強化学習したAIと人とでリアルタイムアクション対戦ゲームをさせたいといった際には,モデル実行のリアルタイム性がかなり重要になってきます.

いいPC買え!といえばそれまでですがそんなお金もないですし,ノートPC等でポータブルに実行したいということもあるでしょう.そこで今回はKeras(TensorFlow)をいかに高速に実行するかについて考えていきます.


実行環境

  • Python3.5.2
  • Keras 1.2.1
  • tensorflow 1.0.0
  • MacBookPro(Late 2013)


高速化

ではやっていきましょう.今回は極力わかりやすく手軽に試せる例としてMNISTサンプルのビギナーとエキスパートを試していきます.今回作成したコードはgithubに置いておきます.


大前提

本当に身も蓋もない話ですが,良いグラボを積める方,AWSに課金できる方,実行環境として特に制約がない方は,今すぐTitanX刺してメモリ盛り盛りPCを買ってください.TensorFlowのCPU,GPU速度比較は様々なサイト様がやられていますが,例えばこの記事(CPU/GPU/AWSでのTensorflow実行速度比較)では,CPU,GPUで数十倍近い差が出ています.

今回の記事で頑張って高速化しても,速くなって元の2~5倍程度ですので,もとからその手段がとれる方はそちらのほうが絶対良いです.そんな手段がとれないから困ってるんだ!とか,もうやってるけどもっと速くしたい!という方は続きを見ていきましょう.


 スタート

まずビギナー版MNISTサンプルで確かめていきましょう.Kerasで何も考えずに実装すると,

# モデル作成

model = Sequential()
model.add(InputLayer(input_shape=input_shape, name='input'))
model.add(Dense(nb_classes))
model.add(Activation('softmax', name='softmax'))

optimizer = SGD(lr=0.5)
model.compile(loss='categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
...
# モデル訓練
model.fit(X_train, Y_train, batch_size=batch_size, nb_epoch=nb_epoch,
verbose=1, validation_data=(X_test, Y_test))
...
# モデル評価
score = model.evaluate(X_test, Y_test, verbose=0)
...
# モデル実行
model.predict(np.array([x])

上記のような形になると思います.なおevaluate等で一気にモデルを実行する方法は,リアルタイム実行時のように逐次新しいデータが入ってくる状況と異なるので,今回は

# X_test は10000個の1チャンネル784次元データ

start = time.perf_counter()
n_loop = 5
for n in range(n_loop):
predictions = [model.predict(np.array([x])) for x in X_test]
print('elapsed time for {} prediction {} [msec]'.format(len(X_test), (time.perf_counter()-start) * 1000 / n_loop))

のようにpredictionを10000回回し,それを5週させて平均経過時間をとることで,モデル実行速度を計測します(ミリ秒精度の計測を行う為time.time()ではなくtime.pref_counter()を利用しています).

ちなみに上記の結果は

elapsed time for 10000 prediction 3768.8394089927897 [msec]

でした.


1. K.functionを使ってバックエンドから実行

from keras import backend as K

pred = K.function([model.input], [model.output])
for n in range(n_loop):
predictions = [pred([np.array([x])]) for x in X_test]

Kerasはfrom keras import backend as Kのようにバックエンドを呼び出すことができます公式ドキュメントにも記載されていますが,K.functionを用いることで,Keras関数のインスタンスを作成できます.こちらからモデルを実行することで,Kerasをそのままたたくよりも若干実行速度を上げることができます.今回の場合では

elapsed time for 10000 prediction 3210.0291186012328 [msec]

のようになりました.


2. TensorFlowで実装

そもそもKerasとTensorFlowでは同じモデルを組んだとしても実行速度,学習速度ともにだいぶ差が出てしまいます.

# モデル作成

x = tf.placeholder(tf.float32, [None, imageDim], name="input")
W = tf.Variable(tf.zeros([imageDim, outputDim]), dtype=tf.float32, name="Weight")
b = tf.Variable(tf.zeros([outputDim]), dtype=tf.float32, name="bias")
y = tf.nn.softmax(tf.matmul(x, W)+b, name="softmax")

# 目的関数設定
cross_entropy = tf.reduce_mean(
 -tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1])
)

# オプティマイザ設定
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

# モデル訓練
sess.run(tf.global_variables_initializer())
for i in range(1000):
batch_xs, batch_ys = tfmnist.train.next_batch(100)
sess.run(train_step,feed_dict={x: batch_xs, y_: batch_ys})

# モデル評価
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
result = sess.run(
accuracy,
feed_dict={x: tfmnist.test.images, y_:tfmnist.test.labels}
)

# モデル実行
sess.run(y, feed_dict={x: np.array([test_x])})

Kerasに比べ細かい設定ができるのはTensorFlowの利点ですが,それでもモデルを書く部分はどうしても煩雑になってしまいます.ただ,TensorFlowで作成したモデルを実行した結果

elapsed time for 10000 prediction 2662.211540598946 [msec]

となり,Keras実装と比べてだいぶ速度が向上していることがわかります.


3. KerasモデルをTensorFlowから叩いて実行

Kerasユーザは涙を飲んでTensorFlowに移行しなければいけないのかというとそういうわけではなく,

モデル作成のみをKerasで行い,残りの部分(trainingやprediction等)はTensorFlowから実行するという方法で実行速度を向上させることができます.

import keras.backend.tensorflow_backend as KTF

import tensorflow as tf

old_session = KTF.get_session()
sess = tf.Session()
KTF.set_session(sess)

# モデル作成
model = Sequential()
model.add(InputLayer(input_shape=input_shape, name='input'))
model.add(Dense(nb_classes))
model.add(Activation('softmax', name='softmax'))

x = tf.placeholder(tf.float32, [None, imageDim], name="input")
y = model(x)
y_ = tf.placeholder(tf.float32, [None, nb_classes])

# 目的関数,オプティマイザ作成,訓練評価実行は上記と同様の為省略

KTF.set_session(old_session)

入力placeholderxを作成し,それをモデルに代入することで,出力yを得ます.あとはTensorFlowの実装方法に従って,目的関数やオプティマイザを設定し,訓練を回します.

この方法だと,実行結果は

elapsed time for 10000 prediction 2685.7926497992594 [msec]

のようになり,モデル部分はKeras実装であっても,実行速度はかなりTensorFlow実装に近いものになりました.


4. Keras,TensorFlowで作成したモデルをC++から実行

Pythonから叩いて簡単に実行速度をあげられる量はせいぜい上記の程度で(PyPy等使えばもっと速くなるかもしれません),これ以上速くしようと思うとC++からモデルを実行する必要があります.TensorFlowには,学習済みモデルをアプリケーションに利用するためのAPIである,TensorFlow Servingを公開しています.このAPIにより,C++側でTensorFlowのモデルをロードすることで,高速にモデルを実行することができます.

Linuxユーザであれば,チュートリアルに従っていけば問題なく動かせるのですが,OSXではまだ動かすのが大変で(私も環境構築に失敗しているので今回は詳細は書けませんでした…),githubにもOSX向けのIssueが数多く立っている状況です.なので今回はServingは利用せずに,直接TensorFlow c++を叩きます.Servingが使えるようになっても,KerasのモデルをC++から叩きたい場合等には参考になると思います.


4.1. 下準備

TensorFlowのディレクトリを直接操作するため,pyenv以下等に配置されているtensorflowフォルダに対し,操作しやすい場所からリンクを貼っておきましょう.pipでインストールしたディレクトリを汚したくない方は,githubから対応バージョンをcloneしてきます.

使用するtensorflowのルートディレクトリから./configureを実行します.使用するコンパイラの指定や,デフォルトオプションの設定等を聞かれますが基本的にはデフォルト指定かyesで問題ありません.ただ,GPUを積まれていない方は,OpenCLやCUDAを有効にするかどうかの質問にはNを回答するようにしましょう.

TensorFlowのコンパイルには,Googleがもともと社内で使用していたビルドツールをオープンソース化したBazelが必要です.インストールはここを参考にしつつ進めます.

OSXであればbrew install bazel & brew upgrade bazelで一発で入れることが可能です.


4.2. graphのエクスポート

C++から読み込める形でモデルデータをエクスポートします.

sess = tf.Session()

#Kerasの場合
import keras.backend.tensorflow_backend as KTF
KTF.set_session(sess)

...
saver = tf.train.Saver()
saver.save(sess, "models/" + "model.ckpt")
tf.train.write_graph(sess.graph.as_graph_def(), "models/", "graph.pb")


4.3. モデルのfreeze化

学習させる必要のないモデルに対しては,重みを固定化することができます.公式ドキュメントには,

What this does is load the GraphDef, pull in the values for all the variables from the latest checkpoint file, and then replace each Variable op with a Const that has the numerical data for the weights stored in its attributes It then strips away all the extraneous nodes that aren’t used for forward inference, and saves out the resulting GraphDef into an output file.

とあり,どうやらパラメータ変数をConstにし,実行に不要なnodeを削除することで,モデルデータサイズを削減できるようです(パラメータ変数をConstにしているので若干アクセス速度も改善するのでしょうか?).

freezeするにはfreeze_graph.pyを用います.tensorflowのrootディレクトリに行き,

bazel build tensorflow/python/tools:freeze_graph && \

bazel-bin/tensorflow/python/tools/freeze_graph \
--input_graph=/path/to/graph.pb \
--input_checkpoint=/path/to/model.ckpt \
--output_graph=/path/to/output/frozen_graph.pb --output_node_names=softmax

と叩くことでoutput_graphで指定したパスに固定化されたgraphが生成されます(初回は諸々のコンパイルが走るのでかなり時間がかかります).

なお,output_node_namesを指定しないと怒られますが,これはTensorFlowでは.

y = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2)+b_fc2, name="softmax")

のように名前をつけることで設定できます.ただしKerasでは,

model.add(Activation('softmax', name='softmax'))

のように指定しても,内部で別名を持ってしまうので,エラーがはかれてしまいます.

この場合

[print(n.name) for n in sess.graph.as_graph_def().node]

のようにnodeの名前を直接printして,内部で設定されている名前を確認してください(手元の環境ではSoftmaxになっていました).


4.4. C++から実行

モデルを読み込み,実行するcppコードを書いていきます.詳しくはgithubの方をご覧いただくとして,主要部分を抜粋して紹介していきます.まず,tensorflow_ROOT/tensorflowの下に今回用のディレクトリを作成(今回はloadgraph)し,そのなかにccファイルを作成していきます(tensorflow_ROOT/tensorlow/loadgraph/mnist_tf.cc).

  GraphDef graph_def;

status = ReadBinaryProto(Env::Default(), graph_file_name, &graph_def);
if (!status.ok()) {
cout << status.ToString() << "\n";
return 1;
}
cout << "loaded graph" << "\n";
// Add the graph to the session
status = session->Create(graph_def);
if (!status.ok()) {
cout << status.ToString() << "\n";
return 1;
}

まず,graphデータを読み込み,sessionを起動します.

  Tensor x(DT_FLOAT, TensorShape({nTests, imageDim}));

MNIST mnist = MNIST("./MNIST_data/");
auto dst = x.flat<float>().data();
for (int i = 0; i < nTests; i++) {
auto img = mnist.testData.at(i).pixelData;
std::copy_n(img.begin(), imageDim, dst);
dst += imageDim;
}

const char* input_name = "input";
vector<pair<string, Tensor>> inputs = {
{input_name, x}
};

次に,入力テンソルxを作成し,MNISTのテストデータを流し込んでいきます.mnist.testDataには,10000件の768次元floatベクトルが格納されているので,それを順次xに登録していきます.そして,python側で作成した名前と,テンソルのペアを作成します.この名前は,

# TensorFlow

x = tf.placeholder(tf.float32, [None, imageDim], name="input")

# Keras
InputLayer(input_shape=input_shape, name='input')

のようにつけたpython側でつけた名前と対応を取る必要があります.出力側も同様にTensorのvectorを作成し,出力名(今回の場合はsoftmax)と出力テンソル,先ほど作成したInput vectorをsessionに登録して走らせます.

  vector<Tensor> outputs;

// Run the session, evaluating our "softmax" operation from the graph
status = session->Run(inputs, {output_name}, {}, &outputs);
if (!status.ok()) {
cout << status.ToString() << "\n";
return 1;
}else{
cout << "Success run graph !! " << "\n";
}

モデルの実行が成功すれば,outputsには出力値が入っているはずなので,

  int nHits = 0;

for (vector<Tensor>::iterator it = outputs.begin() ; it != outputs.end(); ++it) { // ループを回しているが今回はoutputsは一つなので item = outputs.front()と同義
auto items = it->shaped<float, 2>({nTests, 10}); // 10個の数字の分類結果10次元 x テストデータ10000個
for(int i = 0 ; i < nTests ; i++){
int arg_max = 0;
float val_max = items(i, 0);
for (int j = 0; j < 10; j++) {
if (items(i, j) > val_max) {
arg_max = j;
val_max = items(i, j);
}
} // 10次元ベクトルの内最大値のindexを算出
if (arg_max == mnist.testData.at(i).label) {
nHits++;
}
}
}
float accuracy = (float)nHits/nTests;

のように教師データと実行結果を比較してaccuracyを算出します.

最後に,cppファイルと同じ階層にBUILDファイル(依存関係等を記載するファイル.makeファイルのようなものです)を作成し,

cc_binary(

name = "mnistpredict_tf",
srcs = ["mnist_tf.cc", "MNIST.h"],
deps = [
"//tensorflow/core:tensorflow",
],
)

cc_binary(
name = "mnistpredict_keras",
srcs = ["mnist_keras.cc", "MNIST.h"],
deps = [
"//tensorflow/core:tensorflow",
],
)

ビルドをかけます.

bazel build -c opt --copt=-mavx --copt=-mavx2 --copt=-msse4.2 --copt=-msse4.1 --copt=-msse3 --copt=-mfma :mnistpredict_tf

bazel build -c opt --copt=-mavx --copt=-mavx2 --copt=-msse4.2 --copt=-msse4.1 --copt=-msse3 --copt=-mfma :mnistpredict_keras

いろいろオプションをつけていますが,必須というわけではなく,

The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.

The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
...

等いろいろ怒られてしまっていたので今回はつけています.

コンパイルが成功すると,tensorflow_ROOT/bazel-bin/tensorflow/loadgraphの中にBUILDで名前を指定した実行ファイルができているので,

cd tensorflow_ROOT/bazel-bin/tensorflow/loadgraph

./mnistpredict_tf

その階層まで行き,MNIST_TESTフォルダと固定化したgraphデータを持ってきて実行します(MNIST_TESTフォルダの中身は展開する必要があります).


結果比較

それでは,これまで紹介したパターンの結果を全てまとめてみます.”768次元(28×28)のピクセルデータが1枚入力された際に,それがどの数字かの判別結果を返す”という予測工程を1000回繰り返した際にかかる時間(msec)です(それぞれ5回回して平均を取っています).

msec

Keras

Keras(K.function)

Keras(tf)

TensorFlow

Python

3787

3242

2711

2588

C++

578



577

576

そもそもPythonとC++ではループ処理性能が圧倒的に違うので仕方ないですが,単純な比較ではやはりC++の圧勝です.Python実装の中では素のTensorFlowが一番速く,次点でモデルKeras-実行tfタイプが速いという結果になりました.素のKerasはそれらと比較するとかなり遅いですが,K.functionから実行することで若干の速度改善がみられます.


MNISTエキスパート編をためす

Convolution層を使用するエキスパート編でも速度比較をしてみます.モデルは以下を用います.

model.png

ほぼビギナー編と同じなので大まかには省略しますが,気をつける点が1点あります.


注意点:Dropout層がある場合のlearning_phaseの扱い

Keras単体で使っている場合には問題ないのですが,K.functionを使ったり,KerasとTensorFlowを併用する場合に気をつける点としてlearning_phaseの扱いが挙げられます. Dropout層がある場合等,トレーニングとテスト/実行で使用モデルが異なる際,learning_phaseフラグを指定する必要があります.learning_phaseフラグはトレーニング時1,実行時0を指定します.


python側

inputにK.learning_phase()を指定する必要があり,実行時には0を入力します.

# K.functionを使用する場合

pred = K.function([model.input, K.learning_phase()], [model.output])
[pred([np.array([x]), 0]) for x in X_test]

# TensorFlowからKerasモデルを利用する場合
[sess.run(y, feed_dict={x: np.array([test_x]), K.learning_phase(): 0}) for test_x in X_test]


c++側

Bool型のテンソルを作成して0を代入し,keras_learning_phaseという名前でinputに登録しています.

cpp

Tensor lp(DT_BOOL, TensorShape({}));

lp.flat<bool>().setZero();

...

vector<pair<string, Tensor>> inputs = {

{input_name, x}, {"keras_learning_phase", lp}

};


結果比較

ビギナー編と同様に結果比較を行います.

msec

Keras

Keras(K.function)

Keras(tf)

TensorFlow

Python

9693

9087

8571

8124

C++

5528



5530

5512

正直PythonとC++でもっと性能差が出るかなと思ったんですが,あまり違いはありませんでした.Python側を比較するとビギナー編とほぼ同じ序列になっています.


おまけ. OpenBLAS,MKLを使って行列ベクトル計算高速化

今回のテーマとは直接は関係ないのでおまけにしましたが,行列やベクトルに関する基本的な演算の仕様を定めたBLAS (Basic Linear Algebra Subprograms) の選び方によって計算スピードが上がります.


  • Reference BLAS: リファレンス実装.遅い.多くの場合デフォルトで入っているのはおそらくこれ.

  • OpenBLAS: 高速なオープンソース実装.

  • ATLAS: 自動チューニングなオープンソース実装.

  • Intel MKL: Intelによる爆速実装.最近無料になった.

OpenBLASの利用法は以前記事を書いたのでよろしければそちらをご参照ください.

IntelMKLについては,

Linux編:mkl numpyのインストール方法

OSX編:macでmkl&numpyを構築した

等,非常にわかりやすい解説記事をあげてくださっているので,そちらをご覧いただければ良いかと思います.ちなみにanacondaからpythonを入れるとデフォルトでMKLコンパイルのnumpy,scipyが入ります(この方法が圧倒的に楽です).が,

macでmkl&numpyを構築した様の記事によると,anaconda経由よりも,MKLから入れた方が性能が高いということもあるそうなので一概にどちらが良いかは言えません.

どれくらい計算スペックが上がるかという話については,上記に挙げた記事や,

Pythonの特異値分解SVDの速度を比較してみた

のように比較されている記事があるのでそちらをご覧いただくと良いかと思いますが,2割から数倍速くなるようです(手元の環境では3割増しになりました).


おわりに

少し長くなってしまいましたが,Keras,TensorFlow,それらのc++実行でどの程度実行速度が変わってくるのかをみてみました.

結果として,Python側だけでやるならTensorFlowが一番速く,モデル作成等が面倒ならばそこだけKerasを使うハイブリッド型がよさそうです.C++実行にするならばどれを取ってもそこまで大きく速度は変わらないのでどれをとってもPython実装よりかは速くなるといったところでしょうか.

エキスパート編ではPython,C++で1.8倍近く速くなりましたが,思っていたよりも差が出ませんでした.もともとTensorFlowのモデル実行部は後ろでC++をコンパイルしたものが走っているはずなので,今回出た差はモデル実行速度の差ではなく,それ以外のところのループ処理性能の差等が出てきていたのではないかなと思います.なので,画像処理周りと組み合わせる等,モデル実行以外の部分で本当にC++の処理速度が欲しい時以外は,Python側でモデルを回しても大差ないなと感じました.今度はSSDでも試してみようと思います.


参考文献


『 Python 』Article List