特征列
本篇文档将详细介绍特征列。特征列可以视为原始数据和 Estimator 间的中介。特征列非常丰富,可以让你将各种不同的原始数据转化为 Estimator 可用的格式,从而轻松的进行实验。
tf.feature_column.numeric_column
现实世界中,一些的特征(例如经度)是数字的,但是很多(特征)并不是(数字的)。
输入至深度神经网络
深度神经网络可以操作什么样的数据?答案当然是数字(例如,tf.float32
)。毕竟,神经网络的每个神经元都会对权重和输入值进行乘法和加法操作。但是,现实中的输入数据经常包含非数字的(分类的)数据。例如,一个 product_class
特征就可能包含如下三个非数字的值:
- 厨具
- 电子产品
- 运动产品
机器学习模型通常都会以简单的向量代表分类值,在向量中,1 表示某值存在,0 表示值不存在。例如,当 product_class
运动产品的集合时,一个机器学习模型通常会以 [0, 0, 1]
表示 product_class
,含义是:
0
: 厨具不存在0
: 电子产品不存在1
: 运动产品存在
所以,尽管原始数据可能是数字或者分类,机器学习模型都要以数字表示所有的特征。
特征列
如下图所示,您可以通过 Estimator(鸢尾花模型使用了 DNNClassifier
)的 feature_columns
参数指定模型的输入。特征列桥接输入数据(由 input_fn
返回的数据)和模型。
特征列将原始数据和模型需要的数据桥接起来。
特征列方法分为两个主类和一个混合类。
下面让我们更具体的了解这几个方法。
数值列
tf.feature_column.numeric_column
SepalLength
(萼片长度)SepalWidth
(萼片宽度)PetalLength
(花瓣长度)PetalWidth
(花瓣宽度)
虽然 tf.numeric_column
提供了可选的参数,但是像如下所示这样,不带任何参数的调用它却是一个不错的方式,这样可以用默认的数据类型(tf.float32
)来指定模型输入的数值。
# 默认为 tf.float32 标量。
numeric_feature_column = tf.feature_column.numeric_column(key="SepalLength")
如果想指定非默认的数据类型,可以定义 dtype
参数。例如:
# 代表 tf.float64 标量。
numeric_feature_column = tf.feature_column.numeric_column(key="SepalLength",
dtype=tf.float64)
默认情况下,一个数值列仅创建一个值(标量)。使用 shape 参数来定义数据维度。例如:
```python # 代表一个 10 元素的向量,每个元素中包含一个 tf.float32 类型的值。 vector_feature_column = tf.feature_column.numeric_column(key="Bowling", shape=10) # 代表一个 10x5 矩阵,矩阵中每个元素中包含一个 tf.float32 类型的值。 matrix_feature_column = tf.feature_column.numeric_column(key="MyMatrix", shape=[10,5]) ``` ### 分桶列 通常情况下,我们不希望直接将数值传入模型,而是根据取值范围将数值放进不同的类别中。可以通过创建 @{tf.feature_column.bucketized_column$bucketized column} 完成上述功能。例如,考虑一组表示房屋建成年份原始数据。我们应该将年份放入 4 个不同的 buckets 中,而不是把每一个年份数值都作为一个标量数值列:




def make_dataset(latitude, longitude, labels):
assert latitude.shape == longitude.shape == labels.shape
features = {'latitude': latitude.flatten(),
'longitude': longitude.flatten()}
labels=labels.flatten()
return tf.data.Dataset.from_tensor_slices((features, labels))
# 使用 `edges` 属性将经度和纬度分桶
latitude_bucket_fc = tf.feature_column.bucketized_column(
tf.feature_column.numeric_column('latitude'),
list(atlanta.latitude.edges))
longitude_bucket_fc = tf.feature_column.bucketized_column(
tf.feature_column.numeric_column('longitude'),
list(atlanta.longitude.edges))
# 使用 5000 个哈希箱将分桶列 bucketized columns 交叉。
crossed_lat_lon_fc = tf.feature_column.crossed_column(
[latitude_bucket_fc, longitude_bucket_fc], 5000)
fc = [
latitude_bucket_fc,
longitude_bucket_fc,
crossed_lat_lon_fc]
# 建立并训练 Estimator。
est = tf.estimator.LinearRegressor(fc, ...)
你可以采用如下方法中的任何一个来创建一个交叉特征:
- 特征名;也就是,从
input_fn
返回dict
中的名字。 - 任意一个分类列,除了
categorical_column_with_hash_bucket
(因为crossed_column
散列了输入)。
特征列 latitude_bucket_fc
和 longitude_bucket_fc
交叉后,TensorFlow 将会为每个样本创建数据对 (latitude_fc, longitude_fc)
。这将会产生如下这样的全量概率网格:
(0,0), (0,1)... (0,99)
(1,0), (1,1)... (1,99)
... ... ...
(99,0), (99,1)...(99, 99)
但不足是,完整的网格只适用于有限词汇表的输入。与此相比,crossed_column
仅创建 hash_bucket_size
参数规定的数字,而不是创建上面所示这样可能会很大的输入表。特征列通过在输入元组上运行哈希函数,为每个索引分配一个样本,接下来用 hash_bucket_size
进行模运算。
像我们前面讨论过的那样,运行哈希和模函数可以限制分类的数目,但是可能导致类别冲突;也就是,多个(纬度,经度)交叉特征将会最终属于同一个哈希桶。尽管如此,在实际应用中,采用特征交叉仍旧可以为模型的学习能力显著加分。
有点违背直觉的是,当创建交叉特征的时候一般仍旧需要在模型中包括原始(未交叉)特征(如前面的代码片段所示)。独立的经度和纬度信息帮助模型在交叉特征出现哈希碰撞的时候区分不同样本。
指针和嵌入列
指针列和嵌入列不直接作用于特征,它们将分类列作为输入值。
使用指针列时,我们就是在告诉 TensorFlow 去做分类产品样本一样的事。也就是,指针列将每一个类别当作一个独热向量的元素,这个向量中与结果匹配的类是 1,其他都是 0。
在指针列中表示数据。
tf.feature_column.indicator_column
categorical_column = ... # 创建一个任意类别的分类列。
# 将分类列表示为指针列。
indicator_column = tf.feature_column.indicator_column(categorical_column)
现在假设并不只有三个可能的类,而是有一百万个,甚至十亿个。出于很多原因,当类别的数目增长到很大的时候,使用指针类来训练神经网络就变的不可行了。
我们可以用嵌入列来克服这个限制。嵌入列用低维的普通矢量,而非多维度的独热向量来表示数据。普通矢量中每个元素可以包含任何数值,而不仅是 0 和 1。通过为每个元素提供更丰富的数字候选,嵌入列包含的元素数量远少于指标列。
让我们来看一个对比指针列和嵌入列的例子。假设输入样本包括来自仅有 81 个单词的有限候选集中的不同单词。进一步假设,数据集在 4 个单独的样本中提供如下的输入词:
"狗"
"勺子"
"剪刀"
"吉他"
在这个例子中,如下图所示说明了嵌入列和指针列的处理途径。
相比于指针列,嵌入列用维度较低的向量来存储分类数据。(我们只是将随机数放入了嵌入向量;训练决定真实的数字)
当一个样本被处理时,其中一个 categorical_column_with...
函数将样本字符串映射为数字的分类值。例如,一个方法将“勺子”映射为 [32]
。(这里的 32 是我们假想的 - 实际值取决于映射函数。)接下来,你可以用如下两种方式来表示这些数字分类值:
指针列。一个将每个数字分类值转化为 81 元素向量的方法(因为候选集包含 81 个单词),将一个 1 放置在分类值的索引 (0, 32, 79, 80) 上,其余位置都是 0。
嵌入列。一个使用数字分类值
(0, 32, 79, 80)
作为查找表索引的方法。在这个查找表里,每个元素包含一个三元素向量。
嵌入向量中的值是如何魔法般的被分配的呢?实际上,分配发生在训练期间。也就是,模型为了解决你的问题,学习了将输入的数字分类值映射为嵌入向量的最好方式。嵌入列提升了你的模型的能力,因为嵌入向量从训练数据中学到了分类之间的新的关系。
为什么在我们的实例中,嵌入向量的大小是 3?下面的“公式”提供了关于嵌入维数的一般规则
embedding_dimensions = number_of_categories**0.25
也就是,嵌入向量的维度应该是分类数量的开四次根号。既然本例中,词汇的大小是 81,那么推荐的维度数就是3。
3 = 81**0.25
注意这仅是一个一般准则,你可以将嵌入的维度设置为任何你喜欢的值。
tf.feature_column.embedding_column
categorical_column = ... # 创建任意的分类列
# 将分类列表示为嵌入列
# 这意味着要为每一个分类创建一个包含一个元素的独热向量
embedding_column = tf.feature_column.embedding_column(
categorical_column=categorical_column,
dimension=dimension_of_embedding_vector)
将特征列传递给 Estimator
如下列表所示,并不是所有 Estimator 都允许 feature_columns
参数的所有类别:
tf.estimator.LinearRegressor
tf.estimator.DNNRegressor
tf.estimator.DNNLinearCombinedRegressor
linear_feature_columns
参数接受所有特征列类型。dnn_feature_columns
参数只接受稠密列。
其他知识源
更多特征列的实例,参见如下:
学习更多和嵌入相关的知识,参见如下:
- Deep Learning, NLP, and representations
(Chris Olah 的博客) - The TensorFlow Embedding Projector