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

之前实习的时候训练一个给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安装JDK8jdk-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.solibtensorflow_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的任务了,感觉大部分的时间都花在安装三方库和配环境上,不过也是有收获的,这篇文章作为这一套工作的简单总结,文章中的错误或者过时的东西请各位看官大神们大声说出来呀~~时间不早了,明儿还要实习,晚安~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容