最近需要用到Embedding做特征嵌入,但是网上找不到embedding的具体用法,东拼西凑终于看懂了,写篇文章总结一下,顺便整理一下来龙去脉。
Embedding可以说是一种对离散特征进行编码的手段、
而说到离散特征编码,相信大部分人第一是将会想到Onehot编码,举例回顾一下Onehot编码。
mnist数据集相信大家都已经耳熟能详,是一个用于手写数字分类的数据集,共有0-9十个数字,所以其label必然也会有10种:0-9,对应数字0-9。
那么如果使用OneHot编码,那么:
0: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
1: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
2: [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
3: [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
4: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
5: [0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
6: [0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
7: [0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
8: [0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
9: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
我们平常在训练的时候就是这么干的,先将标签进行Onehot编码,方便我们之后进行训练、验证和测试。
独热编码(是因为大部分算法是基于向量空间中的度量来进行计算的,为了使非偏序关系的变量取值不具有偏序性,并且到原点是等距的。
使用one-hot编码,将离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。将离散型特征使用one-hot编码,会让特征之间的距离计算更加合理。
但是OneHot编码是存在问题的,当我们的特征空间非常大的时候,比如对一本词典内的所有词语进行编码,假设字典内有10W个词语,我们将需要10W*10W 的矩阵来对对其编码,显然这样的编码方式冗余性太高,其中大部分的值都是0,蕴含的信息量太少。
这个时候Embedding便应运而生,OneHot最大的问题不是冗余吗?那我Embedding就是来为你消除冗余的(在降维的情况下,Embedding可以对特征进行升维),假设我们现在有10W行10W列的特征矩阵,每一行表示字典中的一个词语,如果我们给该矩阵乘以一个10W行200列的矩阵,那么结果会产生一个10W行*200列的矩阵,每一行仍然表示一个词语,但是我们只用了200个特征就将这个词语与其他词语区分开了。整个矩阵的维度减少了100000/200 = 500被。
再举一个具体的例子:
假设现在现在字典中只有6个词语:太阳,橘子,葡萄,车轮,苹果,榴莲,
如果用onehot进行编码,需要使用6*6=36的特征矩阵:
太阳:[1, 0, 0,0,0,0]
橘子:[0, 1, 0,0,0,0]
葡萄:[0, 0, 1,0,0,0]
车轮:[0, 0, 0,1,0,0]
香蕉:[0, 0, 0,0,1,0]
榴莲:[0, 0, 0,0,0,1]
而我们现在使用三个特征就能将其完全区分开:水果?圆形?大小?
水果 圆形 大小
太阳:[0, 1, 1]
橘子:[1, 1, 1]
葡萄:[1, 1, 1]
车轮:[0, 1, 0]
香蕉:[1, 1, 0]
榴莲:[1, 0, 1]
可以看到我们只用6*3=18的特征矩阵就能完美的将这个六个词语区分开来,原因是在onehot的特征矩阵中每一行除了是1的那个数有意义,其他均没有任何意义。而下面这个矩阵每一行的每一个特征都是具有固定含义的。
我们将上面的矩阵记作A,下面的矩阵记作B,我们可以看作B=A*X
这个X就是我们的Embedding矩阵。我们可以推断出,X的维度是:6行3列。其中每一列都代表着一种特征。但是这些特征并不像上述的例子一样具有很好的可解释性,所以我们一般选择将X设为一个变量矩阵,通过在神经网络中训练得到。
要计算矩阵X的维度,我们首先需要知道特征空间的维度,以及我们要得到的嵌入向量的维度:
比如,我们要将0-9 十个数字嵌入到长度为4的向量中,那么
10 * 10 * X = 10*4
很显然,根据矩阵乘法可以得到 X 的维度是10 *4
举一个简单的神经网络的demo:
def generator(x, y):
reuse = len([t for t in tf.global_variables() if t.name.startswith('generator')]) > 0
with tf.variable_scope('generator', reuse = reuse):
embedding_dict = tf.get_variable(name="embedding_1", shape=(10, 8), dtype=tf.float32)
y = tf.nn.embedding_lookup(embedding_dict, y)
y = slim.flatten(y)
x = tf.concat([x, y], 1)
x = slim.fully_connected(x, 32, activation_fn = tf.nn.relu)
x = slim.fully_connected(x, 128, activation_fn = tf.nn.relu)
x = slim.fully_connected(x, mnist_dim, activation_fn=tf.nn.sigmoid)
return x
这是一个简单的生成对抗网络的生成器,向其中输入X和y两个向量,其中y是mnist的标签,0-9,所以特征维度是10, 现在我们要将其签入长度为8的向量当中去,那么我们创建一个embedding字典矩阵,其中的变量值需要通过学习得到。
然后通过调用tf,nn,embedding_lookup()这个函数来对特征进行编码,需要传入两个参数,一个是刚才创建的embedding字典矩阵,另外一个就是我们需要进行编码的特征。
tf,nn,embedding_lookup()这个函数的本质相当于先特所有特征进行onehot编码,然后在用onehot特征矩阵与字典矩阵进行matmul矩阵乘法运算(上面详细讲过,A*X=B的例子)。
其实说白了embedding这个操作和全连接网络一样,都是矩阵的乘法,可以用一层Dense Neural Network来代替(CV中称为Fully Connected Net(FC)全连接层)
替代的方法也很简单,将特征进行onehot编码然后输入一层dim=特征类数的FC,然后再进入一层dim=签入向量长度的FC,经过训练后得到的向量就是embedding向量了。
同样以将mnist标签进行embedding为例,我们首先对label进行onehot编码,得到的每一个label的onehot向量长度均为10,输入dim=10的FC层,然后再输入dim=8的FC层,得到的结果就是对一个label进行Embedding的结果。
码字不易,如果对你有帮助,请点赞关注!