代码示例 / 结构化数据 / FeatureSpace 高级用例

FeatureSpace 高级用例

作者: Dimitre Oliveira
创建日期: 2023/07/01
最后修改: 2023/07/01
描述: 如何使用 FeatureSpace 进行高级预处理用例。

在 Colab 中查看 GitHub 源代码


介绍

本示例是 使用 FeatureSpace 的结构化数据分类 代码示例的扩展,我们将在这里扩展它以涵盖更复杂的用例, 包括 [keras.utils.FeatureSpace](/api/utils/feature_space#featurespace-class) 预处理工具,如特征哈希、特征交叉、处理缺失值以及将 Keras 预处理层 与 FeatureSpace 集成。

一般任务仍然是结构化数据分类(也称为表格数据分类),使用的数据包括数值特征、整数分类特征和字符串分类特征。

数据集

我们的数据集 由一家 葡萄牙银行机构提供。 这是一个包含 4119 行的 CSV 文件。每一行包含有关基于电话的营销活动的信息,每一列描述客户的一个属性。我们使用这些特征来预测客户是否订阅了产品(银行定期存款)'yes'或'no'。

以下是每个特征的描述:

描述 特征类型
年龄 客户的年龄 数值型
职业 职业类型 分类
婚姻状况 婚姻状态 分类
教育 客户的教育水平 分类
违约 是否有违约信用? 分类
住房 是否有住房贷款? 分类
贷款 是否有个人贷款? 分类
联系方式 联系沟通类型 分类
月份 最近一次联系的月份 分类
星期几 最近一次联系的星期几 分类
时长 最近联系的时长(秒) 数值型
活动 此次活动中为该客户执行的联系次数 数值型
Pdays 客户在上一活动中最后一次联系后的天数 数值型
前次 在此次活动之前为该客户执行的联系次数 数值型
Poutcome 上一营销活动的结果 分类
就业变动率 就业变动率 数值型
消费者价格指数 消费者价格指数 数值型
消费者信心指数 消费者信心指数 数值型
Euribor3m Euribor 3 个月利率 数值型
受雇人数 员工人数 数值型
Y 客户是否订阅了定期存款? 目标

关于特征 duration 的重要说明:这个属性对输出目标的影响很大 (例如,如果 duration=0,那么 y='no')。然而,通话进行之前并不知道时长。并且,在通话结束后,y 显然是已知的。因此,这个输入只应出于基准目的包括,并且如果目的是要有一个现实的预测模型,则应予以丢弃。因此,我们将其删除。


设置

import os

os.environ["KERAS_BACKEND"] = "tensorflow"

import keras
from keras.utils import FeatureSpace
import pandas as pd
import tensorflow as tf
from pathlib import Path
from zipfile import ZipFile

加载数据

让我们下载数据并将其加载到 Pandas 数据框中:

data_url = "https://archive.ics.uci.edu/static/public/222/bank+marketing.zip"
data_zipped_path = keras.utils.get_file("bank_marketing.zip", data_url, extract=True)
keras_datasets_path = Path(data_zipped_path).parents[0]
with ZipFile(f"{keras_datasets_path}/bank-additional.zip", "r") as zip:
    # 提取文件
    zip.extractall(path=keras_datasets_path)

dataframe = pd.read_csv(
    f"{keras_datasets_path}/bank-additional/bank-additional.csv", sep=";"
)

我们将创建一个新特征 previously_contacted 以演示一些有用的 预处理技术,此特征基于 pdays。根据数据集信息,如果 pdays = 999,则表示客户未被之前联系,因此我们将创建一个特征来捕获这一点。

# 删除 `duration` 以避免目标泄漏
dataframe.drop("duration", axis=1, inplace=True)
# 创建新特征 `previously_contacted`
dataframe["previously_contacted"] = dataframe["pdays"].map(
    lambda x: 0 if x == 999 else 1
)

数据集包含4119个样本,每个样本有21列(20个特征加上目标标签),下面是一些样本的预览:

print(f"Dataframe shape: {dataframe.shape}")
print(dataframe.head())
数据框形状: (4119, 21)
   年龄       职业         婚姻状况        教育水平        默认      住房        贷款  \
0   30  蓝领工人       已婚           基础教育.9年     否      是       否   
1   39     服务业       单身        高中              否       否       否   
2   25     服务业       已婚        高中              否      是       否   
3   38     服务业       已婚           基础教育.9年    否  未知      未知   
4   47       行政      已婚      大学学位          否      是       否   
     联系方式    月份      星期几  ...  pdays  previous     poutcome  \
0   手机        五月         周五  ...    999         0  不存在   
1  电话        五月         周五  ...    999         0  不存在   
2  电话        六月         周三  ...    999         0  不存在   
3  电话        六月         周五  ...    999         0  不存在   
4   手机        十一月      周一  ...    999         0  不存在   
  emp.var.rate  cons.price.idx  cons.conf.idx  euribor3m  nr.employed   y  \
0         -1.8          92.893          -46.2      1.313       5099.1  否   
1          1.1          93.994          -36.4      4.855       5191.0  否   
2          1.4          94.465          -41.8      4.962       5228.1  否   
3          1.4          94.465          -41.8      4.959       5228.1  否   
4         -0.1          93.200          -42.0      4.191       5195.8  否   
  previously_contacted  
0                    0  
1                    0  
2                    0  
3                    0  
4                    0  
[5 行 x 21 列]

列“y”表示客户是否已订阅定期存款。


训练/验证划分

让我们将数据划分为训练集和验证集:

valid_dataframe = dataframe.sample(frac=0.2, random_state=0)
train_dataframe = dataframe.drop(valid_dataframe.index)

print(
    f"使用 {len(train_dataframe)} 个样本用于训练,"
    f"{len(valid_dataframe)} 个样本用于验证"
)
使用 3295 个样本用于训练和 824 个样本用于验证

生成 TF 数据集

让我们为每个数据框生成 [tf.data.Dataset](https://www.tensorflow.org/api_docs/python/tf/data/Dataset) 对象,因为我们的目标列 y 是字符串,我们还需要将其编码为整数,以便能够使用它来训练模型。为此,我们将创建一个 StringLookup 层,将字符串“no”和“yes”分别映射为“0”和“1”。

label_lookup = keras.layers.StringLookup(
    # 这里的顺序很重要,因为第一个索引将被编码为 0
    vocabulary=["no", "yes"],
    num_oov_indices=0,
)


def encode_label(x, y):
    encoded_y = label_lookup(y)
    return x, encoded_y


def dataframe_to_dataset(dataframe):
    dataframe = dataframe.copy()
    labels = dataframe.pop("y")
    ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
    ds = ds.map(encode_label, num_parallel_calls=tf.data.AUTOTUNE)
    ds = ds.shuffle(buffer_size=len(dataframe))
    return ds


train_ds = dataframe_to_dataset(train_dataframe)
valid_ds = dataframe_to_dataset(valid_dataframe)

每个 Dataset 生成一个元组 (输入, 目标),其中 输入 是特征的字典,目标 是值 01

for x, y in dataframe_to_dataset(train_dataframe).take(1):
    print(f"输入: {x}")
    print(f"目标: {y}")
输入: {'年龄': <tf.Tensor: shape=(), dtype=int64, numpy=33>, '职业': <tf.Tensor: shape=(), dtype=string, numpy=b'技术员'>, '婚姻状况': <tf.Tensor: shape=(), dtype=string, numpy=b'已婚'>, '教育水平': <tf.Tensor: shape=(), dtype=string, numpy=b'大学学位'>, '默认': <tf.Tensor: shape=(), dtype=string, numpy=b'未知'>, '住房': <tf.Tensor: shape=(), dtype=string, numpy=b'是'>, '贷款': <tf.Tensor: shape=(), dtype=string, numpy=b'否'>, '联系方式': <tf.Tensor: shape=(), dtype=string, numpy=b'手机'>, '月份': <tf.Tensor: shape=(), dtype=string, numpy=b'八月'>, '星期几': <tf.Tensor: shape=(), dtype=string, numpy=b'周二'>, '活动': <tf.Tensor: shape=(), dtype=int64, numpy=1>, 'pdays': <tf.Tensor: shape=(), dtype=int64, numpy=999>, 'previous': <tf.Tensor: shape=(), dtype=int64, numpy=0>, 'poutcome': <tf.Tensor: shape=(), dtype=string, numpy=b'不存在'>, 'emp.var.rate': <tf.Tensor: shape=(), dtype=float64, numpy=1.4>, 'cons.price.idx': <tf.Tensor: shape=(), dtype=float64, numpy=93.444>, 'cons.conf.idx': <tf.Tensor: shape=(), dtype=float64, numpy=-36.1>, 'euribor3m': <tf.Tensor: shape=(), dtype=float64, numpy=4.963>, 'nr.employed': <tf.Tensor: shape=(), dtype=float64, numpy=5228.1>, 'previously_contacted': <tf.Tensor: shape=(), dtype=int64, numpy=0>}
目标: 0

预处理

通常我们的数据不是以适当或最佳格式进行建模的,这就是为什么大多数时候我们需要对特征进行某种预处理,以使它们与模型兼容或尽可能多地提取它们以完成任务。我们需要在训练时执行此预处理步骤,但在推理时,我们也需要确保数据经过相同的处理,这就是 FeatureSpace 发挥作用的地方,我们可以一次定义所有的预处理并在系统的不同阶段重用它。

在这里,我们将看到如何使用 FeatureSpace 执行更复杂的转换及其灵活性,然后将所有内容结合成一个组件,以对我们的模型预处理数据。

FeatureSpace 工具通过使用 adapt() 函数学习如何处理数据,这需要一个仅包含特征的数据集,因此让我们创建一个数据集,并一起创建一个实用程序函数,以展示预处理示例的实际应用:

train_ds_with_no_labels = train_ds.map(lambda x, _: x)


def example_feature_space(dataset, feature_space, feature_names):
    feature_space.adapt(dataset)
    for x in dataset.take(1):
        inputs = {feature_name: x[feature_name] for feature_name in feature_names}
        preprocessed_x = feature_space(inputs)
        print(f"Input: {[{k:v.numpy()} for k, v in inputs.items()]}")
        print(
            f"Preprocessed output: {[{k:v.numpy()} for k, v in preprocessed_x.items()]}"
        )

特征哈希

特征哈希 意味着将一组值哈希或编码到定义的数量的箱中,在这种情况下,我们有 campaign(在此活动中进行的联系次数和客户)这是一个可以假设不同范围值的数值特征,我们将其哈希为 4 个箱子,这意味着原始特征的任何可能值都将被放置到这四个可能的箱子中。这里的输出可以是一个独热编码向量或一个单一数字。

feature_space = FeatureSpace(
    features={
        "campaign": FeatureSpace.integer_hashed(num_bins=4, output_mode="one_hot")
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["campaign"])
Input: [{'campaign': 1}]
Preprocessed output: [{'campaign': array([0., 1., 0., 0.], dtype=float32)}]

特征哈希 也可以用于字符串特征。

feature_space = FeatureSpace(
    features={
        "education": FeatureSpace.string_hashed(num_bins=3, output_mode="one_hot")
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["education"])
Input: [{'education': b'basic.9y'}]
Preprocessed output: [{'education': array([0., 1., 0.], dtype=float32)}]

对于数值特征,我们可以通过使用 float_discretized 选项得到类似的行为,这与 integer_hashed 的主要区别在于,前者在保留某种数值关系的同时对值进行分箱(相近的值可能会被放置在同一个箱子中),而后者(哈希)则无法保证这些数字将被哈希到同一个箱子,它取决于哈希函数。

feature_space = FeatureSpace(
    features={"age": FeatureSpace.float_discretized(num_bins=3, output_mode="one_hot")},
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["age"])
Input: [{'age': 40}]
Preprocessed output: [{'age': array([0., 1., 0.], dtype=float32)}]

特征索引

索引 字符串特征基本上意味着为其创建一个离散的数值表示,这对字符串特征尤其重要,因为大多数模型仅接受数值特征。这种转换会将字符串值放入不同的类别中。这里的输出可以是一个独热编码向量或一个单一数字。

请注意,通过指定 num_oov_indices=1 我们在输出向量中留出了一个位置用于OOV(超出词汇表)值,这对于处理训练后缺失或未见值(在 adapt() 步骤中未见的值)是一个重要工具。

feature_space = FeatureSpace(
    features={
        "default": FeatureSpace.string_categorical(
            num_oov_indices=1, output_mode="one_hot"
        )
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["default"])
Input: [{'default': b'unknown'}]
Preprocessed output: [{'default': array([0., 0., 1., 0.], dtype=float32)}]

我们还可以对整数特征进行 特征索引,这对于某些数据集来说可能非常重要,其中分类特征被数字替换,例如特征 像 sexgender 这样的值(如(1 和 0))之间没有数值关系,它们只是不同的类别,这种行为可以通过这种转换完美捕获。

在这个数据集中,我们可以使用我们之前创建的特征 previously_contacted。在这种情况下,我们希望明确设置 num_oov_indices=0,原因是我们只期望特征有两个可能的值,任何其他值要么是错误的输入,要么是数据创建中的问题,因此我们可能希望代码抛出一个错误,这样我们可以意识到这个问题并加以修复。

feature_space = FeatureSpace(
    features={
        "previously_contacted": FeatureSpace.integer_categorical(
            num_oov_indices=0, output_mode="one_hot"
        )
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["previously_contacted"])
输入: [{'previously_contacted': 0}]
预处理输出: [{'previously_contacted': array([1., 0.], dtype=float32)}]

特征交叉(混合不同类型的特征)

使用 crosses 我们可以在任意数量的混合类型的特征之间进行特征交互,只要它们是分类特征,你可以想象与其拥有一个特征 {'age': 20} 和另一个 {'job': 'entrepreneur'},我们可以有 {'age_X_job': 20_entrepreneur},但是使用 FeatureSpacecrosses 我们可以对每个特征和特征交叉本身应用特定的预处理。这种选择对于特定应用场景非常强大,在银行领域,年龄与工作结合可以具有不同的含义。

我们将交叉 agejob 并将它们的组合输出哈希为一个大小为 8 的向量表示。这里的输出可以是一个独热编码向量或单个数字。

有时,多个特征的组合可能会导致非常大的特征空间,想想将某人的邮政编码与其姓氏交叉,可能性将会成千上万,这就是 crossing_dim 参数如此重要的原因,它限制了交叉特征的输出维度。

请注意, age 的 6 个箱子的可能值与 job 的 12 个值的组合将是 72,因此通过选择 crossing_dim = 8,我们选择约束输出向量。

feature_space = FeatureSpace(
    features={
        "age": FeatureSpace.integer_hashed(num_bins=6, output_mode="one_hot"),
        "job": FeatureSpace.string_categorical(
            num_oov_indices=0, output_mode="one_hot"
        ),
    },
    crosses=[
        FeatureSpace.cross(
            feature_names=("age", "job"),
            crossing_dim=8,
            output_mode="one_hot",
        )
    ],
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["age", "job"])
输入: [{'age': 28}, {'job': b'blue-collar'}]
预处理输出: [{'age': array([0., 0., 1., 0., 0., 0.], dtype=float32)}, {'job': array([0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)}, {'age_X_job': array([0., 0., 0., 0., 1., 0., 0., 0.], dtype=float32)}]

使用 Keras 预处理层的 FeatureSpace

为了成为一个真正灵活和可扩展的特征,我们不能仅依赖这些预定义的转变,我们必须能够重用 Keras/TensorFlow 生态系统中的其他转换并自定义自己的,这就是为什么 FeatureSpace 也被设计为与 Keras 预处理层 一起工作,这样我们就可以使用框架提供的复杂数据转换,你甚至可以创建自己的自定义 Keras 预处理层并以同样的方式使用它。

在这里,我们将使用 [keras.layers.TextVectorization](/api/layers/preprocessing_layers/text/text_vectorization#textvectorization-class) 预处理层从我们的数据中创建一个 TF-IDF 特征。请注意,这个特征并不是 TF-IDF 的真正好用例,这只是为了演示目的。

custom_layer = keras.layers.TextVectorization(output_mode="tf_idf")

feature_space = FeatureSpace(
    features={
        "education": FeatureSpace.feature(
            preprocessor=custom_layer, dtype="string", output_mode="float"
        )
    },
    output_mode="dict",
)
example_feature_space(train_ds_with_no_labels, feature_space, ["education"])
输入: [{'education': b'university.degree'}]
预处理输出: [{'education': array([0.       , 1.4574516, 0.       , 0.       , 0.       , 0.       ,
       0.       , 0.       , 0.       ], dtype=float32)}]

配置最终的 FeatureSpace

现在我们知道如何使用 FeatureSpace 来处理更复杂的用例,让我们选择那些 这看起来对于这个任务更加有用,并创建最终的 FeatureSpace 组件。

为了配置每个特征应该如何预处理,我们实例化一个 keras.utils.FeatureSpace,并传递给它一个字典,该字典将我们的特征名称映射到特征转换函数。

feature_space = FeatureSpace(
    features={
        # 作为整数编码的分类特征
        "previously_contacted": FeatureSpace.integer_categorical(num_oov_indices=0),
        # 作为字符串编码的分类特征
        "marital": FeatureSpace.string_categorical(num_oov_indices=0),
        "education": FeatureSpace.string_categorical(num_oov_indices=0),
        "default": FeatureSpace.string_categorical(num_oov_indices=0),
        "housing": FeatureSpace.string_categorical(num_oov_indices=0),
        "loan": FeatureSpace.string_categorical(num_oov_indices=0),
        "contact": FeatureSpace.string_categorical(num_oov_indices=0),
        "month": FeatureSpace.string_categorical(num_oov_indices=0),
        "day_of_week": FeatureSpace.string_categorical(num_oov_indices=0),
        "poutcome": FeatureSpace.string_categorical(num_oov_indices=0),
        # 需要哈希和分箱的分类特征
        "job": FeatureSpace.string_hashed(num_bins=3),
        # 需要哈希和分箱的数值特征
        "pdays": FeatureSpace.integer_hashed(num_bins=4),
        # 需要归一化和分箱的数值特征
        "age": FeatureSpace.float_discretized(num_bins=4),
        # 需要归一化的数值特征
        "campaign": FeatureSpace.float_normalized(),
        "previous": FeatureSpace.float_normalized(),
        "emp.var.rate": FeatureSpace.float_normalized(),
        "cons.price.idx": FeatureSpace.float_normalized(),
        "cons.conf.idx": FeatureSpace.float_normalized(),
        "euribor3m": FeatureSpace.float_normalized(),
        "nr.employed": FeatureSpace.float_normalized(),
    },
    # 使用自定义交叉维度指定特征交叉。
    crosses=[
        FeatureSpace.cross(feature_names=("age", "job"), crossing_dim=8),
        FeatureSpace.cross(feature_names=("housing", "loan"), crossing_dim=6),
        FeatureSpace.cross(
            feature_names=("poutcome", "previously_contacted"), crossing_dim=2
        ),
    ],
    output_mode="concat",
)

FeatureSpace 适配训练数据

在开始使用 FeatureSpace 构建模型之前,我们必须将其适配训练数据。在 adapt() 过程中,FeatureSpace 将:

  • 索引分类特征的可能值集。
  • 计算数值特征的均值和方差以进行归一化。
  • 计算数值特征的不同盒子的值边界以进行离散化。
  • 任何自定义层所需的其他预处理。

请注意,adapt() 应该在一个 tf.data.Dataset 上调用,该数据集生成特征值的字典 – 不包含标签。

但首先,我们将数据集分批处理。

train_ds = train_ds.batch(32)
valid_ds = valid_ds.batch(32)

train_ds_with_no_labels = train_ds.map(lambda x, _: x)
feature_space.adapt(train_ds_with_no_labels)

此时,可以在一组原始特征值的字典上调用 FeatureSpace,并且因为我们设置了 output_mode="concat",它将返回一个单一的连接向量,每个样本都结合了编码特征和特征交叉。

for x, _ in train_ds.take(1):
    preprocessed_x = feature_space(x)
    print(f"preprocessed_x shape: {preprocessed_x.shape}")
    print(f"preprocessed_x sample: \n{preprocessed_x[0]}")
preprocessed_x shape: (32, 77)
preprocessed_x sample: 
[ 0.          1.          0.          0.         -0.19560693  0.95908785
 -0.22542837  1.          0.          0.          1.          0.
  0.          0.          1.          0.          0.          1.
  0.          0.          0.          0.          0.          0.
  0.          0.8486567   0.781508    1.          0.          0.
  0.          0.          1.          1.          0.          0.
  0.          1.          0.          0.          0.          0.
  1.          0.          0.          0.          0.          0.
  0.          0.          0.8400493   0.          0.          1.
  0.          1.          0.          0.         -0.35691845  1.
  0.          0.          0.          0.          0.          0.
  1.          0.          0.          0.          0.          0.
  0.          1.          0.          1.          0.        ]

保存 FeatureSpace

此时,我们可以选择保存我们的 FeatureSpace 组件,这有许多好处,比如在使用相同模型的不同实验中重用它,节省如果需要重新运行预处理步骤的时间,尤其是在模型部署时,通过加载它可以确保无论设备或环境如何,都会应用相同的预处理步骤,这是减少的一个好方法。 训练/服务偏差.

feature_space.save("myfeaturespace.keras")

使用 FeatureSpace 作为 tf.data 管道的一部分进行预处理

我们将选择以异步方式使用我们的组件,使其成为 tf.data 管道的一部分,如之前的指南所述。这使得在数据送入模型之前,可以在 CPU 上异步并行处理数据。通常,在训练期间这是正确的做法。

让我们创建一个经过预处理的训练和验证数据集:

preprocessed_train_ds = train_ds.map(
    lambda x, y: (feature_space(x), y), num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)

preprocessed_valid_ds = valid_ds.map(
    lambda x, y: (feature_space(x), y), num_parallel_calls=tf.data.AUTOTUNE
).prefetch(tf.data.AUTOTUNE)

模型

我们将利用我们的 FeatureSpace 组件来构建模型,因为我们希望模型能够与我们的预处理函数兼容,让我们使用 FeatureSpace 特征图作为模型的输入。

encoded_features = feature_space.get_encoded_features()
print(encoded_features)
<KerasTensor shape=(None, 77), dtype=float32, sparse=False, name=keras_tensor_56>

这个模型只是为了演示目的而设计,因此不必太关注其架构。

x = keras.layers.Dense(64, activation="relu")(encoded_features)
x = keras.layers.Dropout(0.5)(x)
output = keras.layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs=encoded_features, outputs=output)
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])

训练

让我们训练我们的模型 20 个周期。请注意,特征预处理是在 tf.data 管道中进行的,而不是模型的一部分。

model.fit(
    preprocessed_train_ds, validation_data=preprocessed_valid_ds, epochs=20, verbose=2
)
Epoch 1/20
103/103 - 1s - 6ms/step - accuracy: 0.8844 - loss: 0.3453 - val_accuracy: 0.9114 - val_loss: 0.2612
Epoch 2/20
103/103 - 0s - 2ms/step - accuracy: 0.8974 - loss: 0.3010 - val_accuracy: 0.9078 - val_loss: 0.2641
Epoch 3/20
103/103 - 0s - 2ms/step - accuracy: 0.9005 - loss: 0.2863 - val_accuracy: 0.9066 - val_loss: 0.2630
Epoch 4/20
103/103 - 0s - 2ms/step - accuracy: 0.9002 - loss: 0.2925 - val_accuracy: 0.9053 - val_loss: 0.2653
Epoch 5/20
103/103 - 0s - 2ms/step - accuracy: 0.8995 - loss: 0.2893 - val_accuracy: 0.9078 - val_loss: 0.2624
Epoch 6/20
103/103 - 0s - 2ms/step - accuracy: 0.9002 - loss: 0.2866 - val_accuracy: 0.9078 - val_loss: 0.2628
Epoch 7/20
103/103 - 0s - 2ms/step - accuracy: 0.9026 - loss: 0.2868 - val_accuracy: 0.9090 - val_loss: 0.2621
Epoch 8/20
103/103 - 0s - 2ms/step - accuracy: 0.9023 - loss: 0.2802 - val_accuracy: 0.9078 - val_loss: 0.2623
Epoch 9/20
103/103 - 0s - 2ms/step - accuracy: 0.9047 - loss: 0.2743 - val_accuracy: 0.9078 - val_loss: 0.2628
Epoch 10/20
103/103 - 0s - 2ms/step - accuracy: 0.9062 - loss: 0.2761 - val_accuracy: 0.9090 - val_loss: 0.2650
Epoch 11/20
103/103 - 0s - 2ms/step - accuracy: 0.9050 - loss: 0.2729 - val_accuracy: 0.9090 - val_loss: 0.2668
Epoch 12/20
103/103 - 0s - 2ms/step - accuracy: 0.9029 - loss: 0.2699 - val_accuracy: 0.9078 - val_loss: 0.2670
Epoch 13/20
103/103 - 0s - 2ms/step - accuracy: 0.9056 - loss: 0.2671 - val_accuracy: 0.9078 - val_loss: 0.2641
Epoch 14/20
103/103 - 0s - 2ms/step - accuracy: 0.9032 - loss: 0.2750 - val_accuracy: 0.9078 - val_loss: 0.2643
Epoch 15/20
103/103 - 0s - 2ms/step - accuracy: 0.9083 - loss: 0.2650 - val_accuracy: 0.9102 - val_loss: 0.2658
Epoch 16/20
103/103 - 0s - 2ms/step - accuracy: 0.9102 - loss: 0.2593 - val_accuracy: 0.9102 - val_loss: 0.2639
Epoch 17/20
103/103 - 0s - 2ms/step - accuracy: 0.9074 - loss: 0.2719 - val_accuracy: 0.9102 - val_loss: 0.2655
Epoch 18/20
103/103 - 0s - 2ms/step - accuracy: 0.9059 - loss: 0.2655 - val_accuracy: 0.9102 - val_loss: 0.2670
Epoch 19/20
103/103 - 0s - 2ms/step - accuracy: 0.9099 - loss: 0.2650 - val_accuracy: 0.9102 - val_loss: 0.2646
Epoch 20/20
103/103 - 0s - 2ms/step - accuracy: 0.9068 - loss: 0.2624 - val_accuracy: 0.9078 - val_loss: 0.2661

<keras.src.callbacks.history.History at 0x31eac7eb0>

使用端到端模型对新数据进行推理

现在,我们可以构建我们的推理模型(包括 FeatureSpace)以根据原始特征值的字典进行预测,如下所示:

加载 FeatureSpace

首先,让我们加载几分钟前保存的 FeatureSpace,这在你训练模型但想在不同时间推理时非常方便,可能使用不同的设备或环境。

loaded_feature_space = keras.saving.load_model("myfeaturespace.keras")  # 加载特征空间模型

构建推理端到端模型

要构建推理模型,我们需要特征输入图和预处理编码的 Keras 张量。

dict_inputs = loaded_feature_space.get_inputs()
encoded_features = loaded_feature_space.get_encoded_features()
print(encoded_features)

print(dict_inputs)

outputs = model(encoded_features)
inference_model = keras.Model(inputs=dict_inputs, outputs=outputs)

sample = {
    "age": 30,
    "job": "blue-collar",
    "marital": "married",
    "education": "basic.9y",
    "default": "no",
    "housing": "yes",
    "loan": "no",
    "contact": "cellular",
    "month": "may",
    "day_of_week": "fri",
    "campaign": 2,
    "pdays": 999,
    "previous": 0,
    "poutcome": "nonexistent",
    "emp.var.rate": -1.8,
    "cons.price.idx": 92.893,
    "cons.conf.idx": -46.2,
    "euribor3m": 1.313,
    "nr.employed": 5099.1,
    "previously_contacted": 0,
}

input_dict = {
    name: keras.ops.convert_to_tensor([value]) for name, value in sample.items()
}
predictions = inference_model.predict(input_dict)

print(
    f"This particular client has a {100 * predictions[0][0]:.2f}% probability "
    "of subscribing a term deposit, as evaluated by our model."
)
<KerasTensor shape=(None, 77), dtype=float32, sparse=False, name=keras_tensor_99>
{'previously_contacted': <KerasTensor shape=(None, 1), dtype=int32, sparse=None, name=previously_contacted>, 'marital': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=marital>, 'education': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=education>, 'default': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=default>, 'housing': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=housing>, 'loan': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=loan>, 'contact': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=contact>, 'month': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=month>, 'day_of_week': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=day_of_week>, 'poutcome': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=poutcome>, 'job': <KerasTensor shape=(None, 1), dtype=string, sparse=None, name=job>, 'pdays': <KerasTensor shape=(None, 1), dtype=int32, sparse=None, name=pdays>, 'age': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=age>, 'campaign': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=campaign>, 'previous': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=previous>, 'emp.var.rate': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=emp.var.rate>, 'cons.price.idx': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=cons.price.idx>, 'cons.conf.idx': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=cons.conf.idx>, 'euribor3m': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=euribor3m>, 'nr.employed': <KerasTensor shape=(None, 1), dtype=float32, sparse=None, name=nr.employed>}
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 126ms/step
根据我们的模型,该客户有 9.60% 的概率订购定期存款。