本文轉載自:https://blog.csdn.net/qq_25109263/article/details/81285952
將Keras訓練的模型部署到C++平臺上的可行方案
一. 背景:
本人這幾天由於公司要求將Deep learning的項目遷移到C++的平臺,以便作爲一個子模塊嵌入到整個公司的C++Project當中。在算法研究階段,想必很多人會喜歡keras,因爲keras的代碼簡介性,十分有利於Deeplearning的編程,十分容易上手,tensorflow雖然目前google創造了廣大的生態,支持各種語言的接口,但是本人至今還是不習慣用tensorflow這種先定義圖,再執行運算的方式,不知道大家是否有同感。我之前的研究內容在Python端已經通過keras訓練出來了可用的model,但是keras並沒有提供C++的API,所以似乎我得用另外的框架,如caffe2,tensorflow,caffe,pytorch等去重新train我的model,再用他們的C++API去在C++平臺上運行。通過嘗試caffe2,發現這玩意兒第一來說,環境配置過程雖然簡單,但是很容易出各種error,,配置就是一個坑。第二,其官方的API寫的也太水了吧,跟沒有一樣,只能按照上面的幾個demo去跑,學起來十分費勁。caffe2和tensorflow在python端的方式是差不多的,都是先定義圖,再運行,所以從訓練角度,這兩者都讓我很不舒服。之前用caffe還算熟練,但是若要用caffe,則需要重新training,所以我心心念念地想要如果keras訓練好的Model能直接來用,該多好,所以倒騰幾天,終於實現了這個目標,下面話不多說,給大家講解整個過程
Plus,相關的代碼在我百度雲上可以找到,鏈接路徑如下:
鏈接: https://pan.baidu.com/s/1FHRTC1NZRSyaMuVBMGUihA 密碼: h7x8
二. Pipline:
整個工作流:keras訓練->"my_model.h5"->轉換到pb類型->"my_model.pb"->C++端用tensorflowAPI調用並運行模型
三. 必備環境配置:
本機配置:GTX1080,Ubuntu16.04
(1)一個配有tensorflow和keras的python環境,建議用anaconda去創建
- conda create -n keras python=3.6
- source activate keras
- conda install tensorflow-gpu==1.8
- pip install keras==2.1
(2) CUDA9.0, cudnn7.0.5(安裝CUDA和cudnn的方法請自行查找,不在本文範圍內)
(3)OpenCV3.4(安裝OpenCV的方法請自行查找,不在本文範圍內)
(5)編譯tensorflow C++接口,這在接下來會介紹
四.編譯安裝tensorflow的C++接口,本文編譯的tensorflow C++是1.8版的
1.配置C++版tensorflow使用時的第三方依賴
(1)Protobuf!!!!!這玩意兒是重中之重,它的版本與tensorflow的版本密切相關,它的版本錯了就無法work,我用的3.5.0
先從以下網址下載protobuf-cpp-3.5.0.tar.gz
https://github.com/google/protobuf/releases
再解壓出來,獲得一個protobuf-3.5.0的文件夾
cd prtobuf-3.5.0
./configure
sudo make -j8
make check -j8
sudo make install
sudo ldconfig
以上步驟可以完成Protubuf的源碼的編譯和安裝
如果遇到什麼問題,建議去看Protobuf的官方的編譯安裝指南:
https://github.com/google/protobuf/blob/master/src/README.md
(2)Eigen,這是一個C++端的矩陣運算庫,這個庫只要下載壓縮包,解壓到某個自己知道的路徑下即可
先下載eigen的壓縮包
wget http://bitbucket.org/eigen/eigen/get/3.3.4.tar.bz2
下載之後解壓,重新命名爲eigen3,放到某個路徑下
我存放的路徑是,~/tools/tf-C/
2.編譯安裝Tensorflow
(1)下載安裝編譯工具bazel
先下載Bazel的安裝包
https://github.com/bazelbuild/bazel/releases,我下載的是bazel-0.10.1-installer-linux-x86_64.sh
然後執行安裝
./bazel-0.10.1-installer-linux-x86_64.sh
(2)編譯安裝Tensorflow,我的源碼路徑是~/tensorflow
# 先下載tensorflow源碼
git clone –recursive https://github.com/tensorflow/tensorflow# 進入tensorflow文件夾
cd tensorflow# 切換到1.8版本:
git checkout r1.8# 執行configure
sudo ./configure
這一步需要你指定python路徑,需要有各種y/N的選擇
建議如下:
- python路徑用之前創建的keras的路徑:/home/xxx/anaconda2/envs/keras/bin/python
- 其他的第一個y/N選擇y,後面的都是N
- cuda要選擇y,然後會自動搜索cudnn版本
- nccl選擇默認的1.3,
- 後面的不是選擇N就是默認
詳見Tensorflow官網的提示:
https://www.tensorflow.org/install/install_sources# 使用bazel去編譯,--config=monolithic是爲了解決與OpenCV的衝突問題
sudo bazel build --config=opt --config=cuda --config=monolithic //tensorflow:libtensorflow_cc.so....漫長的等待編譯,大約20分鐘
# 最後顯示類似如下的信息,說明編譯成功了:
....
Target //tensorflow:libtensorflow_cc.so up-to-date:
bazel-bin/tensorflow/libtensorflow_cc.so
INFO: Elapsed time: 1192.883s, Critical Path: 174.02s
INFO: 654 processes: 654 local.
INFO: Build completed successfully, 656 total actions# 再把必要.h頭文件以及編譯出來.so的動態鏈接庫文件複製到指定的一些路徑下:
sudo mkdir /usr/local/include/tf
sudo cp -r bazel-genfiles/ /usr/local/include/tf/
sudo cp -r tensorflow /usr/local/include/tf/
sudo cp -r third_party /usr/local/include/tf/
sudo cp bazel-bin/tensorflow/libtensorflow_cc.so /usr/local/lib/
sudo cp bazel-bin/tensorflow/libtensorflow_framework.so /usr/local/libOK到此爲止,tensorflow C++的接口已經搞定!
五.整個pipline的演示
1.python端用keras訓練一個手寫數字識別的mnist的demo,代碼如下,訓練完會產生一個my_model_ep20.h5的模型文件
from tensorflow.examples.tutorials.mnist import *
from keras.models import *
from keras.layers import *
import numpy as np
# os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 加載數據集
mnist=input_data.read_data_sets("MNIST_data/",one_hot=True)
train_X=mnist.train.images
train_Y=mnist.train.labels
test_X=mnist.test.images
test_Y=mnist.test.labels
train_X=train_X.reshape((55000,28,28,1))
test_X=test_X.reshape((test_X.shape[0],28,28,1))
print("type of train_X:",type(train_X))
print("size of train_X:",np.shape(train_X))
print("train_X:",train_X)
print("type of train_Y:",type(train_Y))
print("size of train_Y:",np.shape(train_Y))
print("train_Y:",train_Y)
print("num of test:",test_X.shape[0])
# 配置模型結構
model=Sequential()
model.add(Conv2D(32,(3,3),activation='relu',input_shape=(28,28,1),padding="same"))
model.add(MaxPool2D(pool_size=(2,2)))
model.add(Dropout(0.5))
model.add(Conv2D(64, (3, 3), activation='relu',padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Conv2D(128, (3, 3), activation='relu',padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Flatten())
model.add(Dense(625,activation="relu"))
model.add(Dropout(0.5))
model.add(Dense(10,activation='softmax'))
model.compile(loss='categorical_crossentropy',optimizer='adadelta',metrics=['accuracy'])
# 訓練模型
epochs=20
model.fit(train_X, train_Y, batch_size=32, epochs=epochs)
# 用測試集去評估模型的準確度
accuracy=model.evaluate(test_X,test_Y,batch_size=20)
print('\nTest accuracy:',accuracy[1])
save_model(model,'my_model_ep{}.h5'.format(epochs))
2.將my_model_ep20.h5的模型轉化爲my_model_ep20.pb的模型,用的腳本爲h5_to_pb.py
from keras.models import load_model
import tensorflow as tf
from keras import backend as K
from tensorflow.python.framework import graph_io
def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):
from tensorflow.python.framework.graph_util import convert_variables_to_constants
graph = session.graph
with graph.as_default():
freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
output_names = output_names or []
output_names += [v.op.name for v in tf.global_variables()]
input_graph_def = graph.as_graph_def()
if clear_devices:
for node in input_graph_def.node:
node.device = ""
frozen_graph = convert_variables_to_constants(session, input_graph_def,
output_names, freeze_var_names)
return frozen_graph
"""----------------------------------配置路徑-----------------------------------"""
epochs=20
h5_model_path='./my_model_ep{}.h5'.format(epochs)
output_path='.'
pb_model_name='my_model_ep{}.pb'.format(epochs)
"""----------------------------------導入keras模型------------------------------"""
K.set_learning_phase(0)
net_model = load_model(h5_model_path)
print('input is :', net_model.input.name)
print ('output is:', net_model.output.name)
"""----------------------------------保存爲.pb格式------------------------------"""
sess = K.get_session()
frozen_graph = freeze_session(K.get_session(), output_names=[net_model.output.op.name])
graph_io.write_graph(frozen_graph, output_path, pb_model_name, as_text=False)
3.測試模型類型轉換後,是否測試效果一致,如果不一致,那就說明轉換失敗
測試my_model_ep20.h5模型:寫了一個load_h5_test.py
import os
import cv2
import numpy as np
from keras.models import load_model
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
"""---------載入已經訓練好的模型---------"""
new_model = load_model('my_model_ep20.h5')
"""---------用opencv載入一張待測圖片-----"""
# 載入圖片
src = cv2.imread('Pictures/6.png')
cv2.imshow("test picture", src)
# 將圖片轉化爲28*28的灰度圖
src = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
dst = cv2.resize(src, (28, 28))
dst=dst.astype(np.float32)
# 將灰度圖轉化爲1*784的能夠輸入的網絡的數組
picture=1-dst/255
picture=np.reshape(picture,(1,28,28,1))
# 用模型進行預測
y = new_model.predict(picture)
print("softmax:")
for i,prob in enumerate(y[0]):
print("class{},Prob:{}".format(i,prob))
result = np.argmax(y)
print("你寫的數字是:", result)
print("對應的概率是:",np.max(y[0]))
cv2.waitKey(20170731)
-
效果如下:
測試my_model_ep20.pb模型,寫了一個load_pb_test.py:
-
import tensorflow as tf
-
import numpy as np
-
import cv2
-
"""-----------------------------------------------定義識別函數-----------------------------------------"""
-
def recognize(jpg_path, pb_file_path):
-
with tf.Graph().as_default():
-
output_graph_def = tf.GraphDef()
-
# 打開.pb模型
-
with open(pb_file_path, "rb") as f:
-
output_graph_def.ParseFromString(f.read())
-
tensors = tf.import_graph_def(output_graph_def, name="")
-
print("tensors:",tensors)
-
# 在一個session中去run一個前向
-
with tf.Session() as sess:
-
init = tf.global_variables_initializer()
-
sess.run(init)
-
op = sess.graph.get_operations()
-
# 打印圖中有的操作
-
for i,m in enumerate(op):
-
print('op{}:'.format(i),m.values())
-
input_x = sess.graph.get_tensor_by_name("conv2d_1_input:0") # 具體名稱看上一段代碼的input.name
-
print("input_X:",input_x)
-
out_softmax = sess.graph.get_tensor_by_name("dense_2/Softmax:0") # 具體名稱看上一段代碼的output.name
-
print("Output:",out_softmax)
-
# 讀入圖片
-
img = cv2.imread(jpg_path, 0)
-
img=cv2.resize(img,(28,28))
-
img=img.astype(np.float32)
-
img=1-img/255;
-
# img=np.reshape(img,(1,28,28,1))
-
print("img data type:",img.dtype)
-
# 顯示圖片內容
-
for row in range(28):
-
for col in range(28):
-
if col!=27:
-
print(img[row][col],' ',end='')
-
else:
-
print(img[row][col])
-
img_out_softmax = sess.run(out_softmax,
-
feed_dict={input_x: np.reshape(img,(1,28,28,1))})
-
print("img_out_softmax:", img_out_softmax)
-
for i,prob in enumerate(img_out_softmax[0]):
-
print('class {} prob:{}'.format(i,prob))
-
prediction_labels = np.argmax(img_out_softmax, axis=1)
-
print("Final class if:",prediction_labels)
-
print("prob of label:",img_out_softmax[0,prediction_labels])
-
pb_path = './my_model_ep20.pb'
-
img = 'Pictures/6.png'
-
recognize(img, pb_path)
效果如下:與上面的h5模型的結果基本一致,說明轉換沒問題
4.在C++中調用my_model_ep20.pb模型,寫了一個hello.cpp,代碼如下:注意,代碼中input_tensor_name和output_tensor_name很關鍵,這兩個是模型的輸入和輸出接口,這兩個名字怎麼確定,就要看你模型定義的時候,對應的tensor的名字了,這個名字可以在load_pb_test.py運行時,看它打印的信息,你就會明白該用什麼名字了
-
#include <fstream>
-
#include <utility>
-
#include <Eigen/Core>
-
#include <Eigen/Dense>
-
#include <iostream>
-
#include "tensorflow/cc/ops/const_op.h"
-
#include "tensorflow/cc/ops/image_ops.h"
-
#include "tensorflow/cc/ops/standard_ops.h"
-
#include "tensorflow/core/framework/graph.pb.h"
-
#include "tensorflow/core/framework/tensor.h"
-
#include "tensorflow/core/graph/default_device.h"
-
#include "tensorflow/core/graph/graph_def_builder.h"
-
#include "tensorflow/core/lib/core/errors.h"
-
#include "tensorflow/core/lib/core/stringpiece.h"
-
#include "tensorflow/core/lib/core/threadpool.h"
-
#include "tensorflow/core/lib/io/path.h"
-
#include "tensorflow/core/lib/strings/stringprintf.h"
-
#include "tensorflow/core/public/session.h"
-
#include "tensorflow/core/util/command_line_flags.h"
-
#include "tensorflow/core/platform/env.h"
-
#include "tensorflow/core/platform/init_main.h"
-
#include "tensorflow/core/platform/logging.h"
-
#include "tensorflow/core/platform/types.h"
-
#include "opencv2/opencv.hpp"
-
using namespace tensorflow::ops;
-
using namespace tensorflow;
-
using namespace std;
-
using namespace cv;
-
using tensorflow::Flag;
-
using tensorflow::Tensor;
-
using tensorflow::Status;
-
using tensorflow::string;
-
using tensorflow::int32 ;
-
// 定義一個函數講OpenCV的Mat數據轉化爲tensor,python裏面只要對cv2.read讀進來的矩陣進行np.reshape之後,
-
// 數據類型就成了一個tensor,即tensor與矩陣一樣,然後就可以輸入到網絡的入口了,但是C++版本,我們網絡開放的入口
-
// 也需要將輸入圖片轉化成一個tensor,所以如果用OpenCV讀取圖片的話,就是一個Mat,然後就要考慮怎麼將Mat轉化爲
-
// Tensor了
-
void CVMat_to_Tensor(Mat img,Tensor* output_tensor,int input_rows,int input_cols)
-
{
-
//imshow("input image",img);
-
//圖像進行resize處理
-
resize(img,img,cv::Size(input_cols,input_rows));
-
//imshow("resized image",img);
-
//歸一化
-
img.convertTo(img,CV_32FC1);
-
img=img/255;
-
//創建一個指向tensor的內容的指針
-
float *p = output_tensor->flat<float>().data();
-
//創建一個Mat,與tensor的指針綁定,改變這個Mat的值,就相當於改變tensor的值
-
cv::Mat tempMat(input_rows, input_cols, CV_32FC1, p);
-
img.convertTo(tempMat,CV_32FC1);
-
// waitKey(0);
-
}
-
int main(int argc, char** argv )
-
{
-
/*--------------------------------配置關鍵信息------------------------------*/
-
string model_path="../my_model_ep20.pb";
-
string image_path="../test_images/6.png";
-
int input_height =28;
-
int input_width=28;
-
string input_tensor_name="conv2d_1_input";
-
string output_tensor_name="dense_2/Softmax";
-
/*--------------------------------創建session------------------------------*/
-
Session* session;
-
Status status = NewSession(SessionOptions(), &session);//創建新會話Session
-
/*--------------------------------從pb文件中讀取模型--------------------------------*/
-
GraphDef graphdef; //Graph Definition for current model
-
Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef); //從pb文件中讀取圖模型;
-
if (!status_load.ok()) {
-
cout << "ERROR: Loading model failed..." << model_path << std::endl;
-
cout << status_load.ToString() << "\n";
-
return -1;
-
}
-
Status status_create = session->Create(graphdef); //將模型導入會話Session中;
-
if (!status_create.ok()) {
-
cout << "ERROR: Creating graph in session failed..." << status_create.ToString() << std::endl;
-
return -1;
-
}
-
cout << "<----Successfully created session and load graph.------->"<< endl;
-
/*---------------------------------載入測試圖片-------------------------------------*/
-
cout<<endl<<"<------------loading test_image-------------->"<<endl;
-
Mat img=imread(image_path,0);
-
if(img.empty())
-
{
-
cout<<"can't open the image!!!!!!!"<<endl;
-
return -1;
-
}
-
//創建一個tensor作爲輸入網絡的接口
-
Tensor resized_tensor(DT_FLOAT, TensorShape({1,input_height,input_width,1}));
-
//將Opencv的Mat格式的圖片存入tensor
-
CVMat_to_Tensor(img,&resized_tensor,input_height,input_width);
-
cout << resized_tensor.DebugString()<<endl;
-
/*-----------------------------------用網絡進行測試-----------------------------------------*/
-
cout<<endl<<"<-------------Running the model with test_image--------------->"<<endl;
-
//前向運行,輸出結果一定是一個tensor的vector
-
vector<tensorflow::Tensor> outputs;
-
string output_node = output_tensor_name;
-
Status status_run = session->Run({{input_tensor_name, resized_tensor}}, {output_node}, {}, &outputs);
-
if (!status_run.ok()) {
-
cout << "ERROR: RUN failed..." << std::endl;
-
cout << status_run.ToString() << "\n";
-
return -1;
-
}
-
//把輸出值給提取出來
-
cout << "Output tensor size:" << outputs.size() << std::endl;
-
for (std::size_t i = 0; i < outputs.size(); i++) {
-
cout << outputs[i].DebugString()<<endl;
-
}
-
Tensor t = outputs[0]; // Fetch the first 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
-
// 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++)
-
{
-
cout << "Class " << j << " prob:" << tmap(0, j) << "," << std::endl;
-
if (tmap(0, j) >= output_prob) {
-
output_class_id = j;
-
output_prob = tmap(0, j);
-
}
-
}
-
// 輸出結果
-
cout << "Final class id: " << output_class_id << std::endl;
-
cout << "Final class prob: " << output_prob << std::endl;
-
return 0;
-
}
然後要寫CmakeList.txt,如下所示:
-
cmake_minimum_required(VERSION 3.10)
-
project(demo)
-
set(CMAKE_CXX_STANDARD 11)
-
# 配置.so文件存在的路徑,默認會去/usr/local/lib下找,
-
# 放在其他地方的.so文件路徑就要用下面的代碼添加
-
link_directories(/home/czj/anaconda2/envs/tf/lib)
-
# 配置.h頭文件路徑,默認會去/usr/local/include下找,
-
# 放在其他地方的.h文件就要用以下代碼添加進來
-
include_directories(
-
/home/czj/tensorflow
-
/home/czj/tensorflow/bazel-genfiles
-
/home/czj/tensorflow/bazel-bin/tensorflow
-
/home/czj/tools/tf-C/eigen3
-
)
-
# 配置生成的可執行文件名爲hello,要用到的源文件爲hello.cpp
-
add_executable(hello hello.cpp)
-
# 以下使能OpenCV的路徑查找
-
find_package(OpenCV REQUIRED)
-
# 以下是將可執行文件與一些.so文件建立動態鏈接關係,
-
# 用到的有libtensorflow_cc.so,libtensorflow_framework.so,以及opencv相關的so
-
target_link_libraries(hello tensorflow_cc tensorflow_framework ${OpenCV_LIBS})
執行以下步驟通過cmake去編譯C++文件,生產可執行文件,然後執行:
# 先創建編譯生成文件夾
mkdir build
cd build
# 在build中執行cmkae,生成makefile
cmake ..
# 執行make,產生可執行文件
make
# 執行可執行文件
./hello
C++端的可執行文件運行結果如下:與上面python代碼的運行結果也基本一致,說明整個pipline是可以work的