embedding层处理类别特征

类别特征在现实里十分常见,处理的方法也很多,最常见的思路是转为one-hot编码,当然这种处理方式比较粗暴,在许多算法里效果也不是很好。还有的处理方式寻找类别特征的最优切分,这个需要特定工具的支持,如LightGBM,细节见这篇文章

本篇文章主要讲解如何利用神经网络的embedding层处理类别特征。可以说,本篇文章是目前讲解利用神经网络的embedding层处理类别特征的最清晰的文章,相信读者一定会有很多收获。

一、前言

Embedding的起源和火爆都是在NLP中的,经典的word2vec都是在做word embedding这件事情,而真正首先在结构数据探索embedding的是在kaggle上的《Rossmann Store Sales》中的rank 3的解决方案,作者在比赛完后为此方法整理一篇论文放在了arXiv,文章名:《Entity Embeddings of Categorical Variables》

其网络结构如下图所示: 
 


结构非常简单,就是embedding层后面接上了两个全连接层,代码用keras写的,构建模型的代码量也非常少,用的keras的sequence model。文章的代码地址

文章有一点值得关注的地方:

使用嵌入后的向量可以提高其他算法(KNN、随机森林、gdbt)的准确性。

因此,如果我们对类别特征做embedding之后,拿到embedding后的特征后,就可以放到其它的一些学习器中进行训练,并且效果在诸多比赛中得到了验证,所以说这是一种处理类别特征的方法。

注意:对于一个任务,本文并不是用神经网络来做预测还是怎么样,重点是利用神经网络中的embedding层得到的结果,来作为原来类别特征的替换,然后用于训练其它模型。当然,直接用神经网络学习也可以。我主要是想突出使用嵌入后的向量可以提高其他算法的准确性。

二、如何利用神经网络的embedding处理类别特征。

我用的神经网络的工具是Keras。Keras对Tensorflow又进行了一层封装,操作简单,功能强大。详情可见参考文献【1】。

比如说,Keras 文档里是这么写embedding的:“把正整数(索引)转换为固定大小的稠密向量”。这句话看起来很难理解。然而,从某种意义上来说,Keras 文档的描述是正确的。那么为什么你应该使用嵌入层呢?这里有两个主要原因:

  1. 独热编码(One-hot encoding)向量是高维且稀疏的。假如说我们在做自然语言处理(NLP)的工作,并且有一个包含 2000 个单词的字典。这意味着当我们使用独热编码时,每个单词由一个含有 2000 个整数的向量来表示,并且其中的 1999 个整数都是 0。在大数据集下这种方法的计算效率是很低的。
  2. 每个嵌入向量会在训练神经网络时更新。下面有几张图片,它们展示了在多维空间之中,词语之间的相似程度,这使得我们能够可视化词语之间的关系。同样,对于任何能够通过使用嵌入层而把它变成向量的东西,我们都能够这么做。换句话说,embedding层能够让我们捕捉原来特征的内在属性,使得一些原来意义上离得“近”的东西,在转换后的向量中确实离得近(欧式空间)。如果这里还不是那么容易理解,我再举一个更形象的例子:新闻有许多板块,如体育,财经,娱乐等,我们可以认为这是一个多分类问题。篮球足球等词语必然是经常出现在体育板块,而不会出现在财经板块,我们训练了神经网络使得多分类问题尽可能地正确,这就意味着在神经网络里篮球足球确实经常“在一起”,否则的话,多分类效果肯定很差。经常“在一起”就意味着离得近,即embedding里距离近。这就达到了embedding的作用:使得一些原来意义上离得“近”的东西(篮球足球),在转换后的向量中(embedding层输出的向量)确实离得近。归根到底:是训练的作用,即标签已经蕴含了这些信息

 

我们知道了embedding的强大作用,这就解释了为什么比赛的效果很好。

那么代码中如何实现呢?很容易理解。


  
  
  1. import numpy as np
  2. from keras.layers.embeddings import Embedding
  3. from keras.models import Sequential
  4. import tensorflow as tf
  5. import random as rn
  6. # ===================================================================================================
  7. # 保证结果的复现
  8. import os
  9. os.environ[ 'PYTHONHASHSEED'] = '0'
  10. np.random.seed( 42)
  11. rn.seed( 12345)
  12. session_conf = tf.ConfigProto(intra_op_parallelism_threads= 1, inter_op_parallelism_threads= 1)
  13. from keras import backend as K
  14. tf.set_random_seed( 1234)
  15. sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
  16. K.set_session(sess)
  17. # ===================================================================================================
  18. '''
  19. 输入数据是32*2,32个样本,2个类别特征,且类别特征的可能值是0到9之间(10个)。
  20. 对这2个特征做one-hot的话,应该为32*20,
  21. embedding就是使1个特征原本应该one-hot的10维变为3维(手动设定,也可以是其它),因为有2个类别特征
  22. 这样输出的结果就应该是32*6
  23. '''
  24. model = Sequential()
  25. model.add(Embedding( 10, 3, input_length= 2))
  26. # 构造输入数据
  27. input_array = np.random.randint( 10, size=( 32, 2))
  28. # 搭建模型
  29. model.compile( 'rmsprop', 'mse')
  30. # 得到输出数据 输出格式为32*2*3。我们最终想要的格式为32*6,其实就是把2*3按照行拉成6维,然后就是我们对类别特征进行
  31. # embedding后得到的结果了。
  32. output_array = model.predict(input_array)
  33. # 查看权重参数
  34. weight = model.get_weights()
  35. '''
  36. 我们肯定好奇:output_array是怎么得到的?
  37. 我们先来看weight的内容:10*3。这是什么意思呢,就是其实就是一个索引的结果表,如果原来特征值为0,那么就找第一行,如果原来特征值为3,
  38. 那么就找第4行。
  39. 0.00312117 -0.0475833 0.0386381
  40. 0.0153809 -0.0185934 0.0234457
  41. 0.0137821 0.00433551 0.018144
  42. 0.0468446 -0.00687895 0.0320682
  43. 0.0313594 -0.0179525 0.03054
  44. 0.00135239 0.0309016 0.0453686
  45. 0.0145149 -0.0165581 -0.0280098
  46. 0.0370018 -0.0200525 -0.0332663
  47. 0.0330335 0.0110769 0.00161555
  48. 0.00262188 -0.0495747 -0.0343777
  49. 以input_array的第一行为例
  50. input_array的第一行是7和4,那么就找第8行和第5行,形成了output_array的第一个2*3,即
  51. 0.0370018 -0.0200525 -0.0332663
  52. 0.0313594 -0.0179525 0.03054
  53. 然后,拉成一个向量0.0370018 -0.0200525 -0.0332663 0.0313594 -0.0179525 0.03054
  54. 这就是原始特征值8和5经过embedding层后的转换结果!
  55. '''

在上述的代码中,我们可以看到2个类别特征的值都在0到9,并且我们没有对模型进行训练,而是直接就搭建了一个网络,就输出结果了。在真实的应用中,不是这样。有2点需要改进:

1、对每一个类别特征构建一个embedding层。对embedding层进行拼接。

2、训练网络,得到训练后的embedding层的输出作为类别特征one-hot的替换,这样的embedding的输出更精确。

三、真实可用的方法

为了解决上述的2个问题,我们这里还是人工构建训练集,我们搭建的模型如图:

从模型中,我们可以看到,这是符合现实世界的数据集的:即既有分类特征,又有连续特征。我们先训练一个网络,embedding_3和embedding_4层的输出结果就是用embedding处理类别特征后的结果。

直接上代码:


  
  
  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Thu Nov 1 10:07:50 2018
  4. @author: anshuai1
  5. """
  6. '''
  7. 完整的利用embeddding处理类别特征的程序。
  8. '''
  9. import numpy as np
  10. import tensorflow as tf
  11. import random as rn
  12. #random seeds for stochastic parts of neural network
  13. np.random.seed( 10)
  14. from tensorflow import set_random_seed
  15. set_random_seed( 15)
  16. from keras.models import Model
  17. from keras.layers import Input, Dense, Concatenate, Reshape, Dropout
  18. from keras.layers.embeddings import Embedding
  19. # ===================================================================================================
  20. # 保证神经网络结果的复现
  21. # ===================================================================================================
  22. import os
  23. os.environ[ 'PYTHONHASHSEED'] = '0'
  24. np.random.seed( 42)
  25. rn.seed( 12345)
  26. session_conf = tf.ConfigProto(intra_op_parallelism_threads= 1, inter_op_parallelism_threads= 1)
  27. from keras import backend as K
  28. tf.set_random_seed( 1234)
  29. sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
  30. K.set_session(sess)
  31. # ===================================================================================================
  32. # 记录类别特征embedding后的维度。key为类别特征索引,value为embedding后的维度
  33. cate_embedding_dimension = { '0': 3, '1': 2}
  34. def build_embedding_network():
  35. # 以网络结构embeddding层在前,dense层在后。即训练集的X必须以分类特征在前,连续特征在后。
  36. inputs = []
  37. embeddings = []
  38. input_cate_feature_1 = Input(shape=( 1,))
  39. embedding = Embedding( 10, 3, input_length= 1)(input_cate_feature_1)
  40. # embedding后是10*1*3,为了后续计算方便,因此使用Reshape转为10*3
  41. embedding = Reshape(target_shape=( 3,))(embedding)
  42. inputs.append(input_cate_feature_1)
  43. embeddings.append(embedding)
  44. input_cate_feature_2 = Input(shape=( 1,))
  45. embedding = Embedding( 4, 2, input_length= 1)(input_cate_feature_2)
  46. embedding = Reshape(target_shape=( 2,))(embedding)
  47. inputs.append(input_cate_feature_2)
  48. embeddings.append(embedding)
  49. input_numeric = Input(shape=( 1,))
  50. embedding_numeric = Dense( 16)(input_numeric)
  51. inputs.append(input_numeric)
  52. embeddings.append(embedding_numeric)
  53. x = Concatenate()(embeddings)
  54. x = Dense( 10, activation= 'relu')(x)
  55. x = Dropout( .15)(x)
  56. output = Dense( 1, activation= 'sigmoid')(x)
  57. model = Model(inputs, output)
  58. model.compile(loss= 'binary_crossentropy', optimizer= 'adam')
  59. return model
  60. # ===================================================================================================
  61. # 程序入口
  62. # ===================================================================================================
  63. '''
  64. 输入数据是32*3,32个样本,2个类别特征,1个连续特征。
  65. 对类别特征做entity embedding,第一个类别特征的可能值是0到9之间(10个),第二个类别特征的可能值是0到3之间(4个)。
  66. 对这2个特征做one-hot的话,应该为32*14,
  67. 对第一个类别特征做embedding使其为3维,对第二个类别特征做embedding使其为2维。3维和2维的设定是根据实验效果和交叉验证设定。
  68. 对连续特征不做处理。
  69. 这样理想输出的结果就应该是32*6,其中,类别特征维度为5,连续特征维度为1。
  70. '''
  71. # ===================================================================================================
  72. # 构造训练数据
  73. # ===================================================================================================
  74. sample_num = 32 #样本数为32
  75. cate_feature_num = 2 #类别特征为2
  76. contious_feature_num = 1 #连续特征为1
  77. # 保证了训练集的复现
  78. rng = np.random.RandomState( 123)
  79. cate_feature_1 = rng.randint( 10, size=( 32, 1))
  80. cate_feature_2 = rng.randint( 4, size=( 32, 1))
  81. contious_feature = rng.rand( 32, 1)
  82. X = []
  83. X.append(cate_feature_1)
  84. X.append(cate_feature_2)
  85. X.append(contious_feature)
  86. # 二分类
  87. Y = np.random.randint( 2, size=( 32, 1))
  88. # ===================================================================================================
  89. # 训练和预测
  90. # ===================================================================================================
  91. # train
  92. NN = build_embedding_network()
  93. NN.fit(X, Y, epochs= 3, batch_size= 4, verbose= 0)
  94. # predict
  95. y_preds = NN.predict(X)[:, 0]
  96. # 画出模型,需要GraphViz包。
  97. #from keras.utils import plot_model
  98. #plot_model(NN, to_file='NN.png')
  99. # ===================================================================================================
  100. # 读embedding层的输出结果
  101. # ===================================================================================================
  102. model = NN # 创建原始模型
  103. for i in range(cate_feature_num):
  104. # 由NN.png图可知,如果把类别特征放前,连续特征放后,cate_feature_num+i就是所有embedding层
  105. layer_name = NN.get_config()[ 'layers'][cate_feature_num+i][ 'name']
  106. intermediate_layer_model = Model(inputs=NN.input,
  107. outputs=model.get_layer(layer_name).output)
  108. # numpy.array
  109. intermediate_output = intermediate_layer_model.predict(X)
  110. intermediate_output.resize([ 32,cate_embedding_dimension[str(i)]])
  111. if i == 0:
  112. X_embedding_trans = intermediate_output
  113. else:
  114. X_embedding_trans = np.hstack((X_embedding_trans,intermediate_output)) #水平拼接
  115. # 取出原来的连续特征。这里的list我转numpy一直出问题,被迫这么写循环了。
  116. for i in range(contious_feature_num):
  117. if i == 0:
  118. X_contious = X[cate_feature_num+i]
  119. else:
  120. X_contious = np.hstack((X_contious,X[cate_feature_num+i]))
  121. # ===================================================================================================
  122. # 在类别特征做embedding后的基础上,拼接连续特征,形成最终矩阵,也就是其它学习器的输入
  123. # ===================================================================================================
  124. '''
  125. 最终的结果:32*6.其中,类别特征维度为5(前5个),连续特征维度为1(最后1个)
  126. '''
  127. X_trans = np.hstack((X_embedding_trans,X_contious))
  128. '''
  129. 好了,我们现在来验证一下embeddding后的结果是不是一个索引的结果表。
  130. 以第一个类别特征为例,利用代码NN.trainable_weights[0].eval(session=sess)我们输出embedding_1层的参数。
  131. -0.0464945 0.0284733 -0.0365357
  132. 0.051283 0.0336468 0.0440866
  133. 0.0370058 -0.0378573 -0.0357488
  134. 0.0249379 0.031956 0.024898
  135. -0.0075664 0.0355627 -0.0149643
  136. -0.0481578 -0.0210528 0.0118361
  137. -0.0178293 -0.0212218 0.0246742
  138. 0.0160812 0.0294887 -0.0069619
  139. 0.0200302 0.0472979 0.0312307
  140. 0.0416624 0.0408308 0.0405323
  141. 这是一个10*3的矩阵,符合我们的想法,即应该one-hot的10维变为3维。
  142. 为了方便,我们只看第一个类别特征cate_feature_1的前5行:
  143. 2
  144. 2
  145. 6
  146. 1
  147. 3
  148. 去索引权重,结果应该是:
  149. 0.0370058 -0.0378573 -0.0357488
  150. 0.0370058 -0.0378573 -0.0357488
  151. -0.0178293 -0.0212218 0.0246742
  152. 0.051283 0.0336468 0.0440866
  153. 0.0249379 0.031956 0.024898
  154. 我们接着去查看X_trans中的前5行前3列:
  155. 发现确实是这样的结果。
  156. 我们又一次证明了embedding层的输出就是类别特征的值索引权重矩阵的结果!
  157. 值得注意的是:embedding层权重矩阵的训练跟其它层没有什么区别,都是反向传播更新的。
  158. '''

参考文献:

【1】Keras中文官方文档

【2】 深度学习中embedding层的理解

【3】https://www.kaggle.com/aquatic/entity-embedding-neural-net/comments

【4】获取keras模型的weights和bias

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值