TensorFlow学习笔记:特征工程

2 min read

特征工程是机器学习流程中重要的一个环节,即使是通常用来做端到端学习的深度学习模型在训练之前也免不了要做一些特征工程相关的工作。Tensorflow平台提供的FeatureColumn API为特征工程提供了强大的支持。

Feature cloumns是原始数据和Estimator模型之间的桥梁,它们被用来把各种形式的原始数据转换为模型能够使用的格式。深度神经网络只能处理数值数据,网络中的每个神经元节点执行一些针对输入数据和网络权重的乘法和加法运算。然而,现实中的有很多非数值的类别数据,比如产品的品牌、类目等,这些数据如果不加转换,神经网络是无法处理的。另一方面,即使是数值数据,在仍给网络进行训练之前有时也需要做一些处理,比如标准化、离散化等。

在Tensorflow中,通过调用tf.feature_column模块来创建feature columns。如下图所示,您可以通过 Estimator(DNNClassifier)的 feature_columns 参数指定模型的输入。特征列作为输入数据(由 input_fn 返回)与模型之间的桥梁。

要创建特征列,需要调用 tf.feature_column 模块的函数。本文档介绍了该模块中的九个函数。如下图所示,所有九个函数都会返回一个 Categorical-Column 或一个 Dense-Column 对象,但却不会返回 bucketized_column,后者继承自这两个类:

我们来更详细地了解这些函数。

数值列(Numeric column)

  • key: 特征的名字。也就是对应的列名称。
  • shape: 该key所对应的特征的默认是1,但是比如one-hot类型的,shape就不是1,而是实际的维度。总之,这里是key所对应的维度,不一定是1.
  • default_value: 如果不存在使用的默认值
  • normalizer_fn: 对该特征下的所有数据进行转换。如果需要进行normalize,那么就是使用normalize的函数.这里不仅仅局限于normalize,也可以是任何的转换方法,比如取对数,取指数,这仅仅是一种变换方法.

形状shape

关于shape,比如shape=3表示[r,g,b]类型的三元列表,类似[0,100,255]。shape=[4,3]表示下图的4行3列的矩阵,类似[[1,0,0] [0,1,0] [0,0,1] [0,0,0]]。

如果需要生成类似(x,y,z)只包含3这个数字的元组,你需要在3后面强加一个逗号:shape = (3,)

创建numeric column的方法如下:

默认情况下,numeric column创建的是一个标量值,也可以指定shape参数来创建向量、矩阵等多维数据。

我们还可以为numeric column指定数值变换的函数normalizer_fn,为对原始数据做一些变换操作。

分桶列(Bucketized column)

Bucketized column用来把numeric column的值按照提供的边界(boundaries)离散化为多个值。离散化是特征工程常用的一种方法。例如,把年份离散化为4个阶段,如下图所示。

模型将按以下方式表示这些分桶:

  • source_column: 必须是numeric_column
  • boundaries: 不同的桶。boundaries=[0., 1., 2.],产生的bucket就是, (-inf, 0.), [0., 1.), [1., 2.), and [2., +inf), 每一个区间分别表示0, 1, 2, 3,所以相当于分桶分了4个.

进一步做一些测试:

分类标识列(Categorical identity column)

可将分类标识列视为分桶列的一种特殊情况。在传统的分桶列中,每个分桶表示一系列值(例如,从 1960 年到 1979 年)。在分类标识列中,每个分桶表示一个唯一整数。例如,假设您想要表示整数范围 [0, 4)。也就是说,您想要表示整数 0、1、2 或 3。在这种情况下,分类标识映射如下所示:

与Bucketized column类似,Categorical identity column用单个唯一值表示bucket。

分类词汇列(Categorical vocabulary column)

我们不能直接向模型中输入字符串。相反,我们必须首先将字符串映射到数值或分类值。分类词汇列提供了一种将字符串表示为独热矢量的好方法。例如:

顾名思义,Categorical vocabulary column把一个vocabulary中的string映射为数值型的类别特征,是做one-hot编码的很好的方法。在tensorflow中有两种提供词汇表的方法,一种是用list,另一种是用file,对应的feature column分别为:

  • feature_column.categorical_column_with_vocabulary_list
  • feature_column.categorical_column_with_vocabulary_file

两者的定义如下:

  • key: feature名字
  • vocabulary_list: 对于category来说,进行转换的也就是category列表.
  • dtype: 仅仅string和int被支持,其他的类型是无法进行这个操作的.
  • default_value: 当不在vocabulary_list中的默认值,这时候num_oov_buckets必须是
  • num_oov_buckets: 用来处理那些不在vocabulary_list中的值,如果是0,那么使用default_value进行填充;如果大于0,则会在[len(vocabulary_list), len(vocabulary_list)+num_oov_buckets]这个区间上重新计算当前特征的值.

  • vocabulary_file: 存储词汇表的文件名
  • 其他参数的含义与feature_column.categorical_column_with_vocabulary_list相同

进一步做一些测试:

注意:

  • input_layer: 只接受dense tensor
  • tables_initializer: 在sparser的时候使用的,如果不进行初始化会出现 Table not initialized. [Node: hash_table_Lookup = LookupTableFindV2 这样的异常

经过哈希处理的列(Hashed Column)

为类别特征提供词汇表有时候会过于繁琐,特别是在词汇表非常大的时候,词汇表会非常消耗内存。tf.feature_column.categorical_column_with_hash_bucket允许用户指定类别的总数,通过hash的方式来得到最终的类别ID。伪代码如下:

用hash的方式产生类别ID,不可避免地会遇到hash冲突的问题,即可有多个原来不相同的类别会产生相同的类别ID。因此,设置hash_bucket_size(>The number of categories)参数会显得比较重要。实践表明,hash冲突不会对神经网络模型造成太大的影响,因为模型可以通过其他特征作进一步区分。

同样来做一些测试,看看最终效果:

组合列(Crossed column)

通过将多个特征组合为一个特征(称为特征组合),模型可学习每个特征组合的单独权重。更具体地说,假设我们希望模型计算佐治亚州亚特兰大的房产价格。这个城市的房产价格在不同位置差异很大。在确定对房产位置的依赖性方面,将纬度和经度表示为单独的特征用处不大;但是,将纬度和经度组合为一个特征则可精确定位位置。假设我们将亚特兰大表示为一个 100×100 的矩形网格区块,按纬度和经度的特征组合标识全部 10000 个区块。借助这种特征组合,模型可以针对与各个区块相关的房价条件进行训练,这比单独的经纬度信号强得多。

下图展示了我们的计划(以红色文本显示城市各角落的纬度和经度值):

Crossed column仅仅适用于sparser特征,产生的依然是sparsor特征。

具体地,Crossed特征对keys的笛卡尔积执行hash操作,再把hash的结果对hash_bucket_size取模得到最终的结果:Hash(cartesian product of features) % hash_bucket_size。

测试代码如下:

指标列和嵌入列(Indicator and embedding columns)

Indicator columns 和 embedding columns 不能直接作用在原始特征上,而是作用在categorical columns上。在前面的众多例子中,我们已经使用过indicator_column来把categorical column得到的稀疏tensor转换为one-hot或者multi-hot形式的稠密tensor,这里就不赘述了。当某些特征的类别数量非常大时,使用indicator_column来把原始数据转换为神经网络的输入就变得非常不灵活,这时通常使用embedding column把原始特征映射为一个低维稠密的实数向量。同一类别的embedding向量间的距离通常可以用来度量类别直接的相似性。

Embedding column与indicator column之间的区别可以用下图表示。

  • categorical_column: 使用categoryical_column产生的sparsor column
  • dimension: 定义embedding的维数
  • combiner: 对于多个entries进行的推导。默认是meam, 但是 sqrtn在词袋模型中,有更好的准确度。
  • initializer: 初始化方法,默认使用高斯分布来初始化。
  • tensor_name_in_ckpt: 可以从check point中恢复
  • ckpt_to_load_from: check point file,这是在 tensor_name_in_ckpt 不为空的情况下设置的.
  • max_norm: 默认是l2
  • trainable: 是否可训练的,默认是true

测试代码:

从上面的测试结果可以看出不在vocabulary里的数据’A’在经过categorical_column_with_vocabulary_list操作时映射为默认值-1,而默认值-1在embeding column时映射为0向量,这是一个很有用的特性,可以用-1来填充一个不定长的ID序列,这样可以得到定长的序列,然后经过embedding column之后,填充的-1值不影响原来的结果。在下一篇文章中,我会通过一个例子来演示这个特性。

shared_embedding_columns

有时候在同一个网络模型中,有多个特征可能需要共享相同的embeding映射空间,比如用户历史行为序列中的商品ID和候选商品ID,这时候可以用到tf.feature_column.shared_embedding_columns。

  • categorical_columns 为需要共享embeding映射空间的类别特征列表
  • 其他参数与embedding column类似

测试代码:

需要注意的是,tf.feature_column.shared_embedding_columns的返回值是一个与参数categorical_columns维数相同的列表。

以下“公式”提供了与嵌入维度数量有关的一般经验法则:embedding_dimensions =  number_of_categories**0.25

Weighted categorical column

有时候我们需要给一个类别特征赋予一定的权重,比如给用户行为序列按照行为发生的时间到某个特定时间的差来计算不同的权重,这是可以用到weighted_categorical_column。

测试代码:

将特征列传递到估算器

特征列可以将您的输入数据(通过从 input_fn 返回的特征字典进行说明)映射到要提供给模型的值。以列表形式将特征列指定到估算器的 feature_columns 参数。请注意,feature_columns 参数因估算器的不同而有所差异:

上述规则的原因超出了这篇介绍博文的范围,不过,我们会在未来的博文中确保对此进行介绍。

输入函数input_fn

FeatureColumn为你的模型的输入数据提供了一个关于如何表示和转换数据的规范。但它们本身不提供数据。需要你通过一个输入函数来提供数据。

输入函数(input function)必须返回一个关于tensors的字典(dictionary)。每个key对应于FeatureColumn的名字。每个key的value是一个tensor:它包含着所有数据实例的特征的值。input function详见Building Input Functions with tf.estimator,线性模型中的input_fn详见代码linear models tutorial code.

输入函数被传给:train() 和 evaluate(),它们会调用training和testing。

线性estimators

Tensorflow estimator类提供了一个统一的training 和 evaluation组件来用于回归和分类模型。它们会处理training和evaluation loops的细节,让用户更关注模型输入和架构本身。

为了构建一个线性estimator,你可以使用tf.estimator.LinearClassifier 和 tf.estimator.LinearRegressor 来处理分类和回归。

对于所有的tensorflow estimators,为了运行,你可以:

  • 实例化estimator类。对于两个linear estimator类,你将一个FeatureColumn’s列表传给构造函数
  • 调用estimator的train()方法来训练
  • 调用estimator的evaluate()来看效果

例如:

嵌套 (Embedding):分类输入数据

分类数据是指用于表示一组有限选项中的一个或多个离散项的输入特征。例如,它可以是某用户观看过的一组影片,某文档中使用的一系列单词,或某人从事的职业。分类数据的最高效表示方式是使用稀疏张量(一种含有极少非零元素的张量)。例如,如果要构建一个影片推荐模型,可以为每部可能的影片分别分配一个唯一的 ID,然后通过用户已观看影片的稀疏张量来表示每位用户,如图所示。

在上图的矩阵中,每一行都是一个显示用户的影片观看记录的样本,并以稀疏张量的形式表示,因为每个用户只会观看所有可能的影片中的一小部分。根据影片图标上方所示的索引,最后一行对应于稀疏张量 [1, 3, 999999]。类似地,我们还可将字词、句子和文档表示为稀疏矢量 – 在这种情况下,词汇表内每个字词所扮演的角色类似于推荐示例中的影片。为了能够在机器学习系统中使用这类表示法,我们需要将每个稀疏矢量表示为数字矢量,从而使语义上相似的项(影片或字词)在矢量空间中具有相似的距离。但如何将字词表示为数字矢量呢?

最简单的方法是:定义一个巨型输入层,并在其中为词汇表内的每个字词设定一个节点,或者至少为您数据中出现的每个字词设定一个节点。如果您的数据中出现了 50 万个独一无二的单词,您可以使用长度为 50 万的矢量来表示每个单词,并将每个字词分配到相应矢量中对应的索引位置。如果为“马”分配的索引是 1247,那么为了将“马”馈入到您的网络中,可以将第 1247 个输入节点设成 1,其余节点设成 0。这种表示法称为独热编码 (one-hot encoding),因为只有一个索引具有非零值。更常见的是,使用一个包含各个单词在大块文本中出现次数的向量。这被称为“词袋”(bag of words) 表示法。在一个词袋矢量中,50 万个节点中的若干个节点将会具有非零值。不过,无论您如何确定非零值,若将节点与字词一一对应,您得到的输入矢量就会比较稀疏 – 即:矢量很大,但非零值相对较少。稀疏表示法存在多项问题(如下所述),这些问题可能会致使模型很难高效地学习。

网络的规模

巨型输入矢量意味着神经网络的对应权重数目会极其庞大。如果您的词汇表内有 M 个字词,而神经网络输入层上方的第一层内有 N 个节点,您便需要为该层训练 MxN 个权重。权重数目过大会进一步引发以下问题:

  • 数据量:模型中的权重越多,高效训练所需的数据就越多。
  • 计算量:权重越多,训练和使用模型所需的计算就越多。这很容易就会超出您硬件的能力范围。

矢量之间缺乏有意义的联系

如果您已将 RGB 通道的像素值馈入到图片分类器中,分析“邻近”值便行得通。不管是从语义上来看,还是从矢量之间的几何距离来看,红蓝色与纯蓝色都是邻近的。不过,对于在索引 1247 处设为 1 以表示“马”的矢量而言,如果说它与在索引 238 处设为 1 以表示“电视机”的矢量不够邻近,那么它与在索引 50430 处设为 1 以表示“羚羊”的矢量亦然。

解决方案:嵌套

上述问题的解决方案就是使用嵌套,也就是将大型稀疏矢量映射到一个保留语义关系的低维空间。在此模块的随后几个部分中,我们将从直观角度、概念角度和编程角度来详细探讨嵌套。

要解决稀疏输入数据的核心问题,您可以将高维度数据映射到低维度空间。通过纸上练习您已了解,即便是小型多维空间,也能自由地将语义上相似的项归到一起,并将相异项分开。矢量空间中的位置(距离和方向)可对良好的嵌套中的语义进行编码。例如,下面的真实嵌套可视化图所展示的几何关系图捕获了国家与其首都之间的语义关系。

收缩网络

尽管我们需要足够的维度来编码丰富的语义关系,但我们也需要足够小的嵌套空间来更快速地训练我们的系统。实用嵌套的量级大致有数百个维度。这可能比您在自然语言任务中使用的词汇规模要小好几个数量级。

嵌套充当查询表

嵌套是一个矩阵,每列表示您词汇中的一项所对应的矢量。要获得某个词汇项的密集矢量,您可以检索该项所对应的列。

但是,如何转换字词矢量的稀疏包呢?要获得表示多个词汇项(例如,一句或一段中的所有字词)的稀疏矢量的密集矢量,您可以检索各项的嵌套,然后将它们相加。

如果稀疏矢量包含词汇项的计数,则您可以将每项嵌套与其对应项的计数相乘,然后再求和。

这些运算可能看起来很眼熟吧。

嵌套查询充当矩阵乘法

我们刚刚阐述的查询、乘法和加法程序等效于矩阵乘法。假设有一个 1 X N 的稀疏表示 S 和一个 N X M 的嵌套表 E,矩阵乘法 S X E 可以得出密集矢量 1 X M。但首要问题是,如何获取 E 呢?我们将在下一部分介绍如何获取嵌套。您可以通过多种方式来获取嵌套,包括 Google 研发的世界一流算法。

嵌套 (Embeddings):获取嵌套

您可以通过多种方式来获取嵌套,包括 Google 研发的世界一流算法。

标准降维技术

目前有很多在低维空间捕获高维空间重要结构的数学技术。理论上,这些技术都可以用来创建用于机器学习系统的嵌套。

例如,主成分分析 (PCA) 已用于创建字词嵌套。在给定一组实例的情况下,例如字词矢量包,PCA 会尝试查找高度相关且可以合并的维度。

Word2vec

Word2vec 是 Google 为了训练字词嵌套而研发的一种算法。Word2vec 基于分布假设,将语义上相似的字词映射到在几何图形上邻近的嵌套矢量。

分布假设指出经常具有相同相邻字词的字词往往在语义上相似。“狗”和“猫”这两个字词经常靠近“兽医”一词出现,这就可以说明这两个字词在语义上相似。正如语言学家约翰·弗斯 (John Firth) 在 1957 年所言:“观其伴而知其意”。

Word2Vec 通过训练神经网络来区分实际共同出现的多组字词与随机出现在一起的字词,从而充分利用此类上下文信息。输入层采用一种稀疏表示法用于组合一个目标字词与一个或多个上下文字词。这一输入层会连接到一个较小的隐藏层。

在其中一版算法中,系统通过用随机噪点字词替代目标字词来举出反面示例。在给出正面示例“the plane flies”的情况下,系统可能会换成“jogging”来创建对比鲜明的反面示例“the jogging flies”。

另一版算法通过将真实的目标字词与随机选择的上下文字词配对来创建反面示例。因此,系统可能会举出正面示例((the, plane)、(flies, plane))和反面示例((compiled, plane)、(who, plane)),然后通过学习分辨哪几对真正地在文字中一起出现。

不过,分类器不是上述任何一版算法的真正用途。在训练模型后,你得到的是一组嵌套。借助将输入层连接到隐藏层的权重,您可以将字词的稀疏表示映射到小型矢量。这类嵌套可在其他分类器中重复利用。

要详细了解 word2vec,请参阅 tensorflow.org 上的教程

将嵌套训练为大型模型的一部分

您也可以将嵌套作为目标任务的神经网络的一部分进行学习。通过这个方法,您可以为自己的特定系统量身定制嵌套,不过耗费的时间可能要比单独训练嵌套的时间长。

一般来说,当您具有稀疏数据(或您想要嵌套的密集数据)时,您可以创建一个嵌套单元,这个嵌套单元其实是大小为 d 的一个特殊类型的隐藏单元。此嵌套层可与任何其他特征和隐藏层组合。和任何 DNN 中一样,最终层将是要进行优化的损失函数。例如,假设我们正在执行协同过滤,目标是根据其他用户的兴趣预测某位用户的兴趣。我们可以将这个问题作为监督式学习问题进行建模,具体做法是随机选取(或留出)用户观看过的一小部分影片作为正类别标签,然后再优化 Softmax 损失。

再举一个例子,如果您想在 DNN 中针对房地产广告词创建嵌套层来预测房价,则您可以将训练数据中的已知房屋售价用作标签来优化 L2 损失。

在学习 d 维嵌套时,每一项都会映射到 d 维空间中的一个点,这样相似项就会在该空间内彼此邻近。图 6 说明了在嵌套层中学到的权重与几何视图之间的关系。输入节点与 d 维嵌套层中的节点之间的边的权重对应于 d 维坐标轴中每一维的坐标值。

https://colab.research.google.com/notebooks/mlcc/intro_to_sparse_data_and_embeddings.ipynb

tf.nn.embedding_lookup

embedding的意义就不用多谈了。简单来说,就是应对高维、稀疏的id类特征,通过将单个id(可能是某个词的id,也可能是某个商品的id)映射成一个稠密向量,变id特征的“精确匹配”为embedding向量的“模糊查找”,从而提升算法的扩展能力。

embedding_lookup理论上就是用矩阵相乘实现的,就可以看成一个特殊的“全连接层”。

假设embedding权重矩阵是一个[vocab_size, embed_size]的稠密矩阵W,vocab_size是需要embed的所有item的个数(比如:所有词的个数,所有商品的个数),embed_size是映射后的向量长度。

所谓embedding_lookup(W, id1),可以想像成一个只在id1位为1的[1, vocab_size]的one_hot向量,与[vocab_size, embed_size]的W矩阵相乘,结果是一个[1, embed_size]的向量,它就是id1对应的embedding向量,实际上就是W矩阵的第id1行。

但是,以上过程只是前代,因为W一般是随机初始化的,是待优化的变量。因此,embedding_lookup除了要完成以上矩阵相乘的过程(实现成“抽取id对应的行”),还要完成自动求导,以实现对W的更新。

另外,embedding_lookup一般在NLP中用得比较多,将一个[batchsize, sequence_len]的输入,映射成[batchsize, sequence_len, embed_size]的矩阵。而在推荐/搜索领域,我们往往需要先embedding, 再将embedding后的多个向量合并成一个向量(即pooling过程)。比如,用户过去一周用过3次微信,1次支付宝,那我们将用户过去一周的app使用习惯表示成

用户app使用习惯向量=3*微信向量+1*支付宝向量

而且,手机app的使用习惯肯定是非常稀疏的,几万种app,只使用了4,5种。所以以上过程要实现成一个稀疏矩阵(特征)与稠密矩阵(权重)相乘的形式。这时,就需要用到tensorflow提供的另一个api: embedding_lookup_sparse。具体使用方式,见以下文章的“稀疏矩阵相乘”一节。

打赏作者
微信支付标点符 wechat qrcode
支付宝标点符 alipay qrcode

如何选择scikit-learn中的算法

scikit-learn中自带了很多机器学习的算法,在日常使用过程中可能产生疑问,以下2张图获取可以帮助你解决
标点符
0 sec read

斯坦福大学的词向量工具:GloVe

GloVe简介 GloVe的全称叫Global Vectors for Word Representation
标点符
2 min read

深入理解fbprophet

Prophet简介 在先前的文章中简单介绍了Facebook的prophet,最近在使用prophet的时候遇
标点符
5 min read

发表评论

电子邮件地址不会被公开。 必填项已用*标注