类别特征在现实里十分常见,处理的方法也很多,最常见的思路是转为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 文档的描述是正确的。那么为什么你应该使用嵌入层呢?这里有两个主要原因:
- 独热编码(One-hot encoding)向量是高维且稀疏的。假如说我们在做自然语言处理(NLP)的工作,并且有一个包含 2000 个单词的字典。这意味着当我们使用独热编码时,每个单词由一个含有 2000 个整数的向量来表示,并且其中的 1999 个整数都是 0。在大数据集下这种方法的计算效率是很低的。
- 每个嵌入向量会在训练神经网络时更新。下面有几张图片,它们展示了在多维空间之中,词语之间的相似程度,这使得我们能够可视化词语之间的关系。同样,对于任何能够通过使用嵌入层而把它变成向量的东西,我们都能够这么做。换句话说,embedding层能够让我们捕捉原来特征的内在属性,使得一些原来意义上离得“近”的东西,在转换后的向量中确实离得近(欧式空间)。如果这里还不是那么容易理解,我再举一个更形象的例子:新闻有许多板块,如体育,财经,娱乐等,我们可以认为这是一个多分类问题。篮球足球等词语必然是经常出现在体育板块,而不会出现在财经板块,我们训练了神经网络使得多分类问题尽可能地正确,这就意味着在神经网络里篮球足球确实经常“在一起”,否则的话,多分类效果肯定很差。经常“在一起”就意味着离得近,即embedding里距离近。这就达到了embedding的作用:使得一些原来意义上离得“近”的东西(篮球足球),在转换后的向量中(embedding层输出的向量)确实离得近。归根到底:是训练的作用,即标签已经蕴含了这些信息。
我们知道了embedding的强大作用,这就解释了为什么比赛的效果很好。
那么代码中如何实现呢?很容易理解。
-
import numpy
as np
-
from keras.layers.embeddings
import Embedding
-
from keras.models
import Sequential
-
import tensorflow
as tf
-
import random
as rn
-
-
# ===================================================================================================
-
# 保证结果的复现
-
import os
-
os.environ[
'PYTHONHASHSEED'] =
'0'
-
-
np.random.seed(
42)
-
-
rn.seed(
12345)
-
-
session_conf = tf.ConfigProto(intra_op_parallelism_threads=
1, inter_op_parallelism_threads=
1)
-
-
from keras
import backend
as K
-
-
tf.set_random_seed(
1234)
-
-
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
-
K.set_session(sess)
-
# ===================================================================================================
-
-
-
'''
-
输入数据是32*2,32个样本,2个类别特征,且类别特征的可能值是0到9之间(10个)。
-
对这2个特征做one-hot的话,应该为32*20,
-
embedding就是使1个特征原本应该one-hot的10维变为3维(手动设定,也可以是其它),因为有2个类别特征
-
这样输出的结果就应该是32*6
-
'''
-
model = Sequential()
-
model.add(Embedding(
10,
3, input_length=
2))
-
-
# 构造输入数据
-
input_array = np.random.randint(
10, size=(
32,
2))
-
-
# 搭建模型
-
model.compile(
'rmsprop',
'mse')
-
-
# 得到输出数据 输出格式为32*2*3。我们最终想要的格式为32*6,其实就是把2*3按照行拉成6维,然后就是我们对类别特征进行
-
# embedding后得到的结果了。
-
output_array = model.predict(input_array)
-
-
# 查看权重参数
-
weight = model.get_weights()
-
-
'''
-
我们肯定好奇:output_array是怎么得到的?
-
我们先来看weight的内容:10*3。这是什么意思呢,就是其实就是一个索引的结果表,如果原来特征值为0,那么就找第一行,如果原来特征值为3,
-
那么就找第4行。
-
0.00312117 -0.0475833 0.0386381
-
0.0153809 -0.0185934 0.0234457
-
0.0137821 0.00433551 0.018144
-
0.0468446 -0.00687895 0.0320682
-
0.0313594 -0.0179525 0.03054
-
0.00135239 0.0309016 0.0453686
-
0.0145149 -0.0165581 -0.0280098
-
0.0370018 -0.0200525 -0.0332663
-
0.0330335 0.0110769 0.00161555
-
0.00262188 -0.0495747 -0.0343777
-
-
以input_array的第一行为例
-
input_array的第一行是7和4,那么就找第8行和第5行,形成了output_array的第一个2*3,即
-
0.0370018 -0.0200525 -0.0332663
-
0.0313594 -0.0179525 0.03054
-
-
然后,拉成一个向量0.0370018 -0.0200525 -0.0332663 0.0313594 -0.0179525 0.03054
-
这就是原始特征值8和5经过embedding层后的转换结果!
-
-
'''
在上述的代码中,我们可以看到2个类别特征的值都在0到9,并且我们没有对模型进行训练,而是直接就搭建了一个网络,就输出结果了。在真实的应用中,不是这样。有2点需要改进:
1、对每一个类别特征构建一个embedding层。对embedding层进行拼接。
2、训练网络,得到训练后的embedding层的输出作为类别特征one-hot的替换,这样的embedding的输出更精确。
三、真实可用的方法
为了解决上述的2个问题,我们这里还是人工构建训练集,我们搭建的模型如图:
从模型中,我们可以看到,这是符合现实世界的数据集的:即既有分类特征,又有连续特征。我们先训练一个网络,embedding_3和embedding_4层的输出结果就是用embedding处理类别特征后的结果。
直接上代码:
-
# -*- coding: utf-8 -*-
-
"""
-
Created on Thu Nov 1 10:07:50 2018
-
-
@author: anshuai1
-
"""
-
'''
-
完整的利用embeddding处理类别特征的程序。
-
'''
-
-
import numpy
as np
-
-
import tensorflow
as tf
-
import random
as rn
-
-
#random seeds for stochastic parts of neural network
-
np.random.seed(
10)
-
from tensorflow
import set_random_seed
-
set_random_seed(
15)
-
-
from keras.models
import Model
-
from keras.layers
import Input, Dense, Concatenate, Reshape, Dropout
-
from keras.layers.embeddings
import Embedding
-
-
-
-
-
# ===================================================================================================
-
# 保证神经网络结果的复现
-
# ===================================================================================================
-
import os
-
os.environ[
'PYTHONHASHSEED'] =
'0'
-
-
np.random.seed(
42)
-
-
rn.seed(
12345)
-
-
session_conf = tf.ConfigProto(intra_op_parallelism_threads=
1, inter_op_parallelism_threads=
1)
-
-
from keras
import backend
as K
-
-
tf.set_random_seed(
1234)
-
-
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
-
K.set_session(sess)
-
# ===================================================================================================
-
-
# 记录类别特征embedding后的维度。key为类别特征索引,value为embedding后的维度
-
cate_embedding_dimension = {
'0':
3,
'1':
2}
-
-
-
-
def build_embedding_network():
-
-
# 以网络结构embeddding层在前,dense层在后。即训练集的X必须以分类特征在前,连续特征在后。
-
inputs = []
-
embeddings = []
-
-
-
input_cate_feature_1 = Input(shape=(
1,))
-
embedding = Embedding(
10,
3, input_length=
1)(input_cate_feature_1)
-
# embedding后是10*1*3,为了后续计算方便,因此使用Reshape转为10*3
-
embedding = Reshape(target_shape=(
3,))(embedding)
-
inputs.append(input_cate_feature_1)
-
embeddings.append(embedding)
-
-
input_cate_feature_2 = Input(shape=(
1,))
-
embedding = Embedding(
4,
2, input_length=
1)(input_cate_feature_2)
-
embedding = Reshape(target_shape=(
2,))(embedding)
-
inputs.append(input_cate_feature_2)
-
embeddings.append(embedding)
-
-
input_numeric = Input(shape=(
1,))
-
embedding_numeric = Dense(
16)(input_numeric)
-
inputs.append(input_numeric)
-
embeddings.append(embedding_numeric)
-
-
x = Concatenate()(embeddings)
-
-
x = Dense(
10, activation=
'relu')(x)
-
x = Dropout(
.15)(x)
-
output = Dense(
1, activation=
'sigmoid')(x)
-
-
model = Model(inputs, output)
-
-
model.compile(loss=
'binary_crossentropy', optimizer=
'adam')
-
-
return model
-
-
# ===================================================================================================
-
# 程序入口
-
# ===================================================================================================
-
'''
-
输入数据是32*3,32个样本,2个类别特征,1个连续特征。
-
对类别特征做entity embedding,第一个类别特征的可能值是0到9之间(10个),第二个类别特征的可能值是0到3之间(4个)。
-
对这2个特征做one-hot的话,应该为32*14,
-
对第一个类别特征做embedding使其为3维,对第二个类别特征做embedding使其为2维。3维和2维的设定是根据实验效果和交叉验证设定。
-
对连续特征不做处理。
-
这样理想输出的结果就应该是32*6,其中,类别特征维度为5,连续特征维度为1。
-
'''
-
# ===================================================================================================
-
# 构造训练数据
-
# ===================================================================================================
-
-
sample_num =
32
#样本数为32
-
cate_feature_num =
2
#类别特征为2
-
contious_feature_num =
1
#连续特征为1
-
-
# 保证了训练集的复现
-
rng = np.random.RandomState(
123)
-
cate_feature_1 = rng.randint(
10, size=(
32,
1))
-
cate_feature_2 = rng.randint(
4, size=(
32,
1))
-
contious_feature = rng.rand(
32,
1)
-
X = []
-
X.append(cate_feature_1)
-
X.append(cate_feature_2)
-
X.append(contious_feature)
-
# 二分类
-
Y = np.random.randint(
2, size=(
32,
1))
-
-
# ===================================================================================================
-
# 训练和预测
-
# ===================================================================================================
-
# train
-
NN = build_embedding_network()
-
NN.fit(X, Y, epochs=
3, batch_size=
4, verbose=
0)
-
# predict
-
y_preds = NN.predict(X)[:,
0]
-
-
# 画出模型,需要GraphViz包。
-
#from keras.utils import plot_model
-
#plot_model(NN, to_file='NN.png')
-
-
# ===================================================================================================
-
# 读embedding层的输出结果
-
# ===================================================================================================
-
-
model = NN
# 创建原始模型
-
for i
in range(cate_feature_num):
-
# 由NN.png图可知,如果把类别特征放前,连续特征放后,cate_feature_num+i就是所有embedding层
-
layer_name = NN.get_config()[
'layers'][cate_feature_num+i][
'name']
-
-
intermediate_layer_model = Model(inputs=NN.input,
-
outputs=model.get_layer(layer_name).output)
-
-
# numpy.array
-
intermediate_output = intermediate_layer_model.predict(X)
-
-
intermediate_output.resize([
32,cate_embedding_dimension[str(i)]])
-
-
if i ==
0:
-
X_embedding_trans = intermediate_output
-
else:
-
X_embedding_trans = np.hstack((X_embedding_trans,intermediate_output))
#水平拼接
-
-
-
# 取出原来的连续特征。这里的list我转numpy一直出问题,被迫这么写循环了。
-
for i
in range(contious_feature_num):
-
if i ==
0:
-
X_contious = X[cate_feature_num+i]
-
else:
-
X_contious = np.hstack((X_contious,X[cate_feature_num+i]))
-
-
-
# ===================================================================================================
-
# 在类别特征做embedding后的基础上,拼接连续特征,形成最终矩阵,也就是其它学习器的输入
-
# ===================================================================================================
-
-
'''
-
最终的结果:32*6.其中,类别特征维度为5(前5个),连续特征维度为1(最后1个)
-
'''
-
X_trans = np.hstack((X_embedding_trans,X_contious))
-
-
-
'''
-
好了,我们现在来验证一下embeddding后的结果是不是一个索引的结果表。
-
以第一个类别特征为例,利用代码NN.trainable_weights[0].eval(session=sess)我们输出embedding_1层的参数。
-
-0.0464945 0.0284733 -0.0365357
-
0.051283 0.0336468 0.0440866
-
0.0370058 -0.0378573 -0.0357488
-
0.0249379 0.031956 0.024898
-
-0.0075664 0.0355627 -0.0149643
-
-0.0481578 -0.0210528 0.0118361
-
-0.0178293 -0.0212218 0.0246742
-
0.0160812 0.0294887 -0.0069619
-
0.0200302 0.0472979 0.0312307
-
0.0416624 0.0408308 0.0405323
-
这是一个10*3的矩阵,符合我们的想法,即应该one-hot的10维变为3维。
-
为了方便,我们只看第一个类别特征cate_feature_1的前5行:
-
2
-
2
-
6
-
1
-
3
-
去索引权重,结果应该是:
-
0.0370058 -0.0378573 -0.0357488
-
0.0370058 -0.0378573 -0.0357488
-
-0.0178293 -0.0212218 0.0246742
-
0.051283 0.0336468 0.0440866
-
0.0249379 0.031956 0.024898
-
我们接着去查看X_trans中的前5行前3列:
-
发现确实是这样的结果。
-
-
我们又一次证明了embedding层的输出就是类别特征的值索引权重矩阵的结果!
-
-
值得注意的是:embedding层权重矩阵的训练跟其它层没有什么区别,都是反向传播更新的。
-
-
'''
参考文献:
【1】Keras中文官方文档
【3】https://www.kaggle.com/aquatic/entity-embedding-neural-net/comments