使用 TensorFlow 缩短模型部署
使用 TensorFlow 缩短模型部署
如何使用 TensorFlow 简化 ML 模型部署和服务
Image generated by 稳定扩散 from prompt “Cool machine learning and AI stuff”.
ü 通常,对于现实生活中的机器学习应用程序,最终目标是将模型部署到生产中以供客户使用。但是对于由机器学习应用程序驱动的系统,不仅仅是模型预测,另外两个主要步骤是预处理和后处理。
预处理 与实际预测之前的所有步骤有关。对于图像分类,它可以是归一化,通常,视觉模型要求输入像素在 0 和 1 之间。对于文本模型,预处理可以是文本标记化或删除空格和标点符号。预处理可以采取多种形式,但归结为处理输入,以便模型可以做出可靠的预测。
Preprocessing pipeline for an NLP task
后期处理 另一方面,负责处理模型输出所需的所有步骤,以使其采用理想的形式。在分类任务的情况下,通常模型会输出每个类的概率,但最终用户通常并不关心这些,所以我们需要处理这些输出,以便它们可以采用标签的形式,在文本情感中分析这些标签通常是“正面”和“负面”。
Postprocessing pipeline
吨 当预处理和后处理成为复杂系统的一部分时,挑战就开始了,该系统可能已经在积极使用并需要维护。
想象一下以下场景——您是一家为医疗保健提供 ML 产品的公司的一员,其中包括一个 API,客户可以使用该 API 发送图像并预测 X 射线是否存在健康问题。最初,工程师创建了一个具有良好结果的基线,该基线使用了 ResNet-50 模型,所以公司开始使用它,但经过几周的研究,科学家们想出了一个改进的版本,使用 MobileNetV2 ,所以团队更换了模型,几周后,他们又想出了一个更好的模型,使用 高效网络B3 .伟大的!在一个月内,您将结果提高了 3 倍,您的客户也更满意了,但这里的挑战是什么?
在那一个月里,您必须调整您的系统以使用 3 种不同类型的模型,每种模型都期望不同的输入。举例来说,假设团队使用基于 TensorFlow 的模型并使用 Keras 应用程序模块 ,在那里你可以看到预训练的 ResNet-50 期望输入的大小为 224 x 224 x 3(宽 x 高 x 通道),通道采用 BGR 格式,并且每个颜色通道相对于 ImageNet 数据集的中心为零。 MobileNetV2 还期望图像大小为 224 x 224 x 3,但期望像素在 -1 和 1 范围内。最后, 高效网络B3 期望图像的大小为 384 x 384 x 3,并且已经将图像缩放作为模型的一部分。
在这种情况下,您有一个带有特定预处理例程的初始管道,然后必须更改它以支持第二个模型,然后再次更改它以支持第三个模型,现在想象一下,如果您需要每周多次交换模型,并行服务多个模型,回滚到以前的模型,根据新模型评估旧模型,很容易混淆不同的预处理例程,丢失代码或忘记重要细节,特别是因为公司可能提供多个为不同的客户提供服务,这只是为了展示事情是多么容易失控。
所以 我们能做什么?虽然管理这样一个系统确实是一项复杂的任务,但我们可以通过将模型的预处理和后处理逻辑融合或嵌入到它们自身中来使事情变得更容易。
Regular pipeline vs pipeline with processing logic embedded
让我们通过一个实际的例子来展示这个概念。
在这里,我将使用 TensorFlow 构建一个简单的系统,并使用“ 克利夫兰诊所心脏病基金会 ”数据集,目标是对患者是否患有心脏病进行分类。本示例基于 Keras 代码示例“ 从零开始结构化数据分类 ”。
您可以按照此示例使用 Colab 笔记本 用于编写完整的代码。
为简化起见,我们将仅使用部分功能,以下是有关它们的一些信息:
年龄: 年龄“数字”
性别: (1 = 男性;0 = 女性)“分类”
CP: 胸痛类型(0、1、2、3、4)“分类”
塔尔: 3 = 正常; 6 = 固定缺陷; 7 = 可逆缺陷“分类”
目标: 心脏病诊断(1 = 真;0 = 假)“目标(二进制)”
我之所以选择这些特性,是因为它们具有不同的数据类型和不同的预处理要求。
TensorFlow 预处理层
TensorFlow 有一个内置的方法来处理不同的数据类型, 预处理层 ,与常规预处理步骤相比,它们的一大优势是您可以将这些层与模型或 TensorFlow 数据集结合起来以优化端到端管道,同时也使部署更加容易。
对于常规用例,需要调整这些层以学习如何处理数据,“调整”步骤类似于我们需要在能够对数据进行归一化之前学习特征的均值和标准差,而这这是我们在第一种情况下要做的。
标准化(年龄特征)
对于数字特征“年龄”,我们将应用特征归一化,换句话说,我们会将输入转换并缩放到以 0 为中心且标准差为 1 的分布中,这正是[ 正常化](https://keras.io/api/layers/preprocessing_layers/numerical/normalization/)
层可以。
首先,我们创建一个 TensorFlow 数据集,该数据集仅从 pandas 数据帧加载数据,并以原始特征和标签的格式进行映射
raw_data = tf.data.Dataset.from_tensor_slices(
(dict(data[[“age”, “sex”, “cp”, “thal”]]), data.target))
在此之后,我们可以创建预处理层,该层将学习如何使用该数据集对特征进行预处理,方法是使用 适应
方法。
age_preprocessing = L.Normalization()
age_preprocessing.adapt(raw_data.map(lambda x, y: x["age"])
.map(lambda x: tf.expand_dims(x, -1)))
print(f"年龄平均值:{age_preprocessing.mean.numpy()[0]:.2f}")
print(f"年龄方差:{age_preprocessing.variance.numpy()[0]:.2f}")
---------- 输出 ----------
平均年龄:54.27
年龄差异:81.74
这里发生的事情是我得到了 原始数据
数据集并仅提取 年龄
通过映射该 lambda 操作来实现特征,然后我扩展维度以适应预处理层的预期格式。
适应数据后,[ 正常化](https://keras.io/api/layers/preprocessing_layers/numerical/normalization/)
层学习到“年龄”特征的均值是 54.27,方差是 81.74。现在基于此,它可以规范化该特征的新数据。
IntegerLookup(性别特征)
接下来,我们可以学习预处理作为分类数字特征的“性别”特征,这意味着每个值实际上表示不同的类别,这里它们是(1 = 男性;0 = 女性)。对于这种特定情况,我们还希望层能够处理未知或错误的值,在正常情况下,如果输入不同于 1 或 0,层会抛出错误并且不会给出输出,但这里这些值将被预处理为“-1”并且模型将返回一个预测,此行为取决于每个上下文。
sex_preprocessing = L.IntegerLookup(output_mode="int")
sex_preprocessing.adapt(raw_data.map(lambda x, y: x["sex"]))
sex_vocab = sex_preprocessing.get_vocabulary()
print(f"词汇大小:{len(sex_vocab)}")
print(f"词汇样本:{sex_vocab}")
---------- 输出 ----------
词汇量:3
词汇样本:[-1, 1, 0]
这里 输出模式=“int”
意味着这一层的输出是 整数
.
适应数据后,[ 整数查找](https://keras.io/api/layers/preprocessing_layers/categorical/integer_lookup/)
层学习到“性”特征具有大小为 3 的词汇表(包括 OOV),并分配 [-1, 1, 0]
作为输入的可能值。请注意,这里这一层的主要好处是处理未知值,因为输出等于输入。
整数查找(cp 功能)
对于另一个分类数字特征“cp”特征,我们还将使用[ 整数查找](https://keras.io/api/layers/preprocessing_layers/categorical/integer_lookup/)
层类似于“性”特征,但我们将改变输出格式。
cp_preprocessing = L.IntegerLookup(output_mode="one_hot")
cp_preprocessing.adapt(raw_data.map(lambda x, y: x["cp"]))
cp_vocab = cp_preprocessing.get_vocabulary()
print(f"词汇大小:{len(cp_vocab)}")
print(f"词汇样本:{cp_vocab}")
---------- 输出 ----------
词汇量:6
词汇样本:[-1, 4, 3, 2, 1, 0]
这里 输出模式=“one_hot”
意味着该层的输出具有 一热编码 格式,在这种情况下,每个输出都有一个类似于“[0, 0, 0, 0, 1, 0]”的格式,这种格式对于给模型提供更多关于特征的信息很有用,特别是如果特征不有一个 布尔值 或者 序数 自然。
适应数据后,[ 整数查找](https://keras.io/api/layers/preprocessing_layers/categorical/integer_lookup/)
层学习到“cp”特征具有大小为 6(包括 OOV)的词汇表,并分配 [-1, 4, 3, 2, 1, 0]
作为输入的可能值,进一步处理为 one-hot 编码格式。
字符串查找(thal 功能)
最后,对于“thal”特征,我们将使用[ 字符串查找](https://keras.io/api/layers/preprocessing_layers/categorical/string_lookup/)
层,对于这个特性,我们有可以作为文本或数字出现的字符串值。
thal_preprocessing = L.StringLookup(output_mode="one_hot")
thal_preprocessing.adapt(raw_data.map(lambda x, y: x["thal"]))
thal_vocab = thal_preprocessing.get_vocabulary()
print(f"词汇大小:{len(thal_vocab)}")
print(f"词汇样本:{thal_vocab}")
---------- 输出 ----------
词汇量:6
词汇样本:['[UNK]', 'normal', 'reversible', 'fixed', '2', '1']
与“cp”功能类似,我们也有 输出模式=“one_hot”
这意味着此功能的输出也将具有 一热编码 格式,因为词汇量很小,这个选项应该很好。
适应数据后,[ 字符串查找](https://keras.io/api/layers/preprocessing_layers/categorical/string_lookup/)
层学习到“thal”特征有一个大小为 6(包括 OOV)的词汇表,并分配 ['[UNK]', '正常', '可逆', '固定', '2', '1']
作为输入的可能值,这里 [UNK]
被分配给未知值(OOV),并且这些值被进一步处理为单热编码格式。
结合图层和数据集
现在我们已经拥有了适合我们数据的所有预处理层,我们可以将它们作为数据集管道的一部分。这样做的动机是 tf.data 管道 可以利用预处理层来更快、更高效地获取数据。另一种选择是将这些层用作模型的一部分并以这种方式对其进行训练,本文在“ 在模型之前或模型内部预处理数据 “ 部分。
这部分我就不赘述了,因为它不是本文的范围,但是你可以看看相关的 Colab 笔记本 ,在“数据集”部分。
造型
在常规设置中,在预处理数据后,您最终会得到一个如下所示的模型:
age_input = L.Input(shape=(1,), dtype=tf.float32)
sex_input = L.Input(shape=(1,), dtype=tf.float32)
cp_input = L.Input(shape=(len(cp_vocab),), dtype=tf.float32)
thal_input = L.Input(shape=(len(thal_vocab),), dtype=tf.float32) concat_inputs = L.Concatenate()([age_input, sex_input,
cp_input, thal_input])
x = L.Dense(32, activation="relu")(concat_inputs)
x = L.Dropout(0.5)(x)
输出 = L.Dense(1, activation="sigmoid")(x)
模型 = tf.keras.models.Model(inputs=[age_input, sex_input,
cp_input, thal_input],
输出=输出)
模型.summary()
---------- 输出 ----------__________________________________________________________________________________
层(类型)输出形状参数#连接到========================================= ==============================
年龄 (InputLayer) [(None, 1)] 0 [] 性别 (InputLayer) [(None, 1)] 0 [] cp (InputLayer) [(None, 6)] 0 [] thal (InputLayer) [(None, 6)] 0 [] 连接(Concatenate) (None, 14) 0 ['age[0][0]',
'性别[0][0]',
'cp[0][0]',
'thal[0][0]'] 密集 (Dense) (None, 32) 480 ['concatenate[0][0]'] dropout (Dropout) (None, 32) 0 ['dense[0][0] '] dense_1(密集)(无,1)33 ['dropout[0][0]'] =========================== ==========================================
总参数:513
可训练参数:513
不可训练参数:0
____________________________________________________________________
Diagram for the regular model
这个模型会存在我们一开始讨论的所有问题,对于部署,我们需要跟踪训练期间使用的确切预处理参数,这需要维护人员付出很多努力。值得庆幸的是,TensorFlow 允许我们将模型预处理逻辑嵌入到我们的模型中。
结合模型和预处理
将预处理层与我们的模型结合起来相当简单,事实上,我们基本上可以结合任何可以变成 TensorFlow 图的 TensorFlow 操作,让我们看看新模型的样子:
age_input = L.Input(shape=(1,), dtype=tf.int64)
sex_input = L.Input(shape=(1,), dtype=tf.int64)
cp_input = L.Input(shape=(1,), dtype=tf.int64)
thal_input = L.Input(shape=(1,), dtype=tf.string) # 预处理
年龄处理 = 年龄预处理(年龄输入)
sex_processed = tf.cast(sex_preprocessing(sex_input),
dtype=tf.float32)
cp_processed = cp_preprocessing(cp_input)
thal_processed = thal_preprocessing(thal_input) # 模型预测
输出=模型({“年龄”:年龄处理,
“性”:sex_processed,
“cp”:cp_processed,
“thal”:thal_processed}) # 后期处理
标签后处理 = 标签后处理(输出)
模型 = tf.keras.models.Model(inputs=[age_input, sex_input,
cp_input, thal_input],
输出=标签后处理)
模型.summary()
---------- 输出 ---------- ____________________________________________________________________
层(类型)输出形状参数#连接到========================================= ==============================
sex (InputLayer) [(None, 1)] 0 [] age (InputLayer) [(None, 1)] 0 [] cp (InputLayer) [(None, 1)] 0 [] sex_preprocessing (None, 1) 0 [ '性别[0][0]']
thal (InputLayer) [(None, 1)] 0 [] age_preprocessing (None, 1) 3 ['age[0][0]'] cp_preprocessing (None, 6) 0 ['cp[0][0]']
tf.cast (TFOpLambda) (None, 1) 0 ['sex_preprocessing[0] [0]'] thal_preprocessing (None, 6) 0 ['thal[0][0]']
模型(功能)(无,1)513 ['age_preprocessing[0][0]',
'cp_preprocessing[0][0]',
'tf.cast[0][0]',
'thal_preprocessing[0][0]'] label_postprocessing (None, 1) 0 ['model[0][0]'] ====================== ===============================================
总参数:516
可训练参数:513
不可训练的参数:3 ____________________________________________________________________
Diagram for the combined model
请注意,我还在那里包含了一个“后处理”层,该层负责将模型输出(0 或 1)映射到实际标签,我省略了创建它的代码,因为它类似于我们之前所做的,但是它也包含在 Colab 笔记本 .
您可以看到模型本质上是相同的,但是第二个模型将所有预处理逻辑嵌入到模型图中,这种方法的优点是您可以保存和加载该模型,并且它将拥有所需的一切运行推理。让我们看看它们有何不同:
第一个模型的推断
样本= {“年龄”:60,“性别”:1,“cp”:1,“thal”:“固定”} 样本= {“年龄”:年龄预处理(样本[“年龄”]),
“性”:性预处理(样本[“性”]),
“cp”:cp_preprocessing(样本[“cp”]),
“thal”:thal_preprocessing(样本[“thal”])} 打印(模型。预测(样本))
---------- 输出 ----------
0
第二个模型的推断
样本= {“年龄”:60,“性别”:1,“cp”:1,“thal”:“固定”} 打印(模型。预测(样本))
---------- 输出 ----------
“有心脏病”
如您所见,使用第二种方法,您的推理服务需要做的就是加载模型并使用它。
结论
在本文中,我们讨论了在模型部署过程中面临的与模型预处理和后处理相关的一些挑战,我们还研究了一种显着减少部署和服务模型的认知负载和维护工作的方法,即嵌入所有将逻辑放入模型本身,所有这些都使用 TensorFlow。
注:“除非另有说明,所有图片均由作者提供。”
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明