TensorFlow 实战(七)(3)https://developer.aliyun.com/article/1522944
在这里,默认值是一个重要的部分。它将决定测试数据中出现的任何看不见的类别会发生什么,这些类别不是训练数据的一部分。我们问题中基于词汇的分类特征是天和月,分别只能有 7 和 12 个不同的值。但可能会出现这样的情况,训练集只有 11 个月,测试集有 12 个月。为了解决这个问题,我们将任何看不见的类别分配给我们可用的最后一个类别 ID(即,num_buckets - 1)。
现在我们有了一组明确定义的数据列,这些数据列包装在 tf.feature_column 对象中,准备馈送给模型。最后,我们看到一个名为 _dnn_regressor() 的函数,它将创建一个 Keras 模型,如下图所示,并将我们创建的列和一些其他超参数传递给它。现在让我们讨论一下这个函数的具体内容。
列表 15.7 定义回归神经网络
def _dnn_regressor(columns, dnn_hidden_units): ❶ input_layers = { colname: tf.keras.layers.Input( name=colname, shape=(), dtype=tf.float32 ) ❷ for colname in _transformed_names(_DENSE_FLOAT_FEATURE_KEYS) } input_layers.update({ colname: tf.keras.layers.Input( name=colname, shape=(), dtype='int32' ) ❸ for colname in _transformed_names(_VOCAB_FEATURE_KEYS) }) input_layers.update({ colname: tf.keras.layers.Input( name=colname, shape=(), dtype='int32' ) ❹ for colname in _transformed_names(_BUCKET_FEATURE_KEYS) }) output = tf.keras.layers.DenseFeatures(columns)(input_layers) ❺ for numnodes in dnn_hidden_units: output = tf.keras.layers.Dense(numnodes, activation='tanh')(output) ❻ output = tf.keras.layers.Dense(1)(output) ❼ model = tf.keras.Model(input_layers, output) ❽ model.compile( loss='mean_squared_error', ❾ optimizer=tf.keras.optimizers.Adam(lr=0.001) ) model.summary(print_fn=absl.logging.info) ❿ return model
❶ 定义一个函数,它以一堆列和一列隐藏维度的列表作为输入。
❷ 模型的输入: 输入字典,其中键是特征名称,值是 Keras 输入层
❸ 通过为基于词汇的分类特征创建输入层更新字典。
❹ 通过为分桶特征创建输入层更新字典。
❺ 由于输入层被定义为字典,我们使用 DenseFeatures 层生成单一的张量输出。
❻ 我们通过创建一系列稠密层来递归计算输出。
❼ 创建一个最终的回归层,它有一个输出节点和线性激活。
❽ 使用输入和输出定义模型。
❾ 编译模型。请注意它使用均方误差作为损失函数。
❿ 通过我们在开始定义的 absl 记录器打印模型的摘要。
我们已按列的形式定义了数据,其中每列都是 TensorFlow 特征列。定义数据后,我们使用一个特殊层叫做 tf.keras.layers.DenseFeatures 来处理这些数据。 DenseFeatures 层 接受
- 特征列列表
- 一个 tf.keras.layers.Input 层的字典,其中每个输入层的键都在特征列列表中找到的列名
有了这些数据,DenseFeatures 层可以将每个输入层映射到相应的特征列,并在最后产生一个单一的张量输出(存储在变量输出中)(图 15.7)。
图 15.7 DenseFeatures 层功能概述
然后我们通过将数据通过几个隐藏层流动来递归计算输出。这些隐藏层的大小(一个整数列表)作为参数传递给函数。我们将使用 tanh 非线性激活作为隐藏层。最终的隐藏输出进入具有线性激活的单节点回归层。
最后,我们使用 Adam 优化器和均方损失作为损失函数对模型进行编译。重要的是要注意,我们必须为模型使用与回归兼容的损失函数。均方误差是用于回归问题的非常常见的损失函数。
Python 中的类型提示
您将看到一些函数的定义方式与我们过去所做的方式不同。例如,函数定义为
def _build_keras_model() -> tf.keras.Model:
或
def run_fn(fn_args: tfx.components.FnArgs):
这是 Python 中的可视类型提示,并且在 Python 中是可用的。这意味着类型不会以任何方式由 Python 解释器强制执行;相反,它们是一种视觉提示,以确保开发人员使用正确的输入和输出类型。在函数中定义参数时,可以使用以下语法定义该参数期望的数据类型 def (: ):。例如,在函数 run_fn() 中,第一个参数 fn_args 必须是 tfx.components.FnArgs 类型。
然后,您还可以将函数返回的输出定义为 def (: ) -> :。例如,_build_keras_model() 函数返回的对象必须是一个 tf.keras.Model 对象。
有些对象需要使用多种数据类型或自定义数据类型(例如,字符串列表)创建复杂数据类型。对于这一点,您可以使用一个名为 typing 的内置 Python 库。typing 允许您方便地定义数据类型。有关更多信息,请参阅 docs.python.org/3/library/typing.xhtml
。
在列表 15.8 中,我们定义了一个函数,给定一组训练数据文件名和评估数据文件名,生成用于训练和评估数据的 tf.data.Dataset 对象。我们将这个特殊函数定义为 _input_fn()。_input_fn() 接受三个参数:
- file_pattern — 一组文件路径,其中文件包含数据
- data_accessor — TFX 中的特殊对象,通过接受文件名列表和其他配置来创建 tf.data.Dataset
- batch_size — 指定数据批次大小的整数
列表 15.8 用于使用输入文件生成 tf.data.Dataset 的函数
from typing import List, Text ❶ def _input_fn(file_pattern: List[Text], ❷ data_accessor: tfx.components.DataAccessor, ❸ tf_transform_output: tft.TFTransformOutput, ❹ batch_size: int = 200) -> tf.data.Dataset: ❺ return data_accessor.tf_dataset_factory( file_pattern, tfxio.TensorFlowDatasetOptions( batch_size=batch_size, label_key=_transformed_name(_LABEL_KEY)), tf_transform_output.transformed_metadata.schema)
❶ typing 库定义了函数输入的类型。
❷ 输入 tfrecord 文件的路径或模式的列表。它是 Text 类型对象(即字符串)的列表。
❸ DataAccessor 用于将输入转换为 RecordBatch
❹ 一个 TFTransformOutput
❺ 表示要合并为单个批次的返回数据集的连续元素的数量
您可以看到我们如何使用类型提示来标记参数以及返回对象。该函数通过调用 tf_dataset_factory() 函数获取 tf.data.Dataset,该函数使用文件路径列表和数据集选项(如批量大小和标签键)进行调用。标签键对于 data_accessor 来说非常重要,因为它能确定输入字段和目标。您可以看到 data_accessor 也需要从 Transform 步骤获取模式。这有助于 data_accessor 将原始示例转换为特征,然后分离输入和标签。在解释了所有关键函数之后,我们现在继续看看所有这些将如何被编排以进行模型训练。
15.2.2 定义模型训练
现在我们需要做的主要任务是模型的实际训练。负责模型训练的 TFX 组件(称为 Trainer)期望有一个名为 run_fn() 的特殊函数,该函数将告诉模型应该如何被训练和最终保存(见清单 15.9)。这个函数接受一个特殊类型的对象 called FnArgs,这是 TensorFlow 中的一个实用对象,可以用来声明需要传递给模型训练函数的与模型训练相关的用户定义参数。
清单 15.9 运行 Keras 模型训练与数据。
def run_fn(fn_args: tfx.components.FnArgs): ❶ absl.logging.info("="*50) absl.logging.info("Printing the tfx.components.FnArgs object") ❷ absl.logging.info(fn_args) ❷ absl.logging.info("="*50) tf_transform_output = tft.TFTransformOutput( fn_args.transform_graph_path ) ❸ train_dataset = _input_fn( fn_args.train_files, fn_args.data_accessor, tf_transform_output, ➥ 40 ❹ ) eval_dataset = _input_fn( fn_args.eval_files, fn_args.data_accessor, tf_transform_output, ➥ 40 ❹ ) model = _build_keras_model() ❺ csv_write_dir = os.path.join( fn_args.model_run_dir,'model_performance' ) ❻ os.makedirs(csv_write_dir, exist_ok=True) csv_callback = tf.keras.callbacks.CSVLogger( os.path.join(csv_write_dir, 'performance.csv'), append=False ❼ ) model.fit( ❽ train_dataset, steps_per_epoch=fn_args.train_steps, validation_data=eval_dataset, validation_steps=fn_args.eval_steps, epochs=10, callbacks=[csv_callback] ) signatures = { ❾ 'serving_default': _get_serve_tf_examples_fn( model, tf_transform_output ).get_concrete_function( tf.TensorSpec( shape=[None], dtype=tf.string, name='examples' ) ), } model.save(fn_args.serving_model_dir, save_format='tf', signatures=signatures)
❶ 定义一个名为 run_fn 的函数,该函数以 tfx.components.FnArgs 对象作为输入。
❷ 记录 fn_args 对象中的值。
❸ 加载 tensorflow_transform 图。
❹ 使用函数 _input_fn(即将讨论)将 CSV 文件中的数据转换为 tf.data.Dataset 对象。
❺ 使用先前定义的函数构建 Keras 模型。
❻ 定义一个目录来存储 Keras 回调 CSVLogger 生成的 CSV 日志。
❼ 定义 CSVLogger 回调。
❽ 使用创建的数据集和 fn_args 对象中存在的超参数来拟合模型。
❾ 为模型定义签名。签名告诉模型在模型部署时通过 API 调用时该做什么。
❿ 将模型保存到磁盘。
让我们首先检查 run_fn()的方法签名。run_fn()接受一个 FnArgs 类型的单一参数作为输入。如前所述,FnArgs 是一个实用对象,它存储了对模型训练有用的键值对集合。这个对象中的大部分元素是由 TFX 组件本身填充的。不过,你也有灵活性传递一些值。我们将定义这个对象中一些最重要的属性。但是一旦我们看到 TFX Trainer 组件生成的完整输出,我们将学习更多关于这个对象的属性列表。表 15.2 为你提供了这个对象中存储的内容的概览。如果你对这些元素的用途不是很理解,不要担心。随着我们的学习,它们会变得更清晰。一旦我们运行 Trainer 组件,它将显示用于每一个属性的值,因为我们在其中包含了记录语句来记录 fn_args 对象。这将帮助我们对当前运行的示例将这些属性进行上下文化,并更清晰地理解它们。
表 15.2 fn_args 类型对象中存储的属性概览
属性 | 描述 | 示例 |
train_files | 训练文件名列表 | [‘…/Transform/transformed_examples/16/Split-train/*’], |
eval_files | 评估/验证文件名列表 | [‘…/Transform/transformed_examples/16/Split-eval/*’] |
train_steps | 训练步数 | 100 |
eval_steps | 评估/验证步数 | 100 |
schema_path | TFX 组件 SchemaGen 生成的模式路径 | ‘…/SchemaGen/schema/15/schema.pbtxt’ |
transform_graph_path | TFX 组件 Transform 生成的转换图路径 | ‘…/SchemaGen/schema/15/schema.pbtxt’ |
serve_model_dir | 存储可提供服务的模型的输出目录 | ‘…/Trainer/model/17/Format-Serving’ |
model_run_dir | 存储模型的输出目录 | ‘…/Trainer/model_run/17’ |
这个函数完成的第一个重要任务是为训练和评估数据生成 tf.data.Dataset 对象。我们定义了一个特殊的函数叫做 _input_fn()来实现这个功能(见 15.8 节)。
定义了数据集之后,我们使用之前讨论过的 _build_keras_model() 函数定义 Keras 模型。然后我们定义了一个 CSVLogger 回调函数来记录性能指标随时间的变化,就像我们之前做的那样。简要回顾一下,tf.keras.callbacks.CSVLogger 会在模型编译期间创建一个 CSV 文件,记录每个周期的所有损失和指标。我们将使用 fn_arg 对象的 model_run_dir 属性来为 CSV 文件创建一个路径,该路径位于模型创建目录内。这样,如果我们运行多个训练试验,每个试验都将与模型一起保存其自己的 CSV 文件。之后,我们像之前无数次那样调用 model.fit() 函数。我们使用的参数很简单,所以我们不会详细讨论它们,也不会不必要地延长这个讨论。
15.2.3 SignatureDefs:定义模型在 TensorFlow 外部的使用方式
一旦模型训练完成,我们必须将模型存储在磁盘上,以便以后可以重用。存储此模型的目的是通过基于 Web 的 API(即 REST API)来查询模型使用输入并获取预测结果。这通常是在在线环境中为客户提供服务的机器学习模型的使用方式。为了让模型理解基于 Web 的请求,我们需要定义称为 SignatureDefs 的东西。签名定义了模型的输入或目标是什么样子的(例如,数据类型)。您可以看到我们定义了一个叫做 signatures 的字典,并将其作为参数传递给 model.save()(清单 15.9)。
signatures 字典应该有键值对,其中键是签名名称,值是使用 @tf.function 装饰器装饰的函数。如果您想快速回顾一下此装饰器的作用,请阅读下面的侧边栏。
@tf.function 装饰器
@tf.function 装饰器接受一个执行各种 TensorFlow 操作的函数,该函数使用 TensorFlow 操作数,然后跟踪所有步骤并将其转换为数据流图。在大多数情况下,TensorFlow 需要显示输入和输出如何在操作之间连接的数据流图。尽管在 TensorFlow 1.x 中,您必须显式构建此图,但 TensorFlow 2.x 以后不再让开发人员负责此责任。每当一个函数被 @tf.function 装饰器装饰时,它会为我们构建数据流图。
还要注意,您不能将任意名称用作签名名称。TensorFlow 有一组根据您的需求定义的签名名称。这些在 TensorFlow 的特殊常量模块中定义(mng.bz/o2Kd
)。有四种签名可供选择:
- PREDICT_METHOD_NAME(值:‘tensorflow/serving/predict’)—这个签名用于预测传入输入的目标。这不期望目标存在。
- REGRESS_METHOD_NAME(值为 ‘tensorflow/serving/regress’)——此签名可用于从示例进行回归。它期望 HTTP 请求体中同时存在输入和输出(即目标值)。
- CLASSIFY_METHOD_NAME(值为 ‘tensorflow/serving/classify’)——与 REGRESS_METHOD_NAME 类似,但用于分类。此签名可用于分类示例。它期望 HTTP 请求中同时存在输入和输出(即目标值)。
- DEFAULT_SERVING_SIGNATURE_DEF_KEY(值为 ‘serving_default’)——这是默认签名名称。模型至少应该有默认的服务签名才能通过 API 使用。如果没有定义其他签名,则请求将经过此签名。
我们只定义了默认签名。签名采用 TensorFlow 函数(即用 @tf.function 装饰的函数)作为值。因此,我们需要定义一个函数(我们将其称为 _get_serve_tf_examples_fn() ),以告诉 TensorFlow 对输入做什么(请参见下一个清单)。
清单 15.10 解析通过 API 请求发送的示例并从中进行预测。
def _get_serve_tf_examples_fn(model, tf_transform_output): ❶ model.tft_layer = tf_transform_output.transform_features_layer() ❷ @tf.function def serve_tf_examples_fn(serialized_tf_examples): ❸ """Returns the output to be used in the serving signature.""" feature_spec = tf_transform_output.raw_feature_spec() ❹ feature_spec.pop(_LABEL_KEY) ❺ parsed_features = tf.io.parse_example(serialized_tf_examples, ➥ feature_spec) ❻ transformed_features = model.tft_layer(parsed_features) ❼ return model(transformed_features) ❽ return serve_tf_examples_fn ❾
❶ 返回一个函数,该函数解析序列化的 tf.Example 并应用特征转换。
❷ 以 Keras 层的形式获取要执行的特征转换。
❸ 被 @tf.function 装饰的函数将被返回。
❹ 获取原始列规范。
❺ 删除标签的特征规范,因为我们不需要在预测中使用它。
❻ 使用特征规范解析序列化示例。
❼ 使用定义的层将原始列转换为特征。
❽ 在提供转换后的特征之后返回模型的输出。
❾ 返回 TensorFlow 函数。
首先要注意的一件重要事情是,_get_serve_tf_examples_fn() 返回一个函数(即 serve_tf_examples_fn ),它是 TensorFlow 函数。_get_serve_tf_examples_fn() 接受两个输入:
- Model — 我们在训练时建立的 Keras 模型。
- tf_transform_output——将原始数据转换为特征的转换图。
返回函数应指示 TensorFlow 在模型部署后通过 API 请求传入的数据要执行什么操作。返回的函数以序列化示例作为输入,将它们解析为符合模型输入规范的正确格式,生成并返回输出。我们不会深入解析此功能的输入和输出,因为我们不会直接调用它,而是访问 TFX,在 API 调用时将访问它。
在这个过程中,函数首先获得原始特征规范映射,这是一个列名映射到 Feature 类型的字典。Feature 类型描述了放入特征中的数据类型。例如,对于我们的数据,特征规范将是这样的:
{ 'DC': VarLenFeature(dtype=tf.float32), 'DMC': VarLenFeature(dtype=tf.float32), 'RH': VarLenFeature(dtype=tf.int64), ... 'X': VarLenFeature(dtype=tf.int64), 'area': VarLenFeature(dtype=tf.float32), 'day': VarLenFeature(dtype=tf.string), 'month': VarLenFeature(dtype=tf.string) }
可以观察到,根据该列中的数据使用了不同的数据类型(例如 float、int、string)。您可以在 www.tensorflow.org/api_docs/python/tf/io/
上看到一列特征类型的列表。接下来,我们删除了具有 _LABEL_KEY 的特征,因为它不应该是输入的一部分。然后我们使用 tf.io.parse_example() 函数通过传递特征规范映射来解析序列化的示例。结果被传递给 TransformFeaturesLayer (mng.bz/nNRa
),它知道如何将一组解析后的示例转换为一批输入,其中每个输入具有多个特征。最后,转换后的特征被传递给模型,该模型返回最终输出(即,预测的森林烧毁面积)。让我们重新审视列表 15.9 中的签名定义:
signatures = { 'serving_default': _get_serve_tf_examples_fn( model, tf_transform_output ).get_concrete_function( tf.TensorSpec( shape=[None], dtype=tf.string, name='examples' ) ), }
您可以看到,我们并不只是简单地传递 _get_serve_tf_examples_fn() 的返回 TensorFlow 函数。相反,我们在返回函数(即 TensorFlow 函数)上调用了 get_concrete_function()。如果您还记得我们之前的讨论,当您执行带有 @tf.function 装饰的函数时,它会执行两件事:
- 追踪函数并创建数据流图以执行函数的工作
- 执行图以返回输出
get_concrete_function() 只做第一个任务。换句话说,它返回了追踪的函数。您可以在 mng.bz/v6K7
上阅读更多相关内容。
使用 TFX Trainer 训练 Keras 模型 15.2.4
我们现在有了训练模型的所有必要条件。再次强调,我们首先定义了一个 Keras 模型,定义了一个运行模型训练的函数,最后定义了指令,告诉模型当通过 API 发送 HTTP 请求时应该如何行事。现在我们将在 TFX 流水线的一部分中训练模型。为了训练模型,我们将使用 TFX Trainer 组件:
from tfx.components import Trainer from tfx.proto import trainer_pb2 import tensorflow.keras.backend as K K.clear_session() n_dataset_size = df.shape[0] batch_size = 40 n_train_steps_mod = 2*n_dataset_size % (3*batch_size) n_train_steps = int(2*n_dataset_size/(3*batch_size)) if n_train_steps_mod != 0: n_train_steps += 1 n_eval_steps_mod = n_dataset_size % (3*batch_size) n_eval_steps = int(n_dataset_size/(3*batch_size)) if n_eval_steps != 0: n_eval_steps += 1 trainer = Trainer( module_file=os.path.abspath("forest_fires_trainer.py"), transformed_examples=transform.outputs['transformed_examples'], schema=schema_gen.outputs['schema'], transform_graph=transform.outputs['transform_graph'], train_args=trainer_pb2.TrainArgs(num_steps=n_train_steps), eval_args=trainer_pb2.EvalArgs(num_steps=n_eval_steps)) context.run(trainer)
在 Trainer 组件之前的代码只是计算了一个周期中所需的正确迭代次数。为了计算这个值,我们首先得到了数据的总大小(记住我们将数据集存储在 DataFrame df 中)。然后我们为训练使用了两个哈希桶,评估使用了一个哈希桶。因此,我们大约有三分之二的训练数据和三分之一的评估数据。最后,如果值不能完全被整除,我们就会加上 +1 来包含数据的余数。
让我们更详细地研究 Trainer 组件的实例化。有几个重要的参数需要传递给构造函数:
- module_file——包含 run_fn() 的 Python 模块的路径。
- transformed_examples——TFX Transform 步骤的输出,特别是转换后的示例。
- schema——TFX SchemaGen 步骤的输出。
- train_args——指定与训练相关的参数的 TrainArgs 对象。(要查看为该对象定义的 proto 消息,请参见
mng.bz/44aw
。) - eval_args—一个指定评估相关参数的 EvalArgs 对象。(要查看为此对象定义的 proto 消息,请参见
mng.bz/44aw
。)
这将输出以下日志。由于日志输出的长度,我们已经截断了某些日志消息的部分:
INFO:absl:Generating ephemeral wheel package for ➥ '/home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in- ➥ TF2/tfx/forest_fires_trainer.py' (including modules: ➥ ['forest_fires_constants', 'forest_fires_transform', ➥ 'forest_fires_trainer']). ... INFO:absl:Training model. ... 43840.0703WARNING:tensorflow:11 out of the last 11 calls to <function ➥ recreate_function.<locals>.restored_function_body at 0x7f53c000ea60> ➥ triggered tf.function retracing. Tracing is expensive and the excessive ➥ number of tracings could be due to (1) creating @tf.function repeatedly ➥ in a loop, (2) passing tensors with different shapes, (3) passing ➥ Python objects instead of tensors. INFO:absl:____________________________________________________________________________ INFO:absl:Layer (type) Output Shape Param # ➥ Connected to INFO:absl:================================================================= ➥ =========== ... INFO:absl:dense_features (DenseFeatures) (None, 31) 0 ➥ DC_xf[0][0] INFO:absl: ➥ DMC_xf[0][0] INFO:absl: ➥ FFMC_xf[0][0] ... INFO:absl: ➥ temp_xf[0][0] INFO:absl: ➥ wind_xf[0][0] INFO:absl:_________________________________________________________________ ➥ ___________ ... INFO:absl:Total params: 12,417 ... Epoch 1/10 9/9 [==============================] - ETA: 3s - loss: 43840.070 - 1s ➥ 32ms/step - loss: 13635.6658 - val_loss: 574.2498 Epoch 2/10 9/9 [==============================] - ETA: 0s - loss: 240.241 - 0s ➥ 10ms/step - loss: 3909.4543 - val_loss: 495.7877 ... Epoch 9/10 9/9 [==============================] - ETA: 0s - loss: 42774.250 - 0s ➥ 8ms/step - loss: 15405.1482 - val_loss: 481.4183 Epoch 10/10 9/9 [==============================] - 1s 104ms/step - loss: 1104.7073 - ➥ val_loss: 456.1211 ... INFO:tensorflow:Assets written to: ➥ /home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in- ➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/Trainer/model/5/Format- ➥ Serving/assets INFO:absl:Training complete. Model written to ➥ /home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in- ➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/Trainer/model/5/Format- ➥ Serving. ModelRun written to ➥ /home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in- ➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/Trainer/model_run/5 INFO:absl:Running publisher for Trainer INFO:absl:MetadataStore with DB connection initialized
在日志消息中,我们可以看到 Trainer 做了大量的繁重工作。首先,它使用 forest_fires_trainer.py 中定义的模型训练代码创建一个 wheel 包。wheel(扩展名为 .whl)是 Python 打包库的方式。例如,当你执行 pip install tensorflow 时,它会首先下载带有最新版本的 wheel 包并在本地安装。如果你有一个本地下载的 wheel 包,你可以使用 pip install 。你可以在 /examples/forest_fires_pipeline/_wheels 目录中找到生成的 wheel 包。然后它打印模型摘要。它为传递给模型的每个特征都有一个输入层。你可以看到 DenseFeatures 层聚合了所有这些输入层,以生成一个 [None, 31] 大小的张量。作为最终输出,模型产生了一个 [None, 1] 大小的张量。然后进行模型训练。你会看到警告,比如
out of the last x calls to <function ➥ recreate_function.<locals>.restored_function_body at 0x7f53c000ea60> ➥ triggered tf.function retracing. Tracing is expensive and the excessive ➥ number of tracings could be due to
当 TensorFlow 函数跟踪发生太多次时,就会出现这个警告。这可能是代码编写不佳的迹象(例如,模型在循环内部被重建多次),有时是不可避免的。在我们的案例中,是后者。Trainer 模块的行为导致了这种行为,我们对此无能为力。最后,组件将模型以及一些实用工具写入到管道根目录的一个文件夹中。到目前为止,我们的管道根目录看起来是这样的(图 15.8)。
图 15.8 运行 Trainer 后的完整目录/文件结构
在 Trainer 的输出日志中,我们可以注意到一个主要问题是训练和验证损失。对于这个问题,它们相当大。我们使用的是计算得出的均方误差。
其中 N 是示例的数量,y[i] 是第 i 个示例,ŷ[i] 是第 i 个示例的预测值。在训练结束时,我们的平方损失约为 481,意味着每个示例约有 22 公顷(即 0.22 平方公里)的误差。这不是一个小错误。如果你调查这个问题,你会意识到这主要是由数据中存在的异常引起的。有些异常是如此之大,以至于它们可能会使模型严重偏离正确方向。我们将在本章的一个即将到来的部分中解决这个问题。你将能够看到传递给 run_fn() 的 FnArgs 对象中的值:
INFO:absl:================================================== INFO:absl:Printing the tfx.components.FnArgs object INFO:absl:FnArgs( working_dir=None, train_files=['.../Transform/transformed_examples/16/Split-train/*'], eval_files=['.../Transform/transformed_examples/16/Split-eval/*'], train_steps=100, eval_steps=100, schema_path='.../SchemaGen/schema/15/schema.pbtxt', schema_file='.../SchemaGen/schema/15/schema.pbtxt', transform_graph_path='.../Transform/transform_graph/16', transform_output='.../Transform/transform_graph/16', data_accessor=DataAccessor( tf_dataset_factory=<function ➥ get_tf_dataset_factory_from_artifact.<locals>.dataset_factory at ➥ 0x7f7a56329a60>, record_batch_factory=<function ➥ get_record_batch_factory_from_artifact.<locals>.record_batch_factory at ➥ 0x7f7a563297b8>, data_view_decode_fn=None ), serving_model_dir='.../Trainer/model/17/Format-Serving', eval_model_dir='.../Trainer/model/17/Format-TFMA', model_run_dir='.../Trainer/model_run/17', base_model=None, hyperparameters=None, custom_config=None ) INFO:absl:==================================================
以下侧边栏讨论了我们在本讨论中的这一点上如何评估模型。
评估保存的模型
在流水线中,我们的模型将以 URL 形式通过 HTTP 接口提供服务。但是与其等待不如手动加载模型并用它来预测数据。这样做将为我们提供两个优势:
- 验证模型是否按预期工作
- 提供对模型输入和输出格式的深入理解
我们不会在本书中详细介绍这个问题,以保持我们讨论的重点在流水线上。但是,已在 tfv/15.1_MLOps_with_tensorflow.ipynb 笔记本中提供了代码,因此您可以进行实验。
接下来,我们将讨论如何检测数据中存在的异常并将其移除,以创建一个干净的数据集来训练我们的模型。
检测和移除异常值
我们的模型目前显示的验证损失约为 568。这里使用的损失是均方误差。我们已经看到,这意味着每个预测偏差 24 公顷(即 0.24 平方公里)。这不是一个可以忽略的问题。我们的数据中有很多异常值,这可能是我们看到如此大的误差边界的一个关键原因。以下图显示了我们早期创建的统计图。
由 StatisticsGen 组件为数据生成的摘要统计图
您可以看到一些列严重偏斜。例如,特征 FFMC 具有最高的密度,约为 80-90,但范围为 18.7-96.2。
为了解决这个问题,我们将使用 tensorflow_data_validation(缩写为 tfdv)库。它提供了有用的功能,如 tfdv.validate_statistics(),可用于根据我们之前生成的数据模式验证数据,以及 tfdv.display_anomalies()函数,以列出异常样本。此外,我们可以编辑模式以修改异常值的标准。例如,要更改允许的 ISI 特征的最大值,您可以执行以下操作:
isi_feature = tfdv.get_feature(schema, 'ISI') isi_feature.float_domain.max = 30.0
最后,您可以使用 tfdv.visualize_statistics()函数可视化原始数据与清理后的数据。最后,您可以使用 TFX 流水线中的 ExampleValidator 对象(mng.bz/XZxv
)确保数据集中没有异常。
运行此操作后,您应该比以前得到更小的损失。例如,在这个实验中,平均观察到了约 150 的损失。这是之前错误的 75%减少。您可以在 tfv/15.1_MLOps_with_tensorflow.ipynb 笔记本中找到此代码。
接下来,我们将看一看一种名为 Docker 的技术,该技术用于在隔离且便携的环境中部署模型。我们将看看如何将我们的模型部署在所谓的 Docker 容器中。
练习 2
而不是使用 one-hot 编码来表示日和月的特征,并将它们追加到 categorical_columns 变量中,让我们假设你想使用嵌入来表示这些特征。您可以使用特征列 tf.feature_column.embedding_column 来完成这个任务。假设嵌入的维度是 32。你有存储在 _VOCAB_FEATURE_KEYS 中的特征名称(包括[‘day’, ‘month’])以及存储在 _MAX_CATEGORICAL_FEATURE_VALUES 中的维度(包括[7, 12])。
15.3 设置 Docker 以提供经过训练的模型
您已经开发了一个数据管道和一个强大的模型,可以根据天气数据预测森林火灾的严重程度。现在,您希望更进一步,通过在一台机器上部署模型并通过 REST API 提供更易访问的服务,这个过程也称为生产化机器学习模型。为了做到这一点,您首先要创建一个专门用于模型服务的隔离环境。您将使用的技术是 Docker。
注意在继续之前,确保您的计算机上已经安装了 Docker。要安装 Docker,请按照此指南:docs.docker.com/engine/install/ubuntu/
。
在 TFX 中,你可以将你的模型部署为一个容器,而这个容器是由 Docker 提供的。根据官方 Docker 网站的说法,Docker 容器是
软件的标准单元,它打包了代码和所有的依赖项,以便应用程序可以在一个计算环境中快速、可靠地运行,并在另一个计算环境中运行。
源:www.docker.com/resources/what-container
Docker 是一种容器技术,它可以帮助您在主机上隔离运行软件(或微服务)。在 Docker 中,您可以创建一个镜像,该镜像将使用各种规格(例如操作系统、库、依赖项)指示 Docker 需要在容器中以正确运行软件。然后,容器就是该镜像的运行时实例。这意味着您可以在一个计算机上创建一个容器,并且可以轻松地在另一台计算机上运行它(只要两台计算机上都安装了 Docker)。虚拟机(VMs)也试图实现类似的目标。有许多资源可以比较和对比 Docker 容器和虚拟机(例如,mng.bz/yvNB
)。
如我们所说,要运行一个 Docker 容器,首先需要一个 Docker 镜像。Docker 有一个公共镜像注册表(称为 Docker Hub),位于 hub.docker.com/
。我们正在寻找的 Docker 镜像是 TensorFlow serving 镜像。这个镜像已经安装了一切用于提供 TensorFlow 模型的服务,使用了 TensorFlow serving (github.com/tensorflow/serving
),这是 TensorFlow 中的一个子库,可以围绕给定的模型创建一个 REST API,以便你可以发送 HTTP 请求来使用模型。你可以通过运行以下命令来下载这个镜像:
docker pull tensorflow/serving:2.6.3-gpu
让我们解析一下这条命令的结构。docker pull 是下载镜像的命令。tensorflow/serving 是镜像名称。Docker 镜像是有版本控制的,意味着每个 Docker 镜像都有一个版本标签(如果你没有提供的话,默认为最新版本)。2.6.3-gpu 是镜像的版本。这个镜像相当大,因为它支持 GPU 执行。如果你没有 GPU,你可以使用 docker pull tensorflow/serving:2.6.3,这个版本更轻量级。一旦命令成功执行,你就可以运行
docker images
列出你下载的所有镜像。有了下载的镜像,你可以使用 docker run 命令来使用给定的镜像启动一个容器。docker run 命令非常灵活,带有许多可以设置和更改的参数。我们使用了其中的几个:
docker run \ --rm \ -it \ --gpus all \ -p 8501:8501 \ --user $(id -u):$(id -g) \ -v ${PWD}/tfx/forest-fires-pushed:/models/forest_fires_model \ -e MODEL_NAME=forest_fires_model \ tensorflow/serving:2.6.3-gpu
理解这里提供的参数是很重要的。通常,在 shell 环境中定义参数时,使用单划线前缀来表示单字符的参数(例如,-p),使用双划线前缀来表示更详细的参数(例如,–gpus):
- –rm—容器是临时运行时,可以在服务运行后移除。–rm 意味着容器将在退出后被移除。
- -it(简写形式为 -i 和 -t)—这意味着你可以进入容器,并在容器内部交互式地运行命令。
- –gpus all—这告诉容器确保 GPU 设备(如果存在)在容器内可见。
- -p—这将容器中的网络端口映射到主机。如果你想将某些服务(例如,用于提供模型的 API)暴露给外部,这一点很重要。例如,TensorFlow serving 默认运行在 8501 端口上。因此,我们将容器的 8501 端口映射到主机的 8501 端口。
- –user ( i d − u ) : (id -u):(id−u):(id -g)—这意味着命令将以与您在主机上登录的用户相同的用户身份运行。每个用户由用户 ID 标识,并分配给一个或多个组(由组 ID 标识)。您可以按照 --user <用户 ID>:<组 ID> 的语法传递用户和组。例如,您当前的用户 ID 可以通过命令 id -u 给出,而组则由 id -g 给出。默认情况下,容器以 root 用户(即通过 sudo 运行)运行命令,这可能会使您的服务更容易受到外部攻击。因此,我们使用较低特权的用户在容器中执行命令。
- -v—这将一个目录挂载到容器内的位置。默认情况下,您在容器内存储的东西对外部是不可见的。这是因为容器有自己的存储空间/卷。如果您需要使容器看到主机上的某些内容,或者反之亦然,则需要将主机上的目录挂载到容器内的路径上。这被称为绑定挂载。例如,在这里,我们将我们推送的模型(将位于 ./tfx/forest-fires-pushed)暴露到容器内部路径 /models/forest_fires_model。
- -e—此选项可用于将特殊环境变量传递给容器。例如,TensorFlow 服务服务期望一个模型名称(它将成为从模型获取结果所需命中的 URL 的一部分)。
此命令在 Ch15-TFX-for-MLOps-in-TF2 目录中的 tfx/run_server.sh 脚本中为您提供。让我们运行 run_server.sh 脚本,看看我们将得到什么。要运行脚本
- 打开一个终端。
- 将 cd 移动到 Ch15-TFX-for-MLOps-in-TF2/tfx 目录中。
- 运行 ./run_server.sh。
它将显示类似于以下的输出:
2.6.3-gpu: Pulling from tensorflow/serving Digest: ➥ sha256:e55c44c088f6b3896a8f66d8736f38b56a8c5687c105af65a58f2bfb0bf90812 Status: Image is up to date for tensorflow/serving:2.6.3-gpu docker.io/tensorflow/serving:2.6.3-gpu 2021-07-16 05:59:37.786770: I tensorflow_serving/model_servers/server.cc:88] Building single TensorFlow ➥ model file config: model_name: forest_fires_model model_base_path: ➥ /models/forest_fires_model 2021-07-16 05:59:37.786895: I tensorflow_serving/model_servers/server_core.cc:464] Adding/updating ➥ models. 2021-07-16 05:59:37.786915: I tensorflow_serving/model_servers/server_core.cc:587] (Re-)adding model: ➥ forest_fires_model 2021-07-16 05:59:37.787498: W tensorflow_serving/sources/storage_path/file_system_storage_path_source.cc: ➥ 267] No versions of servable forest_fires_model found under base path ➥ /models/forest_fires_model. Did you forget to name your leaf directory ➥ as a number (eg. '/1/')? ...
当然,这并不能完全奏效,因为我们提供的目录作为模型位置尚未被填充。我们仍然需要做一些事情,以便将最终模型放置在正确的位置。
在下一节中,我们将完成我们流水线的其余部分。我们将看到如何在流水线中自动评估新训练的模型,如果性能良好,则部署模型,并使用 REST API(即基于 Web 的 API)从模型进行预测。
练习 3
假设您想要下载 TensorFlow Docker 映像(其名称为 tensorflow/tensorflow),版本为 2.5.0,并启动一个容器,将您计算机上的 /tmp/inputs 目录挂载到容器内的 /data 卷中。此外,您希望将容器中的 5000 端口映射到计算机上的 5000 端口。您如何使用 Docker 命令执行此操作?您可以假设您在容器内以 root 用户身份运行命令。
TensorFlow 实战(七)(5)https://developer.aliyun.com/article/1522947