Tensorflow C++ api 筆記

本篇博客記錄筆者最近在在線推理服務(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ì)比 sessionRun()

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 就行了占遥。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市输瓜,隨后出現(xiàn)的幾起案子瓦胎,更是在濱河造成了極大的恐慌,老刑警劉巖前痘,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凛捏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡芹缔,警方通過(guò)查閱死者的電腦和手機(jī)坯癣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)最欠,“玉大人示罗,你說(shuō)我怎么就攤上這事惩猫。” “怎么了蚜点?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵轧房,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我绍绘,道長(zhǎng)奶镶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任陪拘,我火速辦了婚禮厂镇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘左刽。我一直安慰自己捺信,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布欠痴。 她就那樣靜靜地躺著迄靠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喇辽。 梳的紋絲不亂的頭發(fā)上掌挚,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音茵臭,去河邊找鬼疫诽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛旦委,可吹牛的內(nèi)容都是我干的奇徒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼缨硝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼摩钙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起查辩,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤胖笛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后宜岛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體长踊,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年萍倡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了身弊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阱佛,靈堂內(nèi)的尸體忽然破棺而出帖汞,到底是詐尸還是另有隱情,我是刑警寧澤凑术,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布翩蘸,位于F島的核電站,受9級(jí)特大地震影響淮逊,放射性物質(zhì)發(fā)生泄漏催首。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一壮莹、第九天 我趴在偏房一處隱蔽的房頂上張望翅帜。 院中可真熱鬧,春花似錦命满、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至杂抽,卻和暖如春诈唬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缩麸。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工铸磅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杭朱。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓阅仔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親弧械。 傳聞我的和親對(duì)象是個(gè)殘疾皇子八酒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容