Pythonで学習したネットワークをC++で実行する
- 前提条件
- tensorflowをC++で実行する手順の概要
- コマンドライン引数の処理
- tensorflowの初期化
- 学習済みのニューラルネットワークの読み込み
- ネットワークに入力するテンソルデータの作成
- ニューラルネットワークの実行
- サンプルプログラムとコンパイル方法
前提条件
「 tensorflowをソースからコンパイルしてインストールする 」 でコンパイルしたtensorflowの共有ライブラリ(libtensorflow_cc.so)が /usr/local/lib等の検索パスに保存されている。
また、tensorflowのヘッダファイルが適当なディレクトリに保存されている。 (以下の説明では、$HOME/local/tensorflow/include以下にヘッダファイルが保存されていると仮定する)
「 Pythonで学習したネットワークを保存する 」 のtensorflow_save_graph.pyを実行し、 MNISTのデータのダウンロード及びネットワークの学習が完了している。
tensorflowをC++で実行する手順の概要
tensorflowのC++APIの使用してニューラルネットワークの計算を行う手順は以下の通り。
- これは必須ではないが、tensorflowのコマンドラインフラグのAPIを使って、 各種設定をコマンドラインから変更できるようにする。
- tensorflow::port::InitMain を実行し、tensorflowのグローバル状態の初期化を行う。
- pythonで作成した学習済みのニューラルネットワークのファイルをロードし、 新しいセッションを作成する。
- ニューラルネットワークに入力するテンソルデータを作成する
- ニューラルネットワークの計算を実行する
tensorflowのソースコードの
- tensorflow/examples/label_image/main.cc
- tensorflow/cc/tutorials/example_trainer.cc
等を読むことで、C++ APIの使用方法をある程度理解することができる。
以下の例では、上記のソースコードを参考に、 Pythonで学習したMNISTのニューラルネットワークをC++ APIを使って実行するための サンプルプログラムを作成する。
コマンドライン引数の処理
tensorflow::ParseFlagsを実行し、コマンドライン引数の処理を行う。
コンパイル後の実行ファイルを起動する際に、--graph=grame_name.pb のような形式でコマンドライン引数を与えることができるようになる。
引数が与えられない場合はデフォルト値が使用される。
ソースコード
tensorflow::string graph_filename = "trained_graph.pb";
tensorflow::string image_filename = "MNIST_data/t10k-images-idx3-ubyte.gz";
tensorflow::string label_filename = "MNIST_data/t10k-labels-idx1-ubyte.gz";
const bool parse_result = tensorflow::ParseFlags(
&argc, argv,
{tensorflow::Flag("graph", &graph_filename),
tensorflow::Flag("image", &image_filename),
tensorflow::Flag("label", &label_filename)});
if (!parse_result) {
LOG(ERROR) << "Error parsing command-line flags.";
return -1;
}
tensorflowの初期化
tensorflow::port::InitMainを実行し、tensorflowの初期化を行う。
tensorflowに消費されたコマンドライン引数はargc、argvから削除されるので、 InitMain実行後にargcが0でない場合は、解釈不能な引数が与えられたとしてエラーとしている。
ソースコード
tensorflow::port::InitMain(argv[0], &argc, &argv);
if (argc > 1) {
LOG(ERROR) << "Unknown argument " << argv[1];
return -1;
}
学習済みのニューラルネットワークの読み込み
学習済みのニューラルネットワークのロードし、 新しいtensorflowのセッションを作成する手順は以下の通り。
- ReadBinaryProto関数を実行し、プロトコルバッファのファイルから計算グラフの定義をロードする。
- tensorflow::NewSession関数を実行し、新しいセッションを作成する。
- tensorflow::Session::Createメソッドを実行し、セッションで使用する計算グラフを作成する。
ソースコード
tensorflow::Status
LoadGraph(tensorflow::string graph_filename,
std::unique_ptr<tensorflow::Session>* session)
{
tensorflow::GraphDef graph_def;
tensorflow::Status load_graph_status =
ReadBinaryProto(tensorflow::Env::Default(), graph_filename, &graph_def);
if (!load_graph_status.ok()) {
return tensorflow::errors::NotFound("Failed to load compute graph at '",
graph_filename, "'");
}
session->reset(tensorflow::NewSession(tensorflow::SessionOptions()));
tensorflow::Status session_create_status = (*session)->Create(graph_def);
if (!session_create_status.ok()) {
return session_create_status;
}
return tensorflow::Status::OK();
}
ネットワークに入力するテンソルデータの作成
次に、ネットワークに入力するテンソルデータを作成する。
テンソルを作成・初期化するには、以下の手順を踏む必要があるようである。
- tensorflow::Tensorコンストラクタでテンソルを作成する
- tensorflow::Tensor::tensorメソッド等の tensorflow::Tensor型の変数からEigen::Tensor型の変数を作成するメソッドを呼び出す。
- 手順2で作成したEigen::Tensor変数のメソッドを通じてデータの初期化を行う。
tensorflow::Tensor
LoadMnistImages(tensorflow::string filename)
{
const int N_header = 16;
const int N_data = 10000;
const int N_width = 28;
const int N_height = 28;
const int N_vec = N_width * N_height;
auto tensor = tensorflow::Tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({N_data, N_vec}));
auto mat = tensor.tensor<float, 2>();
mat.setZero();
std::ifstream fin(filename, std::ios_base::in | std::ios_base::binary);
assert(!fin.fail());
boost::iostreams::filtering_istream s;
s.push(boost::iostreams::gzip_decompressor());
s.push(fin);
char c;
for (int i=0; i<N_header; ++i) {
s.get(c);
}
for (int n=0; n<N_data; ++n) {
for (int h=0; h<N_height; ++h) {
for (int w=0; w<N_width; ++w) {
s.get(c);
mat(n, h*N_width + w) = static_cast<float>(static_cast<uint8_t>(c)) / 255.0;
}
}
}
return tensor;
}
MNISTのファイルのフォーマットに関しては、以下のURLを参考にした。
また、出力ラベルのテンソルも同様の方法で作成する。
ニューラルネットワークの実行
セッション変数のRunメソッドでニューラルネットワークの計算を行うことができる。 Runメソッドの引数の順番と型は
- const std::vector< std::pair< string, Tensor > > &inputs
- const std::vector< string > &output_tensor_names
- const std::vector< string > &target_node_names
- std::vector< Tensor > *outputs
となっている。各引数の意味と引数の与え方は以下の通り。
- inputsには、ネットワークの入力ノード(placeholder)の変数の名前と入力するテンソルのペアのベクトルを与える。
- output_tensor_namesには、ネットワークの出力ノードの変数の名前のベクトルを与える。 ここで与えた名前のノードの計算結果がoutputsに保存される。
- target_node_namesには、計算は実行はするが結果を出力しないノードの名前のベクトルを与える。 (下の例では空リストとしている)
- outputsにはニューラルネットワークの出力結果を受け取るためのテンソルのベクトルのポインタを渡す。
ソースコード
auto x = LoadMnistImages(image_filename);
auto y_ = LoadMnistLabels(label_filename);
std::vector<tensorflow::Tensor> outputs;
auto session_run_status = session->Run({{"x:0", x}, {"y_:0", y_}},
{"accuracy:0"},
{},
&outputs);
if (!session_run_status.ok()) {
LOG(ERROR) << session_run_status.error_message();
return -1;
}
サンプルプログラムとコンパイル方法
コンパイル方法
g++ main.cpp -std=c++11 -I$HOME/local/tensorflow/include -ltensorflow_cc -lboost_iostreams