部署tensorflow serving+python,java client代码实例

本文介绍使用docker的方法部署tensorflow serving,并提供pythonjava client代码实例。(本文参考了较多博文和tensorflow官方文档,旨在补充多数博文遗留的坑,和精简官方文档的繁琐)。
为了避免bazel编译源码这个大坑(会报一些奇怪的错误,主要是各个依赖项的版本不对应),本文直接选择docker的方式部署tensorflow serving。
注:只需按照步骤一步一步来,就能从零到部署成功,最后会提供一个使用案例:文本分类模型

1 Docker安装

1.1 Mac环境下安装

参考网站
建议选择手动安装,安装完毕后,选择(Check for Updates)更新到最新版本

1.2 centos环境下安装

前提条件:CentOS 7 上,要求系统为64位、系统内核版本为 3.10 以上,通过指令uname -r 查看自己的系统版本

移除旧的版本:

$ sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine

安装依赖项:
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
添加源信息:
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新 yum 缓存:
sudo yum makecache fast
安装 Docker-ce:
sudo yum -y install docker-ce
启动 Docker 后台服务:
sudo systemctl start docker
测试运行 hello-world:
docker run hello-world 或者 直接查看版本 docker --version

2 serving部署

2.1 拉取serving 镜像

docker pull tensorflow/serving
完成之后 查看安装好的镜像
docker images

2.2 导出模型

serving不能直接使用以HDF5和.ckpt方式保存的模型,需要进行一次转化,本文以keras保存的HDF5文件为例进行介绍,.ckpt转换方式大同小异,游客可自行查询。

import tensorflow as tf
from keras import backend as K
from keras.models import Sequential, Model
from os.path import isfile
from keras.models import load_model
import os

def save_model_to_serving(model, export_version, export_path='prod_models'):
    print(model.input, model.output)
    signature = tf.saved_model.signature_def_utils.predict_signature_def(
        inputs={'textdata': model.input}, outputs={'market': model.output})
    export_path = os.path.join(
        tf.compat.as_bytes(export_path),
        tf.compat.as_bytes(str(export_version)))
    builder = tf.saved_model.builder.SavedModelBuilder(export_path)
    legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op')
    builder.add_meta_graph_and_variables(
        sess=K.get_session(),
        tags=[tf.saved_model.tag_constants.SERVING],
        signature_def_map={
            'market_classification': signature,
        },
        legacy_init_op=legacy_init_op)
    builder.save()
model = load_model('自己的路径/blistm-checkpoint-02e-val_acc_0.96.hdf5')
save_model_to_serving(model, "1", "bgru_serving")#bgru_serving表示转换后的模型会存储到该路径下

模型转化结束后会生成下面几个文件


2.3 运行容器

docker run -p 8500:8500 \
      --mount type=bind,source=自己的路径/bgru_serving/,target=/models/market_blstm \
      -e MODEL_NAME=market_blstm -t tensorflow/serving

注:测试建议使用8500端口 ,自己的路径->绝对路径 (重点)
各个参数的含义:

  • -p 8500:8500 :指的是开放8500这个gRPC端口
  • --mount type=bind, source=自己的路径/bgru_serving/, target=/models/market_blstm:把你导出的本地模型文件夹挂载到docker container的/models/market_blstm这个文件夹,tensorflow serving会从容器内的/models/market_blstm文件夹里面找到你的模型
  • --MODEL_NAME:模型名字,在导出模型的时候设置的名字
  • -t 指定使用tensorflow/serving这个镜像,可以替换其他版本,例如tensorflow/serving:latest-gpu,但你需要docker pull tensorflow/serving:latest-gpu把这个镜像拉下来

3 client客户端

3.1 python 案例

注:最好使用python3.5+,不然如果使用高版本的tensorflow会报错
安装依赖库sudo pip3 install tensorflow-serving-api
客户端代码

from __future__ import print_function
from grpc.beta import implementations
import tensorflow as tf
import numpy as np
import re,json,jieba,time
import codecs
import random,time

from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2

def loadData(filename):  #加载json文件 生成字典
    with codecs.open(filename,'r','utf-8') as fr:
        resdict = json.load(fr)
    return resdict


vocab = loadData('vocab_bgru.dict')#   加载词典 ,格式:"中国":12045

def denoise(text): #文本预处理并粉刺,再根据embegging所需的词典生成词的索引矩阵----处理单条文本数据
    x_train_word_ids = []
    tem = []
    patten=re.compile(r'(https|http)?:\/\/(\w|\.|\/|\?|\=|\&|\%)*\b',re.S)
    line = text.strip()
    line = patten.sub('',line.decode("utf-8","ignore"))
    line = re.sub(r'{url(.*)网页链接}','',line)
    line = line.replace('\\','').replace('\n',' ').replace('https://',' ')
    wordlist = [emt.strip() for emt in jieba.cut(line) if len(emt.strip())>=2]
    for i,word in enumerate(wordlist):
        try:code = vocab[word]
        except:
            try:code = vocab[word.encode('utf-8')]
            except:continue
        tem.append(code)
        x_train_word_ids.append(tem)
    if len(x_train_word_ids)==0:return [[0]]
    return x_train_word_ids

def pad_sequences(x_train_word_ids,maxlen=64): #根据denoise函数得到的一条文本的索引矩阵生成符合lstm输入的词向量
    len_x = len(x_train_word_ids[0])
    if len_x>maxlen:
        res = [x_train_word_ids[0][i] for i in range(len_x-maxlen,len_x)]
        return res
    else:
        res = [0]*maxlen
        for i,emt in enumerate(x_train_word_ids[0]):
            res[maxlen-len_x+i]=emt
        return res


tf.app.flags.DEFINE_string('server', '127.0.0.1:8500',
                           'PredictionService host:port') #ip和端口,ip可换成要连接的服务器ip
FLAGS = tf.app.flags.FLAGS

start_time = time.time()

batch_size = 120

host,port = FLAGS.server.split(":")

channel = implementations.insecure_channel(host,int(port))
stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)

request = predict_pb2.PredictRequest()
request.model_spec.name = 'market_blstm' # 这个name跟tensorflow_model_server  --model_name="market_blstm" 对应

request.model_spec.signature_name = 'market_classification' # 这个signature_name  跟2.2模型导出中的market_classification 对应

text_list = ['吴亦凡同款 Sup扎染卫衣 全身顶级数码直喷 印花带做旧感 就是看起来脏脏的 一件衣服印花大几十块 完美还原面料为420G毛圈轻捉毛 质感很好',"360儿童5周年不止5折# 360儿童手表五周年&双十一特惠! 喜欢![失望]"]

x_train = np.array([pad_sequences(denoise(text)) for text in text_list])
request.inputs['textdata'].CopyFrom(
                  tf.contrib.util.make_tensor_proto(x_train, shape=[batch_size,64],dtype=tf.float32)) # shape跟 keras的model.input类型对应,且textdata对应2.2中的textdata
result = stub.Predict(request, 10.0)
reslist = result.outputs['market'].float_val
print(reslist)

结果如下:
[0.013646061532199383, 0.9863539338111877, 0.16853764653205872, 0.8314623832702637] 每两个是一对预测数据,例如0.013646061532199383, 0.9863539338111877表示分别表示text_list中第一条数据属于0类的概率为0.013646061532199383,1类的概率为0.9863539338111877

3.2 java案例

pom.xml文件中的依赖项:

<dependencies>
        <dependency>
            <groupId>com.yesup.oss</groupId>
            <artifactId>tensorflow-client</artifactId>
            <version>1.4-2</version>
        </dependency>

        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty</artifactId>
            <version>1.7.0</version>
        </dependency>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-tcnative-boringssl-static</artifactId>
            <version>2.0.7.Final</version>
        </dependency>

        <dependency>
            <groupId>com.huaban</groupId>
            <artifactId>jieba-analysis</artifactId>
            <version>1.0.2</version>
        </dependency>

        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>

具体代码:

import com.huaban.analysis.jieba.JiebaSegmenter;
import com.huaban.analysis.jieba.WordDictionary;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import net.sf.json.JSONObject;
import org.tensorflow.framework.DataType;
import org.tensorflow.framework.TensorProto;
import org.tensorflow.framework.TensorShapeProto;
import tensorflow.serving.Model;
import tensorflow.serving.Predict;
import tensorflow.serving.PredictionServiceGrpc;

public class TensorServClient {

    PredictionServiceGrpc.PredictionServiceBlockingStub stub = null;

    private JiebaSegmenter segmenter;
    private JSONObject json;

    private static int maxlen = 64;   //padding的最大长度
    private static int batch = 200;

    public TensorServClient(){
        ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1",8500).usePlaintext(true).build();
        //这里还是先用block模式
        stub = PredictionServiceGrpc.newBlockingStub(channel);

        WordDictionary dictAdd = WordDictionary.getInstance();
        dictAdd.loadUserDict(Paths.get("jiebaextradic_java.dict"));//加载自定义词典
        segmenter = new JiebaSegmenter();

        try {
            json = LoadJsonFile.load("vocab_bgru.dict"); //加载词位置索引词典 ,格式:"中国":12045
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
    private ArrayList<Integer> denoise(String line){
        ArrayList<Integer>x_train_word_ids = new ArrayList<Integer>();
        line = line.replaceAll("(http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?^=%&amp;:/~\\+#]*[\\w\\-\\@?^=%&amp;/~\\+#])?","");
        line = line.replaceAll("\\{url(.*)网页链接\\}","");
        line = line.replaceAll("\\\\","").replaceAll("\\r|\\n","").replaceAll("https://","");
        ArrayList<String> wordjiebaList = (ArrayList<String>) segmenter.sentenceProcess(line);
        for (String word:wordjiebaList) {
            try {
                if (this.json.containsKey(word)){
                    x_train_word_ids.add(this.json.getInt(word));
                }
            }catch (Exception e){
                x_train_word_ids.add(0);
            }
        }
        return x_train_word_ids;
    }
    private float[] padSequences(ArrayList<Integer>x_train_word_ids){
        float []res=new float[maxlen];
        int len_x = x_train_word_ids.size();
        if (len_x>maxlen){
            for (int i = len_x-maxlen,j=0; i < len_x; i++,j++) {
                res[j]=x_train_word_ids.get(i);
            }
            return res;
        }else {
            for (int i = 0; i < len_x; i++) {
                res[maxlen-len_x+i]=x_train_word_ids.get(i);
            }
            return res;
        }
    }
    private float[][]gen_predict_data(String []textlist){
        float [][] predict_data = new float[batch][maxlen];
        for (int i = 0; i < textlist.length; i++) {
            predict_data[i]=padSequences(denoise(textlist[i]));
        }
        return predict_data;
    }

public void predict(String[] textlist){
        //        //创建请求
        Predict.PredictRequest.Builder predictRequestBuilder = Predict.PredictRequest.newBuilder();
        //模型名称和模型方法名预设
        Model.ModelSpec.Builder modelSpecBuilder = Model.ModelSpec.newBuilder();
        modelSpecBuilder.setName("market_blstm");
        modelSpecBuilder.setSignatureName("market_classification");
        predictRequestBuilder.setModelSpec(modelSpecBuilder);

        //设置入参,访问默认是最新版本,如果需要特定版本可以使用tensorProtoBuilder.setVersionNumber方法
        TensorProto.Builder tensorProtoBuilder = TensorProto.newBuilder();
        tensorProtoBuilder.setDtype(DataType.DT_FLOAT);
        TensorShapeProto.Builder tensorShapeBuilder = TensorShapeProto.newBuilder();

        tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(batch));
        tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(maxlen));

        tensorProtoBuilder.setTensorShape(tensorShapeBuilder.build());

        float[][]featuresTensorData = gen_predict_data(textlist);

        for (int i = 0; i < featuresTensorData.length; ++i) {
            for (int j = 0; j < featuresTensorData[i].length; ++j) {
                tensorProtoBuilder.addFloatVal(featuresTensorData[i][j]);
            }
        }

        predictRequestBuilder.putInputs("textdata",tensorProtoBuilder.build());
        //访问并获取结果
        Predict.PredictResponse predictResponse = stub.predict(predictRequestBuilder.build());
        TensorProto result = predictResponse.toBuilder().getOutputsOrThrow("market");
        List<Float> reslist = result.getFloatValList();
}

public static void main(String[] args) throws Exception{
        long startTime = System.currentTimeMillis();
        TensorServClient tensorServClient = new TensorServClient();
        long midTime = System.currentTimeMillis();
        String[] textlist = {"吴亦凡同款 Sup扎染卫衣 全身顶级数码直喷 印花带做旧感 就是看起来脏脏的 一件衣服印花大几十块 完美还原面料为420G毛圈轻捉毛 质感很好","360儿童5周年不止5折# 360儿童手表五周年&双十一特惠! 喜欢![失望]",....};//这个数组的长度为 batch ,方便批处理
        tensorServClient.predict(textlist); 
 }
}

注:java 案例中textlist的长度为batch,每个位置上是一条文本;结果与python案例保持一致,亦是两个一对

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

推荐阅读更多精彩内容