利用神经网络的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的强大作用,这就解释了为什么比赛的效果很好。

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

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中文官方文档

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

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

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

  • 35
    点赞
  • 173
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
### 回答1: Embedding 是深度学习模型中的一种常用的,它主要用来将输入的离散变量转换成连续的向量表示。 例如,在自然语言处理中,我们可能会使用 Embedding 来将每个单词映射成一个向量,然后将这些向量传入模型进行处理。这样做的好处是,我们可以利用连续向量的运算来处理单词之间的关系,而不是将单词看作是独立的离散符号。 Embedding 通常在模型的开头使用,用来将输入数据转换成模型能够理解的数值表示,然后再由其他进一步处理。 ### 回答2: Embedding是深度学习模型中一种常用的类型,它被用来将离散的输入数据(例如单词、符号或类别)转换为连续的向量表示。 在自然语言处理任务中,如文本分类、情感分析、机器翻译等,输入数据经常是以离散的形式存在,例如单词或字符。而神经网络模型中的计算操作通常要求输入是连续的向量,为了能够将离散的输入数据用于神经网络模型的计算,就需要使用embeddingEmbedding通过使用学习得到的词向量矩阵,将离散的输入转换为连续的向量表示。这种连续的向量表示能够保持输入数据中的语义信息和关联性,从而更好地反映输入数据的特征。 通过embedding,模型可以学习到每个离散输入的词向量表示,不同单词之间的相似性可以用向量空间的距离来衡量。例如,"狗"和"猫"在向量空间中可能更接近,因为它们在语义上更相似。 嵌入可以根据实际问题的需要调整嵌入向量的维度大小,这样可以通过调整模型参数来改变模型的表达能力。较大的嵌入向量维度可以捕捉较复杂的特征,但会增加模型的复杂性和计算开销。 总之,Embedding是深度学习模型中用于将离散输入数据转换为连续向量表示的,它在处理自然语言处理任务中尤其有用,可以提供更好的特征表示和语义相似性的计算。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值