代码示例 / 结构化数据 / 使用 FeatureSpace 进行结构化数据分类

使用 FeatureSpace 进行结构化数据分类

作者: fchollet
创建日期: 2022/11/09
最后修改: 2022/11/09
描述: 只需几行代码对表格数据进行分类。

在 Colab 中查看 GitHub 源代码


介绍

本示例演示如何从原始 CSV 文件进行结构化数据分类(也称为表格数据分类)。我们的数据包括数值特征、整数分类特征和字符串分类特征。我们将使用实用工具 keras.utils.FeatureSpace 来索引、预处理和编码我们的特征。

代码改编自示例 从头开始的结构化数据分类。虽然之前的示例使用 Keras 预处理层管理其自己的低级特征预处理和编码,但在本示例中,我们将所有内容委托给 FeatureSpace,使得工作流程变得极其快速和简单。

数据集

我们的数据集 由克利夫兰诊所心脏病基金会提供。它是一个包含 303 行的 CSV 文件。每行包含有关患者的信息(一个 样本),每列描述患者的一个属性(一个 特征)。我们使用这些特征来预测患者是否患有心脏病(二元分类)。

以下是每个特征的描述:

描述 特征类型
年龄 年龄(岁) 数值型
性别 (1 = 男;0 = 女) 分类型
CP 胸痛类型(0, 1, 2, 3, 4) 分类型
Trestbpd 入院时的静息血压(单位:mm Hg) 数值型
胆固醇 血清胆固醇(单位:mg/dl) 数值型
FBS 空腹血糖 > 120 mg/dl(1 = 是;0 = 否) 分类型
RestECG 静息心电图结果(0, 1, 2) 分类型
Thalach 达到的最大心率 数值型
Exang 运动诱发的心绞痛(1 = 是;0 = 否) 分类型
Oldpeak 运动相对于静息引起的 ST 衰退 数值型
坡度 峰值运动 ST 段的坡度 数值型
CA 被荧光透视染色的主要血管数量(0-3) 数值型和分类型
Thal 3 = 正常;6 = 固定缺陷;7 = 可逆缺陷 分类型
目标 心脏病诊断(1 = 是;0 = 否) 目标

设置

import os

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

import tensorflow as tf
import pandas as pd
import keras
from keras.utils import FeatureSpace

准备数据

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

file_url = "http://storage.googleapis.com/download.tensorflow.org/data/heart.csv"
dataframe = pd.read_csv(file_url)

数据集包括 303 个样本,每个样本有 14 列(13 个特征,加上目标标签):

print(dataframe.shape)
(303, 14)

这是一些样本的预览:

dataframe.head()
age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal target
0 63 1 1 145 233 1 2 150 0 2.3 3 0 fixed 0
1 67 1 4 160 286 0 2 108 1 1.5 2 3 normal 1
2 67 1 4 120 229 0 2 129 1 2.6 2 2 reversible 0
3 37 1 3 130 250 0 0 187 0 3.5 3 0 normal 0
4 41 0 2 130 204 0 2 172 0 1.4 1 0 normal 0

最后一列“目标”表示患者是否患有心脏病(1)或没有(0)。

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

val_dataframe = dataframe.sample(frac=0.2, random_state=1337)
train_dataframe = dataframe.drop(val_dataframe.index)

print(
    "使用 %d 个样本进行训练,%d 个样本进行验证"
    % (len(train_dataframe), len(val_dataframe))
)
使用 242 个样本进行训练,61 个样本进行验证

我们为每个数据框生成 tf.data.Dataset 对象:

def dataframe_to_dataset(dataframe):
    dataframe = dataframe.copy()
    labels = dataframe.pop("target")
    ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
    ds = ds.shuffle(buffer_size=len(dataframe))
    return ds


train_ds = dataframe_to_dataset(train_dataframe)
val_ds = dataframe_to_dataset(val_dataframe)

每个 Dataset 产生一个元组 (input, target),其中 input 是特征的字典,target 是值 01

for x, y in train_ds.take(1):
    print("输入:", x)
    print("目标:", y)
输入: {'age': <tf.Tensor: shape=(), dtype=int64, numpy=65>, 'sex': <tf.Tensor: shape=(), dtype=int64, numpy=1>, 'cp': <tf.Tensor: shape=(), dtype=int64, numpy=1>, 'trestbps': <tf.Tensor: shape=(), dtype=int64, numpy=138>, 'chol': <tf.Tensor: shape=(), dtype=int64, numpy=282>, 'fbs': <tf.Tensor: shape=(), dtype=int64, numpy=1>, 'restecg': <tf.Tensor: shape=(), dtype=int64, numpy=2>, 'thalach': <tf.Tensor: shape=(), dtype=int64, numpy=174>, 'exang': <tf.Tensor: shape=(), dtype=int64, numpy=0>, 'oldpeak': <tf.Tensor: shape=(), dtype=float64, numpy=1.4>, 'slope': <tf.Tensor: shape=(), dtype=int64, numpy=2>, 'ca': <tf.Tensor: shape=(), dtype=int64, numpy=1>, 'thal': <tf.Tensor: shape=(), dtype=string, numpy=b'正常'>}
目标: tf.Tensor(0, shape=(), dtype=int64)

让我们对数据集进行批处理:

train_ds = train_ds.batch(32)
val_ds = val_ds.batch(32)

配置 FeatureSpace

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

我们有一些“整数类别”特征,如 "FBS",一个“字符串类别”特征("thal"),以及一些数值特征,我们希望对其进行归一化——除了 "age",我们希望将其离散化为几个区间。

我们还使用 crosses 参数来捕获某些类别特征的 特征交互,即创建表示这些类别特征的值共现的额外特征。你可以对任意类别特征集计算特征交叉——不仅限于两个特征的元组。由于结果共现被哈希到一个固定大小的向量中,因此你不需要担心共现空间是否太大。

feature_space = FeatureSpace(
    features={
        # 以整数编码的类别特征
        "sex": "integer_categorical",
        "cp": "integer_categorical",
        "fbs": "integer_categorical",
        "restecg": "integer_categorical",
        "exang": "integer_categorical",
        "ca": "integer_categorical",
        # 以字符串编码的类别特征
        "thal": "string_categorical",
        # 要离散化的数值特征
        "age": "float_discretized",
        # 要归一化的数值特征
        "trestbps": "float_normalized",
        "chol": "float_normalized",
        "thalach": "float_normalized",
        "oldpeak": "float_normalized",
        "slope": "float_normalized",
    },
    # 我们通过哈希值共现创建额外特征
    # 以下类别特征组的值共现。
    crosses=[("sex", "age"), ("thal", "ca")],
    # 这些共现的哈希空间将是 32 维的。
    crossing_dim=32,
    # 我们的工具将对所有类别
    # 特征进行独热编码,并将所有特征拼接成一个
    # 向量(每个样本一个向量)。
    output_mode="concat",
)

进一步自定义 FeatureSpace

通过字符串名称指定特征类型既快速又简单,但有时您可能希望进一步配置每个特征的预处理。例如,在我们的案例中,我们的分类特征没有大量可能的值——每个特征只有少数几个值(例如,特征 "FBS" 的值为 10),并且所有可能的值都在训练集中表示。因此,我们不需要为这些特征保留一个表示“词汇外”值的索引——这本会是默认的行为。下面,我们只需在每个特征中指定 num_oov_indices=0,以告诉特征预处理器跳过“词汇外”索引。

您可以访问的其他自定义选项包括为类型为 "float_discretized" 的特征指定离散化的区间数量,或为特征交叉指定哈希空间的维度。

feature_space = FeatureSpace(
    features={
        # 以整数编码的分类特征
        "sex": FeatureSpace.integer_categorical(num_oov_indices=0),
        "cp": FeatureSpace.integer_categorical(num_oov_indices=0),
        "fbs": FeatureSpace.integer_categorical(num_oov_indices=0),
        "restecg": FeatureSpace.integer_categorical(num_oov_indices=0),
        "exang": FeatureSpace.integer_categorical(num_oov_indices=0),
        "ca": FeatureSpace.integer_categorical(num_oov_indices=0),
        # 以字符串编码的分类特征
        "thal": FeatureSpace.string_categorical(num_oov_indices=0),
        # 需要离散化的数值特征
        "age": FeatureSpace.float_discretized(num_bins=30),
        # 需要标准化的数值特征
        "trestbps": FeatureSpace.float_normalized(),
        "chol": FeatureSpace.float_normalized(),
        "thalach": FeatureSpace.float_normalized(),
        "oldpeak": FeatureSpace.float_normalized(),
        "slope": FeatureSpace.float_normalized(),
    },
    # 用自定义交叉维度指定特征交叉
    crosses=[
        FeatureSpace.cross(feature_names=("sex", "age"), crossing_dim=64),
        FeatureSpace.cross(
            feature_names=("thal", "ca"),
            crossing_dim=16,
        ),
    ],
    output_mode="concat",
)

FeatureSpace 适配到训练数据

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

  • 为分类特征索引可能值的集合。
  • 计算以便标准化的数值特征的均值和方差。
  • 为离散化数值特征计算不同区间的值边界。

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

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

此时,FeatureSpace 可以在原始特征值字典上被调用,并将为每个样本返回一个单一的连接向量,组合编码特征和特征交叉。

for x, _ in train_ds.take(1):
    preprocessed_x = feature_space(x)
    print("preprocessed_x.shape:", preprocessed_x.shape)
    print("preprocessed_x.dtype:", preprocessed_x.dtype)
preprocessed_x.shape: (32, 138)
preprocessed_x.dtype: <dtype: 'float32'>

管理预处理的两种方式:作为 tf.data 管道的一部分,或在模型中进行

您可以利用 FeatureSpace 的两种方式:

tf.data 中进行异步预处理

您可以将其作为数据管道的一部分,放在模型之前。这使得数据在到达模型之前可以在 CPU 上异步并行预处理。如果您在 GPU 或 TPU 上训练,或者希望加快预处理速度,请这样做。通常,这在训练期间总是正确的做法。

在模型中进行同步预处理

您可以将其作为模型的一部分。这意味着模型将期望原始特征值的字典,并且预处理将以同步(阻塞)方式在其余前向传播之前完成。如果您希望有一个能够处理原始特征值的端到端模型,请这样做——但请记住,您的模型将只能在 CPU 上运行,因为大多数类型的特征预处理(例如字符串预处理)不兼容 GPU 或 TPU。

在 GPU / TPU 上或在性能敏感设置中不要这样做。一般来说,当您在 CPU 上进行推理时,您希望进行模型内的预处理。

在我们的案例中,我们将在训练期间在 tf.data 管道中应用 FeatureSpace,但我们将使用包含 FeatureSpace 的端到端模型进行推理。

让我们创建一组预处理批次的训练和验证数据集:

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

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

构建模型

是时候构建一个模型——更确切地说是两个模型:

  • 期望经过预处理特征的训练模型(一个样本 = 一个向量)
  • 期望原始特征的推理模型(一个样本 = 一组原始特征值的字典)
dict_inputs = feature_space.get_inputs()
encoded_features = feature_space.get_encoded_features()

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

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

inference_model = keras.Model(inputs=dict_inputs, outputs=predictions)

训练模型

让我们训练我们的模型 50 个周期。请注意,特征预处理作为 tf.data 管道的一部分发生,而不是作为模型的一部分。

training_model.fit(
    preprocessed_train_ds,
    epochs=20,
    validation_data=preprocessed_val_ds,
    verbose=2,
)
Epoch 1/20
8/8 - 3s - 352ms/step - accuracy: 0.5200 - loss: 0.7407 - val_accuracy: 0.6196 - val_loss: 0.6663
Epoch 2/20
8/8 - 0s - 20ms/step - accuracy: 0.5881 - loss: 0.6874 - val_accuracy: 0.7732 - val_loss: 0.6015
Epoch 3/20
8/8 - 0s - 19ms/step - accuracy: 0.6580 - loss: 0.6192 - val_accuracy: 0.7839 - val_loss: 0.5577
Epoch 4/20
8/8 - 0s - 19ms/step - accuracy: 0.7096 - loss: 0.5721 - val_accuracy: 0.7856 - val_loss: 0.5200
Epoch 5/20
8/8 - 0s - 18ms/step - accuracy: 0.7292 - loss: 0.5553 - val_accuracy: 0.7764 - val_loss: 0.4853
Epoch 6/20
8/8 - 0s - 19ms/step - accuracy: 0.7561 - loss: 0.5103 - val_accuracy: 0.7732 - val_loss: 0.4627
Epoch 7/20
8/8 - 0s - 19ms/step - accuracy: 0.7231 - loss: 0.5374 - val_accuracy: 0.7764 - val_loss: 0.4413
Epoch 8/20
8/8 - 0s - 19ms/step - accuracy: 0.7769 - loss: 0.4564 - val_accuracy: 0.7683 - val_loss: 0.4320
Epoch 9/20
8/8 - 0s - 18ms/step - accuracy: 0.7769 - loss: 0.4324 - val_accuracy: 0.7856 - val_loss: 0.4191
Epoch 10/20
8/8 - 0s - 19ms/step - accuracy: 0.7778 - loss: 0.4340 - val_accuracy: 0.7888 - val_loss: 0.4084
Epoch 11/20
8/8 - 0s - 19ms/step - accuracy: 0.7760 - loss: 0.4124 - val_accuracy: 0.7716 - val_loss: 0.3977
Epoch 12/20
8/8 - 0s - 19ms/step - accuracy: 0.7964 - loss: 0.4125 - val_accuracy: 0.7667 - val_loss: 0.3959
Epoch 13/20
8/8 - 0s - 18ms/step - accuracy: 0.8051 - loss: 0.3979 - val_accuracy: 0.7856 - val_loss: 0.3891
Epoch 14/20
8/8 - 0s - 19ms/step - accuracy: 0.8043 - loss: 0.3891 - val_accuracy: 0.7856 - val_loss: 0.3840
Epoch 15/20
8/8 - 0s - 18ms/step - accuracy: 0.8633 - loss: 0.3571 - val_accuracy: 0.7872 - val_loss: 0.3764
Epoch 16/20
8/8 - 0s - 19ms/step - accuracy: 0.8728 - loss: 0.3548 - val_accuracy: 0.7888 - val_loss: 0.3699
Epoch 17/20
8/8 - 0s - 19ms/step - accuracy: 0.8698 - loss: 0.3171 - val_accuracy: 0.7872 - val_loss: 0.3727
Epoch 18/20
8/8 - 0s - 18ms/step - accuracy: 0.8529 - loss: 0.3454 - val_accuracy: 0.7904 - val_loss: 0.3669
Epoch 19/20
8/8 - 0s - 17ms/step - accuracy: 0.8589 - loss: 0.3359 - val_accuracy: 0.7980 - val_loss: 0.3770
Epoch 20/20
8/8 - 0s - 17ms/step - accuracy: 0.8455 - loss: 0.3113 - val_accuracy: 0.8044 - val_loss: 0.3684

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

我们很快达到了 80% 的验证准确率。


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

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

sample = {
    "age": 60,
    "sex": 1,
    "cp": 1,
    "trestbps": 145,
    "chol": 233,
    "fbs": 1,
    "restecg": 2,
    "thalach": 150,
    "exang": 0,
    "oldpeak": 2.3,
    "slope": 3,
    "ca": 0,
    "thal": "fixed",
}

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

print(
    f"这个患者的心脏病概率为 {100 * predictions[0][0]:.2f}%,"
    "这是我们模型评估的结果。"
)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 273ms/step
这个患者的心脏病概率为 43.13%,这是我们模型评估的结果。