创建一个用于对图片进行分类的简单网站

1. 准备工作

在此 Codelab 中,您将学习如何通过 REST 和 gRPC 使用 TensorFlow Serving 从网站运行图片分类推断。

前提条件

  • 具备网页开发方面的基础知识,例如 HTML 和 JavaScript
  • 了解有关使用 TensorFlow 进行机器学习(例如训练与部署)的基本知识
  • 了解有关终端和 Docker 的基本知识

学习内容

  • 如何在 TensorFlow Hub 上查找预训练的图片分类模型。
  • 如何构建一个简单的网站,并通过 TensorFlow Serving(REST 和 gRPC)使用下载的图片分类模型进行预测。
  • 如何在界面中呈现检测结果。

所需条件

2. 进行设置

如需下载此 Codelab 的代码,请执行以下操作:

  1. 进入此 GitHub 代码库
  2. 点击 Code(代码)> Download zip(下载 Zip 文件),下载此 Codelab 的所有代码。

a72f2bb4caa9a96.png

  1. 解压缩已下载的 ZIP 文件,这会解压缩 codelabs 根文件夹,其中包含您需要的所有资源。

在此 Codelab 中,您只需要代码库的 TFServing/ImageClassificationWeb 子目录(其中包含两个文件夹)中的文件:

  • starter 文件夹包含您在此 Codelab 中执行构建的起始代码。
  • finished 文件夹包含已完成示例应用的完成后的代码。

3. 安装依赖项

如需安装依赖项,请执行以下操作:

  • 在终端中,前往 starter 文件夹,然后安装所需的 NPM 软件包:
npm install

4. 运行起始网站

使用 Web Server for Chrome 加载 TFServing/ImageClassificationWeb/starter/dist/index.html 文件:

  1. 在 Chrome 的地址栏中输入 Chrome://apps/,然后在应用列表中找到 Web Server for Chrome
  2. 启动 Web Server for Chrome,然后选择 TFServing/ImageClassificationWeb/starter/dist/ 文件夹。
  3. 点击 Web Server 开关以启用它,然后在浏览器中前往 http://localhost:8887/

f7b43cd44ebf1f1b.png

运行和探索网站

您现在应该会看到该网站。界面非常简单:其中包含一张您要分类的猫图片,用户可以使用 REST 或 gRPC 将数据发送到后端。后端对图片执行图片分类,并将分类结果返回给网站,网站会显示该结果。

837d97a27c59a0b3.png

点击 Classify(分类)不起作用,因为它还不能与后端进行通信。

5. 使用 TensorFlow Serving 部署图片分类模型

图像分类是一项非常常见的机器学习任务,用于根据图像的主要内容将图像分为预定义的类别。下面是一个对花朵进行分类的示例:

a6da16b4a7665db0.png

TensorFlow Hub 上有许多预训练的图片分类模型。在此 Codelab 中,您将使用热门的 Inception v3 模型。

如需使用 TensorFlow Serving 部署图片分类模型,请执行以下操作:

  1. 下载 Inception v3 模型文件。
  2. 使用 7-Zip 等解压缩工具解压缩下载的 .tar.gz 文件。
  3. 创建 inception_v3 文件夹,然后在其中创建 123 子文件夹。
  4. 将解压缩的 variables 文件夹和 saved_model.pb 文件放入 123 子文件夹中。

您可以将 inception_v3 文件夹称为 SavedModel 文件夹。123 是一个示例版本号。您可以根据需要选择其他数字。

文件夹结构应如下图所示:

21a8675ac8d31907.png

启动 TensorFlow Serving

  • 在您的终端中,使用 Docker 启动 TensorFlow Serving,但需要注意将 PATH/TO/SAVEDMODEL 替换为您计算机上的 inception_v3 文件夹的绝对路径。
docker pull tensorflow/serving

docker run -it --rm -p 8500:8500 -p 8501:8501 -v "PATH/TO/SAVEDMODEL:/models/inception" -e MODEL_NAME=inception tensorflow/serving

Docker 会先自动下载 TensorFlow Serving 映像,此过程需要一分钟时间。之后,TensorFlow Serving 便会启动。日志应如下面的代码段所示:

2022-02-25 06:01:12.513231: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:206] Restoring SavedModel bundle.
2022-02-25 06:01:12.585012: I external/org_tensorflow/tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 3000000000 Hz
2022-02-25 06:01:13.395083: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:190] Running initialization op on SavedModel bundle at path: /models/inception/123
2022-02-25 06:01:13.837562: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:277] SavedModel load for tags { serve }; Status: success: OK. Took 1928700 microseconds.
2022-02-25 06:01:13.877848: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/inception/123/assets.extra/tf_serving_warmup_requests
2022-02-25 06:01:13.929844: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: inception version: 123}
2022-02-25 06:01:13.985848: I tensorflow_serving/model_servers/server_core.cc:486] Finished adding/updating models
2022-02-25 06:01:13.985987: I tensorflow_serving/model_servers/server.cc:367] Profiler service is enabled
2022-02-25 06:01:13.988994: I tensorflow_serving/model_servers/server.cc:393] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2022-02-25 06:01:14.033872: I tensorflow_serving/model_servers/server.cc:414] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 245] NET_LOG: Entering the event loop ...

6. 设置 Envoy 代理

目前,TensorFlow Serving 不会设置 Access-Control-Allow-Origin 标头,因此浏览器会出于安全原因阻止前端 JavaScript 向 TensorFlow Serving 发出的请求。如需解决此问题,您需要使用代理(例如 Envoy)将 JavaScript 发出的请求代理到 TensorFlow Serving 后端。

启动 Envoy

  • 在终端中,下载 Envoy 映像并使用 Docker 启动 Envoy,但需要将 PATH/TO/ENVOY-CUSTOM.YAML 占位符替换为 starter 文件夹中 envoy-custom.yaml 文件的绝对路径。
docker pull envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797

docker run --add-host host.docker.internal:host-gateway --rm -it -p 9901:9901 -p 8000:8000 -p 8080:8080 -v PATH/TO/ENVOY-CUSTOM.YAML:/envoy-custom.yaml envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797 -c /envoy-custom.yaml

Docker 会先自动下载 Envoy 映像。之后,Envoy 便会启动。日志应如下面的代码段所示:

[2022-03-02 07:51:48.563][1][info][main] [source/server/server.cc:436]   response trailer map: 152 bytes: grpc-message,grpc-status
[2022-03-02 07:51:48.681][1][info][main] [source/server/server.cc:772] runtime: {}
[2022-03-02 07:51:48.682][1][info][admin] [source/server/admin/admin.cc:134] admin address: 0.0.0.0:9901
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:127] loading tracing configuration
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:87] loading 0 static secret(s)
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:93] loading 2 cluster(s)
[2022-03-02 07:51:48.687][1][info][config] [source/server/configuration_impl.cc:97] loading 2 listener(s)
[2022-03-02 07:51:48.694][1][info][config] [source/server/configuration_impl.cc:109] loading stats configuration
[2022-03-02 07:51:48.696][1][info][main] [source/server/server.cc:868] starting main dispatch loop
[2022-03-02 07:51:48.881][1][info][runtime] [source/common/runtime/runtime_impl.cc:446] RTDS has finished initialization
[2022-03-02 07:51:48.881][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:207] cm init: all clusters initialized
[2022-03-02 07:51:48.881][1][info][main] [source/server/server.cc:849] all clusters initialized. initializing init manager
[2022-03-02 07:51:48.881][1][info][config] [source/server/listener_manager_impl.cc:784] all dependencies initialized. starting workers
[2022-03-02 07:51:48.902][1][warning][main] [source/server/server.cc:747] there is no configured limit to the number of allowed active connections. Set a limit via the runtime key overload.global_downstream_max_connections

7. 通过 REST 将网站与 TensorFlow 关联起来

现在,后端已准备就绪,您可以将客户端请求发送到 TensorFlow Serving 以对图片进行分类。您可以通过以下两种方式向 TensorFlow Serving 发送请求:

  • REST
  • gRPC

通过 REST 发送请求和接收响应

要通过 REST 发送和接收请求需要执行三个简单的步骤:

  1. 创建 REST 请求。
  2. 将 REST 请求发送到 TensorFlow Serving。
  3. 从 REST 响应中提取预测结果,并显示该结果。

您需要在 src/index.js 文件中完成这些步骤。

创建 REST 请求

目前,classify_img() 函数不会将 REST 请求发送到 TensorFlow Serving。您需要先实现此 REST 分支才能创建 REST 请求:

if (radioButtons[0].checked) {
    console.log('Using REST');
    // TODO: Add code to send a REST request to TensorFlow Serving.

} 

TensorFlow Serving 预期收到包含您使用的 Inception v3 模型的图片张量的 POST 请求,因此您需要将图片的每个像素的 RGB 值提取到一个数组中,然后将该数组封装在 JSON 中,而该 JSON 就是请求的载荷。

  • 将以下代码添加到 REST 分支:
//Create the REST request.
let imgTensor = new Array();
let pixelArray = new Array();
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    pixelArray[i] = new Array();
    for (let j=0; j<inputImgWidth; j++) {
        pixelArray[i][j] = new Array();
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[0]/255); 
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[1]/255); 
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[2]/255); 
    }
}
imgTensor.push(pixelArray);

const RESTURL = 'http://localhost:8000/v1/models/inception:predict';        
let xhr = new XMLHttpRequest();
xhr.open('POST', RESTURL);
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8;');
let data = JSON.stringify({
    instances: imgTensor
});    
xhr.onload = () => {

}
xhr.onerror = () => {
    console.log('REST request error');
}

将 REST 请求发送到 TensorFlow Serving

现在,您可以发送请求了。

  • 在 REST 分支中的上述代码之后添加以下代码:
// Send the REST request.
xhr.send(data);

处理来自 TensorFlow Serving 的 REST 响应

Inception v3 模型会返回一个概率数组,表示图片属于预定义类别的概率。预测成功后,您应在界面中输出最可能的类别。

您需要实现 onload() 监听器来处理响应。

xhr.onload = () => {

}
  • 将以下代码添加到 onload() 监听器:
// Process the REST response.
const response = JSON.parse(xhr.responseText);
const maxIndex = argmax(response['predictions'][0])
document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;

现在,监听器会从响应中提取预测概率,确定对象最有可能的类别,并在界面中显示结果。

运行应用

  1. 在终端中,前往 starter 文件夹,然后使用 webpack 将所有 JavaScript 文件捆绑到一个文件中,以便您可以将该文件嵌入到 dist/index.html 文件中:
npm install -g npx
npm install --save-dev webpack
npx webpack
  1. 在浏览器中刷新 http://localhost:8887/,然后点击 REST > Classify

网站显示 286 作为预测类别,该类别映射到 ImageNet 数据集中的 Egyptian Cat 标签。

c865a93b9b58335d.png

8. 通过 gRPC 将网站与 TensorFlow Serving 关联起来

除了 REST 之外,TensorFlow Serving 还支持 gRPC

b6f4449c2c850b0e.png

gRPC 是一种开放源代码的现代高性能远程过程调用 (RPC) 框架,可以在任何环境中运行。借助可插拔支持,它可以在数据中心内和跨数据中心高效地连接服务,以实现负载均衡、跟踪、健康检查和身份验证。我们发现,在实践中,gRPC 的性能比 REST 更高。

使用 gRPC 发送请求和接收响应

只需完成以下四个简单步骤:

  1. 可选:生成 gRPC 客户端桩代码。
  2. 创建 gRPC 请求。
  3. 向 TensorFlow Serving 发送 gRPC 请求。
  4. 从 gRPC 响应中提取预测结果,并将其显示在界面中。

您需要在 src/index.js 文件中完成这些步骤。

可选:生成 gRPC 客户端桩代码

如需将 gRPC 与 TensorFlow Serving 搭配使用,您需要遵循 gRPC 工作流。如需了解详情,请参阅 gRPC 文档

a9d0e5cb543467b4.png

TensorFlow Serving 和 TensorFlow 会为您定义 .proto 文件。从 TensorFlow 和 TensorFlow Serving 2.8 开始,以下 .proto 文件是必须提供的文件:

tensorflow/core/example/example.proto
tensorflow/core/example/feature.proto
tensorflow/core/protobuf/struct.proto
tensorflow/core/protobuf/saved_object_graph.proto
tensorflow/core/protobuf/saver.proto
tensorflow/core/protobuf/trackable_object_graph.proto
tensorflow/core/protobuf/meta_graph.proto
tensorflow/core/framework/node_def.proto
tensorflow/core/framework/attr_value.proto
tensorflow/core/framework/function.proto
tensorflow/core/framework/types.proto
tensorflow/core/framework/tensor_shape.proto
tensorflow/core/framework/full_type.proto
tensorflow/core/framework/versions.proto
tensorflow/core/framework/op_def.proto
tensorflow/core/framework/graph.proto
tensorflow/core/framework/tensor.proto
tensorflow/core/framework/resource_handle.proto
tensorflow/core/framework/variable.proto

tensorflow_serving/apis/inference.proto
tensorflow_serving/apis/classification.proto
tensorflow_serving/apis/predict.proto
tensorflow_serving/apis/regression.proto
tensorflow_serving/apis/get_model_metadata.proto
tensorflow_serving/apis/input.proto
tensorflow_serving/apis/prediction_service.proto
tensorflow_serving/apis/model.proto
  • 在您的终端中,找到 starter/src/proto/ 文件夹并生成桩:
bash generate_grpc_stub_js.sh

创建 gRPC 请求

与 REST 请求类似,您可以在 gRPC 分支中创建 gRPC 请求.

if (connectionMode[picker.selectedRow(inComponent: 0)] == "REST") {

}
else {
    print("Using gRPC")
    // TODO: Add code to send a gRPC request to TensorFlow Serving.
    
}
  • 将以下代码添加到 gRPC 分支:
// Create the gRPC request.
const PredictModule = require('./proto/generated/tensorflow_serving/apis/predict_pb.js');
const PredictionServiceClientModule = require('./proto/generated/tensorflow_serving/apis/prediction_service_grpc_web_pb.js');
const ModelModule = require('./proto/generated/tensorflow_serving/apis/model_pb.js');
const TensorModule = require('./proto/generated/tensorflow/core/framework/tensor_pb.js');

const GPRCURL = 'http://localhost:8080';
const stub = new PredictionServiceClientModule.PredictionServiceClient(GPRCURL);

const modelSpec = new ModelModule.ModelSpec();
modelSpec.setName('inception');

const tensorProto = new TensorModule.TensorProto();
const tensorShapeProto = new TensorModule.TensorShapeProto();

const batchDim = (new TensorModule.TensorShapeProto.Dim()).setSize(1);
const heightDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgHeight);
const widthDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgWidth);
const channelDim = (new TensorModule.TensorShapeProto.Dim()).setSize(3);

tensorShapeProto.setDimList([batchDim, heightDim, widthDim, channelDim]);

tensorProto.setDtype(proto.tensorflow.DataType.DT_FLOAT);
tensorProto.setTensorShape(tensorShapeProto);
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    for (let j=0; j<inputImgWidth; j++) {
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[0]/255); 
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[1]/255); 
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[2]/255); 
    }
}

const predictionServiceRequest = new PredictModule.PredictRequest();
predictionServiceRequest.setModelSpec(modelSpec);
predictionServiceRequest.getInputsMap().set('inputs', tensorProto);

向 TensorFlow Serving 发送 gRPC 请求

现在,您可以发送请求了。

  • 紧接着上一个代码段中 gRPC 分支的代码之后添加以下代码:
// Send the gRPC request.
stub.predict(predictionServiceRequest, {}, function(err, response) {
    // TODO: Add code to process the response.
});

处理来自 TensorFlow Serving 的 gRPC 响应

最后,您需要实现上述回调函数来处理响应。

  • 将以下代码添加到上一个代码段中的函数正文中:
// Process the gRPC response.
if (err) {
    console.log(err.code);
    console.log(err.message);
} 
else {
    const maxIndex = argmax(response.getOutputsMap().get('logits').getFloatValList());
    document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;
}

现在,监听器会从响应中提取预测概率,确定对象最有可能的类别,并在界面中显示结果。

运行应用

  1. 在终端中,使用 webpack 将所有 JavaScript 文件捆绑到一个可以嵌入到 index.html 文件中的文件中:
npx webpack
  1. 在浏览器中刷新 http://localhost:8887/
  2. 点击 gRPC > Classify

网站会显示 286 的预测类别,该类别对应于 ImageNet 数据集中的 Egyptian Cat 标签。

9. 恭喜

您已使用 TensorFlow Serving 为网站添加了图片分类功能!

了解详情