目录

简介

数据集

设置

准备数据

配置特征空间

进一步自定义特征空间

根据训练数据调整特征空间

制作模型

训练模型

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


本文目标 :只需几行代码就能对表格数据进行分类。

简介


本例演示了如何从原始 CSV 文件开始进行结构化数据分类(也称为表格数据分类)。

我们的数据包括数字特征、整数分类特征和字符串分类特征。

我们将使用实用程序 keras.utils.FeatureSpace 对特征进行索引、预处理和编码。

代码改编自从零开始的结构化数据分类示例。那一个示例使用 Keras 预处理层管理自己的底层特征预处理和编码,而在本示例中,我们将一切委托给 FeatureSpace,使工作流程变得极其快速和简单。

数据集


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

以下是各项功能的说明:

设置

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()

最后一栏 "目标 "表示患者是否患有心脏病(1)。

让我们把数据分成训练集和验证集:

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

print(
    "Using %d samples for training and %d for validation"
    % (len(train_dataframe), len(val_dataframe))
)
Using 242 samples for training and 61 for validation

让我们为每个数据帧生成 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)

每个数据集都会产生一个元组(输入、目标),其中输入是一个特征字典,目标是 0 或 1 的值:

for x, y in train_ds.take(1):
    print("Input:", x)
    print("Target:", y)
Input: {'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'normal'>}
Target: tf.Tensor(0, shape=(), dtype=int64)

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

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

配置特征空间


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

我们有一些 "整数分类 "特征,如 "FBS",一个 "字符串分类 "特征("thal"),以及一些数字特征,我们希望对这些特征进行归一化处理,但 "年龄 "除外,我们希望将其离散化为若干个分区。

我们还使用交叉参数来捕捉某些分类特征的特征交互,也就是说,创建额外的特征来表示这些分类特征的值共存。您可以计算任意分类特征集的交叉特征,而不仅仅是两个特征的元组。由于生成的共现值会散列到一个固定大小的向量中,因此您无需担心共现值空间是否过大。

feature_space = FeatureSpace(
    features={
        # Categorical features encoded as integers
        "sex": "integer_categorical",
        "cp": "integer_categorical",
        "fbs": "integer_categorical",
        "restecg": "integer_categorical",
        "exang": "integer_categorical",
        "ca": "integer_categorical",
        # Categorical feature encoded as string
        "thal": "string_categorical",
        # Numerical features to discretize
        "age": "float_discretized",
        # Numerical features to normalize
        "trestbps": "float_normalized",
        "chol": "float_normalized",
        "thalach": "float_normalized",
        "oldpeak": "float_normalized",
        "slope": "float_normalized",
    },
    # We create additional features by hashing
    # value co-occurrences for the
    # following groups of categorical features.
    crosses=[("sex", "age"), ("thal", "ca")],
    # The hashing space for these co-occurrences
    # wil be 32-dimensional.
    crossing_dim=32,
    # Our utility will one-hot encode all categorical
    # features and concat all features into a single
    # vector (one vector per sample).
    output_mode="concat",
)

进一步自定义特征空间

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

您还可以进行其他自定义设置,包括指定用于离散化 "float_discretized "类型特征的分段数,或用于特征交叉的散列空间维度。

feature_space = FeatureSpace(
    features={
        # Categorical features encoded as integers
        "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),
        # Categorical feature encoded as string
        "thal": FeatureSpace.string_categorical(num_oov_indices=0),
        # Numerical features to discretize
        "age": FeatureSpace.float_discretized(num_bins=30),
        # Numerical features to normalize
        "trestbps": FeatureSpace.float_normalized(),
        "chol": FeatureSpace.float_normalized(),
        "thalach": FeatureSpace.float_normalized(),
        "oldpeak": FeatureSpace.float_normalized(),
        "slope": FeatureSpace.float_normalized(),
    },
    # Specify feature cross with a custom crossing dim.
    crosses=[
        FeatureSpace.cross(feature_names=("sex", "age"), crossing_dim=64),
        FeatureSpace.cross(
            feature_names=("thal", "ca"),
            crossing_dim=16,
        ),
    ],
    output_mode="concat",
)

根据训练数据调整特征空间


请注意,adapt() 应在 tf.data.Dataset 上调用,因为该数据集会生成特征值的二进制数(无标签)。

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

此时,可以调用原始特征值的特征空间,它将为每个样本返回一个单一的串联向量,将编码特征和特征交叉结合起来。

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 管道的一部分,或在模型本身中进行预处理

您可以通过两种方式利用您的 "特色空间":

tf.data 中的异步预处理

您可以将其作为数据管道的一部分,置于模型之前。这样就能在数据进入模型之前,在 CPU 上对数据进行异步并行预处理。如果您在 GPU 或 TPU 上进行训练,或者您想加快预处理速度,可以这样做。通常,在训练过程中这样做总是正确的。

模型中的同步预处理

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

请勿在 GPU / TPU 或对性能敏感的设置中执行此操作。一般来说,在 CPU 上进行推理时,应在模型内进行预处理。

在我们的例子中,我们将在训练过程中在 tf.data 管道中应用特征空间,但我们将使用包含特征空间的端到端模型进行推理。

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

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% 的验证准确率。

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


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

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"This particular patient had a {100 * predictions[0][0]:.2f}% probability "
    "of having a heart disease, as evaluated by our model."
)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 273ms/step
This particular patient had a 43.13% probability of having a heart disease, as evaluated by our model.

 政安晨:【Keras机器学习示例演绎】(四十九)—— 利用 KerasNLP 实现语义相似性-LMLPHP

05-24 20:17