结构数据分类
这篇教程将演示如何对结构数据(例如 CSV 中的表格数据)进行分类。我们将使用 Keras 定义模型,用特征列(feature columns) 将 CSV 中的列映射为特征来训练模型。教程包含完整代码:
- 使用 Pandas 载入 CSV 文件。
- 使用 tf.data 建立输入管道来创建批量并打乱数据。
- 使用特征列将 CSV 中的列映射为用于训练模型的特征。
- 建立,训练,评估模型。
数据集
我们将使用克利夫兰医疗基金会提供的 小数据集。CSV 文件中有几百行,每一行代表一个患者,每一列代表一个属性。我们使用这些信息来预测一个患者是否患有心脏病,这是一个二分类任务。
下表是数据集的信息。注意这里有数值和类别列。
列 | 描述 | 特征类型 | 数据类型 |
---|---|---|---|
Age | 年龄,以年表示 | 数值 | 整型 |
Sex | (1 = 男性;0 = 女性) | 类别 | 整型 |
CP | 胸部疼痛类别(0, 1, 2, 3, 4) | 类别 | 整型 |
Trestbpd | 静息血压(入院时记录,mmHg) | 数值 | 整型 |
Chol | 血清胆固醇,mg/dl | 数值 | 整型 |
FBS | 空腹血糖 > 120 mg/dl(1 = true;0 = false) | 类别 | 整型 |
RestECG | 静息心电图结果(0,1,2) | 类别 | 整型 |
Thalach | 最大心率 | 数值 | 整型 |
Exang | 运动诱发心绞痛(1 = yes;0 = no) | 类别 | 整型 |
Oldpeak | 运动相对于休息引起的 ST 抑制 | 数值 | 整型 |
Slope | 运动峰值 ST 段的斜率 | 数值 | 浮点 |
CA | 荧光染色主要血管的数目(0-3) | 数值 | 整型 |
Thal | 3 = 正常;6 = 固定性缺陷;7 = 可逆转缺陷 | 类别 | 字符串 |
Target | 心脏病诊断(1 = true;0 = false) | 分类 | 整型 |
导入 TensorFlow 和其他库
!pip install -q tensorflow==2.0.0-alpha0
!pip install -q sklearn
from __future__ import absolute_import, division, print_function
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split
使用 Pandas 创建 dataframe
Pandas 有很多实用功能,能够处理结构数据。我们将使用 Pandas 从 URL 下载数据集,并加载为一个 dataframe。
URL = 'https://storage.googleapis.com/applied-dl/heart.csv'
dataframe = pd.read_csv(URL)
dataframe.head()
将 dataframe 分为训练、验证和测试集
我们下载的是一个单个的 CSV 文件,将把它们分成训练、验证和测试集。
train, test = train_test_split(dataframe, test_size=0.2)
train, val = train_test_split(train, test_size=0.2)
print(len(train), 'train examples')
print(len(val), 'validation examples')
print(len(test), 'test examples')
193 train examples
49 validation examples
61 test examples
使用 tf.data 创建输入管道
下一步,我们将使用 tf.data 将 dataframe 包装,这将使我们能够使用 feature column 将 dataframe 中的列映射为特征。如果要处理一个非常大的 CSV 文件(大到不能存入内存),我们可以使用 tf.data 直接从硬盘中读取。
# 从 dataframe 创建一个 tf.data 数据集
def df_to_dataset(dataframe, shuffle=True, batch_size=32):
dataframe = dataframe.copy()
labels = dataframe.pop('target')
ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
if shuffle:
ds = ds.shuffle(buffer_size=len(dataframe))
ds = ds.batch(batch_size)
return ds
batch_size = 5 # 用于演示目的,所以使用小批量
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)
理解输入管道
既然已经创建了输入管道,就让我们调用一下来看看返回的数据类型。我们使用了小批量来保证输出可读。
for feature_batch, label_batch in train_ds.take(1):
print('Every feature:', list(feature_batch.keys()))
print('A batch of ages:', feature_batch['age'])
print('A batch of targets:', label_batch )
Every feature: ['cp', 'chol', 'slope', 'thal', 'trestbps', 'age', 'ca', 'oldpeak', 'exang', 'sex', 'restecg', 'fbs', 'thalach']
A batch of ages: tf.Tensor([55 61 41 41 60], shape=(5,), dtype=int32)
A batch of targets: tf.Tensor([1 0 0 0 0], shape=(5,), dtype=int32)
我们可以看到数据集返回一个列名字典 (来自dataframe),它映射到 dataframe 中的行中的列值。
演示特征列的类型
TensorFlow 提供许多特征列的类型。在这一节,我们将创建不同类别的特征列,来演示如何从 dataframe 中转换一个列。
# 我们将使用这个批量来演示不同类别的特征列
example_batch = next(iter(train_ds))[0]
# 创建一个特征列,并转换为一个批量数据
def demo(feature_column):
feature_layer = layers.DenseFeatures(feature_column)
print(feature_layer(example_batch).numpy())
数值列
特征列的输出是模型的输入,一个 数值列 是最简单的类型,它用于表示实数特征,当使用该列时,模型将会接受来自 dataframe 的列值。
age = feature_column.numeric_column("age")
demo(age)
[[55.]
[61.]
[41.]
[41.]
[60.]]
在心脏病数据集中,大多数列都是数值型。
分桶列
通常,你不会直接向模型馈送数字,而是根据数值范围将值分为不同类别。考虑原始数据中人们的年龄,使用 分桶列 将年龄分为多个桶,而不是直接使用数值列。注意下面的 one-hot 值描述了每行匹配的年龄范围。
age_buckets = feature_column.bucketized_column(age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])
demo(age_buckets)
[[0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]]
类别列
在这个数据集中,thal 表示为一个字符串(例如:‘fixed’,‘normal’ 或 ‘reversible’)。我们不能直接将字符串送入模型,必须先将它们映射为数值。分类词汇列提供了一种将字符串表示为 one-hot 向量的方法。词汇可以使用 categorical_column_with_vocabulary_list 作为列表传递,或者从使用 categorical_column_with_vocabulary_file 从文件载入。
thal = feature_column.categorical_column_with_vocabulary_list(
'thal', ['fixed', 'normal', 'reversible'])
thal_one_hot = feature_column.indicator_column(thal)
demo(thal_one_hot)
[[0. 0. 1.]
[0. 1. 0.]
[0. 0. 1.]
[1. 0. 0.]
[0. 0. 1.]]
在更复杂的数据集中,许多列都可能是类别(例如字符串)。在处理分类数据时,特性列是最有价值的。虽然在这个数据集中只有一个类别列,但是我们将使用它来演示在处理其他数据集时可以使用的几种重要类型的特性列。
嵌入列
假设我们的每个类别有上千(或更多)个值,由于许多原因,随着类别数量的增加,使用 one-hot 编码训练神经网络变得不可行。我们可以使用嵌入列来克服这个限制。嵌入列并非将数据表示为 one-hot 向量,而是将数据表示为低维、密集的向量,每个单元包含任意数字,不只是 0 或 1。嵌入的大小(下例中为 8)是一个必须调节的参数。
关键点:当一个类别列有许多可能的值时使用嵌入列是最好的,我们在这里使用一个用于演示目的,所以你有一个完整的示例,可以在将来针对不同的数据集进行修改。
# 注意嵌入列的输入是我们前面创建的类别列
thal_embedding = feature_column.embedding_column(thal, dimension=8)
demo(thal_embedding)
[[-0.27953956 0.33528438 0.3088762 -0.38723955 -0.5462264 0.1547148
0.04031862 -0.07778737]
[-0.01976893 0.30975693 -0.28382677 -0.4496878 -0.02439527 0.67915285
-0.6771491 -0.23376267]
[-0.27953956 0.33528438 0.3088762 -0.38723955 -0.5462264 0.1547148
0.04031862 -0.07778737]
[-0.18620498 -0.46937296 -0.27169922 -0.12982944 -0.27560276 -0.18293215
0.22800985 -0.36373004]
[-0.27953956 0.33528438 0.3088762 -0.38723955 -0.5462264 0.1547148
0.04031862 -0.07778737]]
经过哈希处理的列
另一个处理有大量数值的类别列的方法是使用 categorical_column_with_hash_bucket。特征列计算输入的一个哈希值,然后选择 hash_bucket_size
分桶之一来编码字符串。当使用这个列时,不需要提供词汇表,并且可以选择使用 hash_bucket 的数量显著小于实际类别的数量,以节省空间。
关键点:这个技巧的一个重要缺点是不同字符串映射到相同分桶时可能会有冲突。实际上,无论如何,这对于某些数据集都可以很好地工作。
thal_hashed = feature_column.categorical_column_with_hash_bucket(
'thal', hash_bucket_size=1000)
demo(feature_column.indicator_column(thal_hashed))
[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
组合特征列
将特征组合成一个特征(称为特征组合),使得模型可以学习每个特征组合单独的权重。这里,我们将创建一个由 age 和 thal 组合的新的特征。注意 crossed_column
仅构建 hash_bucket_size
参数所请求的数字,而不是构建这个可能非常庞大的输入表。
crossed_feature = feature_column.crossed_column([age_buckets, thal], hash_bucket_size=1000)
demo(feature_column.indicator_column(crossed_feature))
[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
选择使用的列
我们已经见过如何使用不同类别的特征列。现在我们将使用它们来训练模型。我们任意选择一些列来训练模型。
关键点:如果你的目标是建立一个精确的模型,尝试使用一个更大的数据集,仔细思考最有意义的特征,以及它们表示的方式。
feature_columns = []
# 数值列
for header in ['age', 'trestbps', 'chol', 'thalach', 'oldpeak', 'slope', 'ca']:
feature_columns.append(feature_column.numeric_column(header))
# 分桶列
age_buckets = feature_column.bucketized_column(age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])
feature_columns.append(age_buckets)
# 指标列
thal = feature_column.categorical_column_with_vocabulary_list(
'thal', ['fixed', 'normal', 'reversible'])
thal_one_hot = feature_column.indicator_column(thal)
feature_columns.append(thal_one_hot)
# 嵌入列
thal_embedding = feature_column.embedding_column(thal, dimension=8)
feature_columns.append(thal_embedding)
# 组合列
crossed_feature = feature_column.crossed_column([age_buckets, thal], hash_bucket_size=1000)
crossed_feature = feature_column.indicator_column(crossed_feature)
feature_columns.append(crossed_feature)
创建一个特征层
既然定义好了特征列,我们将使用 DenseFeatures 层来讲它们输入模型。
feature_layer = tf.keras.layers.DenseFeatures(feature_columns)
先前,我们使用小批量来演示特征列如何工作。现在使用更大的批量重新创建一个输入管道。
batch_size = 32
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)
创建、编译和训练模型
model = tf.keras.Sequential([
feature_layer,
layers.Dense(128, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(1, activation='sigmoid')
])
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
model.fit(train_ds,
validation_data=val_ds,
epochs=5)
Epoch 1/5
7/7 [==============================] - 1s 82ms/step - loss: 0.8949 - accuracy: 0.6312 - val_loss: 0.7286 - val_accuracy: 0.6939
Epoch 2/5
7/7 [==============================] - 0s 38ms/step - loss: 0.5068 - accuracy: 0.7623 - val_loss: 0.7950 - val_accuracy: 0.6939
Epoch 3/5
7/7 [==============================] - 0s 35ms/step - loss: 0.6906 - accuracy: 0.7502 - val_loss: 0.4876 - val_accuracy: 0.7143
Epoch 4/5
7/7 [==============================] - 0s 34ms/step - loss: 0.6740 - accuracy: 0.7446 - val_loss: 0.6021 - val_accuracy: 0.7755
Epoch 5/5
7/7 [==============================] - 0s 35ms/step - loss: 0.4928 - accuracy: 0.7345 - val_loss: 0.4757 - val_accuracy: 0.7755
loss, accuracy = model.evaluate(test_ds)
print("Accuracy", accuracy)
2/2 [==============================] - 0s 19ms/step - loss: 0.5669 - accuracy: 0.6721
Accuracy 0.6721311
关键点:通常,使用更大更复杂的数据集进行深度学习,将看到最佳结果。当处理像这个数据集一样的小数据集时,我们建议使用决策树或者随机森林作为一个基准。本教程的目的不是训练一个精确的模型,而是演示处理结构数据的机制,因此,在将来处理自己的数据集时,可以使用这些代码作为起点。
下一步
学习对结构数据进行分类最好的方法就是自己尝试。我们建议寻找一些其他的数据集,使用跟上面相似的代码训练模型。要提升准确率,需要仔细思考模型中需要包含哪些特征,以及表示的形式。