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

FeatureSpace 高级用例

作者: Dimitre Oliveira
创建时间 2023/07/01
上次修改时间 2023/07/01
描述: 如何将 FeatureSpace 用于高级预处理用例。

ⓘ 本示例使用 Keras 3

在 Colab 中查看 GitHub 源代码


介绍

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

总体任务仍然是使用包含数值特征、整数分类特征和字符串分类特征的数据进行结构化数据分类(也称为表格数据分类)。

数据集

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

以下是每个特征的描述

描述 特征类型
Age 客户的年龄 数值型
Job 工作类型 分类型
Marital 婚姻状况 分类型
Education 客户的教育水平 分类型
Default 是否有信贷违约? 分类型
Housing 是否有住房贷款? 分类型
Loan 是否有个人贷款? 分类型
Contact 联系方式 分类型
Month 上次联系的月份 分类型
Day_of_week 上次联系的星期几 分类型
Duration 上次联系时长(秒) 数值型
Campaign 本次活动中针对该客户进行的联系次数 数值型
Pdays 上次从之前的活动联系客户后经过的天数 数值型
Previous 本次活动之前针对该客户进行的联系次数 数值型
Poutcome 之前营销活动的成果 分类型
Emp.var.rate 就业变化率 数值型
Cons.price.idx 消费者物价指数 数值型
Cons.conf.idx 消费者信心指数 数值型
Euribor3m 欧元银行同业拆息利率(3 个月) 数值型
Nr.employed 雇员人数 数值型
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:
    # Extract files
    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,则表示该客户之前未被联系过,因此,让我们创建一个特征来捕获这一点。

# Droping `duration` to avoid target leak
dataframe.drop("duration", axis=1, inplace=True)
# Creating the new feature `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())
Dataframe shape: (4119, 21)
   age          job  marital          education default  housing     loan  \
0   30  blue-collar  married           basic.9y      no      yes       no   
1   39     services   single        high.school      no       no       no   
2   25     services  married        high.school      no      yes       no   
3   38     services  married           basic.9y      no  unknown  unknown   
4   47       admin.  married  university.degree      no      yes       no   
     contact month day_of_week  ...  pdays  previous     poutcome  \
0   cellular   may         fri  ...    999         0  nonexistent   
1  telephone   may         fri  ...    999         0  nonexistent   
2  telephone   jun         wed  ...    999         0  nonexistent   
3  telephone   jun         fri  ...    999         0  nonexistent   
4   cellular   nov         mon  ...    999         0  nonexistent   
  emp.var.rate  cons.price.idx  cons.conf.idx  euribor3m  nr.employed   y  \
0         -1.8          92.893          -46.2      1.313       5099.1  no   
1          1.1          93.994          -36.4      4.855       5191.0  no   
2          1.4          94.465          -41.8      4.962       5228.1  no   
3          1.4          94.465          -41.8      4.959       5228.1  no   
4         -0.1          93.200          -42.0      4.191       5195.8  no   
  previously_contacted  
0                    0  
1                    0  
2                    0  
3                    0  
4                    0  
[5 rows x 21 columns]

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


训练/验证拆分

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

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

print(
    f"Using {len(train_dataframe)} samples for training and "
    f"{len(valid_dataframe)} for validation"
)
Using 3295 samples for training and 824 for validation

生成 TF 数据集

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

label_lookup = keras.layers.StringLookup(
    # the order here is important since the first index will be encoded as 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 生成一个元组 (input, target),其中 input 是一个特征字典,target 是值 01

for x, y in dataframe_to_dataset(train_dataframe).take(1):
    print(f"Input: {x}")
    print(f"Target: {y}")
Input: {'age': <tf.Tensor: shape=(), dtype=int64, numpy=33>, 'job': <tf.Tensor: shape=(), dtype=string, numpy=b'technician'>, 'marital': <tf.Tensor: shape=(), dtype=string, numpy=b'married'>, 'education': <tf.Tensor: shape=(), dtype=string, numpy=b'university.degree'>, 'default': <tf.Tensor: shape=(), dtype=string, numpy=b'unknown'>, 'housing': <tf.Tensor: shape=(), dtype=string, numpy=b'yes'>, 'loan': <tf.Tensor: shape=(), dtype=string, numpy=b'no'>, 'contact': <tf.Tensor: shape=(), dtype=string, numpy=b'cellular'>, 'month': <tf.Tensor: shape=(), dtype=string, numpy=b'aug'>, 'day_of_week': <tf.Tensor: shape=(), dtype=string, numpy=b'tue'>, 'campaign': <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'nonexistent'>, '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>}
Target: 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()]}"
        )

特征哈希

特征哈希 指的是将一组值哈希或编码到定义数量的 bin 中,在本例中,我们有 campaign(本次活动中针对该客户进行的联系次数),它是一个可以取不同值范围的数值特征,我们将将其哈希到 4 个 bin 中,这意味着原始特征的任何可能值都将被放置到这 4 个可能的 bin 之一中。此处的输出可以是独热编码向量或单个数字。

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 之间的区别在于,前者对值进行分箱,同时保持一定数值关系(相近的值很可能被放置在同一个 bin 中),而后者(哈希)不能保证这些数字会被哈希到同一个 bin 中,它取决于哈希函数。

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"])
Input: [{'previously_contacted': 0}]
Preprocessed output: [{'previously_contacted': array([1., 0.], dtype=float32)}]

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

使用交叉,我们可以对任意数量的混合类型特征进行特征交互,只要它们是分类特征,就可以考虑拥有一个特征 {'age': 20} 和另一个 {'job': 'entrepreneur'},我们就可以拥有 {'age_X_job': 20_entrepreneur},但是使用 FeatureSpace交叉,我们可以对每个单独特征以及特征交叉本身应用特定的预处理。此选项对于特定用例可能非常强大,这里可能是一个不错的选择,因为年龄与职业相结合可能在银行业领域有不同的含义。

我们将交叉 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"])
Input: [{'age': 28}, {'job': b'blue-collar'}]
Preprocessed output: [{'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"])
Input: [{'education': b'university.degree'}]
Preprocessed output: [{'education': array([0.       , 1.4574516, 0.       , 0.       , 0.       , 0.       ,
       0.       , 0.       , 0.       ], dtype=float32)}]

配置最终的 FeatureSpace

现在我们知道了如何将 FeatureSpace 用于更复杂的用例,让我们选择对这项任务最有用的用例并创建最终的 FeatureSpace 组件。

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

feature_space = FeatureSpace(
    features={
        # Categorical features encoded as integers
        "previously_contacted": FeatureSpace.integer_categorical(num_oov_indices=0),
        # Categorical features encoded as string
        "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),
        # Categorical features to hash and bin
        "job": FeatureSpace.string_hashed(num_bins=3),
        # Numerical features to hash and bin
        "pdays": FeatureSpace.integer_hashed(num_bins=4),
        # Numerical features to normalize and bin
        "age": FeatureSpace.float_discretized(num_bins=4),
        # Numerical features to normalize
        "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(),
    },
    # Specify feature cross with a custom crossing dim.
    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 个 epochs。请注意,特征预处理正在作为 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
This particular client has a 9.60% probability of subscribing a term deposit, as evaluated by our model.