
之前實習的時候訓練一個給ASR文本添加大小寫和标點的模型,架構用的是tensorflow r1.2(本文其實和tensorflow版本無關)。模型訓好後mentor說要轉成C++上線,當時差點崩潰,由于太懶,不想換架構重寫就隻好試試tensorflow的C++ API了,由于公司伺服器的權限問題也是躺了不少的坑,這裡簡單總結一下TF模型轉C++ API以及轉gRPC服務的基本步驟和遇到的一些很迷的Errors。
關于Tensorflow模型到gRPC服務,tensorflow有個神奇API叫Tensorflow Serving,大家可以試一試。不過本文不是采用這種方式,而是先轉C++接口,再用gRPC寫接口服務,其實原理是一樣的。
Tensorflow C++ API
Tensorflow提供的C++API能夠恢複python訓練好的模型計算圖和參數到C++環境中;通過向Placeholder傳入資料便可以得到Eigen::Tensor類型的傳回。python下的Tensorflow依賴numpy矩陣運算庫,而在C++下依賴Eigen::Tensor庫,是以在使用C++ API之前需要先安裝好對應版本的Eigen庫;
因為模型最後是一層CRF,是以還需要用Eigen重寫Viterbi解碼,還好隻是簡單的DP問題;這裡簡單介紹一下提到的模型的結構:525通道CNN + HighWayNet + bi-LSTM + CRF;
下載下傳Tensorflow源碼
下載下傳最新的Tensorflow源碼,這個和你使用什麼版本Tensorflow訓練模型沒有關系。之後就需要把Tensorflow編譯成我們需要的動态連結庫;
$ 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)。以上組合并不固定,經供參考(本文是Tensorflow1.7)。由于在公司伺服器上工作,所有的third-party都需要安裝在自己的目錄。
1 .
非root安裝JDK8:jdk-8u161-linux-x64.tar.gz。Bazel依賴JDK8,wget下載下傳後解壓,把jdk添加到環境變量,把以下代碼添加到
$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
檔案之後執行一下指令:
chmod +x bazel-<version>-installer-linux-x86_64.sh
./bazel-<version>-installer-linux-x86_64.sh --user
bazel被裝到了
$HOME/bin
目錄下,添加到環境就OK了;之後輸入
bazel version
看看版本是否安裝成功;
安裝Eigen3
之前提到了Tensorflow依賴Eigen矩陣運算庫,在編譯之前需要安裝對應的版本;關于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添加到環境變量:
export CPLUS_INCLUDE_PATH="$HOME/tools/include/:$CPLUS_INCLUDE_PATH"
export CPLUS_INCLUDE_PATH="$HOME/tools/include/eigen3/:$CPLUS_INCLUDE_PATH"
安裝Protobuf
protocbuf是一種很強大的跨平台的資料标準,可以用于結構化資料序列化,用于通訊協定、資料存儲等領域的語言無關、平台無關的序列化結構資料格式,在之後的gRPC中也會用到;
同樣Protobuf的版本也會直接決定tensorflow是否編譯成功,和安裝Eigen同樣的方法,去
workspace.bzl
中找protobuf下載下傳對應的版本;下載下傳好後進入protobuf目錄輸入以下指令安裝,并添加到環境:
./autogen.sh
./configure --prefix=$HOME/tools/bin
make
make install
安裝nsync
和Eigen同樣的方式下載下傳,添加環境路徑即可:
export CPLUS_INCLUDE_PATH="$HOME/tools/include/nsync/public:$CPLUS_INCLUDE_PATH"
跳過這步安裝會出現:
fatal error : nsync_cv.h: No such file or dictionary
的錯誤;
編譯Tensorflow
經過以上的充足準備,終于可以編譯Tensorflow啦,進入tensorflow下載下傳目錄,輸入以下指令:
./configure
bazel build //tensorflow:libtensorflow_cc.so
其中
./configure
之後沒有選擇CUDA支援,全部為no。經過4/5分鐘之後,在bazel-bin/tensorflow下就會看到
libtensorflow_cc.so
和
libtensorflow_framework.so
兩個動态庫;之後需要把這兩個庫複制到
$HOME/tools/lib
中,這樣就可以連接配接來編譯我們的模型了,之後的任務就是寫Tensorflow的C++ API接口啦。
C++重寫python API
在python API中主要有以下三步驟:
1 . 建立Session,讀入計算圖,恢複參數;
2 . 擷取需要的輸入,輸出Tensor (graph.get_tensor_by_name);
3 . 給輸入Tensor傳值,run模型,得到輸出結果;
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)
同樣的結構用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;
// 建立session
status = session->Create(graph_def.graph_def());
if(!status.ok())
cout<<"Session created failed"<<endl<<status.ToString())<<endl;
// 恢複模型參數
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核心函數,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);
}
如果模型輸出不是最終結果,還需要進行行加工,這時就需要對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);//建立和score相同大小的全零數組
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
之後在目前目錄會生成一個可執行檔案,這樣就大功告成啦~輸入一句沒有标點的句子試一試,得到如下結果,試驗成功;
We are living in the New York City now, and how is it going recent, Tom?
Tensorflow gRPC服務
有了C++ API就可以愉快的寫gRPC服務了,那gPRC服務到底是什麼呢?google家的RPC,傳送官方文檔:定義一個服務,指定其能夠被遠端調用的方法(包含參數和傳回類型)。用戶端應用可以像調用本地對象一樣直接調用另一台不同的機器上服務端應用的方法。
- 安裝gRPC 直接github clone就好了:
$ git clone https://github.com/grpc/grpc.git
進入grpc目錄,更新三方依賴源碼,由于我們安裝了
protobuf
,是以可以進入
.gitmodules
檔案,删掉protobuf項,之後将已經安裝的protobuf目錄放入
grpc/third_party
就好:
$ git submodule update --init
更新完之後進入Makefile查找:
ldconfig
,把動态連結庫指向自己的目錄;
把ldconfig替換成
ldconfig -r $HOME/tools/bin
,之後執行安裝:
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端都能夠異步工作,也就是請求不産生阻塞;gPRC提供了很強的異步服務機制來實作客戶和服務之間的異步無阻塞,這裡将簡單分析一下client和server端的異步機制:
1 .
Client端:用戶端會生成一個隊列
CompletionQueue
,并用CallData類來記錄RPC的狀态和标簽,每個request對應一個Calldata,在接收到請求的時候将其放入CompletionQueue中,并調用Finish函數向伺服器端發送請求,尋求應答後立即傳回處理新的待發送請求(無阻塞);另開一個線程去等待處理CompletionQueue中的服務端應答;
2 .
Server端:服務端有兩個任務:接收
request
和處理
request
并傳回
Client
;服務端用
ServerData
類來接收
request
,為了不讓
Service
處理請求過程中有新的
request
到來産生阻塞,服務端将
ServerData
放入
CompletionQueue
隊列後建立一個
ServerData
去接收新的請求(無阻塞);另開一個線程處理隊列中各種狀态的
ServiceData
,并實作應答;
以上是我自己的了解,如果有錯誤請大家指出;根據這種了解實作Tensorflow的gRPC服務就不難了,服務端在建立Service之前先restore model,服務啟動之後直接調用API即可,異步的實作和上面提到的流程一樣,grpc有一個官方的案例非常不錯
/grpc/examples/cpp/helloworld/helloworld_async_client2.cc
。
測試Tensorflow gRPC的結果如下:
總結
這麼折騰下來總算是完成了Mentor的任務了,感覺大部分的時間都花在安裝三方庫和配環境上,不過也是有收獲的,這篇文章作為這一套工作的簡單總結,文章中的錯誤或者過時的東西請各位看官大神們大聲說出來呀~~時間不早了,明兒還要實習,晚安~