作者: fchollet
创建时间 2022/11/09
上次修改时间 2022/11/09
描述:用几行代码对表格数据进行分类。
本示例演示了如何从原始 CSV 文件开始进行结构化数据分类(也称为表格数据分类)。我们的数据包括数值特征、整数类别特征和字符串类别特征。我们将使用实用程序 keras.utils.FeatureSpace
来索引、预处理和编码我们的特征。
代码改编自示例 从头开始进行结构化数据分类。虽然之前的示例使用 Keras 预处理层管理其自身的低级特征预处理和编码,但在本示例中,我们将所有操作委托给 FeatureSpace
,从而使工作流程变得极其快速和简单。
我们的数据集 由克利夫兰诊所基金会提供,用于心脏病研究。它是一个包含 303 行的 CSV 文件。每一行包含有关患者的信息(一个样本),每一列描述患者的一个属性(一个特征)。我们使用这些特征来预测患者是否患有心脏病(二元分类)。
以下是每个特征的描述
列 | 描述 | 特征类型 |
---|---|---|
年龄 | 年龄(岁) | 数值型 |
性别 | (1 = 男性;0 = 女性) | 类别型 |
CP | 胸痛类型(0、1、2、3、4) | 类别型 |
Trestbpd | 静息血压(入院时以毫米汞柱为单位) | 数值型 |
Chol | 血清胆固醇(毫克/分升) | 数值型 |
FBS | 空腹血糖是否高于 120 毫克/分升(1 = 是;0 = 否) | 类别型 |
RestECG | 静息心电图结果(0、1、2) | 类别型 |
Thalach | 达到的最大心率 | 数值型 |
Exang | 运动诱发心绞痛(1 = 是;0 = 否) | 类别型 |
Oldpeak | 运动时相对于静息时的 ST 段压低 | 数值型 |
Slope | 峰值运动 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()
年龄 | 性别 | cp | trestbps | chol | fbs | restecg | thalach | exang | oldpeak | slope | ca | thal | 目标 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 63 | 1 | 1 | 145 | 233 | 1 | 2 | 150 | 0 | 2.3 | 3 | 0 | 固定 | 0 |
1 | 67 | 1 | 4 | 160 | 286 | 0 | 2 | 108 | 1 | 1.5 | 2 | 3 | 正常 | 1 |
2 | 67 | 1 | 4 | 120 | 229 | 0 | 2 | 129 | 1 | 2.6 | 2 | 2 | 可逆 | 0 |
3 | 37 | 1 | 3 | 130 | 250 | 0 | 0 | 187 | 0 | 3.5 | 3 | 0 | 正常 | 0 |
4 | 41 | 0 | 2 | 130 | 204 | 0 | 2 | 172 | 0 | 1.4 | 1 | 0 | 正常 | 0 |
最后一列“目标”表示患者是否患有心脏病(1)或没有(0)。
让我们将数据分成训练集和验证集
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)
每个 Dataset
生成一个元组 (输入,目标)
,其中 输入
是特征字典,目标
是值 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)
FeatureSpace
要配置每个特征应如何进行预处理,我们实例化一个 keras.utils.FeatureSpace
,并将一个字典传递给它,该字典将我们的特征名称映射到描述特征类型的字符串。
我们有一些“整数类别”特征,例如 "FBS"
,一个“字符串类别”特征("thal"
),以及一些数值特征,我们希望对其进行归一化——除了 "age"
,我们希望将其离散化为多个区间。
我们还使用 crosses
参数来捕获某些类别特征的特征交互,也就是说,创建表示这些类别特征的值共现的附加特征。您可以针对任意类别特征集(而不仅仅是两个特征的元组)计算特征交叉。由于生成的共现被哈希到固定大小的向量中,因此您无需担心共现空间是否太大。
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",
)
FeatureSpace
通过字符串名称指定特征类型既快速又简单,但有时您可能希望进一步配置每个特征的预处理。例如,在我们的案例中,我们的类别特征没有大量可能的取值——每个特征只有少数几个值(例如,特征 "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",
)
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 个 epoch。请注意,特征预处理是作为 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"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.