通常tensorflow訓練深度學習網絡都是在python語言中實現的,因爲在python環境中安裝tensorflow非常方便並且tensorflow針對python的接口也非常友好,但有些時候我們又必須在C++環境中進行開發。所以我們希望利用python去訓練網絡,訓練完後將網絡凍結生成pb文件,然後通過C++版的tensorflow進行調用。但編譯C++版的tensorflow相對python要麻煩些,所以本文主要是做個編譯記錄方便以後查看。當然網上也有很多參考文獻,但自己在編譯過程中總是會遇到一些問題,所以本文會記錄的更加細緻。
說下運行環境,Ubuntu16.04,python3.5,CUDA 9.0,cuDNN8.0, tensorflow1.9-GPU(python環境下的),準備編譯安裝Tensorflow1.9-GPU(C++)版。
列個主要步驟:1.安裝protobuf 2.安裝Bazel 3.clone tensorflow源碼並編譯 4.安裝Eigen庫 5.通過Cmake讀取訓練好的pb文件 6.通過qtcreator讀取訓練好的pb文件
1 安裝Protobuf
安裝Protobuf有一點很重要,要與所編譯的tensorflow版本相對應,否則後期會遇到很多問題。但具體哪個版本的Tensorflow對應哪個版本的Protobuf我也沒找到相關文獻,因爲在我安裝過程中遇到過很多問題,最後試了好幾個版本後在找到合適的。這裏我使用的Protobuf版本是3.5.0(對應tensorflow1.9)。首先貼出protobuf官方的鏈接。
如圖所示,點擊最下面的選項Source Code(tar.gz)進行下載,下載後解壓到個人文件夾(平時要養成文件歸類的好習慣啊,要不然打開文件夾一團糟頭都大), 打開文件夾反擊鼠標——打開終端。在編譯安裝protobuf之前需要先安裝一些工具(automake libtool)。
在終端依次輸入指令:
sudo apt-get install automake libtool
./autogen.sh
./configure
make
sudo make install
sudo ldconfig
# sudo make uninstall 安裝錯版本後卸載指令
protoc --version # 查看protobuf版本
2 安裝Bazel
同樣安裝Bazel也要與Tensorflow版本相對應,這裏我使用的Bazel版本是0.15.2.給出Bazel的下載鏈接。
下載二進制文件bazel-0.15.2-installer-linux-x86_64.sh.
接着打開終端運行Bazel安裝程序
chmod +x bazel-0.15.2-installer-linux-x86_64.sh
./bazel-0.15.2-installer-linux-x86_64.sh --user
安裝完後設置環境。打開~/.bashrc文件(sudo gedit ~/.bashrc)並在文件最後加入如下指令
export PATH="$PATH:$HOME/bin"
保存後使其生效,輸入指令(source ~/.bashrc)
3 Clone Tensorflow源碼並進行編譯
首先從github上clone Tensorflow的源碼,打開終端輸入指令如下
git clone --recursive https://github.com/tensorflow/tensorflow
文件下載完後,通過終端打開文件(由於現在Tensorflow已經更新到1.13版本,而我要的是1.9的,所以先切換分支)
cd ./tensorflow
git checkout r1.9
./configure
在configure過程中基本都可以選No,具體每項的含義大家可以百度,但需要注意的是,如果要用GPU在build Tensorflow with CUDA support ? 選項中一定要選擇Y,並輸入對應的CUDA以及cuDNN版本(Tensorflow1.9-GPU所需的CUDA是9.0,cuDNN是8.0)。
配置完成後用Bazel進行編譯:
bazel build --config=opt //tensorflow:libtensorflow_cc.so # CPU版
bazel build --config=opt --config=cuda //tensorflow:libtensorflow_cc.so # GPU版
注意: 若在C++環境中需要使用opencv環境,建議使用以下指令編譯:(若不使用該指令可能會遇到opencv imread圖像失效問題,問題詳情見鏈接)
bazel build --config=monolithic //tensorflow:libtensorflow_cc.so
一般都要編譯很長時間30-60min的樣子,編譯完後的大致信息如下所示:
Target //tensorflow:libtensorflow_cc.so up-to-date:
bazel-bin/tensorflow/libtensorflow_cc.so
INFO: Elapsed time: 1233.631s, Critical Path: 48.36s
INFO: 2724 processes: 2724 local.
INFO: Build completed successfully, 2842 total actions
注意:看下路徑 ./tensorflow/tensorflow/contrib/makefile下有沒有downloads文件夾。如果沒有的話需要在./tensorflow/tensorflow/contrib/makefile文件夾下打開終端執行一個sh腳本文件:
./download_dependencies.sh
執行腳本文件後,會開始下載一些依賴文件,下載完後就會有downloads文件夾了。
4 安裝Eigen庫
首先打開上一步的downloads文件夾,裏面會有個eigen文件夾,進入eigen文件夾打開終端依次輸入如下指令:
mkdir build
cd build
cmake ..
make
sudo make install
安裝完畢後,在usr/local/include目錄下會出現eigen3文件夾
5 Cmake讀取訓練好的pb文件
(這一步基本都是按照網上網友的流程走的)建立一個Python項目生成一個訓練模型,首先在項目文件夾中建立一個model文件夾,在創建Python文件執行以下代碼:
import tensorflow as tf
import numpy as np
import os
tf.app.flags.DEFINE_integer('training_iteration', 1000,
'number of training iterations.')
tf.app.flags.DEFINE_integer('model_version', 1, 'version number of the model.')
tf.app.flags.DEFINE_string('work_dir', 'model/', 'Working directory.')
FLAGS = tf.app.flags.FLAGS
sess = tf.InteractiveSession()
x = tf.placeholder('float', shape=[None, 5],name="inputs")
y_ = tf.placeholder('float', shape=[None, 1])
w = tf.get_variable('w', shape=[5, 1], initializer=tf.truncated_normal_initializer)
b = tf.get_variable('b', shape=[1], initializer=tf.zeros_initializer)
sess.run(tf.global_variables_initializer())
y = tf.add(tf.matmul(x, w) , b,name="outputs")
ms_loss = tf.reduce_mean((y - y_) ** 2)
train_step = tf.train.GradientDescentOptimizer(0.005).minimize(ms_loss)
train_x = np.random.randn(1000, 5)
# let the model learn the equation of y = x1 * 1 + x2 * 2 + x3 * 3
train_y = np.sum(train_x * np.array([1, 2, 3,4,5]) + np.random.randn(1000, 5) / 100, axis=1).reshape(-1, 1)
for i in range(FLAGS.training_iteration):
loss, _ = sess.run([ms_loss, train_step], feed_dict={x: train_x, y_: train_y})
if i%100==0:
print("loss is:",loss)
graph = tf.graph_util.convert_variables_to_constants(sess, sess.graph_def,
["inputs", "outputs"])
tf.train.write_graph(graph, ".", FLAGS.work_dir + "liner.pb",
as_text=False)
print('Done exporting!')
print('Done training!')
執行完後會在model文件下生成一個liner.pb文件。
在使用C++調用pb文件之前升級一下Cmake(這裏需要升級到3.10版本以上),給出下載鏈接。
我下載的是3.11版(cmake-3.11.0-Linux-x86_64.tar.gz)
下載完後進行解壓,解壓後將文件的./cmake-3.11.0-Linux-x86_64./bin目錄添加到~./bashrc文件中。
$ gedit ~/.bashrc # 打開~/.bashrc
將bin目錄鏈接添加到~./bashrc文件最後:
export PATH=/home/wz/cmake-3.11.0/bin:$PATH
接着source一下:
source ~/.bashrc
查看cmake版本:
cmake --version
接下來建立C++項目,在文件夾中創建以下幾個文件:
model_loader_base.h:
#ifndef CPPTENSORFLOW_MODEL_LOADER_BASE_H
#define CPPTENSORFLOW_MODEL_LOADER_BASE_H
#include <iostream>
#include <vector>
#include "tensorflow/core/public/session.h"
#include "tensorflow/core/platform/env.h"
using namespace tensorflow;
namespace tf_model {
/**
* Base Class for feature adapter, common interface convert input format to tensors
* */
class FeatureAdapterBase{
public:
FeatureAdapterBase() {};
virtual ~FeatureAdapterBase() {};
virtual void assign(std::string, std::vector<double>*) = 0; // tensor_name, tensor_double_vector
std::vector<std::pair<std::string, tensorflow::Tensor> > input;
};
class ModelLoaderBase {
public:
ModelLoaderBase() {};
virtual ~ModelLoaderBase() {};
virtual int load(tensorflow::Session*, const std::string) = 0; //pure virutal function load method
virtual int predict(tensorflow::Session*, const FeatureAdapterBase&, const std::string, double*) = 0;
tensorflow::GraphDef graphdef; //Graph Definition for current model
};
}
#endif //CPPTENSORFLOW_MODEL_LOADER_BASE_H
ann_model_loader.h:
#ifndef CPPTENSORFLOW_ANN_MODEL_LOADER_H
#define CPPTENSORFLOW_ANN_MODEL_LOADER_H
#include "model_loader_base.h"
#include "tensorflow/core/public/session.h"
#include "tensorflow/core/platform/env.h"
using namespace tensorflow;
namespace tf_model {
/**
* @brief: Model Loader for Feed Forward Neural Network
* */
class ANNFeatureAdapter: public FeatureAdapterBase {
public:
ANNFeatureAdapter();
~ANNFeatureAdapter();
void assign(std::string tname, std::vector<double>*) override; // (tensor_name, tensor)
};
class ANNModelLoader: public ModelLoaderBase {
public:
ANNModelLoader();
~ANNModelLoader();
int load(tensorflow::Session*, const std::string) override; //Load graph file and new session
int predict(tensorflow::Session*, const FeatureAdapterBase&, const std::string, double*) override;
};
}
#endif //CPPTENSORFLOW_ANN_MODEL_LOADER_H
ann_model_loader.cpp:
#include <iostream>
#include <vector>
#include <map>
#include "ann_model_loader.h"
//#include <tensor_shape.h>
using namespace tensorflow;
namespace tf_model {
/**
* ANNFeatureAdapter Implementation
* */
ANNFeatureAdapter::ANNFeatureAdapter() {
}
ANNFeatureAdapter::~ANNFeatureAdapter() {
}
/*
* @brief: Feature Adapter: convert 1-D double vector to Tensor, shape [1, ndim]
* @param: std::string tname, tensor name;
* @parma: std::vector<double>*, input vector;
* */
void ANNFeatureAdapter::assign(std::string tname, std::vector<double>* vec) {
//Convert input 1-D double vector to Tensor
int ndim = vec->size();
if (ndim == 0) {
std::cout << "WARNING: Input Vec size is 0 ..." << std::endl;
return;
}
// Create New tensor and set value
Tensor x(tensorflow::DT_FLOAT, tensorflow::TensorShape({1, ndim})); // New Tensor shape [1, ndim]
auto x_map = x.tensor<float, 2>();
for (int j = 0; j < ndim; j++) {
x_map(0, j) = (*vec)[j];
}
// Append <tname, Tensor> to input
input.push_back(std::pair<std::string, tensorflow::Tensor>(tname, x));
}
/**
* ANN Model Loader Implementation
* */
ANNModelLoader::ANNModelLoader() {
}
ANNModelLoader::~ANNModelLoader() {
}
/**
* @brief: load the graph and add to Session
* @param: Session* session, add the graph to the session
* @param: model_path absolute path to exported protobuf file *.pb
* */
int ANNModelLoader::load(tensorflow::Session* session, const std::string model_path) {
//Read the pb file into the grapgdef member
tensorflow::Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef);
if (!status_load.ok()) {
std::cout << "ERROR: Loading model failed..." << model_path << std::endl;
std::cout << status_load.ToString() << "\n";
return -1;
}
// Add the graph to the session
tensorflow::Status status_create = session->Create(graphdef);
if (!status_create.ok()) {
std::cout << "ERROR: Creating graph in session failed..." << status_create.ToString() << std::endl;
return -1;
}
return 0;
}
/**
* @brief: Making new prediction
* @param: Session* session
* @param: FeatureAdapterBase, common interface of input feature
* @param: std::string, output_node, tensorname of output node
* @param: double, prediction values
* */
int ANNModelLoader::predict(tensorflow::Session* session, const FeatureAdapterBase& input_feature,
const std::string output_node, double* prediction) {
// The session will initialize the outputs
std::vector<tensorflow::Tensor> outputs; //shape [batch_size]
// @input: vector<pair<string, tensor> >, feed_dict
// @output_node: std::string, name of the output node op, defined in the protobuf file
tensorflow::Status status = session->Run(input_feature.input, {output_node}, {}, &outputs);
if (!status.ok()) {
std::cout << "ERROR: prediction failed..." << status.ToString() << std::endl;
return -1;
}
//Fetch output value
std::cout << "Output tensor size:" << outputs.size() << std::endl;
for (std::size_t i = 0; i < outputs.size(); i++) {
std::cout << outputs[i].DebugString();
}
std::cout << std::endl;
Tensor t = outputs[0]; // Fetch the first tensor
int ndim = t.shape().dims(); // Get the dimension of the tensor
auto tmap = t.tensor<float, 2>(); // Tensor Shape: [batch_size, target_class_num]
int output_dim = t.shape().dim_size(1); // Get the target_class_num from 1st dimension
std::vector<double> tout;
// Argmax: Get Final Prediction Label and Probability
int output_class_id = -1;
double output_prob = 0.0;
for (int j = 0; j < output_dim; j++) {
std::cout << "Class " << j << " prob:" << tmap(0, j) << "," << std::endl;
if (tmap(0, j) >= output_prob) {
output_class_id = j;
output_prob = tmap(0, j);
}
}
// Log
std::cout << "Final class id: " << output_class_id << std::endl;
std::cout << "Final value is: " << output_prob << std::endl;
(*prediction) = output_prob; // Assign the probability to prediction
return 0;
}
}
main.cpp
#include <iostream>
#include "tensorflow/core/public/session.h"
#include "tensorflow/core/platform/env.h"
#include "ann_model_loader.h"
using namespace tensorflow;
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cout << "WARNING: Input Args missing" << std::endl;
return 0;
}
std::string model_path = argv[1]; // Model_path *.pb file
// TensorName pre-defined in python file, Need to extract values from tensors
std::string input_tensor_name = "inputs";
std::string output_tensor_name = "outputs";
// Create New Session
Session* session;
Status status = NewSession(SessionOptions(), &session);
if (!status.ok()) {
std::cout << status.ToString() << "\n";
return 0;
}
// Create prediction demo
tf_model::ANNModelLoader model; //Create demo for prediction
if (0 != model.load(session, model_path)) {
std::cout << "Error: Model Loading failed..." << std::endl;
return 0;
}
// Define Input tensor and Feature Adapter
// Demo example: [1.0, 1.0, 1.0, 1.0, 1.0] for Iris Example, including bias
int ndim = 5;
std::vector<double> input;
for (int i = 0; i < ndim; i++) {
input.push_back(1.0);
}
// New Feature Adapter to convert vector to tensors dictionary
tf_model::ANNFeatureAdapter input_feat;
input_feat.assign(input_tensor_name, &input); //Assign vec<double> to tensor
// Make New Prediction
double prediction = 0.0;
if (0 != model.predict(session, input_feat, output_tensor_name, &prediction)) {
std::cout << "WARNING: Prediction failed..." << std::endl;
}
std::cout << "Output Prediction Value:" << prediction << std::endl;
return 0;
}
接着在文件夾中建立一個CMakeList.txt文件,在文件中寫入:
cmake_minimum_required(VERSION 3.10)
project(cpptensorflow)
set(CMAKE_CXX_STANDARD 11)
link_directories(/home/wz/cpptest/tensorflow/bazel-bin/tensorflow)
include_directories(
/home/wz/cpptest/tensorflow
/home/wz/cpptest/tensorflow/bazel-genfiles
/home/wz/cpptest/tensorflow/bazel-bin/tensorflow
/usr/local/include/eigen3
)
add_executable(cpptensorflow main.cpp ann_model_loader.h model_loader_base.h ann_model_loader.cpp)
target_link_libraries(cpptensorflow tensorflow_cc tensorflow_framework)
注意將所有的directories改成你自己相應文件所在的目錄。編輯好保存,文件夾中應該有這麼幾個文件:
接着在該文件夾中打開終端輸入指令,開始編譯項目:
mkdir build
cd build
cmake ..
make
編譯完後會在新建的build文件夾中生成cpptensorflow可執行文件,接着在終端中調用該執行文件:
./cpptensorflow /home/wz/PycharmProjects/create_C++_TEST/model/liner.pb
注意,將後面的liner.pb文件所在目錄改成你自己pb文件所在的位置,最後可得到以下結果:
到這就算成功了!
6 使用qtcreator環境讀取pb文件
(1)將"tensorflow/bazel-genfiles/tensorflow/"中的cc和core文件夾中的內容copy到"tensorflow/tensorflow/"中,然後選擇合併覆蓋。
(2)進入tensorflow/bazel-bin/tensorflow文件夾下,會有一個編譯生成的libtensorflow_cc.so文件。若你是通過bazel build --config=opt //tensorflow:libtensorflow_cc.so指令進行編譯的,那麼將libtensorflow_cc.so和libtensorflow_framework.so兩個文件複製到/usr/local/lib/文件夾下:
sudo cp libtensorflow_cc.so /usr/local/lib/
sudo cp libtensorflow_framework.so /usr/local/lib/
若你是通過bazel build --config=monolithic //tensorflow:libtensorflow_cc.so指令進行編譯的,那麼你只需要將libtensorflow_cc.so文件複製到/usr/local/lib文件夾下:
sudo cp libtensorflow_cc.so /usr/local/lib
(3)接下來安裝qtcreator和opencv,具體安裝流程可參考該博文。
(4)打開qtcreator建立一個新的空項目,然後將剛使用的Cmake進行編譯的四個文件添加進項目(ann_model_loader.cpp,ann_model_loader.h,model_loader_base.h,main.cpp)。
(5)接着配置.pro文件,由於我需要使用opencv,所以我是通過bazel build --config=monolithic //tensorflow:libtensorflow_cc.so指令編譯的,現在需要將可能用到的頭文件和可能需要鏈接的文件寫入.pro文件中,以下是我的編寫的.pro文件:
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
main.cpp
INCLUDEPATH += /usr/local/include \
/usr/local/include/opencv \
/usr/local/include/opencv2 \
/home/wz/tf_c_install/tensor_source/tensorflow \
/usr/local/include/eigen3
LIBS += /usr/local/lib/libtensorflow_cc.so \
/usr/local/lib/libopencv_calib3d.so \
/usr/local/lib/libopencv_shape.so.3.4.4 \
/usr/local/lib/libopencv_calib3d.so.3.4 \
/usr/local/lib/libopencv_stitching.so \
/usr/local/lib/libopencv_calib3d.so.3.4.4 \
/usr/local/lib/libopencv_stitching.so.3.4 \
/usr/local/lib/libopencv_core.so \
/usr/local/lib/libopencv_stitching.so.3.4.4 \
/usr/local/lib/libopencv_core.so.3.4 \
/usr/local/lib/libopencv_superres.so \
/usr/local/lib/libopencv_core.so.3.4.4 \
/usr/local/lib/libopencv_superres.so.3.4 \
/usr/local/lib/libopencv_dnn.so \
/usr/local/lib/libopencv_superres.so.3.4.4 \
/usr/local/lib/libopencv_dnn.so.3.4 \
/usr/local/lib/libopencv_videoio.so \
/usr/local/lib/libopencv_dnn.so.3.4.4 \
/usr/local/lib/libopencv_videoio.so.3.4 \
/usr/local/lib/libopencv_features2d.so \
/usr/local/lib/libopencv_videoio.so.3.4.4 \
/usr/local/lib/libopencv_features2d.so.3.4 \
/usr/local/lib/libopencv_video.so \
/usr/local/lib/libopencv_features2d.so.3.4.4 \
/usr/local/lib/libopencv_video.so.3.4 \
/usr/local/lib/libopencv_flann.so \
/usr/local/lib/libopencv_video.so.3.4.4 \
/usr/local/lib/libopencv_flann.so.3.4 \
/usr/local/lib/libopencv_videostab.so \
/usr/local/lib/libopencv_flann.so.3.4.4 \
/usr/local/lib/libopencv_videostab.so.3.4 \
/usr/local/lib/libopencv_highgui.so \
/usr/local/lib/libopencv_videostab.so.3.4.4 \
/usr/local/lib/libopencv_highgui.so.3.4 \
/usr/local/lib/libopencv_highgui.so.3.4.4 \
/usr/local/lib/libopencv_imgcodecs.so \
/usr/local/lib/libopencv_imgcodecs.so.3.4 \
/usr/local/lib/libopencv_imgcodecs.so.3.4.4 \
/usr/local/lib/libopencv_imgproc.so \
/usr/local/lib/libopencv_imgproc.so.3.4 \
/usr/local/lib/libopencv_imgproc.so.3.4.4 \
/usr/local/lib/libopencv_ml.so \
/usr/local/lib/libopencv_ml.so.3.4 \
/usr/local/lib/libopencv_ml.so.3.4.4 \
/usr/local/lib/libopencv_objdetect.so \
/usr/local/lib/libopencv_objdetect.so.3.4 \
/usr/local/lib/libopencv_objdetect.so.3.4.4 \
/usr/local/lib/libopencv_photo.so \
/usr/local/lib/libopencv_photo.so.3.4 \
/usr/local/lib/libopencv_photo.so.3.4.4 \
/usr/local/lib/libopencv_shape.so \
/usr/local/lib/libopencv_shape.so.3.4 \
使用時記得將路徑改成自己的路徑,接着就可以編譯了,若編譯過程中提示缺少什麼頭文件,不要慌,打開tensorflow/tensorflow/文件夾搜索提示中缺少的頭文件,一般都能找到,然後將這個頭文件所在目錄添加到.pro文件的INCLUDEPATH中。到這一步基本就沒什麼問題了,撒花!!!
(在我的下一篇博文中進一步結合代碼講解了如何調用Tensorflow提供的object detection模塊的pb文件,有興趣的同學可以看看)
最後給些重要參考鏈接:
tensorflow c++接口,python訓練模型,c++調用
Ubuntu16.04 安裝/更新/升級cmake到 cmake3.9.1的具體安裝過程