Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)(2)https://developer.aliyun.com/article/1482462
安装和启动 TensorFlow Serving
有许多安装 TF Serving 的方法:使用系统的软件包管理器,使用 Docker 镜像,从源代码安装等。由于 Colab 运行在 Ubuntu 上,我们可以像这样使用 Ubuntu 的apt
软件包管理器:
url = "https://storage.googleapis.com/tensorflow-serving-apt" src = "stable tensorflow-model-server tensorflow-model-server-universal" !echo 'deb {url} {src}' > /etc/apt/sources.list.d/tensorflow-serving.list !curl '{url}/tensorflow-serving.release.pub.gpg' | apt-key add - !apt update -q && apt-get install -y tensorflow-model-server %pip install -q -U tensorflow-serving-api
这段代码首先将 TensorFlow 的软件包存储库添加到 Ubuntu 的软件包源列表中。然后它下载 TensorFlow 的公共 GPG 密钥,并将其添加到软件包管理器的密钥列表中,以便验证 TensorFlow 的软件包签名。接下来,它使用apt
来安装tensorflow-model-server
软件包。最后,它安装tensorflow-serving-api
库,这是我们与服务器通信所需的库。
现在我们想要启动服务器。该命令将需要基本模型目录的绝对路径(即my_mnist_model
的路径,而不是0001
),所以让我们将其保存到MODEL_DIR
环境变量中:
import os os.environ["MODEL_DIR"] = str(model_path.parent.absolute())
然后我们可以启动服务器:
%%bash --bg tensorflow_model_server \ --port=8500 \ --rest_api_port=8501 \ --model_name=my_mnist_model \ --model_base_path="${MODEL_DIR}" >my_server.log 2>&1
在 Jupyter 或 Colab 中,%%bash --bg
魔术命令将单元格作为 bash 脚本执行,在后台运行。>my_server.log 2>&1
部分将标准输出和标准错误重定向到my_server.log文件。就是这样!TF Serving 现在在后台运行,其日志保存在my_server.log中。它加载了我们的 MNIST 模型(版本 1),现在正在分别等待 gRPC 和 REST 请求,端口分别为 8500 和 8501。
现在服务器已经启动运行,让我们首先使用 REST API,然后使用 gRPC API 进行查询。
通过 REST API 查询 TF Serving
让我们从创建查询开始。它必须包含您想要调用的函数签名的名称,当然还有输入数据。由于请求必须使用 JSON 格式,我们必须将输入图像从 NumPy 数组转换为 Python 列表:
import json X_new = X_test[:3] # pretend we have 3 new digit images to classify request_json = json.dumps({ "signature_name": "serving_default", "instances": X_new.tolist(), })
请注意,JSON 格式是 100%基于文本的。请求字符串如下所示:
>>> request_json '{"signature_name": "serving_default", "instances": [[[0, 0, 0, 0, ... ]]]}'
现在让我们通过 HTTP POST 请求将这个请求发送到 TF Serving。这可以使用requests
库来完成(它不是 Python 标准库的一部分,但在 Colab 上是预安装的):
import requests server_url = "http://localhost:8501/v1/models/my_mnist_model:predict" response = requests.post(server_url, data=request_json) response.raise_for_status() # raise an exception in case of error response = response.json()
如果一切顺利,响应应该是一个包含单个"predictions"
键的字典。相应的值是预测列表。这个列表是一个 Python 列表,所以让我们将其转换为 NumPy 数组,并将其中包含的浮点数四舍五入到第二位小数:
>>> import numpy as np >>> y_proba = np.array(response["predictions"]) >>> y_proba.round(2) array([[0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 1\. , 0\. , 0\. ], [0\. , 0\. , 0.99, 0.01, 0\. , 0\. , 0\. , 0\. , 0\. , 0\. ], [0\. , 0.97, 0.01, 0\. , 0\. , 0\. , 0\. , 0.01, 0\. , 0\. ]])
万岁,我们有了预测!模型几乎 100%确信第一张图片是 7,99%确信第二张图片是 2,97%确信第三张图片是 1。这是正确的。
REST API 简单易用,当输入和输出数据不太大时效果很好。此外,几乎任何客户端应用程序都可以在没有额外依赖的情况下进行 REST 查询,而其他协议并不总是那么容易获得。然而,它基于 JSON,这是基于文本且相当冗长的。例如,我们不得不将 NumPy 数组转换为 Python 列表,每个浮点数最终都表示为一个字符串。这非常低效,无论是在序列化/反序列化时间方面——我们必须将所有浮点数转换为字符串然后再转回来——还是在有效载荷大小方面:许多浮点数最终使用超过 15 个字符来表示,这相当于 32 位浮点数超过 120 位!这将导致在传输大型 NumPy 数组时出现高延迟和带宽使用。因此,让我们看看如何改用 gRPC。
提示
在传输大量数据或延迟重要时,最好使用 gRPC API,如果客户端支持的话,因为它使用紧凑的二进制格式和基于 HTTP/2 framing 的高效通信协议。
通过 gRPC API 查询 TF Serving
gRPC API 期望一个序列化的PredictRequest
协议缓冲区作为输入,并输出一个序列化的PredictResponse
协议缓冲区。这些 protobufs 是tensorflow-serving-api
库的一部分,我们之前安装过。首先,让我们创建请求:
from tensorflow_serving.apis.predict_pb2 import PredictRequest request = PredictRequest() request.model_spec.name = model_name request.model_spec.signature_name = "serving_default" input_name = model.input_names[0] # == "flatten_input" request.inputs[input_name].CopyFrom(tf.make_tensor_proto(X_new))
这段代码创建了一个PredictRequest
协议缓冲区,并填充了必需的字段,包括模型名称(之前定义的),我们想要调用的函数的签名名称,最后是输入数据,以Tensor
协议缓冲区的形式。tf.make_tensor_proto()
函数根据给定的张量或 NumPy 数组创建一个Tensor
协议缓冲区,这里是X_new
。
接下来,我们将向服务器发送请求并获取其响应。为此,我们将需要grpcio
库,该库已预先安装在 Colab 中:
import grpc from tensorflow_serving.apis import prediction_service_pb2_grpc channel = grpc.insecure_channel('localhost:8500') predict_service = prediction_service_pb2_grpc.PredictionServiceStub(channel) response = predict_service.Predict(request, timeout=10.0)
代码非常简单:在导入之后,我们在 TCP 端口 8500 上创建一个到localhost的 gRPC 通信通道,然后我们在该通道上创建一个 gRPC 服务,并使用它发送一个带有 10 秒超时的请求。请注意,调用是同步的:它将阻塞,直到收到响应或超时期限到期。在此示例中,通道是不安全的(没有加密,没有身份验证),但 gRPC 和 TF Serving 也支持通过 SSL/TLS 的安全通道。
接下来,让我们将PredictResponse
协议缓冲区转换为张量:
output_name = model.output_names[0] # == "dense_1" outputs_proto = response.outputs[output_name] y_proba = tf.make_ndarray(outputs_proto)
如果您运行此代码并打印y_proba.round(2)
,您将获得与之前完全相同的估计类概率。这就是全部内容:只需几行代码,您现在就可以远程访问您的 TensorFlow 模型,使用 REST 或 gRPC。
部署新的模型版本
现在让我们创建一个新的模型版本并导出一个 SavedModel,这次导出到my_mnist_model/0002目录:
model = [...] # build and train a new MNIST model version model_version = "0002" model_path = Path(model_name) / model_version model.save(model_path, save_format="tf")
在固定的时间间隔(延迟可配置),TF Serving 会检查模型目录是否有新的模型版本。如果找到一个新版本,它会自动优雅地处理过渡:默认情况下,它会用前一个模型版本回答待处理的请求(如果有的话),同时用新版本处理新请求。一旦每个待处理的请求都得到回答,之前的模型版本就会被卸载。您可以在 TF Serving 日志(my_server.log)中看到这个过程:
[...] Reading SavedModel from: /models/my_mnist_model/0002 Reading meta graph with tags { serve } [...] Successfully loaded servable version {name: my_mnist_model version: 2} Quiescing servable version {name: my_mnist_model version: 1} Done quiescing servable version {name: my_mnist_model version: 1} Unloading servable version {name: my_mnist_model version: 1}
提示
如果 SavedModel 包含assets/extra目录中的一些示例实例,您可以配置 TF Serving 在开始使用它来处理请求之前在这些实例上运行新模型。这称为模型预热:它将确保一切都被正确加载,避免第一次请求的长响应时间。
这种方法提供了平稳的过渡,但可能会使用过多的 RAM,特别是 GPU RAM,通常是最有限的。在这种情况下,您可以配置 TF Serving,使其处理所有挂起的请求与先前的模型版本,并在加载和使用新的模型版本之前卸载它。这种配置将避免同时加载两个模型版本,但服务将在短时间内不可用。
正如您所看到的,TF Serving 使部署新模型变得简单。此外,如果您发现第二个版本的效果不如预期,那么回滚到第一个版本就像删除my_mnist_model/0002目录一样简单。
提示
TF Serving 的另一个重要特性是其自动批处理能力,您可以在启动时使用--enable_batching
选项来激活它。当 TF Serving 在短时间内接收到多个请求时(延迟可配置),它会在使用模型之前自动将它们批处理在一起。通过利用 GPU 的性能,这将显著提高性能。一旦模型返回预测结果,TF Serving 会将每个预测结果分发给正确的客户端。通过增加批处理延迟(参见--batching_parameters_file
选项),您可以在一定程度上牺牲一点延迟以获得更大的吞吐量。
如果您希望每秒获得许多查询,您将希望在多台服务器上部署 TF Serving 并负载平衡查询(请参见图 19-2)。这将需要在这些服务器上部署和管理许多 TF Serving 容器。处理这一问题的一种方法是使用诸如Kubernetes之类的工具,它是一个简化跨多台服务器容器编排的开源系统。如果您不想购买、维护和升级所有硬件基础设施,您将希望在云平台上使用虚拟机,如 Amazon AWS、Microsoft Azure、Google Cloud Platform、IBM Cloud、Alibaba Cloud、Oracle Cloud 或其他平台即服务(PaaS)提供商。管理所有虚拟机,处理容器编排(即使借助 Kubernetes 的帮助),照顾 TF Serving 配置、调整和监控——所有这些都可能成为一项全职工作。幸运的是,一些服务提供商可以为您处理所有这些事务。在本章中,我们将使用 Vertex AI:它是今天唯一支持 TPUs 的平台;它支持 TensorFlow 2、Scikit-Learn 和 XGBoost;并提供一套不错的人工智能服务。在这个领域还有其他几家提供商也能够提供 TensorFlow 模型的服务,比如 Amazon AWS SageMaker 和 Microsoft AI Platform,所以请确保也查看它们。
图 19-2。使用负载平衡扩展 TF Serving
现在让我们看看如何在云上提供我们出色的 MNIST 模型!
在 Vertex AI 上创建一个预测服务
Vertex AI 是 Google Cloud Platform(GCP)内的一个平台,提供各种与人工智能相关的工具和服务。您可以上传数据集,让人类对其进行标记,将常用特征存储在特征存储中,并将其用于训练或生产中,使用多个 GPU 或 TPU 服务器进行模型训练,并具有自动超参数调整或模型架构搜索(AutoML)功能。您还可以管理已训练的模型,使用它们对大量数据进行批量预测,为数据工作流程安排多个作业,通过 REST 或 gRPC 以规模化方式提供模型服务,并在名为Workbench的托管 Jupyter 环境中对数据和模型进行实验。甚至还有一个Matching Engine服务,可以非常高效地比较向量(即,近似最近邻)。GCP 还包括其他 AI 服务,例如计算机视觉、翻译、语音转文本等 API。
在我们开始之前,有一些设置需要处理:
- 登录您的 Google 账户,然后转到Google Cloud Platform 控制台(参见图 19-3)。如果您没有 Google 账户,您将需要创建一个。
- 如果这是您第一次使用 GCP,您将需要阅读并接受条款和条件。新用户可以获得免费试用,包括价值 300 美元的 GCP 信用,您可以在 90 天内使用(截至 2022 年 5 月)。您只需要其中的一小部分来支付本章中将使用的服务。注册免费试用后,您仍然需要创建一个付款配置文件并输入您的信用卡号码:这是用于验证目的——可能是为了避免人们多次使用免费试用,但您不会被收取前 300 美元的费用,之后只有在您选择升级到付费账户时才会收费。
图 19-3. Google Cloud Platform 控制台 - 如果您以前使用过 GCP 并且您的免费试用已经过期,那么您在本章中将使用的服务将会花费一些钱。这不应该太多,特别是如果您记得在不再需要这些服务时关闭它们。在运行任何服务之前,请确保您理解并同意定价条件。如果服务最终花费超出您的预期,我在此不承担任何责任!还请确保您的计费账户是活动的。要检查,请打开左上角的☰导航菜单,点击计费,然后确保您已设置付款方式并且计费账户是活动的。
- GCP 中的每个资源都属于一个 项目。这包括您可能使用的所有虚拟机、存储的文件和运行的训练作业。当您创建一个帐户时,GCP 会自动为您创建一个名为“我的第一个项目”的项目。如果您愿意,可以通过转到项目设置来更改其显示名称:在 ☰ 导航菜单中,选择“IAM 和管理员 → 设置”,更改项目的显示名称,然后单击“保存”。请注意,项目还有一个唯一的 ID 和编号。您可以在创建项目时选择项目 ID,但以后无法更改。项目编号是自动生成的,无法更更改。如果您想创建一个新项目,请单击页面顶部的项目名称,然后单击“新项目”并输入项目名称。您还可以单击“编辑”来设置项目 ID。确保此新项目的计费处于活动状态,以便可以对服务费用进行计费(如果有免费信用)。
警告
请始终设置提醒,以便在您知道只需要几个小时时关闭服务,否则您可能会让其运行数天或数月,从而产生潜在的显著成本。 - 现在您已经拥有 GCP 帐户和项目,并且计费已激活,您必须激活所需的 API。在☰导航菜单中,选择“API 和服务”,确保启用了 Cloud Storage API。如果需要,点击+启用 API 和服务,找到 Cloud Storage,并启用它。还要启用 Vertex AI API。
您可以继续通过 GCP 控制台完成所有操作,但我建议改用 Python:这样您可以编写脚本来自动化几乎任何您想要在 GCP 上完成的任务,而且通常比通过菜单和表单点击更方便,特别是对于常见任务。
在您使用任何 GCP 服务之前,您需要做的第一件事是进行身份验证。在使用 Colab 时最简单的解决方案是执行以下代码:
from google.colab import auth auth.authenticate_user()
认证过程基于 OAuth 2.0:一个弹出窗口会要求您确认您希望 Colab 笔记本访问您的 Google 凭据。如果您接受,您必须选择与 GCP 相同的 Google 帐户。然后,您将被要求确认您同意授予 Colab 对 Google Drive 和 GCP 中所有数据的完全访问权限。如果您允许访问,只有当前笔记本将具有访问权限,并且仅在 Colab 运行时到期之前。显然,只有在您信任笔记本中的代码时才应接受此操作。
警告
如果您不使用来自 https://github.com/ageron/handson-ml3 的官方笔记本,则应格外小心:如果笔记本的作者心怀不轨,他们可能包含代码来对您的数据进行任何操作。
现在让我们创建一个 Google Cloud Storage 存储桶来存储我们的 SavedModels(GCS 的存储桶是您数据的容器)。为此,我们将使用预先安装在 Colab 中的google-cloud-storage
库。我们首先创建一个Client
对象,它将作为与 GCS 的接口,然后我们使用它来创建存储桶:
from google.cloud import storage project_id = "my_project" # change this to your project ID bucket_name = "my_bucket" # change this to a unique bucket name location = "us-central1" storage_client = storage.Client(project=project_id) bucket = storage_client.create_bucket(bucket_name, location=location)
提示
如果您想重用现有的存储桶,请将最后一行替换为bucket = storage_client.bucket(bucket_name)
。确保location
设置为存储桶的地区。
GCS 使用单个全球命名空间用于存储桶,因此像“machine-learning”这样的简单名称很可能不可用。确保存储桶名称符合 DNS 命名约定,因为它可能在 DNS 记录中使用。此外,存储桶名称是公开的,因此不要在名称中放入任何私人信息。通常使用您的域名、公司名称或项目 ID 作为前缀以确保唯一性,或者只需在名称中使用一个随机数字。
如果您想要,可以更改区域,但请确保选择支持 GPU 的区域。此外,您可能需要考虑到不同区域之间价格差异很大,一些区域产生的 CO₂比其他区域多得多,一些区域不支持所有服务,并且使用单一区域存储桶可以提高性能。有关更多详细信息,请参阅Google Cloud 的区域列表和Vertex AI 的位置文档。如果您不确定,最好选择"us-central1"
。
接下来,让我们将my_mnist_model目录上传到新的存储桶。在 GCS 中,文件被称为blobs(或objects),在幕后它们都只是放在存储桶中,没有任何目录结构。Blob 名称可以是任意的 Unicode 字符串,甚至可以包含斜杠(/
)。GCP 控制台和其他工具使用这些斜杠来产生目录的幻觉。因此,当我们上传my_mnist_model目录时,我们只关心文件,而不是目录。
def upload_directory(bucket, dirpath): dirpath = Path(dirpath) for filepath in dirpath.glob("**/*"): if filepath.is_file(): blob = bucket.blob(filepath.relative_to(dirpath.parent).as_posix()) blob.upload_from_filename(filepath) upload_directory(bucket, "my_mnist_model")
这个函数现在运行良好,但如果有很多文件要上传,它会非常慢。通过多线程可以很容易地大大加快速度(请参阅笔记本中的实现)。或者,如果您有 Google Cloud CLI,则可以使用以下命令:
!gsutil -m cp -r my_mnist_model gs://{bucket_name}/
接下来,让我们告诉 Vertex AI 关于我们的 MNIST 模型。要与 Vertex AI 通信,我们可以使用google-cloud-aiplatform
库(它仍然使用旧的 AI Platform 名称而不是 Vertex AI)。它在 Colab 中没有预安装,所以我们需要安装它。之后,我们可以导入该库并进行初始化——只需指定一些项目 ID 和位置的默认值——然后我们可以创建一个新的 Vertex AI 模型:我们指定一个显示名称,我们模型的 GCS 路径(在这种情况下是版本 0001),以及我们希望 Vertex AI 使用的 Docker 容器的 URL 来运行此模型。如果您访问该 URL 并向上导航一个级别,您将找到其他可以使用的容器。这个支持带有 GPU 的 TensorFlow 2.8:
from google.cloud import aiplatform server_image = "gcr.io/cloud-aiplatform/prediction/tf2-gpu.2-8:latest" aiplatform.init(project=project_id, location=location) mnist_model = aiplatform.Model.upload( display_name="mnist", artifact_uri=f"gs://{bucket_name}/my_mnist_model/0001", serving_container_image_uri=server_image, )
现在让我们部署这个模型,这样我们就可以通过 gRPC 或 REST API 查询它以进行预测。为此,我们首先需要创建一个端点。这是客户端应用程序在想要访问服务时连接的地方。然后我们需要将我们的模型部署到这个端点:
endpoint = aiplatform.Endpoint.create(display_name="mnist-endpoint") endpoint.deploy( mnist_model, min_replica_count=1, max_replica_count=5, machine_type="n1-standard-4", accelerator_type="NVIDIA_TESLA_K80", accelerator_count=1 )
这段代码可能需要几分钟才能运行,因为 Vertex AI 需要设置一个虚拟机。在这个例子中,我们使用一个相当基本的 n1-standard-4
类型的机器(查看https://homl.info/machinetypes 获取其他类型)。我们还使用了一个基本的 NVIDIA_TESLA_K80
类型的 GPU(查看https://homl.info/accelerators 获取其他类型)。如果您选择的区域不是 "us-central1"
,那么您可能需要将机器类型或加速器类型更改为该区域支持的值(例如,并非所有区域都有 Nvidia Tesla K80 GPU)。
注意
Google Cloud Platform 实施各种 GPU 配额,包括全球范围和每个地区:您不能在未经 Google 授权的情况下创建成千上万个 GPU 节点。要检查您的配额,请在 GCP 控制台中打开“IAM 和管理员 → 配额”。如果某些配额太低(例如,如果您需要在特定地区更多的 GPU),您可以要求增加它们;通常需要大约 48 小时。
Vertex AI 将最初生成最少数量的计算节点(在这种情况下只有一个),每当每秒查询次数变得过高时,它将生成更多节点(最多为您定义的最大数量,这种情况下为五个),并在它们之间负载均衡查询。如果一段时间内 QPS 速率下降,Vertex AI 将自动停止额外的计算节点。因此,成本直接与负载、您选择的机器和加速器类型以及您在 GCS 上存储的数据量相关。这种定价模型非常适合偶尔使用者和有重要使用高峰的服务。对于初创公司来说也是理想的:价格保持低延迟到公司真正开始运营。
恭喜,您已经将第一个模型部署到云端!现在让我们查询这个预测服务:
response = endpoint.predict(instances=X_new.tolist())
我们首先需要将要分类的图像转换为 Python 列表,就像我们之前使用 REST API 向 TF Serving 发送请求时所做的那样。响应对象包含预测结果,表示为 Python 浮点数列表的列表。让我们将它们四舍五入到两位小数并将它们转换为 NumPy 数组:
>>> import numpy as np >>> np.round(response.predictions, 2) array([[0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 1\. , 0\. , 0\. ], [0\. , 0\. , 0.99, 0.01, 0\. , 0\. , 0\. , 0\. , 0\. , 0\. ], [0\. , 0.97, 0.01, 0\. , 0\. , 0\. , 0\. , 0.01, 0\. , 0\. ]])
是的!我们得到了与之前完全相同的预测结果。我们现在在云上有一个很好的预测服务,我们可以从任何地方安全地查询,并且可以根据 QPS 的数量自动扩展或缩小。当您使用完端点后,请不要忘记将其删除,以避免无谓地支付费用:
endpoint.undeploy_all() # undeploy all models from the endpoint endpoint.delete()
现在让我们看看如何在 Vertex AI 上运行作业,对可能非常大的数据批次进行预测。
在 Vertex AI 上运行批量预测作业
如果我们需要进行大量预测,那么我们可以请求 Vertex AI 为我们运行预测作业,而不是重复调用我们的预测服务。这不需要端点,只需要一个模型。例如,让我们在测试集的前 100 张图像上运行一个预测作业,使用我们的 MNIST 模型。为此,我们首先需要准备批处理并将其上传到 GCS。一种方法是创建一个文件,每行包含一个实例,每个实例都格式化为 JSON 值——这种格式称为 JSON Lines——然后将此文件传递给 Vertex AI。因此,让我们在一个新目录中创建一个 JSON Lines 文件,然后将此目录上传到 GCS:
batch_path = Path("my_mnist_batch") batch_path.mkdir(exist_ok=True) with open(batch_path / "my_mnist_batch.jsonl", "w") as jsonl_file: for image in X_test[:100].tolist(): jsonl_file.write(json.dumps(image)) jsonl_file.write("\n") upload_directory(bucket, batch_path)
现在我们准备启动预测作业,指定作业的名称、要使用的机器和加速器的类型和数量,刚刚创建的 JSON Lines 文件的 GCS 路径,以及 Vertex AI 将保存模型预测的 GCS 目录的路径:
batch_prediction_job = mnist_model.batch_predict( job_display_name="my_batch_prediction_job", machine_type="n1-standard-4", starting_replica_count=1, max_replica_count=5, accelerator_type="NVIDIA_TESLA_K80", accelerator_count=1, gcs_source=[f"gs://{bucket_name}/{batch_path.name}/my_mnist_batch.jsonl"], gcs_destination_prefix=f"gs://{bucket_name}/my_mnist_predictions/", sync=True # set to False if you don't want to wait for completion )
提示
对于大批量数据,您可以将输入拆分为多个 JSON Lines 文件,并通过gcs_source
参数列出它们。
这将需要几分钟的时间,主要是为了在 Vertex AI 上生成计算节点。一旦这个命令完成,预测将会以类似prediction.results-00001-of-00002的文件集合中可用。这些文件默认使用 JSON Lines 格式,每个值都是包含实例及其对应预测(即 10 个概率)的字典。实例按照输入的顺序列出。该作业还会输出prediction-errors文件,如果出现问题,这些文件对于调试可能会有用。我们可以使用batch_prediction_job.iter_outputs()
迭代所有这些输出文件,所以让我们遍历所有的预测并将它们存储在y_probas
数组中:
y_probas = [] for blob in batch_prediction_job.iter_outputs(): if "prediction.results" in blob.name: for line in blob.download_as_text().splitlines(): y_proba = json.loads(line)["prediction"] y_probas.append(y_proba)
现在让我们看看这些预测有多好:
>>> y_pred = np.argmax(y_probas, axis=1) >>> accuracy = np.sum(y_pred == y_test[:100]) / 100 0.98
很好,98%的准确率!
JSON Lines 格式是默认格式,但是当处理大型实例(如图像)时,它太冗长了。幸运的是,batch_predict()
方法接受一个instances_format
参数,让您可以选择另一种格式。它默认为"jsonl"
,但您可以将其更改为"csv"
、"tf-record"
、"tf-record-gzip"
、"bigquery"
或"file-list"
。如果将其设置为"file-list"
,那么gcs_source
参数应指向一个文本文件,其中每行包含一个输入文件路径;例如,指向 PNG 图像文件。Vertex AI 将读取这些文件作为二进制文件,使用 Base64 对其进行编码,并将生成的字节字符串传递给模型。这意味着您必须在模型中添加一个预处理层来解析 Base64 字符串,使用tf.io.decode_base64()
。如果文件是图像,则必须使用类似tf.io.decode_image()
或tf.io.decode_png()
的函数来解析结果,如第十三章中所讨论的。
当您完成使用模型后,如果需要,可以通过运行mnist_model.delete()
来删除它。您还可以删除在您的 GCS 存储桶中创建的目录,可选地删除存储桶本身(如果为空),以及批量预测作业。
for prefix in ["my_mnist_model/", "my_mnist_batch/", "my_mnist_predictions/"]: blobs = bucket.list_blobs(prefix=prefix) for blob in blobs: blob.delete() bucket.delete() # if the bucket is empty batch_prediction_job.delete()
您现在知道如何将模型部署到 Vertex AI,创建预测服务,并运行批量预测作业。但是如果您想将模型部署到移动应用程序,或者嵌入式设备,比如加热控制系统、健身追踪器或自动驾驶汽车呢?
将模型部署到移动设备或嵌入式设备
机器学习模型不仅限于在拥有多个 GPU 的大型集中式服务器上运行:它们可以更接近数据源运行(这被称为边缘计算),例如在用户的移动设备或嵌入式设备中。去中心化计算并将其移向边缘有许多好处:它使设备即使未连接到互联网时也能智能化,通过不必将数据发送到远程服务器来减少延迟并减轻服务器负载,并且可能提高隐私性,因为用户的数据可以保留在设备上。
然而,将模型部署到边缘也有其缺点。与强大的多 GPU 服务器相比,设备的计算资源通常很少。一个大模型可能无法适应设备,可能使用过多的 RAM 和 CPU,并且可能下载时间过长。结果,应用可能变得无响应,设备可能会发热并迅速耗尽电池。为了避免这一切,您需要制作一个轻量级且高效的模型,而不会牺牲太多准确性。 TFLite库提供了几个工具,帮助您将模型部署到边缘,主要有三个目标:
- 减小模型大小,缩短下载时间并减少 RAM 使用量。
- 减少每次预测所需的计算量,以减少延迟、电池使用量和发热。
- 使模型适应特定设备的限制。
为了减小模型大小,TFLite 的模型转换器可以接受 SavedModel 并将其压缩为基于 FlatBuffers 的更轻量级格式。这是一个高效的跨平台序列化库(有点像协议缓冲区),最初由谷歌为游戏创建。它设计成可以直接将 FlatBuffers 加载到 RAM 中,无需任何预处理:这样可以减少加载时间和内存占用。一旦模型加载到移动设备或嵌入式设备中,TFLite 解释器将执行它以进行预测。以下是如何将 SavedModel 转换为 FlatBuffer 并保存为 .tflite 文件的方法:
converter = tf.lite.TFLiteConverter.from_saved_model(str(model_path)) tflite_model = converter.convert() with open("my_converted_savedmodel.tflite", "wb") as f: f.write(tflite_model)
提示
您还可以使用 tf.lite.TFLiteConverter.from_keras_model(model)
将 Keras 模型直接保存为 FlatBuffer 格式。
转换器还优化模型,既缩小模型大小,又减少延迟。它修剪所有不需要进行预测的操作(例如训练操作),并在可能的情况下优化计算;例如,3 × a + 4 ×_ a_ + 5 × a 将被转换为 12 × a。此外,它尝试在可能的情况下融合操作。例如,如果可能的话,批量归一化层最终会合并到前一层的加法和乘法操作中。要了解 TFLite 可以对模型进行多少优化,可以下载其中一个预训练的 TFLite 模型,例如Inception_V1_quant(点击tflite&pb),解压缩存档,然后打开优秀的Netron 图形可视化工具并上传*.pb文件以查看原始模型。这是一个庞大而复杂的图形,对吧?接下来,打开优化后的.tflite*模型,惊叹于其美丽!
除了简单地使用较小的神经网络架构之外,您可以减小模型大小的另一种方法是使用较小的位宽:例如,如果您使用半精度浮点数(16 位)而不是常规浮点数(32 位),模型大小将缩小 2 倍,代价是(通常很小的)准确度下降。此外,训练速度将更快,您将使用大约一半的 GPU 内存。
TFLite 的转换器可以进一步将模型权重量化为固定点、8 位整数!与使用 32 位浮点数相比,这导致了四倍的大小减小。最简单的方法称为后训练量化:它只是在训练后量化权重,使用一种相当基本但高效的对称量化技术。它找到最大绝对权重值m,然后将浮点范围–m到+m映射到固定点(整数)范围–127 到+127。例如,如果权重范围从–1.5 到+0.8,则字节–127、0 和+127 将分别对应于浮点–1.5、0.0 和+1.5(参见图 19-5)。请注意,当使用对称量化时,0.0 始终映射为 0。还请注意,在此示例中不会使用字节值+68 到+127,因为它们映射到大于+0.8 的浮点数。
图 19-5。从 32 位浮点数到 8 位整数,使用对称量化
要执行这种训练后的量化,只需在调用convert()
方法之前将DEFAULT
添加到转换器优化列表中:
converter.optimizations = [tf.lite.Optimize.DEFAULT]
这种技术显著减小了模型的大小,使得下载速度更快,占用的存储空间更少。在运行时,量化的权重在使用之前会被转换回浮点数。这些恢复的浮点数与原始浮点数并不完全相同,但也不会相差太远,因此精度损失通常是可以接受的。为了避免一直重新计算浮点值,这样会严重减慢模型的速度,TFLite 会对其进行缓存:不幸的是,这意味着这种技术并不会减少 RAM 的使用量,也不会加快模型的速度。它主要用于减小应用程序的大小。
减少延迟和功耗的最有效方法是对激活进行量化,使得计算可以完全使用整数,而无需任何浮点运算。即使使用相同的位宽(例如,32 位整数而不是 32 位浮点数),整数计算使用的 CPU 周期更少,消耗的能量更少,产生的热量也更少。如果还减少位宽(例如,降至 8 位整数),可以获得巨大的加速。此外,一些神经网络加速器设备(如 Google 的 Edge TPU)只能处理整数,因此权重和激活的完全量化是强制性的。这可以在训练后完成;它需要一个校准步骤来找到激活的最大绝对值,因此您需要向 TFLite 提供代表性的训练数据样本(不需要很大),它将通过模型处理数据并测量量化所需的激活统计信息。这一步通常很快。
量化的主要问题是它会失去一点准确性:这类似于在权重和激活中添加噪声。如果准确性下降太严重,那么您可能需要使用量化感知训练。这意味着向模型添加虚假量化操作,以便它在训练过程中学会忽略量化噪声;最终的权重将更加稳健地适应量化。此外,校准步骤可以在训练过程中自动处理,这简化了整个过程。
我已经解释了 TFLite 的核心概念,但要完全编写移动或嵌入式应用程序需要一本专门的书。幸运的是,一些书籍存在:如果您想了解有关为移动和嵌入式设备构建 TensorFlow 应用程序的更多信息,请查看 O’Reilly 的书籍TinyML: Machine Learning with TensorFlow on Arduino and Ultra-Low Power Micro-Controllers,作者是 Pete Warden(TFLite 团队的前负责人)和 Daniel Situnayake,以及AI and Machine Learning for On-Device Development,作者是 Laurence Moroney。
那么,如果您想在网站中使用您的模型,在用户的浏览器中直接运行呢?
在网页中运行模型
在客户端,即用户的浏览器中运行您的机器学习模型,而不是在服务器端运行,可以在许多场景下非常有用,例如:
- 当您的网络应用经常在用户的连接不稳定或缓慢的情况下使用(例如,徒步者的网站),因此在客户端直接运行模型是使您的网站可靠的唯一方法。
- 当您需要模型的响应尽可能快时(例如,用于在线游戏)。消除查询服务器进行预测的需要肯定会减少延迟,并使网站更加响应。
- 当您的网络服务基于一些私人用户数据进行预测,并且您希望通过在客户端进行预测来保护用户的隐私,以便私人数据永远不必离开用户的设备。
对于所有这些场景,您可以使用TensorFlow.js(TFJS)JavaScript 库。该库可以在用户的浏览器中加载 TFLite 模型并直接进行预测。例如,以下 JavaScript 模块导入了 TFJS 库,下载了一个预训练的 MobileNet 模型,并使用该模型对图像进行分类并记录预测结果。您可以在https://homl.info/tfjscode上尝试这段代码,使用 Glitch.com,这是一个允许您免费在浏览器中构建 Web 应用程序的网站;点击页面右下角的预览按钮查看代码的运行情况:
import "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"; import "https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@1.0.0"; const image = document.getElementById("image"); mobilenet.load().then(model => { model.classify(image).then(predictions => { for (var i = 0; i < predictions.length; i++) { let className = predictions[i].className let proba = (predictions[i].probability * 100).toFixed(1) console.log(className + " : " + proba + "%"); } }); });
甚至可以将这个网站转变成一个渐进式 Web 应用程序(PWA):这是一个遵守一系列标准的网站,使其可以在任何浏览器中查看,甚至可以在移动设备上作为独立应用程序安装。例如,在移动设备上尝试访问https://homl.info/tfjswpa:大多数现代浏览器会询问您是否想要将 TFJS 演示添加到主屏幕。如果您接受,您将在应用程序列表中看到一个新图标。点击此图标将在其自己的窗口中加载 TFJS 演示网站,就像常规移动应用程序一样。PWA 甚至可以配置为离线工作,通过使用服务工作者:这是一个在浏览器中以自己独立线程运行的 JavaScript 模块,拦截网络请求,使其可以缓存资源,从而使 PWA 可以更快地运行,甚至完全离线运行。它还可以传递推送消息,在后台运行任务等。PWA 允许您管理 Web 和移动设备的单个代码库。它们还使得更容易确保所有用户运行您应用程序的相同版本。您可以在 Glitch.com 上玩这个 TFJS 演示的 PWA 代码,网址是https://homl.info/wpacode。
提示
在https://tensorflow.org/js/demos上查看更多在您的浏览器中运行的机器学习模型的演示。
TFJS 还支持在您的网络浏览器中直接训练模型!而且速度相当快。如果您的计算机有 GPU 卡,那么 TFJS 通常可以使用它,即使它不是 Nvidia 卡。实际上,TFJS 将在可用时使用 WebGL,由于现代网络浏览器通常支持各种 GPU 卡,TFJS 实际上支持的 GPU 卡比常规的 TensorFlow 更多(后者仅支持 Nvidia 卡)。
在用户的网络浏览器中训练模型可以特别有用,可以确保用户的数据保持私密。模型可以在中央进行训练,然后在浏览器中根据用户的数据进行本地微调。如果您对这个话题感兴趣,请查看联邦学习。
再次强调,要全面涵盖这个主题需要一本完整的书。如果您想了解更多关于 TensorFlow.js 的内容,请查看 O’reilly 图书《云端、移动和边缘的实用深度学习》(Anirudh Koul 等著)或《学习 TensorFlow.js》(Gant Laborde 著)。
现在您已经看到如何将 TensorFlow 模型部署到 TF Serving,或者通过 Vertex AI 部署到云端,或者使用 TFLite 部署到移动和嵌入式设备,或者使用 TFJS 部署到 Web 浏览器,让我们讨论如何使用 GPU 加速计算。
使用 GPU 加速计算
在第十一章中,我们看了几种可以显著加快训练速度的技术:更好的权重初始化、复杂的优化器等等。但即使使用了所有这些技术,使用单个 CPU 的单台机器训练大型神经网络可能需要几个小时、几天,甚至几周,具体取决于任务。由于 GPU 的出现,这种训练时间可以缩短到几分钟或几小时。这不仅节省了大量时间,还意味着您可以更轻松地尝试各种模型,并经常使用新数据重新训练您的模型。
在之前的章节中,我们在 Google Colab 上使用了启用 GPU 的运行时。您只需从运行时菜单中选择“更改运行时类型”,然后选择 GPU 加速器类型;TensorFlow 会自动检测 GPU 并使用它加速计算,代码与没有 GPU 时完全相同。然后,在本章中,您看到了如何将模型部署到 Vertex AI 上的多个启用 GPU 的计算节点:只需在创建 Vertex AI 模型时选择正确的启用 GPU 的 Docker 镜像,并在调用endpoint.deploy()
时选择所需的 GPU 类型。但是,如果您想购买自己的 GPU 怎么办?如果您想在单台机器上的 CPU 和多个 GPU 设备之间分发计算(参见图 19-6)?这是我们现在将讨论的内容,然后在本章的后面部分我们将讨论如何在多个服务器上分发计算。
图 19-6。在多个设备上并行执行 TensorFlow 图
获取自己的 GPU
如果你知道你将会长时间大量使用 GPU,那么购买自己的 GPU 可能是经济上合理的。你可能也想在本地训练模型,因为你不想将数据上传到云端。或者你只是想购买一张用于游戏的 GPU 卡,并且想将其用于深度学习。
如果您决定购买 GPU 卡,那么请花些时间做出正确的选择。您需要考虑您的任务所需的 RAM 数量(例如,图像处理或 NLP 通常至少需要 10GB),带宽(即您可以将数据发送到 GPU 和从 GPU 中发送数据的速度),核心数量,冷却系统等。Tim Dettmers 撰写了一篇优秀的博客文章来帮助您选择:我鼓励您仔细阅读。在撰写本文时,TensorFlow 仅支持具有 CUDA Compute Capability 3.5+的 Nvidia 卡(当然还有 Google 的 TPU),但它可能会将其支持扩展到其他制造商,因此请务必查看TensorFlow 的文档以了解今天支持哪些设备。
如果您选择 Nvidia GPU 卡,您将需要安装适当的 Nvidia 驱动程序和几个 Nvidia 库。这些包括计算统一设备架构库(CUDA)工具包,它允许开发人员使用支持 CUDA 的 GPU 进行各种计算(不仅仅是图形加速),以及CUDA 深度神经网络库(cuDNN),一个 GPU 加速的常见 DNN 计算库,例如激活层、归一化、前向和反向卷积以及池化(参见第十四章)。cuDNN 是 Nvidia 的深度学习 SDK 的一部分。请注意,您需要创建一个 Nvidia 开发者帐户才能下载它。TensorFlow 使用 CUDA 和 cuDNN 来控制 GPU 卡并加速计算(参见图 19-7)。
图 19-7. TensorFlow 使用 CUDA 和 cuDNN 来控制 GPU 并加速 DNNs
安装了 GPU 卡和所有必需的驱动程序和库之后,您可以使用nvidia-smi
命令来检查一切是否正确安装。该命令列出了可用的 GPU 卡,以及每张卡上运行的所有进程。在这个例子中,这是一张 Nvidia Tesla T4 GPU 卡,大约有 15GB 的可用内存,并且当前没有任何进程在运行:
$ nvidia-smi Sun Apr 10 04:52:10 2022 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 460.32.03 Driver Version: 460.32.03 CUDA Version: 11.2 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |===============================+======================+======================| | 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 | | N/A 34C P8 9W / 70W | 3MiB / 15109MiB | 0% Default | | | | N/A | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=============================================================================| | No running processes found | +-----------------------------------------------------------------------------+
要检查 TensorFlow 是否真正看到您的 GPU,请运行以下命令并确保结果不为空:
>>> physical_gpus = tf.config.list_physical_devices("GPU") >>> physical_gpus [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
管理 GPU 内存
默认情况下,TensorFlow 在第一次运行计算时会自动占用几乎所有可用 GPU 的 RAM,以限制 GPU RAM 的碎片化。这意味着如果您尝试启动第二个 TensorFlow 程序(或任何需要 GPU 的程序),它将很快耗尽 RAM。这种情况并不像您可能认为的那样经常发生,因为通常您会在一台机器上运行一个单独的 TensorFlow 程序:通常是一个训练脚本、一个 TF Serving 节点或一个 Jupyter 笔记本。如果出于某种原因需要运行多个程序(例如,在同一台机器上并行训练两个不同的模型),那么您需要更均匀地在这些进程之间分配 GPU RAM。
如果您的机器上有多个 GPU 卡,一个简单的解决方案是将每个 GPU 卡分配给单个进程。为此,您可以设置CUDA_VISIBLE_DEVICES
环境变量,以便每个进程只能看到适当的 GPU 卡。还要设置CUDA_DEVICE_ORDER
环境变量为PCI_BUS_ID
,以确保每个 ID 始终指向相同的 GPU 卡。例如,如果您有四个 GPU 卡,您可以启动两个程序,将两个 GPU 分配给每个程序,通过在两个单独的终端窗口中执行以下命令来实现:
$ CUDA_DEVICE_ORDER=PCI_BUS_IDCUDA_VISIBLE_DEVICES=0,1python3program_1.py*`#` `and``in``another``terminal:`*$ CUDA_DEVICE_ORDER=PCI_BUS_IDCUDA_VISIBLE_DEVICES=3,2python3program_2.py • 1
程序 1 将只看到 GPU 卡 0 和 1,分别命名为"/gpu:0"
和"/gpu:1"
,在 TensorFlow 中,程序 2 将只看到 GPU 卡 2 和 3,分别命名为"/gpu:1"
和"/gpu:0"
(注意顺序)。一切都将正常工作(参见图 19-8)。当然,您也可以在 Python 中通过设置os.environ["CUDA_DEVICE_ORDER"]
和os.environ["CUDA_VISIBLE_DEVICES"]
来定义这些环境变量,只要在使用 TensorFlow 之前这样做。
图 19-8。每个程序获得两个 GPU
另一个选项是告诉 TensorFlow 只获取特定数量的 GPU RAM。这必须在导入 TensorFlow 后立即完成。例如,要使 TensorFlow 只在每个 GPU 上获取 2 GiB 的 RAM,您必须为每个物理 GPU 设备创建一个逻辑 GPU 设备(有时称为虚拟 GPU 设备),并将其内存限制设置为 2 GiB(即 2,048 MiB):
for gpu in physical_gpus: tf.config.set_logical_device_configuration( gpu, [tf.config.LogicalDeviceConfiguration(memory_limit=2048)] )
假设您有四个 GPU,每个 GPU 至少有 4 GiB 的 RAM:在这种情况下,可以并行运行两个像这样的程序,每个程序使用所有四个 GPU 卡(请参见图 19-9)。如果在两个程序同时运行时运行nvidia-smi
命令,则应该看到每个进程在每张卡上占用 2 GiB 的 RAM。
图 19-9。每个程序都可以获得四个 GPU,但每个 GPU 只有 2 GiB 的 RAM
另一个选项是告诉 TensorFlow 只在需要时获取内存。同样,在导入 TensorFlow 后必须立即执行此操作:
for gpu in physical_gpus: tf.config.experimental.set_memory_growth(gpu, True)
另一种方法是将TF_FORCE_GPU_ALLOW_GROWTH
环境变量设置为true
。使用这个选项,TensorFlow 一旦分配了内存就不会释放它(再次,为了避免内存碎片化),除非程序结束。使用这个选项很难保证确定性行为(例如,一个程序可能会崩溃,因为另一个程序的内存使用量激增),因此在生产环境中,您可能会选择之前的选项之一。然而,有一些情况下它非常有用:例如,当您使用一台机器运行多个 Jupyter 笔记本时,其中几个使用了 TensorFlow。在 Colab 运行时,TF_FORCE_GPU_ALLOW_GROWTH
环境变量被设置为true
。
最后,在某些情况下,您可能希望将一个 GPU 分成两个或更多逻辑设备。例如,如果您只有一个物理 GPU,比如在 Colab 运行时,但您想要测试一个多 GPU 算法,这将非常有用。以下代码将 GPU#0 分成两个逻辑设备,每个设备有 2 GiB 的 RAM(同样,在导入 TensorFlow 后立即执行):
tf.config.set_logical_device_configuration( physical_gpus[0], [tf.config.LogicalDeviceConfiguration(memory_limit=2048), tf.config.LogicalDeviceConfiguration(memory_limit=2048)] )
这两个逻辑设备被称为"/gpu:0"
和"/gpu:1"
, 你可以像使用两个普通 GPU 一样使用它们。你可以像这样列出所有逻辑设备:
>>> logical_gpus = tf.config.list_logical_devices("GPU") >>> logical_gpus [LogicalDevice(name='/device:GPU:0', device_type='GPU'), LogicalDevice(name='/device:GPU:1', device_type='GPU')]
现在让我们看看 TensorFlow 如何决定应该使用哪些设备来放置变量和执行操作。
将操作和变量放在设备上
Keras 和 tf.data 通常会很好地将操作和变量放在它们应该在的位置,但如果您想要更多控制,您也可以手动将操作和变量放在每个设备上:
- 通常,您希望将数据预处理操作放在 CPU 上,并将神经网络操作放在 GPU 上。
- GPU 通常具有相对有限的通信带宽,因此重要的是要避免不必要的数据传输进出 GPU。
- 向机器添加更多的 CPU RAM 是简单且相对便宜的,因此通常有很多,而 GPU RAM 是内置在 GPU 中的:它是一种昂贵且有限的资源,因此如果一个变量在接下来的几个训练步骤中不需要,它可能应该放在 CPU 上(例如,数据集通常应该放在 CPU 上)。
默认情况下,所有变量和操作都将放置在第一个 GPU 上(命名为"/gpu:0"
),除非变量和操作没有 GPU 内核:这些将放置在 CPU 上(始终命名为"/cpu:0"
)。张量或变量的device
属性告诉您它被放置在哪个设备上。
>>> a = tf.Variable([1., 2., 3.]) # float32 variable goes to the GPU >>> a.device '/job:localhost/replica:0/task:0/device:GPU:0' >>> b = tf.Variable([1, 2, 3]) # int32 variable goes to the CPU >>> b.device '/job:localhost/replica:0/task:0/device:CPU:0'
您现在可以安全地忽略前缀/job:localhost/replica:0/task:0
;我们将在本章后面讨论作业、副本和任务。正如您所看到的,第一个变量被放置在 GPU#0 上,这是默认设备。但是,第二个变量被放置在 CPU 上:这是因为整数变量没有 GPU 内核,或者涉及整数张量的操作没有 GPU 内核,因此 TensorFlow 回退到 CPU。
如果您想在与默认设备不同的设备上执行操作,请使用tf.device()
上下文:
>>> with tf.device("/cpu:0"): ... c = tf.Variable([1., 2., 3.]) ... >>> c.device '/job:localhost/replica:0/task:0/device:CPU:0'
注意
CPU 始终被视为单个设备("/cpu:0"
),即使您的计算机有多个 CPU 核心。放置在 CPU 上的任何操作,如果具有多线程内核,则可能在多个核心上并行运行。
如果您明确尝试将操作或变量放置在不存在或没有内核的设备上,那么 TensorFlow 将悄悄地回退到默认选择的设备。当您希望能够在不具有相同数量的 GPU 的不同机器上运行相同的代码时,这是很有用的。但是,如果您希望获得异常,可以运行tf.config.set_soft_device_placement(False)
。
现在,TensorFlow 如何在多个设备上执行操作呢?
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)(4)https://developer.aliyun.com/article/1482470