之前實習的時候訓練一個給ASR文本添加大小寫和標點的模型圣贸,框架用的是tensorflow r1.2(本文其實和tensorflow版本無關(guān))博敬。模型訓好后mentor說要轉(zhuǎn)成C++上線双揪,當時差點崩潰片吊,由于太懶,不想換框架重寫就只好試試tensorflow的C++ API了,由于公司服務器的權(quán)限問題也是躺了不少的坑疮装,這里簡單總結(jié)一下TF模型轉(zhuǎn)C++ API以及轉(zhuǎn)gRPC服務的基本步驟和遇到的一些很迷的Errors。
關(guān)于Tensorflow模型到gRPC服務粘都,tensorflow有個神奇API叫Tensorflow Serving廓推,大家可以試一試。不過本文不是采用這種方式翩隧,而是先轉(zhuǎn)C++接口樊展,再用gRPC寫接口服務,其實原理是一樣的堆生。
-
Tensorflow C++ API
Tensorflow提供的C++API能夠恢復python訓練好的模型計算圖和參數(shù)到C++環(huán)境中专缠;通過向Placeholder傳入數(shù)據(jù)便可以得到Eigen::Tensor類型的返回。python下的Tensorflow依賴numpy矩陣運算庫淑仆,而在C++下依賴Eigen::Tensor庫涝婉,所以在使用C++ API之前需要先安裝好對應版本的Eigen庫;
-
下載Tensorflow源碼
下載最新的Tensorflow源碼渔工,這個和你使用什么版本Tensorflow訓練模型沒有關(guān)系。之后就需要把Tensorflow編譯成我們需要的動態(tài)鏈接庫桥温;
$ git clone https://github.com/tensorflow/tensorflow.git
-
安裝Bazel
這里需要注意一下引矩,版本太新和太舊的Bazel在編譯Tensorflow的時候都會報錯,這里舉例我用過的版本組合:Bazel-0.10.0(Tensorflow-r1.7);Bazel-0.8.0(Tensorflow-r1.5)脓魏;Bazel-0.4.5(Tensorflow-r1.2)兰吟。以上組合并不固定,經(jīng)供參考(本文是Tensorflow1.7)茂翔。由于在公司服務器上工作混蔼,所有的third-party都需要安裝在自己的目錄。
?1 . 非root安裝JDK8:jdk-8u161-linux-x64.tar.gz珊燎。Bazel依賴JDK8惭嚣,wget下載后解壓,把jdk添加到環(huán)境變量悔政,把以下代碼添加到$HOME/.bashrc
:
export JAVA_HOME="$HOME/tools/java/jdk1.8.0_161"
export JAVA_BIN=$JAVA_HOME/bin
export JAVA_LIB=$JAVA_HOME/lib
export CLASSPATH=.:$JAVA_LIB/tools.jar:$JAVA_LIB/dt.jar
export PATH=$JAVA_BIN:$PATH
?2 . 安裝Bazel:各版本地址release晚吞,本文是bazel-0.10.0,下載好.sh
文件之后執(zhí)行一下命令:
chmod +x bazel-<version>-installer-linux-x86_64.sh
./bazel-<version>-installer-linux-x86_64.sh --user
??bazel被裝到了$HOME/bin
目錄下谋国,添加到環(huán)境就OK了槽地;之后輸入bazel version
看看版本是否安裝成功;
-
安裝Eigen3
??之前提到了Tensorflow依賴Eigen矩陣運算庫芦瘾,在編譯之前需要安裝對應的版本捌蚊;關(guān)于Eigen同樣是一個坑,不對應的版本依然會讓Tensorflow編譯失敗近弟,這里提供一個最保險的方法缅糟,就是去tensorflow的tensorflow/tensorflow/workspace.bzl
里下載;在 workspace.bzl
中找到:
tf_http_archive(
name = "eigen_archive",
urls = [
"https://mirror.bazel.build/bitbucket.org/eigen/eigen/get/2355b229ea4c.tar.gz",
"https://bitbucket.org/eigen/eigen/get/2355b229ea4c.tar.gz",
],
??下載其中任何一個鏈接都可祷愉,下載好之后解壓窗宦,將Eigen添加到環(huán)境變量:
export CPLUS_INCLUDE_PATH="$HOME/tools/include/:$CPLUS_INCLUDE_PATH"
export CPLUS_INCLUDE_PATH="$HOME/tools/include/eigen3/:$CPLUS_INCLUDE_PATH"
-
安裝Protobuf
??protocbuf是一種很強大的跨平臺的數(shù)據(jù)標準,可以用于結(jié)構(gòu)化數(shù)據(jù)序列化二鳄,用于通訊協(xié)議赴涵、數(shù)據(jù)存儲等領域的語言無關(guān)、平臺無關(guān)的序列化結(jié)構(gòu)數(shù)據(jù)格式订讼,在之后的gRPC中也會用到髓窜;
??同樣Protobuf的版本也會直接決定tensorflow是否編譯成功,和安裝Eigen同樣的方法躯嫉,去workspace.bzl
中找protobuf下載對應的版本纱烘;下載好后進入protobuf目錄輸入以下命令安裝,并添加到環(huán)境:
./autogen.sh
./configure --prefix=$HOME/tools/bin
make
make install
-
安裝nsync
??和Eigen同樣的方式下載祈餐,添加環(huán)境路徑即可:
export CPLUS_INCLUDE_PATH="$HOME/tools/include/nsync/public:$CPLUS_INCLUDE_PATH"
??跳過這步安裝會出現(xiàn):fatal error : nsync_cv.h: No such file or dictionary
的錯誤擂啥;
-
編譯Tensorflow
??經(jīng)過以上的充足準備,終于可以編譯Tensorflow啦帆阳,進入tensorflow下載目錄哺壶,輸入以下命令:
./configure
bazel build //tensorflow:libtensorflow_cc.so
??其中./configure
之后沒有選擇CUDA支持屋吨,全部為no。經(jīng)過4/5分鐘之后山宾,在bazel-bin/tensorflow下就會看到libtensorflow_cc.so
和libtensorflow_framework.so
兩個動態(tài)庫至扰;之后需要把這兩個庫復制到$HOME/tools/lib
中,這樣就可以連接來編譯我們的模型了资锰,之后的任務就是寫Tensorflow的C++ API接口啦敢课。
-
C++重寫python API
??在python API中主要有以下三步驟:
??? 1 . 創(chuàng)建Session,讀入計算圖绷杜,恢復參數(shù)直秆;
??? 2 . 獲取需要的輸入,輸出Tensor (graph.get_tensor_by_name);
??? 3 . 給輸入Tensor傳值鞭盟,run模型圾结,得到輸出結(jié)果;
??C++ API也是相同的步驟齿诉;這里先給出python API的代碼筝野,Tensor的名字最好提前設定好,如果沒有的話也可以直接Tensor.name
查看:
def model_restore(self,model_file):
sess = tf.Session()
ckpt_file = tf.train.latest_checkpoint(self.model_file)
saver = tf.train.import_meta_graph(ckpt_file+".meta")
saver.restore(sess,ckpt_file)
return sess
def recover(self,sess,paragraph):
# 輸入 : 無標點粤剧,大寫字符串
# 輸出 : 帶標點歇竟,大寫字符串
char_paragraph = get_char_id(paragraph)
graph = tf.get_default_graph()
#讀入Tensor
inputs = graph.get_tensor_by_name('word_id:0')
logits_c = graph.get_tensor_by_name('Capt-Softmax/Reshape:0')
logits_p = graph.get_tensor_by_name('Punc-Softmax/Reshape:0')
tm_c = graph.get_tensor_by_name('loss/crf_capt/transitions:0')
tm_p = graph.get_tensor_by_name('loss/crf_punc/transitions:0')
feed_dict[inputs] = char_paragraph
#運行模型
logits_capt,logits_punc,transition_matrix_capt,transition_matrix_punc = sess.run([logits_c,logits_p,tm_c,tm_p],feed_dict=feed_dict)
return self.sequence_viterbi_decode(label_pred_capt,label_pred_punc,word)
??同樣的結(jié)構(gòu)用C++重寫之后的代碼如下,恢復模型部分:
void RecoverTool::modelLoader(const string& checkpoint_path){
const string graph_path = checkpoint_path+".meta";
// 讀入模型的計算圖
tensorflow::MetaGraphDef graph_def;
tensorflow::Status status = tensorflow::ReadBinaryProto(tensorflow::Env::Default(), graph_path, &graph_def);
if(!status.ok())
cout<<"Graph restore failed from "<<checkpoint_path<<endl<<status.ToString())<<endl;
// 創(chuàng)建session
status = session->Create(graph_def.graph_def());
if(!status.ok())
cout<<"Session created failed"<<endl<<status.ToString())<<endl;
// 恢復模型參數(shù)
tensorflow::Tensor checkpointTensor(tensorflow::DT_STRING,tensorflow::TensorShape());
checkpointTensor.scalar<string>()() = checkpoint_path;
status = session->Run(
{{graph_def.saver_def().filename_tensor_name(), checkpointPathTensor},},
{},
{graph_def.saver_def().restore_op_name()},
nullptr);
if(!status.ok())
cout<<"Model restore failed from "<<checkpoint_path<<endl<<status.ToString())<<endl;
}
??API核心函數(shù)俊扳,C++中Tensor返回的是Eigen::Tensor
類型途蒋;
string RecoverTool::recover(const string& paragraph){
// placeholder vector
vector<pair<string, tensorflow::Tensor>> input = utils.get_input_tensor_vector(paragraph);
// 模型輸出
vector<tensorflow::Tensor> outputs;
// 運行model
tensorflow::Status status = session->Run(input, {"Capt-Softmax/Reshape:0","Punc-Softmax/Reshape:0","loss/crf_capt/transitions:0","loss/crf_punc/transitions:0"}, {}, &outputs);
if(!status.ok())
cout<<"Model run falied"<<endl<<status.ToString()<<endl;
tensorflow::Tensor log_capt = outputs[0];
tensorflow::Tensor tran_capt = outputs[2];
auto logits_capt = log_capt.tensor<float,3>();
auto trans_capt = tran_capt.tensor<float,2>();
Eigen::Tensor<float,2> logit_capt(logits_capt.dimension(1),logits_capt.dimension(2));
Eigen::Tensor<float,2> transitions_capt(trans_capt.dimension(0),trans_capt.dimension(1));
for(int num_step(0);num_step<logits_capt.dimension(1);++num_step){
for(int char_step(0);char_step<logits_capt.dimension(2);++char_step){
logit_capt(num_step,char_step) = logits_capt(0,num_step,char_step);
}
}
for(int tag_ind1(0);tag_ind1<trans_capt.dimension(0);++tag_ind1){
for(int tag_ind2(0);tag_ind2<trans_capt.dimension(1);++tag_ind2){
transitions_capt(tag_ind1,tag_ind2) = trans_capt(tag_ind1,tag_ind2);
}
}
stack<int> captLabel = viterbi_decode(logit_capt,transitions_capt);
return paragraphDecode(captLabel,puncLabel,paragraph);
}
??如果模型輸出不是最終結(jié)果猛遍,還需要進行行加工馋记,這時就需要對Eigen的API有稍微的了解了,我用Eigen寫了一個簡單的CRF-Viterbi_decode
代碼懊烤,分享在這里供大家參考:
stack<int> Utils::viterbi_decode(Eigen::Tensor<float,2> score,Eigen::Tensor<float,2> trans_matrix){
//score: [seq_len,num_tags]
//trans_matrix: [num_tags.num_tags]
stack<int> viterbi;
Eigen::Tensor<float,2> trellis = score.constant(0.0f);//創(chuàng)建和score相同大小的全零數(shù)組
Eigen::Tensor<int,2> backpointers(score.dimension(0),score.dimension(1));
backpointers.setZero();
trellis.chip(0,0) = score.chip(0,0);
for(int i(1);i<score.dimension(0);++i){
Eigen::Tensor<float,2> v = trans_matrix.constant(0.0f);
for(int j(0);j<trans_matrix.dimension(1);++j)
v.chip(j,1) = trellis.chip(i-1,0);
v+=trans_matrix;
Eigen::array<int, 1> dims({0});
Tensor<float,1> maxCur = v.maximum(dims);
trellis.chip(i,0) = maxCur+score.chip(i,0);
backpointers.chip(i,0) = argmax(v,0);
}
viterbi.push(argmax_Dim1(trellis.chip(trellis.dimension(0)-1,0),0));
for(int i(backpointers.dimension(0)-1);i>0;--i){
viterbi.push(backpointers(i,viterbi.top()));
}
return viterbi;
}
-
編譯TF模型
??通過以上步驟梯醒,我們就可以編譯C++ API了,這里我們用make進行編譯,鏈接上之前編譯的libtensorflow_cc.so和libtensorflow_framework.so,命令如下珊随,也可以寫一個Makefile缺厉;
g++ -std=c++11 -g -Wall -D_DEBUG -Wshadow -Wno-sign-compare -w `pkg-config --cflags --libs protobuf`\
-I/home/xiaodl/tensorflow/bazel-genfiles -I/home/xiaodl/tensorflow/ -L/home/xiaodl/tools/lib\
-ltensorflow_framework -ltensorflow_cc -lprotobuf Utils.cc Recover.cc main.cc -o recover
??之后在當前目錄會生成一個可執(zhí)行文件,這樣就大功告成啦~輸入一句沒有標點的句子試一試街立,得到如下結(jié)果,試驗成功;
We are living in the New York City now, and how is it going recent, Tom?
-
Tensorflow gRPC服務
??有了C++ API就可以愉快的寫gRPC服務了猫胁,那gPRC服務到底是什么呢?google家的RPC跛锌,傳送官方文檔:定義一個服務弃秆,指定其能夠被遠程調(diào)用的方法(包含參數(shù)和返回類型)。客戶端應用可以像調(diào)用本地對象一樣直接調(diào)用另一臺不同的機器上服務端應用的方法菠赚。
-
安裝gRPC
??直接github clone就好了:
$ git clone https://github.com/grpc/grpc.git
??進入grpc目錄脑豹,更新三方依賴源碼,由于我們安裝了protobuf
衡查,所以可以進入.gitmodules
文件瘩欺,刪掉protobuf項,之后將已經(jīng)安裝的protobuf目錄放入grpc/third_party
就好:
$ git submodule update --init
??更新完之后進入Makefile查找:ldconfig
拌牲,把動態(tài)鏈接庫指向自己的目錄击碗;
??把ldconfig替換成
ldconfig -r $HOME/tools/bin
,之后執(zhí)行安裝:
make
make install prefix=$HOME/tools/
??如果不是在grpc/third_party
中安裝的protobuf们拙,在make過程中很有可能出如下錯誤:
In file included from src/compiler/php_generator.cc:23:0:
./src/compiler/php_generator_helpers.h: In function ‘grpc::string grpc_php_generator::GetPHPServiceFilename(const FileDescriptor*, const ServiceDescriptor*, const string&)’:
./src/compiler/php_generator_helpers.h:51:23: error: ‘const class google::protobuf::FileOptions’ has no member named ‘has_php_namespace’; did you mean ‘has_csharp_namespace’?
if (file->options().has_php_namespace()) {
^~~~~~~~~~~~~~~~~
has_csharp_namespace
??可以通過如下方式來解決這個錯誤:
make clean
make HAS_SYSTEM_PROTOBUF=false
??最新版本的grpc需要protobuf的版本是3.5.0
稍途,安裝成功之后可以去/grpc/examples/cpp/
下測試grpc是否能正常工作;如果安裝的protobuf版本不對會報錯砚婆,更新protobuf到3.5.0
即可械拍,注意還要和Tensorflow要求的protobuf版本匹配才行;
-
Tensorflow C++ API 的gRPC服務
??到這里總算可以開始寫服務了装盯,在實際運用中要求服務的client和server端都能夠異步工作坷虑,也就是請求不產(chǎn)生阻塞;gPRC提供了很強的異步服務機制來實現(xiàn)客戶和服務之間的異步無阻塞埂奈,這里將簡單分析一下client和server端的異步機制:
?1 . Client端:客戶端會生成一個隊列
CompletionQueue
迄损,并用CallData類來記錄RPC的狀態(tài)和標簽,每個request對應一個Calldata账磺,在接收到請求的時候?qū)⑵浞湃隒ompletionQueue中芹敌,并調(diào)用Finish函數(shù)向服務器端發(fā)送請求,尋求應答后立即返回處理新的待發(fā)送請求(無阻塞)垮抗;另開一個線程去等待處理CompletionQueue中的服務端應答氏捞;?2 . Server端:服務端有兩個任務:接收
request
和處理request
并返回Client
;服務端用ServerData
類來接收request
冒版,為了不讓Service
處理請求過程中有新的request
到來產(chǎn)生阻塞液茎,服務端將ServerData
放入CompletionQueue
隊列后新建一個ServerData
去接收新的請求(無阻塞);另開一個線程處理隊列中各種狀態(tài)的ServiceData
辞嗡,并實現(xiàn)應答捆等;
??以上是我自己的理解,如果有錯誤請大家指出续室;根據(jù)這種理解實現(xiàn)Tensorflow的gRPC服務就不難了栋烤,服務端在創(chuàng)建Service之前先restore model,服務啟動之后直接調(diào)用API即可猎贴,異步的實現(xiàn)和上面提到的流程一樣班缎,grpc有一個官方的案例非常不錯/grpc/examples/cpp/helloworld/helloworld_async_client2.cc
蝴光。
??測試Tensorflow gRPC的結(jié)果如下:
-
總結(jié)
??這么折騰下來總算是完成了Mentor的任務了,感覺大部分的時間都花在安裝三方庫和配環(huán)境上达址,不過也是有收獲的蔑祟,這篇文章作為這一套工作的簡單總結(jié),文章中的錯誤或者過時的東西請各位看官大神們大聲說出來呀~~時間不早了沉唠,明兒還要實習疆虚,晚安~