本篇博客記錄筆者最近在在線推理服務(wù)中使用 Tensorflow C++ 接口的若干心得和疑(tu)惑(cao),整個(gè)流程包括創(chuàng)建 session 嵌戈,加載 graph ,填充 tensor ,運(yùn)行 session ,等等胖烛。注意,因?yàn)?tensorflow 2.0 沒(méi)有普及诅迷,考慮穩(wěn)定性佩番,本篇博客代碼均基于 tensorflow 1.12 。
1. session
1.1 session & client_session
我們知道 tensorflow 所有節(jié)點(diǎn)都處于 graph罢杉,而 graph 則和 session 綁定答捕,所以線上的實(shí)時(shí)預(yù)測(cè)服務(wù)在初始化時(shí)需要?jiǎng)?chuàng)建 session 并載入 graph。網(wǎng)上找到的很多例子都是用 session
屑那,而官網(wǎng)上只提供了 client_session
的接口拱镐,兩者的主要區(qū)別在于 Run()
函數(shù)的參數(shù)不一樣,client_session
如下:
Status Run(
const FeedType & inputs,
const std::vector< Output > & fetch_outputs,
const std::vector< Operation > & run_outputs,
std::vector< Tensor > *outputs
) const
這個(gè) FeedType 定義如下:
typedef std::unordered_map<Output, Input::Initializer, OutputHash> FeedType;
對(duì)比 session
的 Run()
:
virtual Status Run(const RunOptions& run_options,
const std::vector<std::pair<string, Tensor> >& inputs,
const std::vector<string>& output_tensor_names,
const std::vector<string>& target_node_names,
std::vector<Tensor>* outputs, RunMetadata* run_metadata);
看出區(qū)別沒(méi)有持际?哈希表的 key 不同沃琅,一個(gè)是自定義類 Output,一個(gè)是 string蜘欲。
我們要運(yùn)行會(huì)話進(jìn)行預(yù)測(cè)益眉,不妨把模型當(dāng)成一個(gè)黑盒子,那么關(guān)鍵的步驟有兩步姥份,喂數(shù)據(jù)和取結(jié)果郭脂,而喂數(shù)據(jù)要給不同的 placeholder
喂不同的數(shù)據(jù),取結(jié)果則需要知道從哪個(gè) operation
取結(jié)果澈歉,所以關(guān)鍵是要有一個(gè)哈希表記錄 placeholder 或者 operation展鸡。client_session
是用 tensorflow c++ api 的 Placeholder 對(duì)象或者 Operation 對(duì)象作為哈希表的 key ,而 session
則是用 string埃难。
所以莹弊,訓(xùn)練可以用 client_session
或者 session
,而預(yù)測(cè)只能用 session
涡尘。因?yàn)槿绻怯?xùn)練的話忍弛,可以掉用 tf c++ api 創(chuàng)建 session
graph
placeholder
,可以得到 Placeholder
對(duì)象再 Run()
考抄,但是預(yù)測(cè)過(guò)程是從模型文件中建立 graph细疚,無(wú)法得到 placeholder 對(duì)象,所以也就無(wú)法使用 client_session
了川梅。而 session
此時(shí)可以大展身手了疯兼,只需要訓(xùn)練方為需要輸入和輸出的節(jié)點(diǎn)命名,預(yù)測(cè)方就可以通過(guò)名稱找到對(duì)應(yīng)的節(jié)點(diǎn)挑势,喂數(shù)據(jù)或者取結(jié)果就都可以進(jìn)行了镇防。
值得注意的是,client_session
也是通過(guò)封裝 session
來(lái)實(shí)現(xiàn)的潮饱。所以為什么 tensorflow 官方文檔只有 client_session
而沒(méi)有 session
来氧,實(shí)在令人困惑啊。
1.2 創(chuàng)建 session
tensorflow::NewSession()
可用于創(chuàng)建 session
tensorflow::SessionOptions options;
auto session = std::unique_ptr<tensorflow::Session>(tensorflow::NewSession(options));
2. load graph
關(guān)于圖香拉,模型文件在存儲(chǔ)圖時(shí)將圖給“骨肉分離”了啦扬。骨為結(jié)構(gòu),存儲(chǔ)節(jié)點(diǎn)與節(jié)點(diǎn)之間的連接凫碌,肉為數(shù)值扑毡,存儲(chǔ) variable 大小,有篇博客很好地解釋了 tf 的圖:Tensorflow框架實(shí)現(xiàn)中的“三”種圖盛险。有兩種方式加載圖瞄摊。
方法一 session->Run()
執(zhí)行 restore_op勋又,代碼如下:
tensorflow::MetaGraphDef graph_def;
std::string meta_graph_path = "model.meta";
auto status = ReadBinaryProto(tensorflow::Env::Default(), meta_graph_path, &graph_def);
if (!status.ok()) {
...
}
status = session->Create(graph_def.graph_def());
if (!status.ok()) {
...
}
auto restore_op_name = graph_def.saver_def().restore_op_name();
auto filename_tensor_name = graph_def.saver_def().filename_tensor_name();
tensorflow::Input::Initializer filename({model_dir});
status = session->Run({{filename_tensor_name, filename.tensor}}, {}, {restore_op_name}, nullptr);
if (!status.ok()) {
...
}
方法二 tensorflow::SavedModelBundle
這種方法沒(méi)有深究。
用哪種方式其實(shí)是由訓(xùn)練方導(dǎo)出模型的方式?jīng)Q定的换帜⌒ㄈ溃可以查看 SavedModelBundle
源碼,其實(shí)和方法一類似惯驼,分別調(diào)用了 ReadBinaryProto()
session->Run()
蹲嚣,不同之處在于其讀取的 pb 文件名稱是固定的,“saved_model.pb”祟牲,也就是說(shuō)方法二無(wú)法自定義 pb 名稱隙畜。
3. initalize tensor
初始化 tensor 有三種方式,可以參考 stackoverflow 上的一個(gè)回答 How to fill a tensor in C++
3.1 tensorflow::Input::Initializer
一種方法是使用 tensorflow::Input::Initializer
说贝,用法如下:
tensorflow::Input::Initializer x0_index({0, 0, 1, 1}, tensorflow::TensorShape({2, 2}));
不過(guò)使用過(guò)程中出現(xiàn)過(guò)一個(gè)報(bào)錯(cuò):
Invalid argument: Expects arg[0] to be int64 but int32 is provided
這是因?yàn)?Initalizer 用模板傳參议惰,不能正確識(shí)別類型(我需要 int64,但是模板識(shí)別為 int32)狂丝,導(dǎo)致在使用 tensor 時(shí)報(bào)錯(cuò).
官方 api 可以看到
tensorflow::Input::Initializer::Initializer(
const std::initializer_list< T > & v,
const TensorShape & shape
)
解決方法是换淆,每個(gè)數(shù)據(jù)加上 LL 后綴
3.2 x.tensor<>()() = XX
另一種方法是逐個(gè)賦值:
tensorflow::Tensor x0(tensorflow::DT_FLOAT, tensorflow::TensorShape({2,2}));
x0.tensor<float, 2>()(0,0) = 1;
x0.tensor<float, 2>()(0,1) = 2;
x0.tensor<float, 2>()(1,0) = 2;
x0.tensor<float, 2>()(1,1) = 3;
逐個(gè)賦值看似更麻煩,但是在遍歷一個(gè)容器(例如 vector )填充 tensor 時(shí)更方便几颜,這是因?yàn)?sd::initializer_list
只支持初始化列表的初始化方式倍试,無(wú)法用其它容器的迭代器初始化,而且初始化后也不能 push蛋哭,所以也不能寫個(gè) for 循環(huán)喂數(shù)據(jù)給它县习,總之很不好用。tensorflow 用sd::initializer_list
這個(gè)容器應(yīng)該是出于性能考慮谆趾,但是很不方便躁愿,不過(guò)不用擔(dān)心,還有第三種方法沪蓬。
中間還遇到一個(gè)數(shù)據(jù)類型問(wèn)題彤钟,就是編譯如下的代碼:
tensorflow::Tensor x_index(tensorflow::DT_INT64, tensorflow::TensorShape({100, 100, 2}));
for (int64_t i = 0; i < 100; ++i) {
for (int64_t j = 0; j < 100; ++j) {
x_index.tensor<int64_t, 3>()(i, j, 0) = i;
x_index.tensor<int64_t, 3>()(i, j, 1) = j;
}
}
報(bào)錯(cuò):
tensorflow/core/framework/types.h:357:3: error: static assertion failed: Specified Data Type not supported
查看源文件,發(fā)現(xiàn)問(wèn)題所在跷叉,tensorflow 會(huì)將 基本數(shù)據(jù)類型(例如 float) 轉(zhuǎn)成 tensorflow 自定義數(shù)據(jù)類型(例如 DT_FLOAT)逸雹,而 int64_t 不在其轉(zhuǎn)換范圍內(nèi)。解決方法是云挟,要么使用 long long int 梆砸,要么使用 tensorflow 自定義的 int64
,如下:
x_index.tensor<long long int, 3>()(i, j, 0) = i;
x_index.tensor<tensorflow::int64, 3>()(i, j, 0) = i;
3.3 flat()
第三種方式 flat()
std::copy_n(x_indices.begin(), x_indices.size(), x_index.flat<tensorflow::int64>().data());
個(gè)人覺(jué)得這種方式最優(yōu)园欣,先把所有元素按順序放進(jìn)隨便一個(gè)只要不是 sd::initializer_list
的容器中(按順序是指從低維到高維每個(gè)維度依次遍歷帖世,例如二維矩陣按照從左到右從上到下的順序),然后再灌進(jìn) tensor沸枯。flat()
這個(gè)函數(shù)很形象日矫,把高維的 tensor 拍扁了赂弓,拍成一維數(shù)組,這樣 fill tensor 就方便多了搬男,不用像 3.2 那樣再考慮某個(gè)元素放在第幾維的位置拣展。
4. sparse tensor
為什么上面代碼的 tensor 變量名都是 x_index 呢,其實(shí)都是為了構(gòu)建稀疏張量做準(zhǔn)備的缔逛。稀疏張量只有很少的元素非零动漾,所以只需標(biāo)示出非零元素的位置和值非洲。一個(gè) sparse tensor 由 x_index, x_value, x_shape 三部分構(gòu)成,x_index 表示 tensor 非零元素的位置溉卓,x_value 表示非零元素的值于毙,x_shape 表示 tensor 的形狀敦冬。構(gòu)建一個(gè) sparse tensor 代碼如下:
tensorflow::Input::Initializer x0_index({0LL, 0LL, 1LL, 1LL}, tensorflow::TensorShape({2, 2}));
tensorflow::Input::Initializer x0_value({1.0f, 2.0f}, tensorflow::TensorShape({2}));
tensorflow::TensorShape x0_shape({2, 2});
tensorflow::sparse::SparseTensor x0_sparse_tensor(x0_index.tensor, x0_value.tensor, x0_shape);
構(gòu)建好了 SparseTensor 后,就可以使用了唯沮,怎么用呢脖旱?嗯,沒(méi)法用介蛉。是的萌庆,Run()
沒(méi)有 SparseTensor 作為輸入?yún)?shù)的重載函數(shù)。查看 tensorflow/core/public/session.h:
virtual Status Run(const std::vector<std::pair<string, Tensor> >& inputs,
const std::vector<string>& output_tensor_names,
const std::vector<string>& target_node_names,
std::vector<Tensor>* outputs) = 0;
Run()
只支持類型為 Tensor 的輸入币旧,所以 tensorflow c++ 提供 SparseTensor 接口是在逗你玩践险?只能看看不能用?
如果要使用 SparseTensor 吹菱,有一種解決方法巍虫,就是將 feed_dict 里輸入張量 從一個(gè) placeholder
改為 x_index, x_value, x_shape 三個(gè) placeholder
,然后再以此生成 SparseTensor鳍刷,python 代碼如下:
x0_index = tf.placeholder(tf.int64, name = 'x0_index')
x0_value = tf.placeholder(tf.float32, name = 'x0_value')
x0_shape = tf.placeholder(tf.int64, name = 'x0_shape')
x0 = tf.SparseTensor(x0_index, x0_value, x0_shape)
這樣 C++ 端的代碼只需要喂這三個(gè)非稀疏 Tensor 就行了占遥。