之前的筆記有一篇完成了對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預測有點過分啦,效果不是很好啊。
你,
一會看我,
一會看雲。
我覺得,
你看我時很遠,
你看雲時很近。
--顧城