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

FeatureSpace 高级用例

作者: Dimitre Oliveira
创建日期 2023/07/01
最后修改日期 2025/01/03
描述:如何将 FeatureSpace 用于更高级的预处理用例。

ⓘ 本示例使用 Keras 3

在 Colab 中查看 GitHub 源代码


简介

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

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

数据集

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

以下是每个特征的描述

描述 特征类型
年龄 客户年龄 数值型
工作 职业类型 类别型
婚姻状况 婚姻状况 类别型
教育程度 客户的教育水平 类别型
默认 是否有逾期信用? 类别型
住房 是否有住房贷款? 类别型
贷款 是否有个人贷款? 类别型
联系方式 联系通信类型 类别型
月份 今年的最后一次联系月份 类别型
星期几 最后一次联系的星期几 类别型
时长 最后一次联系的时长(秒) 数值型
活动 此活动期间为该客户执行的联系次数 数值型
Pdays 自上次联系客户以来的天数 数值型
之前 在此活动之前为该客户执行的联系次数 数值型
Poutcome 先前营销活动的结果 类别型
Emp.var.rate 就业变化率 数值型
Cons.price.idx 消费者价格指数 数值型
Cons.conf.idx 消费者信心指数 数值型
Euribor3m 三个月欧元区银行间拆借利率 数值型
Nr.employed 员工人数 数值型
Y 客户是否订阅了定期存款? 目标

关于 duration 特征的重要说明:此属性极大地影响了输出目标(例如,如果 duration=0 则 y='no')。但是,在通话之前是不知道 duration 的。同样,在通话结束后 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)
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=56>, 'job': <tf.Tensor: shape=(), dtype=string, numpy=b'admin.'>, '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'no'>, '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'jul'>, 'day_of_week': <tf.Tensor: shape=(), dtype=string, numpy=b'fri'>, 'campaign': <tf.Tensor: shape=(), dtype=int64, numpy=5>, '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.918>, 'cons.conf.idx': <tf.Tensor: shape=(), dtype=float64, numpy=-42.7>, 'euribor3m': <tf.Tensor: shape=(), dtype=float64, numpy=4.957>, '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 中的一个。这里的输出可以是一个 one-hot 编码向量或单个数字。

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'university.degree'}]
Preprocessed output: [{'education': array([0., 0., 1.], 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': 56}]
Preprocessed output: [{'age': array([0., 0., 1.], dtype=float32)}]

特征索引

对字符串特征进行索引本质上是为其创建离散的数值表示,这对于字符串特征尤其重要,因为大多数模型只接受数值特征。此转换会将字符串值放置到不同的类别中。这里的输出可以是一个 one-hot 编码向量或单个数字。

请注意,通过指定 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'no'}]
Preprocessed output: [{'default': array([0., 1., 0., 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 的向量表示中。这里的输出可以是一个 one-hot 编码向量或单个数字。

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

请注意,6 个 age bin 和 12 个 job 值的可能组合将是 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': 33}, {'job': b'admin.'}]
Preprocessed output: [{'age': array([0., 0., 1., 0., 0., 0.], dtype=float32)}, {'job': array([1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)}, {'age_X_job': array([0., 1., 0., 0., 0., 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'high.school'}]
Preprocessed output: [{'education': array([0.       , 0.       , 1.6840783, 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.          0.          1.          0.         -0.19560708  0.8937782
  0.7249699   0.          1.          0.          0.          0.
  1.          0.          0.          1.          0.          0.
  0.          0.          0.          1.          0.          0.
  0.          0.6566938   0.71815234  0.          0.          1.
  0.          1.          0.          0.          0.          1.
  1.          0.          0.          0.          1.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.33757654  0.          0.          1.
  0.          1.          0.          0.         -0.35691857  1.
  0.          0.          0.          0.          0.          1.
  0.          0.          0.          0.          0.          0.
  1.          0.          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 个 epoch 的训练。请注意,特征预处理发生在 tf.data 流水线的一部分,而不是模型的一部分。

model.fit(
    preprocessed_train_ds, validation_data=preprocessed_valid_ds, epochs=10, verbose=2
)
Epoch 1/10

103/103 - 15s - 149ms/step - accuracy: 0.8753 - loss: 0.3639 - val_accuracy: 0.9102 - val_loss: 0.2747

Epoch 2/10

103/103 - 12s - 121ms/step - accuracy: 0.8965 - loss: 0.3058 - val_accuracy: 0.9078 - val_loss: 0.2716

Epoch 3/10

103/103 - 12s - 121ms/step - accuracy: 0.8947 - loss: 0.2972 - val_accuracy: 0.9053 - val_loss: 0.2712

Epoch 4/10

103/103 - 12s - 116ms/step - accuracy: 0.9002 - loss: 0.2877 - val_accuracy: 0.9102 - val_loss: 0.2677

Epoch 5/10

103/103 - 13s - 124ms/step - accuracy: 0.8974 - loss: 0.2815 - val_accuracy: 0.9041 - val_loss: 0.2688

Epoch 6/10

103/103 - 13s - 129ms/step - accuracy: 0.8986 - loss: 0.2917 - val_accuracy: 0.9066 - val_loss: 0.2658

Epoch 7/10

103/103 - 12s - 120ms/step - accuracy: 0.9029 - loss: 0.2779 - val_accuracy: 0.9053 - val_loss: 0.2670

Epoch 8/10

103/103 - 13s - 124ms/step - accuracy: 0.9011 - loss: 0.2809 - val_accuracy: 0.9090 - val_loss: 0.2660

Epoch 9/10

103/103 - 13s - 121ms/step - accuracy: 0.9008 - loss: 0.2748 - val_accuracy: 0.9041 - val_loss: 0.2689

Epoch 10/10

103/103 - 13s - 123ms/step - accuracy: 0.9038 - loss: 0.2768 - val_accuracy: 0.9053 - val_loss: 0.2674

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

端到端模型在新数据上的推理

现在,我们可以构建我们的推理模型(包括 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=False, name=previously_contacted>, 'marital': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=marital>, 'education': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=education>, 'default': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=default>, 'housing': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=housing>, 'loan': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=loan>, 'contact': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=contact>, 'month': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=month>, 'day_of_week': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=day_of_week>, 'poutcome': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=poutcome>, 'job': <KerasTensor shape=(None, 1), dtype=string, sparse=False, name=job>, 'pdays': <KerasTensor shape=(None, 1), dtype=int32, sparse=False, name=pdays>, 'age': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=age>, 'campaign': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=campaign>, 'previous': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=previous>, 'emp.var.rate': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=emp.var.rate>, 'cons.price.idx': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=cons.price.idx>, 'cons.conf.idx': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=cons.conf.idx>, 'euribor3m': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=euribor3m>, 'nr.employed': <KerasTensor shape=(None, 1), dtype=float32, sparse=False, name=nr.employed>}

/home/humbulani/tensorflow-env/env/lib/python3.11/site-packages/keras/src/models/functional.py:248: UserWarning: The structure of `inputs` doesn't match the expected structure.
Expected: {'age': 'age', 'campaign': 'campaign', 'cons.conf.idx': 'cons.conf.idx', 'cons.price.idx': 'cons.price.idx', 'contact': 'contact', 'day_of_week': 'day_of_week', 'default': 'default', 'education': 'education', 'emp.var.rate': 'emp.var.rate', 'euribor3m': 'euribor3m', 'housing': 'housing', 'job': 'job', 'loan': 'loan', 'marital': 'marital', 'month': 'month', 'nr.employed': 'nr.employed', 'pdays': 'pdays', 'poutcome': 'poutcome', 'previous': 'previous', 'previously_contacted': 'previously_contacted'}
Received: inputs={'age': 'Tensor(shape=(1,))', 'job': 'Tensor(shape=(1,))', 'marital': 'Tensor(shape=(1,))', 'education': 'Tensor(shape=(1,))', 'default': 'Tensor(shape=(1,))', 'housing': 'Tensor(shape=(1,))', 'loan': 'Tensor(shape=(1,))', 'contact': 'Tensor(shape=(1,))', 'month': 'Tensor(shape=(1,))', 'day_of_week': 'Tensor(shape=(1,))', 'campaign': 'Tensor(shape=(1,))', 'pdays': 'Tensor(shape=(1,))', 'previous': 'Tensor(shape=(1,))', 'poutcome': 'Tensor(shape=(1,))', 'emp.var.rate': 'Tensor(shape=(1,))', 'cons.price.idx': 'Tensor(shape=(1,))', 'cons.conf.idx': 'Tensor(shape=(1,))', 'euribor3m': 'Tensor(shape=(1,))', 'nr.employed': 'Tensor(shape=(1,))', 'previously_contacted': 'Tensor(shape=(1,))'}
  warnings.warn(msg)

1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 1s/step



1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/step

This particular client has a 10.85% probability of subscribing a term deposit, as evaluated by our model.