【填坑】基于TensorFlow C++ API 的 gRPC 服務

之前實習的時候訓練一個給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庫;

因為模型最后是一層CRF蔗怠,所以還需要用Eigen重寫Viterbi解碼墩弯,還好只是簡單的DP問題;這里簡單介紹一下提到的模型的結(jié)構(gòu):525通道CNN + HighWayNet + bi-LSTM + CRF寞射;
  • 下載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安裝JDK8jdk-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.solibtensorflow_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é),文章中的錯誤或者過時的東西請各位看官大神們大聲說出來呀~~時間不早了沉唠,明兒還要實習疆虚,晚安~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市满葛,隨后出現(xiàn)的幾起案子径簿,更是在濱河造成了極大的恐慌,老刑警劉巖嘀韧,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篇亭,死亡現(xiàn)場離奇詭異,居然都是意外死亡锄贷,警方通過查閱死者的電腦和手機译蒂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谊却,“玉大人柔昼,你說我怎么就攤上這事⊙妆妫” “怎么了捕透?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長碴萧。 經(jīng)常有香客問我乙嘀,道長,這世上最難降的妖魔是什么勿决? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任乒躺,我火速辦了婚禮招盲,結(jié)果婚禮上低缩,老公的妹妹穿的比我還像新娘。我一直安慰自己曹货,他們只是感情好咆繁,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顶籽,像睡著了一般玩般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礼饱,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天坏为,我揣著相機與錄音究驴,去河邊找鬼。 笑死匀伏,一個胖子當著我的面吹牛洒忧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播够颠,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼熙侍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了履磨?” 一聲冷哼從身側(cè)響起蛉抓,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎剃诅,沒想到半個月后巷送,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡矛辕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年惩系,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片如筛。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡堡牡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出杨刨,到底是詐尸還是另有隱情晤柄,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布妖胀,位于F島的核電站芥颈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赚抡。R本人自食惡果不足惜爬坑,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涂臣。 院中可真熱鬧盾计,春花似錦、人聲如沸赁遗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岩四。三九已至哭尝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間剖煌,已是汗流浹背材鹦。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工逝淹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桶唐。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓创橄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親莽红。 傳聞我的和親對象是個殘疾皇子妥畏,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

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