安装好C++版的TensorFlow之后,我们就可以用C++来部署python训练好的TensorFlow模型了。安装C++版的TensorFlow的教程可以参考这里。部署TensorFlow模型主要分为两步,第一步是用python训练模型,然后保存模型为.pb格式的二进制文件;第二步则是在C++中加载python保存的模型并进行预测。
1、python训练模型并保存
这里我们以mnist数据集为例,训练一个三层的多层感知机对手写数字图片进行识别,具体的训练代码如下:
#coding=utf-8
import pickle
import numpy as np
import tensorflow as tf
import time
import os
from tensorflow.python.framework.graph_util import convert_variables_to_constants
# 定义一个mnist数据集的类
class mnistReader():
def __init__(self, mnistPath, onehot = True):
self.mnistPath = mnistPath
self.onehot = onehot
self.batch_index = 0
print ('read:',self.mnistPath)
fo = open(self.mnistPath, 'rb')
self.train_set,self.valid_set,self.test_set = pickle.load(fo, encoding='bytes')
fo.close()
self.data_label_train = list(zip(self.train_set[0], self.train_set[1]))
np.random.shuffle(self.data_label_train)
# 获取下一个训练集的batch
def next_train_batch(self, batch_size = 100):
if self.batch_index < len(self.data_label_train)/batch_size:
print ("batch_index:",self.batch_index )
datum = self.data_label_train[self.batch_index*batch_size:(self.batch_index+1)*batch_size]
self.batch_index+=1
return self._decode(datum, self.onehot)
else:
self.batch_index=0
np.random.shuffle(self.data_label_train)
datum=self.data_label_train[self.batch_index*batch_size:(self.batch_index+1)*batch_size]
self.batch_index+=1
return self._decode(datum,self.onehot)
# 获取测试集的数据
def test_data(self):
tdata, tlabel = self.test_set
data_label_test=list(zip(tdata,tlabel))
return self._decode(data_label_test,self.onehot)
# 把一个batch的训练数据转换为可以放入模型训练的数据
def _decode(self, datum, onehot):
rdata=list()
rlabel=list()
if onehot:
for d,l in datum:
img = np.reshape(d, (28,28))
img = np.expand_dims(img, 2)
rdata.append(img)
hot = np.zeros(10)
hot[int(l)] = 1
rlabel.append(hot)
else:
for d,l in datum:
img = np.reshape(d, (28,28))
img = np.expand_dims(img,2)
rdata.append(img)
rlabel.append(int(l))
return rdata,rlabel
#多层感知机模型(只有一个隐藏层)
def multi_perceptron():
batch_size = 100 # batch大小
height = 28 # 图片高度
width = 28 # 图片宽度
channel = 1 # 图片通道数
in_units = 784 # 多层感知机的输入
h1_units=300 # MLP隐藏层的输出节点数
mnist_path = "E:/testdata/mnist.pkl" # mnist数据集路径
save_path = "./output" #保存模型的路径
images = tf.placeholder(tf.float32, shape = [None, height, width, channel],name = "images")
labels = tf.placeholder(tf.float32,[None, 10], name = "labels")
w1=tf.Variable(tf.truncated_normal([in_units,h1_units],stddev=0.1))
b1=tf.Variable(tf.zeros([h1_units]))
w2=tf.Variable(tf.truncated_normal([h1_units,10],stddev=0.1))
b2=tf.Variable(tf.zeros([10]))
# 网络各层的计算
inputs = tf.reshape(images, [-1, in_units])
hidden1 = tf.nn.relu(tf.matmul(inputs, w1) + b1)
hidden1_drop = tf.nn.dropout(hidden1, 0.75)
logits = tf.nn.softmax(tf.matmul(hidden1_drop, w2) + b2, name = "logits")
# 定义交叉熵为损失函数和优化器
cross_entropy=tf.reduce_mean(-tf.reduce_sum(labels*tf.log(logits),reduction_indices=[1]))
train_step=tf.train.AdagradOptimizer(0.3).minimize(cross_entropy)
# 开始训练
print ("begin training ...\n")
sess = tf.InteractiveSession() # 创建会话
tf.global_variables_initializer().run() # 初始化全局变量
mnist = mnistReader(mnistPath = mnist_path)
for i in range(1000):
image_batch, label_batch = mnist.next_train_batch()
train_step.run({images:image_batch, labels:label_batch})
# 计算测试集上的准确率
correction_prediction = tf.equal(tf.argmax(labels, 1), tf.argmax(logits, 1))
accuracy = tf.reduce_mean(tf.cast(correction_prediction, tf.float32))
test_data,test_label = mnist.test_data()
print (accuracy.eval({images : test_data, labels :test_label}))
# 保存模型的参数
saver = tf.train.Saver()
saver.save(sess, os.path.join(save_path, "model.ckpt"))
graph = convert_variables_to_constants(sess, sess.graph_def, ["logits"])
tf.train.write_graph(graph, save_path, 'model.pb',as_text=False)
print ("save model sucessful")
if __name__ == '__main__':
multi_perceptron() # 多层感知机得到的准确率大概为0.98左右
模型训练好之后,保存模型文件为./output/model.pb。
2、使用C++加载python训练好的模型并进行预测
(1) 文件结构如下,需要将python保存的模型复制到build文件夹下:
├── src
| └── hello.cpp
|
├── CMakeLists.txt
|
├── build
| └──model.pb
| |
| └──digit.jpg
(2)hello.cpp的内容为(C++程序主要参考官网的实现例子):
#include <fstream>
#include <utility>
#include <vector>
#include <Eigen/Core>
#include <Eigen/Dense>
#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/platform/env.h"
#include "tensorflow/core/platform/init_main.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/types.h"
#include "tensorflow/core/public/session.h"
#include "tensorflow/core/util/command_line_flags.h"
using namespace std;
using namespace tensorflow;
using namespace tensorflow::ops;
using tensorflow::Flag;
using tensorflow::Tensor;
using tensorflow::Status;
using tensorflow::string;
using tensorflow::int32;
//读取一个文件放入一个tensor之中
static Status ReadEntireFile(tensorflow::Env* env, const string& filename,Tensor* output) {
tensorflow::uint64 file_size = 0;
TF_RETURN_IF_ERROR(env->GetFileSize(filename, &file_size)); //获取文件的大小
string contents;
contents.resize(file_size); //文件内容
std::unique_ptr<tensorflow::RandomAccessFile> file; //创建一个指向文件的智能指针
TF_RETURN_IF_ERROR(env->NewRandomAccessFile(filename, &file)); //将这个指针指向文件
tensorflow::StringPiece data; //声明stringpiece类型的data存放文件内容
TF_RETURN_IF_ERROR(file->Read(0, file_size, &data, &(contents)[0])); //将文件读入data之中
if (data.size() != file_size) {
return tensorflow::errors::DataLoss("Truncated read of '", filename,"'
expected ", file_size, " got ", data.size());
}
output->scalar<string>()() = string(data); //将data的值赋给output的tensor
return Status::OK();
}
//从图像读取数据并放入tensor之中
Status ReadTensorFromImageFile(const string& file_name, const int input_height,
const int input_width, const float input_mean,
const float input_std,
std::vector<Tensor>* out_tensors) {
auto root = tensorflow::Scope::NewRootScope();
using namespace ::tensorflow::ops; // NOLINT(build/namespaces)
string input_name = "file_reader";
string output_name = "normalized";
//把文件读入一个叫input的tensor中
Tensor input(tensorflow::DT_STRING, tensorflow::TensorShape()); //声明一个string类型的tensor,名为input
TF_RETURN_IF_ERROR(ReadEntireFile(tensorflow::Env::Default(), file_name, &input)); //把文件读入这个tensor中
//用一个placeholder来读取input的数据
auto file_reader = Placeholder(root.WithOpName("input"), tensorflow::DataType::DT_STRING);
std::vector<std::pair<string, tensorflow::Tensor>> inputs = {{"input", input},};
//确定是什么类型的文件并且进行解码
const int wanted_channels = 1;
tensorflow::Output image_reader;
if (tensorflow::str_util::EndsWith(file_name, ".png")) {
image_reader = DecodePng(root.WithOpName("png_reader"), file_reader,DecodePng::Channels(wanted_channels));
}
else if (tensorflow::str_util::EndsWith(file_name, ".gif")) {
// gif decoder returns 4-D tensor, remove the first dim
image_reader = Squeeze(root.WithOpName("squeeze_first_dim"),DecodeGif(root.WithOpName("gif_reader"), file_reader));
}
else if (tensorflow::str_util::EndsWith(file_name, ".bmp")) {
image_reader = DecodeBmp(root.WithOpName("bmp_reader"), file_reader);
}
else {
// Assume if it's neither a PNG nor a GIF then it must be a JPEG.
image_reader = DecodeJpeg(root.WithOpName("jpeg_reader"), file_reader,DecodeJpeg::Channels(wanted_channels));
}
//现在把图像数据转为float,这样我们才对它进数据操作
auto float_caster = Cast(root.WithOpName("float_caster"), image_reader, tensorflow::DT_FLOAT);
// The convention for image ops in TensorFlow is that all images are expected
// to be in batches, so that they're four-dimensional arrays with indices of
// [batch, height, width, channel]. Because we only have a single image, we
// have to add a batch dimension of 1 to the start with ExpandDims().
auto dims_expander = ExpandDims(root.WithOpName("expand"), float_caster, 0);
// Bilinearly resize the image to fit the required dimensions.
// auto resized = ResizeBilinear( //图像进行双线性插值到固定尺寸
//root, dims_expander,
//Const(root.WithOpName("size"), {input_height, input_width}));
// Subtract the mean and divide by the scale. //减去图像均值
//Div(root.WithOpName(output_name), Sub(root, resized, {input_mean}),
//{input_std});
// float input_max = 255;
// Div(root.WithOpName("div"),dims_expander,input_max); //图像除以255,进行归一化处理
// This runs the GraphDef network definition that we've just constructed, and
// returns the results in the output tensor.
tensorflow::GraphDef graph;
TF_RETURN_IF_ERROR(root.ToGraphDef(&graph));
std::unique_ptr<tensorflow::Session> session(tensorflow::NewSession(tensorflow::SessionOptions()));
TF_RETURN_IF_ERROR(session->Create(graph));
TF_RETURN_IF_ERROR(session->Run({inputs}, {"expand"}, {}, out_tensors));
return Status::OK();
}
//主函数
int main(int argc, char** argv ){
Session* session;
Status status = NewSession(SessionOptions(), &session); //创建新会话Session
string model_path="model.pb"; //保存的模型路径
GraphDef graphdef; //当前模型的图定义
Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef); //从pb文件中读取图模型;
if (!status_load.ok()) {
std::cout << "ERROR: Loading model failed..." << model_path << std::endl;
std::cout << status_load.ToString() << "\n";
return -1;
}
Status status_create = session->Create(graphdef); //将图模型导入会话Session中;
if (!status_create.ok()) {
std::cout << "ERROR: Creating graph in session failed..." << status_create.ToString() << std::endl;
return -1;
}
cout << "Session successfully created."<< endl;
//开始进行预测
string image_path= argv[1]; //图片路径
int input_height =28; //高度
int input_width=28; //宽度
int input_mean=0; //均值
int input_std=1; //方差
std::vector<Tensor> resized_tensors; //用于保存读取的图片的tensor数组
//从图片读取数据并放入一个tensor之中
cout<<"begin load image..."<<endl;
Status read_tensor_status = ReadTensorFromImageFile(image_path, input_height, input_width, input_mean,
input_std, &resized_tensors);
if (!read_tensor_status.ok()) {
LOG(ERROR) << read_tensor_status;
cout<<"resing error"<<endl;
return -1;
}
cout<<"load image successful!"<<endl;
const Tensor& resized_tensor = resized_tensors[0];
std::cout << resized_tensor.DebugString()<<endl; //打印出输入模型的tensor形状
vector<tensorflow::Tensor> outputs;
string output_node = "logits"; //这里的输出名logits要和模型的输出相匹配
//开始预测,这里的输入名images要和模型的输入相匹配
Status status_run = session->Run({{"images", resized_tensor}}, {output_node}, {}, &outputs);
if (!status_run.ok()) {
std::cout << "ERROR: RUN failed..." << std::endl;
std::cout << status_run.ToString() << "\n";
return -1;
}
//取出输出值
std::cout << "Output tensor size:" << outputs.size() << std::endl;
for (std::size_t i = 0; i < outputs.size(); i++) {
std::cout << outputs[i].DebugString()<<endl; //打印出模型输出的tensor的形状
}
Tensor t = outputs[0]; // 取出第一个tensor
int ndim2 = t.shape().dims(); // 得到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:获取最终的预测label和概率值
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);
}
}
// 打印日志
std::cout << "Final class id: " << output_class_id << std::endl;
std::cout << "Final class prob: " << output_prob << std::endl;
//auto f=t.shaped<float,2>({2,5});
//cout << f <<endl;
//Eigen::array<int, 1> reduction_dims{0};
//cout<< f.maximum(reduction_dims)<<endl;
//Eigen::array<int, 2> offsets = {0, 0};
//Eigen::array<int, 2> extents = {2, 1};
//auto slice = f.slice(offsets, extents);
//float *index = slice.data();
//cout << "slice" << endl << slice << endl;
//cout << slice.argmax()<<endl;
return 0;
}
(3)CMakeLists的内容为
cmake_minimum_required (VERSION 2.8.8) # cmake的最低版本号
project (tf_test) # 工程名
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11 -W") # 设置编译器,这里设为C++编译器
link_directories(path_to_tensorflow/bazel-bin/tensorflow) # 链接库搜索目录
include_directories( # 头文件的搜索目录
path_to_tensorflow/
path_to_tensorflow/bazel-genfiles
path_to_tensorflow/bazel-bin/tensorflow
path_to_tensorflow/tensorflow/contrib/makefile/downloads/nsync/public
path_to_tensorflow/tensorflow/contrib/makefile/downloads/absl
path_to_tensorflow/tensorflow/contrib/makefile/gen/protobuf/include
/usr/local/include/eigen3
)
add_executable(tf_test hello.cpp) # 将源码编译为目标文件
target_link_libraries(tf_test tensorflow_cc tensorflow_framework) # 把动态链接库链接到目标文件中
(4)编译和运行
cd build
cmake ..
make
./tf_test digit.jpg
输出结果为:
Session successfully created.
begin load image...
load image successful!
Tensor<type: float shape: [1,28,28,1] values: [[[253][21][0]]]...>
Output tensor size:1
Tensor<type: float shape: [1,10] values: [0 1 0...]...>
Class 0 prob:0,
Class 1 prob:1,
Class 2 prob:0,
Class 3 prob:0,
Class 4 prob:0,
Class 5 prob:0,
Class 6 prob:0,
Class 7 prob:0,
Class 8 prob:0,
Class 9 prob:0,
Final class id: 1
Final class prob: 1
参考:https://blog.csdn.net/zwx1995zwx/article/details/79064064
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/examples/label_image/main.cc
版权声明:本文为MOU_IT原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。