之前的笔记有一篇完成了对tensorflow的编译,然后简单写了个测试程度,所以,应该是可以用了的,然后上篇简单写了个基于Keras的手写数字识别的的模型需要你连了一下,那么现在就试试用tensorflow的 C++ API调用训练好的模型测试下。这里推荐一个GitHub上找到的项目,用的C++调用tensorflow API的。
首先,前面把模型保存为了.h5文件,但是,很遗憾,我查到的资料都是用C++加载的.pb文件,所以我还是得把模型保存为.pb文件或者将.h5文件转存为.pb文件,然后上帝说有光就有了光,我说要有个保存模型为.pb文件的,上帝就给了一个代码:
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=200
h5_model_path='./best_model_ep{}.h5'.format(epochs)
output_path='.'
pb_model_name='./best_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)
就这样,大神给了一个将.h5模型转存为.pb文件的代码。接下来就是加载模型、加载数据、然后测试的步骤了。
首先,tensorflow要运行的话,要先创建一个会话(Session),C++创建一个会话的代码:
Session* session;
Status status = NewSession(SessionOptions(), &session);
这里是先声明一个Session的指针,然后调用NewSession()函数来创建一个会话,NewSession()函数的原型为
Status NewSession(const SessionOptions& options, Session** out_session);
Session* NewSession(const SessionOptions& options);
上面两个原型,tensorflow是比较推荐用第一种的,第一种的话是传入一个Session的二级指针,然后会返回一个状态量,通过访问状态量的ok()来判断是否返回成功,第二个函数则是直接返回一个Session的指针。
接下来就是创建一个图模型变量和从本地加载训练好的模型到图变量中:
GraphDef graphdef;
Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef);
同样,也是会返回一个状态量,也可以判断是否加载成功。加载模型图后要将图导入到会话中,这之后就需要调用会话的Creat()函数来创建会话的图模型:
Status status_create = session->Create(graphdef);
好了,前面的铺垫差不多就这样了,现在就来加载数据吧。
说到加载图像数据,怎么可以少了我们的OpenCV呢,用OpenCV从本地加载一幅图像,然后需要将其转为tensor张量,怎么转呢,大神给了代码,然后我理解了一下,就是cv::Mat是有一个指向数据的指针*data的,然后tensorflow的张量tensor也有一个指向数据的指针:
/// typedef float T;
/// Tensor my_ten(...built with Shape{planes: 4, rows: 3, cols: 5}...);
/// // 1D Eigen::Tensor, size 60:
/// auto flat = my_ten.flat<T>();
/// // 2D Eigen::Tensor 12 x 5:
/// auto inner = my_ten.flat_inner_dims<T>();
/// // 2D Eigen::Tensor 4 x 15:
/// auto outer = my_ten.shaped<T, 2>({4, 15});
/// // CHECK fails, bad num elements:
/// auto outer = my_ten.shaped<T, 2>({4, 8});
/// // 3D Eigen::Tensor 6 x 5 x 2:
/// auto weird = my_ten.shaped<T, 3>({6, 5, 2});
/// // CHECK fails, type mismatch:
/// auto bad = my_ten.flat<int32>();
template <typename T>
typename TTypes<T>::Flat flat()
{
return shaped<T, 1>({NumElements()});
}
这是一个模板函数,返回一个指向数据的指针,所以有想到很巧妙的通过构建一个Mat变量,将其指针指向tensor,这样对Mat进行赋值的时候,tensor不就获取了数据了吗。因为cv::Mat有一个构造函数是:
Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP);
其第三个变量是数据类型,第四个变量就是数据指针,通过它,我们输入tensor的指针,然后在构建Mat的时候他们的数据是同一块内存的数据,并且对Mat进行操作的时候也会改变tensor的值。那么如何构建一个tensor呢,tensor是tensorflow的一个类来的,其类名为:Tensor,Tensor有一个构造函数如下:
Tensor(DataType type, const TensorShape& shape);
第一个输入变量是tensor的数据类型,第二个变量是tensor的形状,这样构建一个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=1-img/255;
float *p = output_tensor->flat<float>().data();
cv::Mat tempMat(input_rows, input_cols, CV_32FC1, p);
img.convertTo(tempMat,CV_32FC1);
}
代码来自那个GitHub上的项目,但是呢,这个代码其实是有一点小问题的就是关于下面这句的操作我持怀疑态度,感觉有一点问题,因为我这边出了点小问题,然后把它改了:
img.convertTo(img,CV_32FC1);
好了,这样输入数据tensor也有了,模型也加载了,会话也创建了,是该跑一下了。在跑一个模型的时候,我们需要指定从模型哪里输入,从哪里获取输出,也就是说,其实我们可以获取中间层作为输入,获取中间层作为输出,有什么用呢,可视化中间层的时候不就很有用了吗。这里我就用模型的第一层作为输入,模型的输出层作为输出,怎么获取这两层呢?是可以通过层的命名来获取的,怎么知道层的命名呢,一般就是自己定义层的时候给个命名,不过呢,python的API里面应该是可以获取层的命名的,尤其输入输出层,C++不知道怎么用,后面再研究。前面转pb文件的python代码里也给出了相关的方法,很简单就是:
print('input is :', net_model.input.name)
print ('output is:', net_model.output.name)
有了层的命名后,就用这命名从模型中取得层:
string input_tensor_name="conv2d_1_input";
string output_tensor_name="dense_2/Softmax";
vector<tensorflow::Tensor> outputs;
string output_node = output_tensor_name;
Status status_run = session->Run( { { input_tensor_name, resized_tensor } }, { output_node }, {}, &outputs);
可以看到,调用的是session->Run()函数,并且结果是保存再一个std::vector<tensorflow::Tensor>里面的。每个Tensor都有一个tensor()的模板函数:
template <typename T>
T* Tensor::base() const
{
return buf_ == nullptr ? nullptr : buf_->base<T>();
}
template <typename T, size_t NDIMS>
typename TTypes<T, NDIMS>::Tensor Tensor::tensor()
{
CheckTypeAndIsAligned(DataTypeToEnum<T>::v());
return typename TTypes<T, NDIMS>::Tensor(base<T>(), shape().AsEigenDSizes<NDIMS>());
}
通过tensor()获取到的变量,可以访问返回值的一些信息,包括数据的尺寸、预测值等信息:
Tensor t = outputs[0];
auto tmap = t.tensor<float, 2>();
int output_dim = t.shape().dim_size(1);
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);
}
}
最后显示下我昨天简单训练的手写数字识别模型:
这个8预测有点过分啦,效果不是很好啊。
你,
一会看我,
一会看云。
我觉得,
你看我时很远,
你看云时很近。
--顾城