导读

在工业界一般会采用了tensorflow-serving进行模型的部署,而在模型构建时会因人而异会使用不同的深度学习框架,这就需要在使用指定深度学习框架训练出模型后,统一将模型转为pb格式,便于使用tensorflow-serving进行部署,本人在部署的过程中碰到了很多的问题。为此,文本对整个流程进行总结,首先介绍如何使用不同的深度学习框架构建模型,获得训练好的模型后将其转为pb格式的模型,然后采用容器+tensorflow-serving进行模型部署,最后探讨使用http和grpc进行inference的实践和性能对比。

深度学习模型构建和模型保存

深度学习框架比较流行的包括tensorflow,keras,pytorch,cntk,mxnet和theano。篇幅原因,本文介绍tensorflow和keras的模型构建,主要是因为团队主要使用这两个框架进行模型构建。需要注意的是,tensorflow2.0+版本已经将keras作为框架的默认API。tensorflow2.0和tensorflow1.x版本相差较大,而keras各个版本在模型构建方面差别不大,并且tensorflow2.0使用keras构建和原生构建模型基本一致,只需要将keras替换成tensorflow.keras。使用keras和tensorflow2.x构建模型如下:

import tensorflow.keras as kerasfrom tensorflow.keras.layers import Conv2D, Flatten, Dense, Dropoutfrom tensorflow.keras.models import Modelfrom tensorflow.keras import utils# 使用原生keras构建模型时,将tensorflow.keras替换成keras即可# 1. data load(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()# 2.  data preprocessx_train = x_train/255.0x_test = x_test/255.0x_train = x_train.reshape(x_train.shape[0], x_train.shape[1], x_train.shape[2], 1)x_test = x_test.reshape(x_test.shape[0], x_test.shape[1], x_test.shape[2], 1)print("train samples: {}, test samples: {}".format(x_train.shape[0], x_test.shape[0]))print("input shape: {}".format(x_train.shape[1:]))y_train = utils.to_categorical(y_train)y_test = utils.to_categorical(y_test)print("label shape:{}".format(y_train.shape[1:]))print(y_test[:10])# 3. build modelinput_data = Input(shape=(28, 28, 1), name='input')x = Conv2D(32, kernel_size=(3, 3), activation="relu", name="conv1")(input_data)x = Conv2D(64, kernel_size=(3, 3), activation="relu", name="conv2")(x)x = Flatten()(x)x = Dense(128, activation="relu")(x)x = Dropout(0.5)(x)output = Dense(10, activation="softmax", name="output")(x)model = Model(input=input_data, output=output)# 4. compile and trainmodel.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])model.fit(x_train, y_train, batch_size=32, epochs=2)# 5. evaluateaccuracy = model.evaluate(x_test, y_test)print("loss:%3.f, accuracy:%.3f" % (accuracy[0], accuracy[1]))

使用tensorflow构建模型训练的教程很多,感兴趣的可以在网上搜一搜。训练好模型后,需要将训练好的模型转换成tfserving部署的pb模型,tensorflow2.x构建的模型转换成pb非常方便,代码如下:

import tensorflow as tf# model 为上述训练的模型,也可以调用tf.keras.models.load_model加载已经保存的h5模型tf.keras.models.save_model(model, "./tf2x_save_model")# 包含的模型包括saved_model.pb和variables文件夹,variables文件下包括variables.data-00000-of-00001和variables.index文件。

使用原生的keras构建模型无法使用tensorflow2.x进行训练。因为需要使用tensorflow1.x对h5进行转换。将h5转换成pb模型的方法有很多,但使用tensorflow1.x版本有时也无法正确的将h5转成pb模型,有时转成功后inference的结果与本地的不一致。造成这种问题一般都是版本问题或参数配置不对,这里列举常用的几种。

方法一:

tf.contrib.saved_model.save_keras_model(model, save_path)

方法二:

with keras.backend.get_session() as sess:    tf.saved_model.simple_save(        sess,        save_path,        inputs={'input': model.input},        outputs={t.name: t for t in model.outputs})

方法三:

from keras import backend as Kfrom tensorflow.python import saved_modelfrom tensorflow.python.saved_model.signature_def_utils_impl import predict_signature_defbuilder = saved_model.builder.SavedModelBuilder(save_path)signature = predict_signature_def(    inputs={"input": model.input,            },    outputs={"output": model.output})sess = K.get_session()builder.add_meta_graph_and_variables(sess=sess,                                     tags=[saved_model.tag_constants.SERVING],                                     signature_def_map={                                         "mnist": signature})builder.save()

上述三种方法可以将h5模型转换为pb模型,个人推荐优先使用方法三,因为其可以指定的参数较多,这些参数会体现在模型的元数据中(下面会介绍获取模型的元数据方法),具有更好的扩展性和灵活性。 接下来是部署模型并验证模型。

tfserving模型部署

模型部署可以使用官方构建的镜像(参考文献中给出), 该镜像库中包含不同版本的tensorflow-serving(对应不同的tag),可以根据需要下载,本文在模型部署和inerence时使用的是tensorflow-serving2.2.0版本。下载镜像需要安装docker,docker的安装可以参考参考文献部分。

# 下载tfserving2.2.0版本镜像docker pull tensorflow/serving:2.2.0

准备好已经训练出来的pb模型,如模型保存在/data1/tfserving/models/m/1/, 该目录下包含saved_model.pb和variables文件夹,variables下包括variables.data-00000-of-00001和variables.index, 其中variables也可能不会生成,具体依赖转换模型使用的接口。路径中的m表示模型名,1表示模型版本号。启动命令如下:

docker run -p 8501:8501 -p 8500:8500 --mount type=bind,source=/data1/tfserving/models/m,target=/models/m -e MODEL_NAME=m -t tensorflow/serving:2.2.0

启动时暴露8500和8501端口,便于对外提供服务,source为存在在母机上模型存放的路径,target为在容器中模型存放的路径,MODEL_NAME为m,需要与模型路径中的模型名保持一致。启动成功后,可以看到如下提示,提示中也说明了8500为grpc服务端口,8501为http服务端口。

模型训练指定多块GPU_保存模型后无法训练

模型inference

使用tensorflow-serving模型部署后,支持http和grpc两种inference方式,下面介绍这两种inference接口的使用。

http inference

部署模型进行inference时,因为部署的模型很有可能是其他业务侧提供的,无法直接知道模型的输入和输出格式,此时可以通过接口获取模型的metadata,根据metadata准备数据。通过http获取模型的metadata方式如下:

import requestsroot_url = "http://127.0.0.1:8501"url = "%s/v1/models/m/metadata" % root_urlresp = requests.get(url)

返回的metadata如下:

{  "model_spec":{    "name": "m",    "signature_name": "",    "version": "1"  },  "metadata": {        "signature_def": {            "signature_def": {                "serving_default": {                    "inputs": {                        "input": {     "dtype": "DT_FLOAT",     "tensor_shape": {      "dim": [{        "size": "-1",        "name": ""       },{        "size": "28",        "name": ""       },{        "size": "28",        "name": ""       },{        "size": "1",        "name": ""       }],      "unknown_rank": false     },     "name": "serving_default_conv1_input:0"    }},   "outputs": {    "output": {     "dtype": "DT_FLOAT",     "tensor_shape": {      "dim": [{        "size": "-1",        "name": ""       },{        "size": "10",        "name": ""       }],      "unknown_rank": false     },     "name": "StatefulPartitionedCall:0"    }},   "method_name": "tensorflow/serving/predict"  }

      上述返回的metadata中,描述了模型名称,模型版本,输入的数据格式和输出的数据格式,输入格式为(-1, 28, 28, 1),输出为(-1, 10)。知道输入输出格式就可以进行inference了。inference的代码如下:

import numpy as np# 1. generate urlroot_url = "http://127.0.0.1:8501"version = 1url = "%s/v1/models/m/versions/%s:predict" % (root_url, version)# 2. generate datanp.random.seed(0)input_data = np.random.rand(2, 28, 28, 1).astype(np.float32)print(input_data.shape)data = {    "instances": input_data.tolist()}# 3. post data for inferencestart = time.time()resp = requests.post(url, json=data)# 4. parse resultif resp.status_code == 200:    result = json.loads(resp.text)    print("predictions", result.get("predictions"))    print("time_used:", time.time() - start)# no right result return

上述的inference代码中随机生成了两个(28,28,1)的样本,然后通过http的请求部署好的模型,得到的结果也包含两个样本的预测结果,每个结果为1*10的向量,表示属于0-9数字的概率,如下:

[[0.0078, 0.0064, 0.1581, 0.0823, 0.0073, 0.0228, 0.00423, 0.0189, 0.6845, 0.0074], [0.0071, 0.0017, 0.2068, 0.0241, 0.0093, 0.0063, 0.0059, 0.0021, 0.7179, 0.0187]]

grpc inference

import numpy as npimport tensorflow as tffrom tensorflow_serving.apis import predict_pb2from tensorflow_serving.apis import prediction_service_pb2_grpcimport grpc# 1. generate datanp.random.seed(0)input_data = np.random.rand(2, 28, 28, 1).astype(np.float32)# 2. grpc inferencechannel = grpc.insecure_channel(root_url)stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)request = predict_pb2.PredictRequest()request.model_spec.name = "m"  # 模型名称request.model_spec.signature_name = ""  # 签名名称# cov1_input为模型输入层的名称,可在构建模型时自定义request.inputs["input"].CopyFrom(    tf.make_tensor_proto(input_data.tolist(),                    shape=list(input_data.shape)))result_future = stub.Predict.future(request, 10.0)  # 10 secs timeoutresponse = result_future.result()# 4. parse result# dense_1 是输出层的名称,可以通过metadata查看result = np.reshape(response.outputs["ouput"].float_val, (input_shape[0], 10))

因为指定了生成的测试数据的随机种子,所以http和grpc两个测试得到的结果完全一样的。grpc的输出结果的格式通过response.outputs["output"].float_val解析后是一个1维的araaylist,需要通过reshape进行转换。

在模型部署阶段,使用tensorflow0-fserving1.14.0部署tensorflow1.x生成的pb模型,使用tensorflow-serving2.1.0部署tensorflow2.x生成的模型。通过实验测试发现,tensorflow1.15以上版本生成的pb模型可以使用tensorflow-serving2.x进行部署,tensorflow1.14以下的版本可以是有tensorflow-serving1.14进行部署。

两种接口效率比较

tensorflow-serving服务提供了http和grpc,两者的速率是不一样的,本文使用mnist模型进行了测试,模型使用docker启动,请求数据的脚本运行在母机上,每次测试的sample数为100, 测试结果如下:

测试次数

http inference平均耗时(s)

grpcinference平均耗时(s)

1

1.400

1.747

2

1.376

1.725

3

1.365

1.772

4

1.341

1.757

5

1.333

1.637

平均耗时

1.363

1.728

另外,我们也使用目标检测的ssd模型对http和grpc进行inference的速率测试,模型输入格式为(320, 320,3),输出的目标数为100个,每次请求一张图片。测试结果如下:

测试次数

http inference平均耗时

grpc inference平均耗时

1

0.1473

0.0428

2

0.1425

0.0314

3

0.1456

0.0312

4

0.1413

0.0675

5

0.1496

0.0315

6

0.1462

0.03245

平均耗时

0.146

0.036

对比两表发现,当较小的模型和输入size时,使用http进行inference耗时较短,当较大模型和输入size时,使用grpc优势较为明显。当输入数据较大时,使用http通讯时数据序列化和反序列化的耗时较多,而grpc对序列化耗时性能比http快。当数据较小时,

总结

本文介绍了从模型构建到模型转换,再到模型部署和inference的全流程。在使用过程中,碰到最大的问题在于将h5模型转换成pb模型及模型inference阶段。很多情况下,tensorflow和keras在不同版本下,相同的代码可能无法正确的将h5转换成pb模型或者转换之后无法使用tfserving部署(部署了也无法得到正确的结果)。在模型的inference阶段,可以使用http和grpc两种,在使用过程中,往往需要反复确认数据的输入和输出格式,无法快速的对接。