使用 TensorFlow Serving 和 Flask 部署 Keras 模型

作者: Himanshu Rawlan
链接: https://towardsdatascience.com/deploying-keras-models-using-tensorflow-serving-and-flask-508ba00f1037
来源: Towards Data Science

通常,我们需要抽象出机械学习模型细节,并将其部署或集成到到易于使用的 API 中。例如:我们可以提供一个 URl 地址,其他人可以向它发送一个 POST 请求然后得到模型预测结果的 JSON 响应,而不必关心技术细节。

在本教程中,我们会创建一个 TensorFlow Serving 服务来部署我们在 Keras 中构建的 InceptionV3 图像分类卷积神经网络(CNN)。接着我们会创建一个简单的 Flask 服务来接受 POST 请求和执行 Tensorflow serving 服务所需的一些图像预处理,并返回一个 JSON 响应。

什么是 TensorFlow Serving?

Serving(服务) 是你训练机械学习模型后应用它的方式。

TensorFlow Serving 是的把模型应用于生产的过程更容易、更快。它允许你安全地部署新的模型并进行实验,同时保持相同的服务器体系结构和 API。开箱即用,它提供与 TensorFlow 的集成,但可以扩展为其他类型的模型。

安装 TensorFlow Serving

前提:请创建一个 python 虚拟环境并在其中安装带有 TensorFlow 后端的 Keras。在这里阅读更多。

注意:本教程的所有命令都是在 Ubuntu 18.04.1 LTS 的 python 虚拟环境中执行的。

现在,在同样的虚拟环境中运行以下命令(使用 sudo 获取 root 权限):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ apt install curl

$ echo "deb [arch=amd64] http://storage.googleapis.com/tensorflow-serving-apt stable tensorflow-model-server tensorflow-model-server-universal" | sudo tee /etc/apt/sources.list.d/tensorflow-serving.list && curl https://storage.googleapis.com/tensorflow-serving-apt/tensorflow-serving.release.pub.gpg | sudo apt-key add -

$ apt-get update

$ apt-get install tensorflow-model-server

$ tensorflow_model_server --version
TensorFlow ModelServer: 1.10.0-dev
TensorFlow Library: 1.11.0

$ python  --version
Python 3.6.6

你可以使用下面的命令升级到更新版本的 tensorflow-model-server

1
$ apt-get upgrade tensorflow-model-server

我们将要构建的目录

在开始之前了解目录结构有助于我们清楚的了解每个步骤所在的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(tensorflow) ubuntu@Himanshu:~/Desktop/Medium/keras-and-tensorflow-serving$ tree -c
└── keras-and-tensorflow-serving
├── README.md
├── my_image_classifier
│ └── 1
│ ├── saved_model.pb
│ └── variables
│ ├── variables.data-00000-of-00001
│ └── variables.index
├── test_images
│ ├── car.jpg
│ └── car.png
├── flask_server
│ ├── app.py
│ ├── flask_sample_request.py
└── scripts
├── download_inceptionv3_model.py
├── inception.h5
├── auto_cmd.py
├── export_saved_model.py
├── imagenet_class_index.json
└── serving_sample_request.py
6 directories, 15 files

可以从下面的 GitHub 仓库获取所有的文件:

himanshurawlani/keras-and-tensorflow-serving
Deploying Keras models using TensorFlow Serving and Flask — himanshurawlani/keras-and-tensorflow-servinggithub.com

导出 Tensorflow Serving 使用的 Keras model

本教程中,我们将使用 download_inceptionv3_model.py 脚本下载并安装 Keras 中具有 Imagenet 权重的 InceptionV3 卷积神经网络(CNN)。你可以在 keras.applications 库 (here) 下载提供的其他任何模型,或者如果你已经在 Keras 构建了你自己的模型,则可以跳过这一步。

1
2
3
4
5
from keras.applications.inception_v3 import InceptionV3
from keras.layers import Input

inception_model = InceptionV3(weights='imagenet', input_tensor=Input(shape=(224, 224, 3)))
inception_model.save('inception.h5')

执行上面的脚本你应该得到以下输出:

1
2
3
4
$ python download_inceptionv3_model.py
Using TensorFlow backend.
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.5/inception_v3_weights_tf_dim_ordering_tf_kernels.h5
96116736/96112376 [==============================] - 161s 2us/step

现在我们的 InceptionV3 CNN(inception.h5)以 Keras 格式保存。我们希望以 TensorFlow server 可以处理的格式导出模型。执行 export_saved_model.py 脚本完成此操作。

TensorFlow 提供 SavedModel 格式作为导出模型的标准格式。在底层的实现中,Keras 模型完全根据 TensorFlow 对象指定,因此我们可以使用 Tensorflow 方法将其导出。TensorFlow 提供了一个方便的函数 tf.saved_model.simple_save() ,它抽象了一些细节并且适用于大多数用例。

输出:

1
2
$ python export_saved_model.py
WARNING:tensorflow:No training configuration found in save file: the model was *not* compiled. Compile it manually.

出现该警告是因为我们下载了一个预先训练过的模型。我们可以使用这个模型进行推理,但如果想要进一步训练它,则需要在下载它之后执行 compile() 函数。现在我们忽略这个警告也无妨。执行该脚本(export_saved_model.py)后,一下文件会保存在 my_image_classifier 目录:

1
2
3
4
5
6
7
├── my_image_classifier
└── 1
├── saved_model.pb
└── variables
├── variables.data-00000-of-00001
└── variables.index
2 directories, 3 files

假设我们希望在将来更新我们的模型(也因为我们收集了更多的训练数据并在更新了的数据集上训练了模型),我们可以这样做:

  1. 在新的 keras 模型上运行相同的脚本。
  2. export_saved_model.py 中更新 export_path = ‘../my_image_classifier/1’export_path = ‘../my_image_classifier/2’

TensorFlow Serving 会自动检测出 my_image_classifier 目录下模型的新版本,并在服务器中更新它。

启动 TensorFlow Serving 服务

执行以下命令在本地启动 TensorFlow Serving 服务:

1
$ tensorflow_model_server --model_base_path=/home/ubuntu/Desktop/Medium/keras-and-tensorflow-serving/my_image_classifier --rest_api_port=9000 --model_name=ImageClassifier
  • --model_base_path: 必须是绝对路径,否则会报错:
1
Failed to start server. Error: Invalid argument: Expected model ImageClassifier to have an absolute path or URI; got base_path()=./my_image_classifier
  • --rest_api_port:TensorFlow Serving 会在 8500 端口启动一个 gRPC ModelServer,并且 RESET API 可在 9000 端口调用。
  • --model_name:这是你用于发送 POST 请求的服务器的名称。你可以输入任何名称。

测试 TensorFlow Serving 服务

脚本 serving_sample_request.py 向 TensorFlow Serving 服务发送一个 POST 请求。输入图片通过命令行参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import argparse
import json

import numpy as np
import requests
from keras.applications import inception_v3
from keras.preprocessing import image

# Argument parser for giving input image_path from command line
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
help="path of the image")
args = vars(ap.parse_args())

image_path = args['image']
# Preprocessing our input image
img = image.img_to_array(image.load_img(image_path, target_size=(224, 224))) / 255.

# this line is added because of a bug in tf_serving(1.10.0-dev)
img = img.astype('float16')

payload = {
"instances": [{'input_image': img.tolist()}]
}

# sending post request to TensorFlow Serving server
r = requests.post('http://localhost:9000/v1/models/ImageClassifier:predict', json=payload)
pred = json.loads(r.content.decode('utf-8'))

# Decoding the response
# decode_predictions(preds, top=5) by default gives top 5 results
# You can pass "top=10" to get top 10 predicitons
print(json.dumps(inception_v3.decode_predictions(np.array(pred['predictions']))[0]))

输出:

1
2
3
$ python serving_sample_request.py -i ../test_images/car.png
Using TensorFlow backend.
[["n04285008", "sports_car", 0.998414], ["n04037443", "racer", 0.00140099], ["n03459775", "grille", 0.000160794], ["n02974003", "car_wheel", 9.57862e-06], ["n03100240", "convertible", 6.01581e-06]]

与后续调用相比,TensorFlow Serving 服务响应第一次请求的时间略长。

为什么需要 Flask 服务

如你所见,我们已经在 serving_sample_request.py (前端调用者)执行了一些图像预处理步骤。以下是在 TensorFlow serving 服务层之上创建 Flask 服务的原因:

  • 当我们向前端团队提供 API 时,我们需要确保他们不被预处理的技术细节淹没。
  • 我们可能并不总是有 Python 后段服务器(比如:node.js 服务器),因此使用 numpy 和 keras 库进行预处理可能会很麻烦。
  • 如果我们打算提供多个模型,那么我们不得不创建多个 TensorFlow Serving 服务并且在前端代码添加新的 URL。但 Flask 服务会保持域 URL 相同,而我们只需要添加一个新的路由(一个函数)。
  • 可以在 Flask 应用中执行基于订阅的访问、异常处理和其他任务。

我们要做的是消除 TensorFlow Serving 服务和前端的紧耦合。

在本教程,我们将在同一台机器上和与 TensorFlow Serving 相同的虚拟环境中创建一个 Flask 服务,并使用已安装的库。理想情况下,两者(TensorFlow Serving 服务、Flask 服务)应该在不同的机器上运行,因为更多的请求会导致 Flask 服务器因执行图像预处理而变慢。此外,如果请求数量非常高,单个 Flask 可能无法满足需求。如果有多个前端调用者,我们可能还需要一个排队系统。尽管如此,我们可以使用这个方法开发一个令人满意的概念试验。

创建一个 Flask 服务

前提:在 python 虚拟环境中安装 Flask

创建我们的 Flask 服务只需要一个 app.py 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import base64
import json
from io import BytesIO

import numpy as np
import requests
from flask import Flask, request, jsonify
from keras.applications import inception_v3
from keras.preprocessing import image

# from flask_cors import CORS

app = Flask(__name__)


# Uncomment this line if you are making a Cross domain request
# CORS(app)

# Testing URL
@app.route('/hello/', methods=['GET', 'POST'])
def hello_world():
return 'Hello, World!'


@app.route('/imageclassifier/predict/', methods=['POST'])
def image_classifier():
# Decoding and pre-processing base64 image
img = image.img_to_array(image.load_img(BytesIO(base64.b64decode(request.form['b64'])),
target_size=(224, 224))) / 255.

# this line is added because of a bug in tf_serving(1.10.0-dev)
img = img.astype('float16')

# Creating payload for TensorFlow serving request
payload = {
"instances": [{'input_image': img.tolist()}]
}

# Making POST request
r = requests.post('http://localhost:9000/v1/models/ImageClassifier:predict', json=payload)

# Decoding results from TensorFlow Serving server
pred = json.loads(r.content.decode('utf-8'))

# Returning JSON response to the frontend
return jsonify(inception_v3.decode_predictions(np.array(pred['predictions']))[0])

切换工作目录到 app.py 所在的目录,运行以下命令启动 Flask 服务:

1
$ export FLASK_ENV=development && flask run --host=0.0.0.0
  • FLASK_ENV=development:启用调试模式,它基本上向你提供完整的错误日志。不要在生产环境中使用它。
  • flask run 命令自动执行当前目录下的 app.py 文件。
  • --host=0.0.0.0:这允许你从其他机器向 Flask 服务发起请求。要从其他机器发起请求,你必须指定运行 Falsk 服务的机器的 IP 地址来代替 localhost

输出:

1
2
3
4
5
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 1xx-xxx-xx4
Using TensorFlow backend.

使用和之前一样的命令启动 TensorFlow Serving 服务:

1
$ tensorflow_model_server --model_base_path=/home/ubuntu/Desktop/Medium/keras-and-tensorflow-serving/my_image_classifier --rest_api_port=9000 --model_name=ImageClassifier

auto_cmd.py 是一个用于自动启动和停止这两个服务(TensorFlow Serving 和 Falsk)的脚本。你可以修改这个脚本适用两个以上的服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import os
import signal
import subprocess

# Making sure to use virtual environment libraries
activate_this = "/home/ubuntu/tensorflow/bin/activate_this.py"
exec(open(activate_this).read(), dict(__file__=activate_this))

# Change directory to where your Flask's app.py is present
os.chdir("/home/ubuntu/Desktop/Medium/keras-and-tensorflow-serving/flask_server")
tf_ic_server = ""
flask_server = ""

try:
tf_ic_server = subprocess.Popen(["tensorflow_model_server "
"--model_base_path=/home/ubuntu/Desktop/Medium/keras-and-tensorflow-serving/my_image_classifier "
"--rest_api_port=9000 --model_name=ImageClassifier"],
stdout=subprocess.DEVNULL,
shell=True,
preexec_fn=os.setsid)
print("Started TensorFlow Serving ImageClassifier server!")

flask_server = subprocess.Popen(["export FLASK_ENV=development && flask run --host=0.0.0.0"],
stdout=subprocess.DEVNULL,
shell=True,
preexec_fn=os.setsid)
print("Started Flask server!")

while True:
print("Type 'exit' and press 'enter' OR press CTRL+C to quit: ")
in_str = input().strip().lower()
if in_str == 'q' or in_str == 'exit':
print('Shutting down all servers...')
os.killpg(os.getpgid(tf_ic_server.pid), signal.SIGTERM)
os.killpg(os.getpgid(flask_server.pid), signal.SIGTERM)
print('Servers successfully shutdown!')
break
else:
continue
except KeyboardInterrupt:
print('Shutting down all servers...')
os.killpg(os.getpgid(tf_ic_server.pid), signal.SIGTERM)
os.killpg(os.getpgid(flask_server.pid), signal.SIGTERM)
print('Servers successfully shutdown!')

记得修改第 10 行中的路径使其指向你的 app.py 所在目录。你可能还需要修改第 6 行使其指向你的虚拟环境的 bin。然后,你可以在任何目录下通过终端执行以下命令来执行上面的脚本:

1
$ python auto_cmd.py

测试 Falsk 服务和 TensorFlow Serving 服务

我们可以使用 flask_sample_request.py 脚本发起一个简单的请求。该脚本基本上模仿了来自前端的请求:

  1. 获取输入图像,将其编码为 base64 格式,并使用 POST 请求把它发送到 Flask 服务。
  2. Flask 服务解码该 base64 图像并为 TensorFlow Serving 服务预处理它。
  3. 接着 Flask 服务向 TensorFlow 发送一个 POST 请求并其解码响应内容。
  4. 格式化已解码的响应内容并发送回前端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# importing the requests library
import argparse
import base64

import requests

# defining the api-endpoint
API_ENDPOINT = "http://localhost:5000/imageclassifier/predict/"

# taking input image via command line
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
help="path of the image")
args = vars(ap.parse_args())

image_path = args['image']
b64_image = ""
# Encoding the JPG,PNG,etc. image to base64 format
with open(image_path, "rb") as imageFile:
b64_image = base64.b64encode(imageFile.read())

# data to be sent to api
data = {'b64': b64_image}

# sending post request and saving response as response object
r = requests.post(url=API_ENDPOINT, data=data)

# extracting the response
print("{}".format(r.text))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ python flask_sample_request.py -i ../test_images/car.png
[
[
"n04285008",
"sports_car",
0.998414
],
[
"n04037443",
"racer",
0.00140099
],
[
"n03459775",
"grille",
0.000160794
],
[
"n02974003",
"car_wheel",
9.57862e-06
],
[
"n03100240",
"convertible",
6.01581e-06
]
]

我们的 Flask 服务现在只有一个路由用于我们的单个 TensorFlow Serving 服务。我们可以在不同或相同的机械上创建多个 TensorFlow Serving 服务,提供多个模型。为此,只需要在文件 app.py 中添加更多的路由(函数)并在其中执行所用模型的特定预处理。我们可以把这些路由提供给前端团队,根据需要调用模型。

处理跨域 HTTP 请求

考虑使用 Angular 发起 POST 请求的场景,我们的 Flask 服务收到 OPTIONS header 而不是 POST,因为:

  • 当 web 应用请求一个不同源(域,协议,端口)的资源时,它会发起一个跨源 HTTP 请求。
  • CORS(Cross Origin Resource Sharing)(跨域资源共享)是一种使用附加 HTTP headers 来告诉浏览器,让一个在一个源(域)上运行的 web 应用有权访问一个不同源的服务器上的资源的机制。在这里了解更多关于 CORS 的信息。

因此,Angular 不会从 Flask 服务得到任何响应。要解决这个问题,我们需要在 app.py 中启用 Flask-CORS。在这里了解更多。

结尾

以上就是部署我们的机械学习模型供使用所需的全部工作。TensorFlow Serving 使得将机械学习集成到网站和其他应用中变得非常容易。由于 keras 提供了丰富的预建模型(这里),即使只有最少限度的机器学习和深度学习算法知识,也可能开发出超级有用的应用。