用TensorFlow框架搭建神经网络已经是大众所知的事情。今天我们来聊一聊如何用TensorFlow 对数据进行特征工程处理。
在TensorFlow中还有一些不被大家熟知的数据处理API。这些API与TensorFlow框架结合紧密,使用方便。用这些API做数据前期的特征处理,可以提高效率。
一、接口介绍
TensorFlow使用特征列接口来进行数据特征工程的处理。框架中一共包含有两个特征列接口:特征列接口和序列特征列接口。
1.了解特征列接口
特征列(tf.feature_column)接口是TensorFlow中专门用于处理特征工程的高级API。用tf.feature_column接口可以很方便地对输入数据进行特征转化。
特征列就像是原始数据与估算器之间的中介,它可以将输入数据转化成需要的特征样式,以便传入模型进行训练。
2.了解序列特征列接口
序列特征列接口(tf.contrib.feature_column.sequence_feature_column)是TensorFlow中专门用于处理序列特征工程的高级API。它是在tf.feature_column接口之上的又一次封装。该API目前还在contrib模块中,未来有可能被移植到主版本中。
在序列任务中,使用序列特征列接口(sequence_feature_column)会大大减少程序的开发量。
在序列特征列接口中一共包含以下几个函数。
提示:
在TensorFlow 2.x版本中没有contrib模块。所以sequence_feature_column可以TensorFlow 1.x版本的使用。
二、实例演示
TensorFlow的特征列接口几乎可以涵盖于特征工程相关的所有转化操作,使用起来非常容易,下面通过介个例子来演示一下:
1.代码实现:用feature_column模块处理连续值特征列
连续值类型是TensorFlow中最简单、最常见的特征列数据类型。本实例通过4个小例子演示连续值特征列常见的使用方法。
1.显示一个连续值特征列
编写代码定义函数test_one_column。在test_one_column函数中具体完成了以下步骤:
(1)定义一个特征列。
(2)将带输入的样本数据封装成字典类型的对象。
(3)将特征列与样本数据一起传入tf.feature_column.input_layer函数,生成张量。
(4)建立会话,输出张量结果。
在第(3)步中用feature_column接口的input_layer函数生成张量。input_layer函数生成的张量相当于一个输入层,用于往模型中传入具体数据。input_layer函数的作用与占位符定义函数tf.placeholder的作用类似,都用来建立数据与模型之间的连接。
通过这几个步骤便可以将特征列的内容完全显示出来。具体代码如下:
代码7-3 用feature_column模块处理连续值特征列
因为在创建特征列price时只提供了名称“price”(见代码第6行),所以在创建字典features时,其内部的key必须也是“price”(见代码第8行)。
定义好函数test_one_column之后,便可以直接调用它(见代码第15行)。整个代码运行之后,显示以下结果:
[[1.]
[5.]]
结果中的数组来自于代码第8行字典对象features的value值。在第8行代码中,将值为[[1.],[5.]]的数据传入了字典features中。
在字典对象features中,关键字key的值是“price”,它所对应的值value可以是任意的一个数值。在模型训练时,这些值就是“price”属性所对应的具体数据。
2.通过占位符输入特征列
将占位符传入字典对象的值value中,实现特征列的输入过程。具体代码如下:
代码7-3 用feature_column模块处理连续值特征列(续)
在代码第19行,生成了带有占位符的字典对象features。
代码第23~25行,在会话中以注入机制传入数值[[1.], [5.]],生成转换后的具体列值。
整个代码运行之后,输出以下结果:
[[1.]
[5.]]
3.支持多维数据的特征列
在创建特征列时,还可以让一个特征列对应的数据有多维,即在定义特征列时为其指定形状。
提示:
特征列中的形状是指单条数据的形状,并非整个数据的形状。
具体代码如下:
代码7-3 用feature_column模块处理连续值特征列(续)
在代码第31行,在创建price特征列时,指定了形状为[1,2],即1行2列。
接着用两种方法向price特征列注入数据(见代码第32、33行)
在代码第34、35行中,都用tf.feature_column模块的input_layer方法将字典features与features1注入特征列price中,并得到了张量net与net1。
代码运行后,张量net与net1的输出结果如下:
[[1. 2.] [5. 6.]]
[[3. 4.] [7. 8.]]
结果输出了两行数据,每一行都是一个形状为[2,2]的数组。这两个数组分别是字典features、features1经过特征列输出的结果。
提示:
代码第30行的作用是将图重置。该操作可以将当前图中的所有变量删除。这种做法可以避免在Spyder编译器下多次运行图时产生数据残留问题。
4.带有默认顺序的多个特征列
如果要创建的特征列有多个,则系统默认会按照每个列的名称由小到大进行排序,然后将数据按照约束的顺序输入模型。具体代码如下:
代码7-3 用feature_column模块处理连续值特征列(续)
在上面代码中,实现了以下操作。
(1)定义了3个特征列(见代码第42、43、44行)。
(2)定义了一个字典features,用于具体输入(见代码第46行)。
(3)用input_layer方法创建输入层张量(见代码第53行)。
(4)建立会话(session),输出输入层结果(见代码第55行)。
将程序运行后,输出以下结果:
[[1. 3. 4.]]
输出的结果为[[1. 3. 4.]]所对应的列,顺序为price_a、price_b、price_c。而input_layer中的列顺序为price_c、price_a、price_b(见代码第53行),二者并不一样。这表示,输入层的顺序是按照列的名称排序的,与input_layer中传入的顺序无关。
提示:
将input_layer中传入的顺序当作输入层的列顺序,这是一个非常容易犯的错误。
输入层的列顺序只与列的名称和类型有关(7.4.3小节“5. 多特征列的顺序”中还会讲到列顺序与列类型的关系),与传入input_layer中的顺序无关。
2.代码实现:将连续值特征列转化成离散值特征列
下面将连续值特征列转化成离散值特征列。
1.将连续值特征按照数值大小分类
用tf.feature_column.bucketized_column函数将连续值按照指定的阈值进行分段,从而将连续值映射到离散值上。具体代码如下:
代码7-4 将连续值特征列转化成离散值特征列
代码运行后,输出以下结果:
[[2. 1. 0. 0.]
[6. 0. 0. 1.]]
输出的结果中有两条数据,每条数据有4个元素:
从结果中可以看到,tf.feature_column.bucketized_column函数将连续值price按照3段来划分(小于3、3~5之间、大于5),并将它们生成one-hot编码。
2.将整数值直接映射到one-hot编码
如果连续值特征列的数据是整数,则还可以直接用tf.feature_column. categorical_column_with_identity函数将其映射成one-hot编码。
函数tf.feature_column.categorical_column_with_identity的参数和返回值解读如下。
具体代码如下:
代码7-4 将连续值特征列转化成离散值特征列(续)
代码运行后,输出以下结果:
[[2. 0. 0. 1. 0. 0. 0.]
[4. 0. 0. 0. 0. 1. 0.]]
结果输出了两行信息。每行的第1列为连续值price列内容,后面6列为one-hot编码。
因为在代码第23行,将price列转化为one-hot时传入的参数是6,代表分成6类。所以在输出结果中,one-hot编码为6列。
3.代码实现:将离散文本特征列转化为one-hot与词向量
离散型文本数据存在多种组合形式,所以无法直接将其转化成离散向量(例如,名字属性可以是任意字符串,但无法统计总类别个数)。
处理离散型文本数据需要额外的一套方法。下面具体介绍。
1.将离散文本按照指定范围散列的方法
将离散文本特征列转化为离散特征列,与将连续值特征列转化为离散特征列的方法相似,可以将离散文本分段。只不过分段的方式不是比较数值的大小,而是用hash算法进行散列。
用tf.feature_column.categorical_column_with_hash_bucket方法可以将离散文本特征按照hash算法进行散列,并将其散列结果转化成为离散值。
该方法会返回一个_HashedCategoricalColumn类型的张量。该张量属于稀疏矩阵类型,不能直接输入tf.feature_column.input_layer函数中进行结果输出,只能用稀疏矩阵的输入方法来运行结果。
具体代码如下:
代码7-5 将离散文本特征列转化为one-hot编码与词向量
本段代码运行后,会按以下步骤执行:
(1)将输入的['a']、['x']使用hash算法进行散列。
(2)设置散列参数hash_bucket_size的值为5。
(3)将第(1)步生成的结果按照参数hash_bucket_size进行散列。
(4)输出最终得到的离散值(0~4之间的整数)。
上面的代码运行后,输出以下结果:
稀疏矩阵:
SparseTensorValue(indices=array([[0, 0],
[1, 0]], dtype=int64), values=array([4, 0], dtype=int64), dense_shape=array([2, 1], dtype=int64))
稠密矩阵:
[[4]
[0]]
从最终的输出结果可以看出,程序将字符a转化为数值4;将字符b转化为数值0。
将离散文本转化成特征值后,就可以传入模型,并参与训练了。
提示:
有关稀疏矩阵的更多介绍可以参考《深度学习之TensorFlow——入门、原理与进阶实战》一书中的9.4.17小节。
2.将离散文本按照指定词表与指定范围混合散列
除用hash算法对离散文本数据进行散列外,还可以用词表的方法将离散文本数据进行散列。
用tf.feature_column.categorical_column_with_vocabulary_list方法可以将离散文本数据按照指定的词表进行散列。该方法不仅可以将离散文本数据用词表来散列,还可以与hash算法混合散列。其返回的值也是稀疏矩阵类型。同样不能将返回的值直接传入tf.feature_column.input_layer函数中,只能用“1. 将离散文本按照指定范围散列”中的方法将其显示结果。
具体代码如下:
代码7-5 将离散文本特征列转化为one-hot编码与词向量(续)
代码第29、30行向tf.feature_column.categorical_column_with_vocabulary_list方法传入了3个参数,具体意义如下所示。
提示:
tf.feature_column.categorical_column_with_vocabulary_list方法还有第4个参数:default_value,该参数默认值为1。
如果在调用tf.feature_column.categorical_column_with_vocabulary_list方法时没有传入num_oov_buckets参数,则程序将只按照词表进行分类。
在按照词表进行分类的过程中,如果name中的值在词表中找不到匹配项,则会用参数default_value来代替。
第33、38行代码,用_LazyBuilder函数构建程序的输入部分。该函数可以同时支持值为稠密矩阵和稀疏矩阵的字典对象。
运行代码,输出以下结果:
稀疏矩阵:
SparseTensorValue(indices=array([[0, 0],
[1, 0],
[2, 0]], dtype=int64), values=array([0, 1, 4], dtype=int64), dense_shape=array([3, 1], dtype=int64))
稀疏矩阵2:
SparseTensorValue(indices=array([[0, 0],
[1, 0],
[2, 0]], dtype=int64), values=array([0, 1, 4], dtype=int64), dense_shape=array([3, 1], dtype=int64))
稠密矩阵:
[[0]
[1]
[4]]
结果显示了3个矩阵:前两个是稀疏矩阵,最后一个为稠密矩阵。这3个矩阵的值是一样的。具体解读如下。
提示:
在使用词表时要引入lookup_ops模块,并且,在会话中要用lookup_ops.tables_initializer()对其进行初始化,否则程序会报错。
3.将离散文本特征列转化为one-hot编码
在实际应用中,将离散文本进行散列之后,有时还需要对散列后的结果进行二次转化。下面就来看一个将散列值转化成one-hot编码的例子。
代码7-5 将离散文本特征列转化为one-hot编码与词向量(续)
代码运行后,输出以下结果:
[[0. 0. 0. 0. 1.]
[1. 0. 0. 0. 0.]]
结果中输出了两条数据,分别代表字符“a”“x”在散列后的one-hot编码。
4.将离散文本特征列转化为词嵌入向量
词嵌入可以理解为one-hot编码的升级版。它使用多维向量更好地描述词与词之间的关系。下面就来使用代码实现词嵌入的转化。
代码7-5 将离散文本特征列转化为one-hot编码与词向量(续)
在词嵌入转化过程中,具体步骤如下:
(1)将传入的字符“a”与“x”转化为0~4之间的整数。
(2)将该整数转化为词嵌入列。
代码第91行,将数据字典features、词嵌入列embedding_col、列变量对象cols_to_vars一起传入输入层input_layer函数中,得到最终的转化结果net。
代码运行后,输出以下结果:
[[ 0.08975066 0.34540504 0.85922384]
[-0.22819372 -0.34707746 -0.76360196]]
从结果中可以看到,每个整数都被转化为3个词嵌入向量。这是因为,在调用tf.feature_column.embedding_column函数时传入的维度dimension是3(见代码第83行)。
提示:
在使用词嵌入时,系统内部会自动定义指定个数的张量作为学习参数,所以运行之前一定要对全局张量进行初始化(见代码第94行)。本实例显示的值,就是系统内部定义的张量被初始化后的结果。
另外,还可以参照《深度学习之TensorFlow工程化项目实战》一书7.5节的方式为词向量设置一个初始值。通过具体的数值可以更直观地查看词嵌入的输出内容。
5.多特征列的顺序
在大多数情况下,会将转化好的特征列统一放到input_layer函数中制作成一个输入样本。
input_layer函数支持的输入类型有以下4种:
如果要将7.4.3小节中的hash值或词表散列的值传入input_layer函数中,则需要先将其转化成indicator_column类型或embedding_column类型。
当多个类型的特征列放在一起时,系统会按照特征列的名字进行排序。
具体代码如下:
代码7-5 将离散文本特征列转化为one-hot编码与词向量(续)
上面代码中构建了3个输入的特征列:
其中,embedding_column列与indicator_column列由categorical_column_with_hash_bucket方法列转化而来(见代码第104、106行)。
代码运行后输出以下结果:
asparse_feature_indicator
asparse_feature_embedding
numeric_col
[[-1.0505784 -0.4121129 -0.85744965 0. 0. 0. 0. 1. 3.]
[-0.2486877 0.5705532 0.32346958 1. 0. 0. 0. 0. 6.]]
输出结果的前3行分别是one_hot_col列、embedding_col列与numeric_col列的名称。
输出结果的最后两行是输入层input_layer所输出的多列数据。从结果中可以看出,一共有两条数据,每条数据有9列。这9列数据可以分为以下3个部分。
这个三个部分的排列顺序与其名字的字符串排列顺序是完全一致的(名字的字符串排列顺序为 asparse_feature_embedding、asparse_feature_indicator、numeric_col)。
4.代码实现:根据特征列生成交叉列
在《深度学习之TensorFlow工程化项目实战》一书7.2节中用tf.feature_column.crossed_column函数将多个单列特征混合起来生成交叉列,并将交叉列作为新的样本特征,与原始的样本数据一起输入模型进行计算。
本小节将详细介绍交叉列的计算方式,以及函数tf.feature_column.crossed_column的使用方法。
具体代码如下:
代码7-6 根据特征列生成交叉列
代码第5行用tf.feature_column.crossed_column函数将特征列b和c混合在一起,生成交叉列。该函数有以下两个必填参数。
提示:
tf.feature_column.crossed_column函数的输入参数key是一个列表类型。该列表的元素可以是指定的列名称(字符串形式),也可以是具体的特征列对象(张量形式)。
如果传入的是特征列对象,则还要考虑特征列类型的问题。因为tf.feature_column.crossed_column函数不支持对numeric_column类型的特征列做交叉运算,所以,如果要对numeric_column类型的列做交叉运算,则需要用bucketized_column函数或categorical_column_with_identity函数将numeric_column类型转化后才能使用。
代码运行后,输出以下结果:
SparseTensorValue(indices=array([[0, 0],
[0, 1],
[1, 0],
[1, 1],
[1, 2],
[1, 3]], dtype=int64), values=array([3, 1, 3, 1, 0, 4], dtype=int64), dense_shape=array([2, 4], dtype=int64))
[[ 3 1 -1 -1] [ 3 1 0 4]]
程序运行后,交叉矩阵会将以下两矩阵进行交叉合并。具体计算方法见式(7.1):
式(7.1)中,size就是传入crossed_column函数的参数hash_bucket_size,其值为5,表示输出的结果都在0~4之间。
在生成的稀疏矩阵中,[0,2]与[0,3]这两个位置没有值,所以在将其转成稠密矩阵时需要为其加两个默认值“1”。于是在输出结果的最后1行,显示了稠密矩阵的内容[[ 3 1 -1 -1] [ 3 1 0 4]]。该内容中用两个“1”进行补位。
三、在自然语言场景下的实例演示
下面以一个NLP数据的预处理场景为例,演示一下sequence_feature_column接口的具体使用。
1.代码实现:构建模拟数据
假设有一个字典,里面只有3个词,其向量分别为0、1、2。
用稀疏矩阵模拟两个具有序列特征的数据a和b。每个数据有两个样本:模拟数据a的内容是[2][0,1]。模拟数据b的内容是[1][2,0]。
具体代码如下:
代码7-7 序列特征工程
代码第5、10行分别用tf.SparseTensor函数创建两个稀疏矩阵类型的模拟数据。
2.代码实现:构建词嵌入初始值
词嵌入过程将字典中的词向量应用到多维数组中。在代码中,定义两套用于映射词向量的多维数组(embedding_values_a与embedding_values_b),并对其进行初始化。
提示:
在实际使用中,对多维数组初始化的值,会被定义成1~1之间的浮点数。这里都将其初始化成较大的值,是为了在测试时让显示效果更加明显。
具体代码如下:
代码7-7 序列特征工程(续)
3.代码实现:构建词嵌入特征列与共享特征列
使用函数sequence_categorical_column_with_identity可以创建带有序列特征的离散列。该离散列会将词向量进行词嵌入转化,并将转化后的结果进行离散处理。
使用函数shared_embedding_columns可以创建共享列。共享列可以使多个词向量共享一个多维数组进行词嵌入转化。具体代码如下:
代码7-7 序列特征工程(续)
4.代码实现:构建序列特征列的输入层
用函数tf.contrib.feature_column.sequence_input_layer构建序列特征列的输入层。该函数返回两个张量:
具体代码如下:
代码7-7 序列特征工程(续)
代码第52行,用sequence_input_layer函数生成了输入层input_layer张量。该张量中的内容是按以下步骤产生的。
(1)定义原始词向量。
(2)定义词嵌入的初始值。
(3)将词向量中的值作为索引,去第(2)步的数组中取值,完成词嵌入的转化。
提示:
sequence_feature_column接口在转化词嵌入时,可以对数据进行自动对齐和补0操作。在使用时,可以直接将其输出结果输入RNN模型里进行计算。
由于模拟数据a、b中第一个元素的长度都是1,而最大的长度为2。系统会自动以2对齐,将不足的数据补0。
(4)将embedding_column_b和 embedding_column_a两个特征列传入函数sequence_input _layer中,得到input_layer。根据7.4.3小节介绍的规则,该输入层中数据的真实顺序为:特征列embedding_column_a在前,特征列embedding_column_b在后。最终input_layer的值为:[[5.,6.,14., 15., 16.],[0,0, 0,0,0]][[1.,2., 17., 18., 19.],[3.,4. 11., 12., 13.]]。
代码第61行,将运行图中的所有张量打印出来。可以通过观察TensorFlow内部创建词嵌入张量的情况,来验证共享特征列的功能。
5.代码实现:建立会话输出结果
建立会话输出结果。具体代码如下:
代码7-7 序列特征工程(续)
代码运行后,输出以下内容:
(1)输出3个词嵌入张量。第3个为共享列张量。
['sequence_input_layer/a_embedding/embedding_weights:0', 'sequence_input_layer/b_embedding/embedding_weights:0', 'sequence_input_layer_1/a_b_shared_embedding/embedding_weights:0']
(2)输出词嵌入的初始化值。
[[1. 2.]
[3. 4.]
[5. 6.]]
[[11. 12. 13.]
[14. 15. 16.]
[17. 18. 19.]]
[[1. 2.]
[3. 4.]
[5. 6.]]
输出的结果共有9行,每3行为一个数组:
(3)输出张量input_layer的内容。
[1 2]
[[[ 5. 6. 14. 15. 16.] [ 0. 0. 0. 0. 0.]]
[[ 1. 2. 17. 18. 19.] [ 3. 4. 11. 12. 13.]]]
输出的结果第1行是原始词向量的大小。后面两行是input_layer的具体内容。
(4)输出张量input_layer2的内容。
[1 2]
[[[5. 6. 3. 4.] [0. 0. 0. 0.]]
[[1. 2. 5. 6.] [3. 4. 1. 2.]]]
模拟数据sparse_input_a与sparse_input_b同时使用了共享词嵌入embedding_values_a。每个序列的数据被转化成两个维度的词嵌入数据。
以上内容来自于《深度学习之TensorFlow工程化项目实战》一书。如果你想更全面的了解TensorFlow的更多接口和使用方法,请参考此书。