作者: Dimitre Oliveira
创建日期 2023/01/02
上次修改日期 2023/01/02
描述: 如何使用 TensorFlow Serving 提供 TensorFlow 模型服务。
构建机器学习模型后,下一步是提供服务。您可以通过将模型作为端点服务公开来实现。可以使用许多框架来完成此操作,但 TensorFlow 生态系统有自己的解决方案,称为 TensorFlow Serving。
来自 TensorFlow Serving 的 GitHub 页面
TensorFlow Serving 是一款灵活、高性能的机器学习模型服务系统,专为生产环境而设计。它处理机器学习的推理方面,在训练后获取模型并管理其生命周期,通过高性能的、引用计数的查找表为客户端提供版本化的访问。TensorFlow Serving 提供了与 TensorFlow 模型的开箱即用集成,但可以轻松扩展以服务其他类型的模型和数据。"
注意一些特性
本指南使用 Keras 应用 API 创建了一个简单的 MobileNet 模型,然后使用 TensorFlow Serving 提供服务。重点在于 TensorFlow Serving,而不是 TensorFlow 中的建模和训练。
注意:您可以在 此链接 找到包含完整工作代码的 Colab 笔记本。
import os
os.environ["KERAS_BACKEND"] = "tensorflow"
import json
import shutil
import requests
import numpy as np
import tensorflow as tf
import keras
import matplotlib.pyplot as plt
在这里,我们从 Keras 应用 加载预训练的 MobileNet,这是我们将要提供的服务模型。
model = keras.applications.MobileNet()
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/mobilenet/mobilenet_1_0_224_tf.h5
17225924/17225924 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
大多数模型无法直接在原始数据上运行,它们通常需要某种预处理步骤来调整数据以满足模型要求,在本例中,从 MobileNet 的 API 页面 可以看出,其输入图像需要三个基本步骤
[0, 1]
范围[-1, 1]
范围(224, 224, 3)
的图像,表示 (高度,宽度,通道)
我们可以使用以下函数完成所有这些操作
def preprocess(image, mean=0.5, std=0.5, shape=(224, 224)):
"""Scale, normalize and resizes images."""
image = image / 255.0 # Scale
image = (image - mean) / std # Normalize
image = tf.image.resize(image, shape) # Resize
return image
关于使用“keras.applications”API 进行预处理和后处理的说明
Keras 应用 API 中可用的所有模型也提供 preprocess_input
和 decode_predictions
函数,这些函数分别负责每个模型的预处理和后处理,并且已经包含了这些步骤所需的所有逻辑。这是使用 Keras 应用模型处理输入和输出的推荐方法。在本指南中,我们没有使用它们,以便更清晰地展示自定义签名的优势。
在同一上下文中,大多数模型输出的值需要额外的处理才能满足用户需求,例如,用户不想知道给定图像每个类的 logits 值,用户想要知道它属于哪个类。对于我们的模型,这意味着在模型输出之上进行以下转换
# Download human-readable labels for ImageNet.
imagenet_labels_url = (
"https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt"
)
response = requests.get(imagenet_labels_url)
# Skipping background class
labels = [x for x in response.text.split("\n") if x != ""][1:]
# Convert the labels to the TensorFlow data format
tf_labels = tf.constant(labels, dtype=tf.string)
def postprocess(prediction, labels=tf_labels):
"""Convert from probs to labels."""
indices = tf.argmax(prediction, axis=-1) # Index with highest prediction
label = tf.gather(params=labels, indices=indices) # Class name
return label
现在让我们下载一张香蕉图片,看看所有内容是如何组合在一起的。
response = requests.get("https://i.imgur.com/j9xCCzn.jpeg", stream=True)
with open("banana.jpeg", "wb") as f:
shutil.copyfileobj(response.raw, f)
sample_img = plt.imread("./banana.jpeg")
print(f"Original image shape: {sample_img.shape}")
print(f"Original image pixel range: ({sample_img.min()}, {sample_img.max()})")
plt.imshow(sample_img)
plt.show()
preprocess_img = preprocess(sample_img)
print(f"Preprocessed image shape: {preprocess_img.shape}")
print(
f"Preprocessed image pixel range: ({preprocess_img.numpy().min()},",
f"{preprocess_img.numpy().max()})",
)
batched_img = tf.expand_dims(preprocess_img, axis=0)
batched_img = tf.cast(batched_img, tf.float32)
print(f"Batched image shape: {batched_img.shape}")
model_outputs = model(batched_img)
print(f"Model output shape: {model_outputs.shape}")
print(f"Predicted class: {postprocess(model_outputs)}")
Original image shape: (540, 960, 3)
Original image pixel range: (0, 255)
Preprocessed image shape: (224, 224, 3)
Preprocessed image pixel range: (-1.0, 1.0)
Batched image shape: (1, 224, 224, 3)
Model output shape: (1, 1000)
Predicted class: [b'banana']
要将我们训练好的模型加载到 TensorFlow Serving 中,我们首先需要将其保存为 SavedModel 格式。这将在定义良好的目录层次结构中创建一个 protobuf 文件,并将包含版本号。TensorFlow Serving 允许我们选择在进行推理请求时要使用哪个版本的模型或“可服务对象”。每个版本都将导出到给定路径下的不同子目录中。
model_dir = "./model"
model_version = 1
model_export_path = f"{model_dir}/{model_version}"
tf.saved_model.save(
model,
export_dir=model_export_path,
)
print(f"SavedModel files: {os.listdir(model_export_path)}")
INFO:tensorflow:Assets written to: ./model/1/assets
INFO:tensorflow:Assets written to: ./model/1/assets
SavedModel files: ['variables', 'saved_model.pb', 'assets', 'fingerprint.pb']
我们将使用命令行实用程序 saved_model_cli
来查看 MetaGraphDefs(模型)和 SignatureDefs(您可以调用的方法)在我们的 SavedModel 中。请参阅 TensorFlow 指南中 关于 SavedModel CLI 的讨论。
!saved_model_cli show --dir {model_export_path} --tag_set serve --signature_def serving_default
The given SavedModel SignatureDef contains the following input(s):
inputs['inputs'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 224, 224, 3)
name: serving_default_inputs:0
The given SavedModel SignatureDef contains the following output(s):
outputs['output_0'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1000)
name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
这告诉我们很多关于模型的信息!例如,我们可以看到它的输入具有 4D 形状 (-1, 224, 224, 3)
,这意味着 (批次大小,高度,宽度,通道)
,还要注意,此模型需要特定的图像形状 (224, 224, 3)
,这意味着我们可能需要在将图像发送到模型之前重新调整其形状。我们还可以看到模型的输出具有 (-1, 1000)
形状,它们是 ImageNet 数据集中 1000 个类的 logits。
这些信息并不能告诉我们所有内容,例如像素值需要在 [-1, 1]
范围内,但这是一个良好的开端。
我们准备使用 Aptitude 安装 TensorFlow Serving,因为此 Colab 在 Debian 环境中运行。我们将 tensorflow-model-server
包添加到 Aptitude 知道的包列表中。请注意,我们正在以 root 身份运行。
注意:此示例以原生方式运行 TensorFlow Serving,但 您也可以在 Docker 容器中运行它,这是开始使用 TensorFlow Serving 的最简单方法之一。
wget 'http://storage.googleapis.com/tensorflow-serving-apt/pool/tensorflow-model-server-universal-2.8.0/t/tensorflow-model-server-universal/tensorflow-model-server-universal_2.8.0_all.deb'
dpkg -i tensorflow-model-server-universal_2.8.0_all.deb
在这里,我们开始运行 TensorFlow Serving 并加载我们的模型。加载后,我们可以开始使用 REST 发出推理请求。有一些重要的参数
port
:您将用于 gRPC 请求的端口。rest_api_port
:您将用于 REST 请求的端口。model_name
:您将在 REST 请求的 URL 中使用此参数。它可以是任何内容。model_base_path
:这是保存模型的目录的路径。查看 TFServing API 参考 以获取所有可用的参数。
# Environment variable with the path to the model
os.environ["MODEL_DIR"] = f"{model_dir}"
%%bash --bg
nohup tensorflow_model_server \
--port=8500 \
--rest_api_port=8501 \
--model_name=model \
--model_base_path=$MODEL_DIR >server.log 2>&1
# We can check the logs to the server to help troubleshooting
!cat server.log
输出
[warn] getaddrinfo: address family for nodename not supported
[evhttp_server.cc : 245] NET_LOG: Entering the event loop ...
# Now we can check if tensorflow is in the active services
!sudo lsof -i -P -n | grep LISTEN
输出
node 7 root 21u IPv6 19100 0t0 TCP *:8080 (LISTEN)
kernel_ma 34 root 7u IPv4 18874 0t0 TCP 172.28.0.12:6000 (LISTEN)
colab-fil 63 root 5u IPv4 17975 0t0 TCP *:3453 (LISTEN)
colab-fil 63 root 6u IPv6 17976 0t0 TCP *:3453 (LISTEN)
jupyter-n 81 root 6u IPv4 18092 0t0 TCP 172.28.0.12:9000 (LISTEN)
python3 101 root 23u IPv4 18252 0t0 TCP 127.0.0.1:44915 (LISTEN)
python3 132 root 3u IPv4 20548 0t0 TCP 127.0.0.1:15264 (LISTEN)
python3 132 root 4u IPv4 20549 0t0 TCP 127.0.0.1:37977 (LISTEN)
python3 132 root 9u IPv4 20662 0t0 TCP 127.0.0.1:40689 (LISTEN)
tensorflo 1101 root 5u IPv4 35543 0t0 TCP *:8500 (LISTEN)
tensorflo 1101 root 12u IPv4 35548 0t0 TCP *:8501 (LISTEN)
现在让我们为推理请求创建一个 JSON 对象,看看我们的模型对其进行分类的效果如何
我们将发送一个预测请求,作为 POST 请求到我们服务器的 REST 端点,并以示例形式传递。我们将请求我们的服务器提供我们可服务模型的最新版本,而不指定特定的版本。
data = json.dumps(
{
"signature_name": "serving_default",
"instances": batched_img.numpy().tolist(),
}
)
url = "https://127.0.0.1:8501/v1/models/model:predict"
def predict_rest(json_data, url):
json_response = requests.post(url, data=json_data)
response = json.loads(json_response.text)
rest_outputs = np.array(response["predictions"])
return rest_outputs
rest_outputs = predict_rest(data, url)
print(f"REST output shape: {rest_outputs.shape}")
print(f"Predicted class: {postprocess(rest_outputs)}")
输出
REST output shape: (1, 1000)
Predicted class: [b'banana']
gRPC 基于远程过程调用 (RPC) 模型,是一种用于实现 RPC API 的技术,它使用 HTTP 2.0 作为其底层传输协议。gRPC 通常适用于低延迟、高可扩展性和分布式系统。如果您想了解更多关于 REST 与 gRPC 的权衡,请查看这篇文章。
import grpc
# Create a channel that will be connected to the gRPC port of the container
channel = grpc.insecure_channel("localhost:8500")
pip install -q tensorflow_serving_api
from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc
# Create a stub made for prediction
# This stub will be used to send the gRPCrequest to the TF Server
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
# Get the serving_input key
loaded_model = tf.saved_model.load(model_export_path)
input_name = list(
loaded_model.signatures["serving_default"].structured_input_signature[1].keys()
)[0]
def predict_grpc(data, input_name, stub):
# Create a gRPC request made for prediction
request = predict_pb2.PredictRequest()
# Set the name of the model, for this use case it is "model"
request.model_spec.name = "model"
# Set which signature is used to format the gRPC query
# here the default one "serving_default"
request.model_spec.signature_name = "serving_default"
# Set the input as the data
# tf.make_tensor_proto turns a TensorFlow tensor into a Protobuf tensor
request.inputs[input_name].CopyFrom(tf.make_tensor_proto(data.numpy().tolist()))
# Send the gRPC request to the TF Server
result = stub.Predict(request)
return result
grpc_outputs = predict_grpc(batched_img, input_name, stub)
grpc_outputs = np.array([grpc_outputs.outputs['predictions'].float_val])
print(f"gRPC output shape: {grpc_outputs.shape}")
print(f"Predicted class: {postprocess(grpc_outputs)}")
输出
gRPC output shape: (1, 1000)
Predicted class: [b'banana']
请注意,对于此模型,我们始终需要预处理和后处理所有样本以获得所需的输出,如果维护和服务由大型团队开发的多个模型,并且每个模型可能需要不同的处理逻辑,这可能会变得非常棘手。
TensorFlow 允许我们自定义模型图以嵌入所有这些处理逻辑,这使得模型服务变得更容易,有多种方法可以实现这一点,但由于我们将使用 TFServing 来服务模型,因此我们可以直接在服务签名中自定义模型图。
我们可以使用以下代码导出包含预处理和后处理逻辑作为默认签名的相同模型,这允许此模型对原始数据进行预测。
def export_model(model, labels):
@tf.function(input_signature=[tf.TensorSpec([None, None, None, 3], tf.float32)])
def serving_fn(image):
processed_img = preprocess(image)
probs = model(processed_img)
label = postprocess(probs)
return {"label": label}
return serving_fn
model_sig_version = 2
model_sig_export_path = f"{model_dir}/{model_sig_version}"
tf.saved_model.save(
model,
export_dir=model_sig_export_path,
signatures={"serving_default": export_model(model, labels)},
)
!saved_model_cli show --dir {model_sig_export_path} --tag_set serve --signature_def serving_default
INFO:tensorflow:Assets written to: ./model/2/assets
INFO:tensorflow:Assets written to: ./model/2/assets
The given SavedModel SignatureDef contains the following input(s):
inputs['image'] tensor_info:
dtype: DT_FLOAT
shape: (-1, -1, -1, 3)
name: serving_default_image:0
The given SavedModel SignatureDef contains the following output(s):
outputs['label'] tensor_info:
dtype: DT_STRING
shape: (-1)
name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
请注意,此模型具有不同的签名,其输入仍然是 4D,但现在形状为 (-1, -1, -1, 3)
,这意味着它支持任何高度和宽度大小的图像。其输出也具有不同的形状,它不再输出 1000 长度的 logits。
我们可以使用以下 API 测试模型使用特定签名的预测
batched_raw_img = tf.expand_dims(sample_img, axis=0)
batched_raw_img = tf.cast(batched_raw_img, tf.float32)
loaded_model = tf.saved_model.load(model_sig_export_path)
loaded_model.signatures["serving_default"](**{"image": batched_raw_img})
{'label': <tf.Tensor: shape=(1,), dtype=string, numpy=array([b'banana'], dtype=object)>}
现在让我们指定可服务模型的特定版本。请注意,当我们使用自定义签名保存模型时,我们使用了不同的文件夹,第一个模型保存在文件夹 /1
(版本 1)中,而具有自定义签名的模型保存在文件夹 /2
(版本 2)中。默认情况下,TFServing 将服务所有共享相同基本父文件夹的模型。
data = json.dumps(
{
"signature_name": "serving_default",
"instances": batched_raw_img.numpy().tolist(),
}
)
url_sig = "https://127.0.0.1:8501/v1/models/model/versions/2:predict"
print(f"REST output shape: {rest_outputs.shape}")
print(f"Predicted class: {rest_outputs}")
输出
REST output shape: (1,)
Predicted class: ['banana']
channel = grpc.insecure_channel("localhost:8500")
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
input_name = list(
loaded_model.signatures["serving_default"].structured_input_signature[1].keys()
)[0]
grpc_outputs = predict_grpc(batched_raw_img, input_name, stub)
grpc_outputs = np.array([grpc_outputs.outputs['label'].string_val])
print(f"gRPC output shape: {grpc_outputs.shape}")
print(f"Predicted class: {grpc_outputs}")
输出
gRPC output shape: (1, 1)
Predicted class: [[b'banana']]