【推荐系统学习总结】当我们谈论embedding时我们在谈论什么

参考了下列文章:

https://mp.weixin.qq.com/s?__biz=MzI1MzY0MzE4Mg==&mid=2247483890&idx=1&sn=bd96178202507f9358b17f7c6aa91443&chksm=e9d01133dea7982568ae47e215c670bb86f144d2b47161af5b370471a5fd2a60a0d209aee328&scene=21#wechat_redirect

https://blog.csdn.net/weixin_42078618/article/details/84553940

https://blog.csdn.net/weixin_42078618/article/details/82999906

https://www.jianshu.com/p/af8f20fe7dd3

最近在看推荐系统的内容。在相关的任务中,无论是CTR预估还是TopN推荐,输入的特征经常是类别型变量,所以就屡屡看到embedding这个词。看了很多资料,才稍稍理解embedding的原理和应用。做一个笔记避免遗忘。

一、传统类别特征的问题

当面对非数值特征时,一个非常自然的想法是one-hot编码。也就是对于每一个非数值列,它有多少种类别,我们就把它分成多少个特征。原来的类别特征按照对应位置为1,其他位置为0的原则投射过来。

引用自 https://blog.csdn.net/weixin_42078618/article/details/82999906,不妥删

假设,我们中文,一共只有10个字。。。只是假设啊,那么我们用0-9就可以表示完

比如,这十个字就是“我从哪里来,要到何处去”

其分别对应“0-9”,如下:

我  从  哪  里  来  要  到  何  处  去

0    1    2    3   4    5   6    7    8   9

那么,其实我们只用一个列表就能表示所有的对话

如:我  从  哪  里  来  要  到  何  处  去  ——>>>[0 1 2 3 4 5 6 7 8 9]

或:我  从  何  处  来  要  到  哪  里  去  ——>>>[0 1 7 8 4 5 6 2 3 9]

但是,我们看看one-hot编码方式(详见:https://blog.csdn.net/tengyuan93/article/details/78930285

他把上面的编码方式弄成这样

    # 我从哪里来,要到何处去

    [

    [1 0 0 0 0 0 0 0 0 0]

    [0 1 0 0 0 0 0 0 0 0]

    [0 0 1 0 0 0 0 0 0 0]

    [0 0 0 1 0 0 0 0 0 0]

    [0 0 0 0 1 0 0 0 0 0]

    [0 0 0 0 0 1 0 0 0 0]

    [0 0 0 0 0 0 1 0 0 0]

    [0 0 0 0 0 0 0 1 0 0]

    [0 0 0 0 0 0 0 0 1 0]

    [0 0 0 0 0 0 0 0 0 1]

    ]

    

    # 我从何处来,要到哪里去

    [

    [1 0 0 0 0 0 0 0 0 0]

    [0 1 0 0 0 0 0 0 0 0]

    [0 0 0 0 0 0 0 1 0 0]

    [0 0 0 0 0 0 0 0 1 0]

    [0 0 0 0 1 0 0 0 0 0]

    [0 0 0 0 0 1 0 0 0 0]

    [0 0 0 0 0 0 1 0 0 0]

    [0 0 1 0 0 0 0 0 0 0]

    [0 0 0 1 0 0 0 0 0 0]

    [0 0 0 0 0 0 0 0 0 1]

    ]

即:把每一个字都对应成一个十个(样本总数/字总数)元素的数组/列表,其中每一个字都用唯一对应的数组/列表对应,数组/列表的唯一性用1表示。如上,“我”表示成[1。。。。],“去”表示成[。。。。1],这样就把每一系列的文本整合成一个稀疏矩阵。

那问题来了,稀疏矩阵(二维)和列表(一维)相比,有什么优势。

很明显,计算简单嘛,稀疏矩阵做矩阵计算的时候,只需要把1对应位置的数相乘求和就行,也许你心算都能算出来;而一维列表,你能很快算出来?何况这个列表还是一行,如果是100行、1000行和或1000列呢?

所以,one-hot编码的优势就体现出来了,计算方便快捷、表达能力强。

然而,缺点也随着来了。

比如:中文大大小小简体繁体常用不常用有十几万,然后一篇文章100W字,你要表示成100W X 10W的矩阵???

这是它最明显的缺点。过于稀疏时,过度占用资源。

比如:其实我们这篇文章,虽然100W字,但是其实我们整合起来,有99W字是重复的,只有1W字是完全不重复的。那我们用100W X 10W的岂不是白白浪费了99W X 10W的矩阵存储空间。

因此,特别是在数据量巨大的当代,one hot编码是不太可行的。我们说不可行的原因是它维度太高。这一方面对存储和计算提出了挑战,一方面过分稀疏的矩阵又造成了一种令人不能忍的资源浪费。

二、embedding的引入

维度太高了怎么办?很容易想到,降维啊。

我们已经知道很多著名的降维方法,比如PCA,SVD,等等。这里要说到的embedding也是一种降维方法,它一方面在类别特征的降维问题上笑傲江湖,一方面在NLP领域获得了极为广泛的应用。

下面用一个最简单的例子说明了embedding的原则。

引用自 https://blog.csdn.net/weixin_42078618/article/details/82999906,不妥删

接下来给大家看一张图

链接:https://spaces.ac.cn/archives/4122

假设:我们有一个2 x 6的矩阵,然后乘上一个6 x 3的矩阵后,变成了一个2 x 3的矩阵。

先不管它什么意思,这个过程,我们把一个12个元素的矩阵变成6个元素的矩阵,直观上,大小是不是缩小了一半?

也许你已经想到了!!!对!!!不管你想的对不对,但是embedding层,在某种程度上,就是用来降维的,降维的原理就是矩阵乘法。在卷积网络中,可以理解为特殊全连接层操作,跟1x1卷积核异曲同工!!!484很神奇!!!

用训练集的角度来看,第一个矩阵显然表示两个都是6维的样本,即这是一个类别性特征,它的可能类别有6个。中间的表示embedding层的作用,等号右边表示经过embedding的向量。两个6维的样本变成了两个3维的样本。对于这个确定的类别特征(或者我们叫它field)而言,样本个数不变,但类别编码后的长度变了。(感觉自己在说废话哈哈哈哈哈)

或者说,训练集中的有一个field,假设它是用户最喜欢的食物,可选范围有火锅、汉堡、炒菜、烤肉、日料、西餐这六种,那么one-hot编码会将这一个field映射成6维,其实在后面的预测等进一步计算中,它就变成了6个feature。而embedding可能会将它映射成3维。

这么做的好处,一个是降维,还有一个则是挖掘特征之间的隐含关系。这也是embedding能在NLP中大放异彩的主要原因之一。

下面这个例子给出了非常通俗易懂的解释。

引用自 https://blog.csdn.net/weixin_42078618/article/details/84553940,不妥删

首先,继续假设我们有一句话,叫“公主很漂亮”,如果我们使用one-hot编码,可能得到的编码如下:

    公 [0 0 0 0 1]

    主 [0 0 0 1 0]

    很 [0 0 1 0 0]

    漂 [0 1 0 0 0]

    亮 [1 0 0 0 0]

咋一眼看过似乎没毛病,其实本来人家也没毛病,或者假设咱们的词袋更大一些

    公 [0 0 0 0 1 0 0 0 0 0]

    主 [0 0 0 1 0 0 0 0 0 0]

    很 [0 0 1 0 0 0 0 0 0 0]

    漂 [0 1 0 0 0 0 0 0 0 0]

    亮 [1 0 0 0 0 0 0 0 0 0]

 

假设吧,就假设咱们的词袋一共就10个字,则这一句话的编码如上所示。

这样的编码,最大的好处就是,不管你是什么字,我们都能在一个一维的数组里用01给你表示出来。并且不同的字绝对不一样,以致于一点重复都没有,表达本征的能力极强。

但是,因为其完全独立,其劣势就出来了。表达关联特征的能力几乎为0!!!

 

我给你举个例子,我们又有一句话“王妃很漂亮”

那么在这基础上,我们可以把这句话表示为

 

    王 [0 0 0 0 0 0 0 0 0 1]

    妃 [0 0 0 0 0 0 0 0 1 0]

    很 [0 0 1 0 0 0 0 0 0 0]

    漂 [0 1 0 0 0 0 0 0 0 0]

    亮 [1 0 0 0 0 0 0 0 0 0]

 

从中文表示来看,我们一下就跟感觉到,王妃跟公主其实是有很大关系的,比如:公主是皇帝的女儿,王妃是皇帝的妃子,可以从“皇帝”这个词进行关联上;公主住在宫里,王妃住在宫里,可以从“宫里”这个词关联上;公主是女的,王妃也是女的,可以从“女”这个字关联上。

但是呢,我们用了one-hot编码,公主和王妃就变成了这样:

 

    公 [0 0 0 0 1 0 0 0 0 0]

    主 [0 0 0 1 0 0 0 0 0 0]

    王 [0 0 0 0 0 0 0 0 0 1]

    妃 [0 0 0 0 0 0 0 0 1 0]

 

你说,你要是不看前面的中文注解,你知道这四行向量有什么内部关系吗?看不出来,那怎么办?

既然,通过刚才的假设关联,我们关联出了“皇帝”、“宫里”和“女”三个词,那我们尝试这么去定义公主和王妃

公主一定是皇帝的女儿,我们假设她跟皇帝的关系相似度为1.0;公主从一出生就住在宫里,直到20岁才嫁到府上,活了80岁,我们假设她跟宫里的关系相似度为0.25;公主一定是女的,跟女的关系相似度为1.0;

王妃是皇帝的妃子,没有亲缘关系,但是有存在着某种关系,我们就假设她跟皇帝的关系相似度为0.6吧;妃子从20岁就住在宫里,活了80岁,我们假设她跟宫里的关系相似度为0.75;王妃一定是女的,跟女的关系相似度为1.0;

于是公主王妃四个字我们可以这么表示:

           皇    宫 

           帝    里    女

    公主 [ 1.0  0.25  1.0]

    王妃 [ 0.6  0.75  1.0]

这样我们就把公主和王妃两个词,跟皇帝、宫里、女这几个字(特征)关联起来了,我们可以认为:

 

公主=1.0 *皇帝 +0.25*宫里 +1.0*女

王妃=0.6 *皇帝 +0.75*宫里 +1.0*女

或者这样,我们假设没歌词的每个字都是对等(注意:只是假设,为了方便解释):

           皇     宫 

           帝     里     女

    公   [ 0.5  0.125   0.5]

    主   [ 0.5  0.125   0.5]

    王   [ 0.3  0.375   0.5]

    妃   [ 0.3  0.375   0.5]

 

这样,我们就把一些词甚至一个字,用三个特征给表征出来了。然后,我们把皇帝叫做特征(1),宫里叫做特征(2),女叫做特征(3),于是乎,我们就得出了公主和王妃的隐含特征关系:

王妃=公主的特征(1) * 0.6 +公主的特征(2) * 3 +公主的特征(3) * 1

于是乎,我们把文字的one-hot编码,从稀疏态变成了密集态,并且让相互独立向量变成了有内在联系的关系向量。

所以,embedding层做了个什么呢?它把我们的稀疏矩阵,通过一些线性变换(在CNN中用全连接层进行转换,也称为查表操作),变成了一个密集矩阵,这个密集矩阵用了N(例子中N=3)个特征来表征所有的文字,在这个密集矩阵中,表象上代表着密集矩阵跟单个字的一一对应关系,实际上还蕴含了大量的字与字之间,词与词之间甚至句子与句子之间的内在关系(如:我们得出的王妃跟公主的关系)。他们之间的关系,用的是嵌入层学习来的参数进行表征。从稀疏矩阵到密集矩阵的过程,叫做embedding,很多人也把它叫做查表,因为他们之间也是一个一一映射的关系。

更重要的是,这种关系在反向传播的过程中,是一直在更新的,因此能在多次epoch后,使得这个关系变成相对成熟,即:正确的表达整个语义以及各个语句之间的关系。这个成熟的关系,就是embedding层的所有权重参数。

Embedding是NPL领域最重要的发明之一,他把独立的向量一下子就关联起来了。这就相当于什么呢,相当于你是你爸的儿子,你爸是A的同事,B是A的儿子,似乎跟你是八竿子才打得着的关系。结果你一看B,是你的同桌。Embedding层就是用来发现这个秘密的武器。

这里给出的一个形象的思路是,我们原来,是将“公”简单粗暴地映射到“公,主,很,漂,亮,王,妃”这几个字形成的词库里,有几个字就有几维。而现在,我们是将它映射到“皇帝,宫里,女”这三个特征构成的特征空间里。感觉有点像SVM中高维空间的意思,但是SVM中高维和低维是对应的,这里是没有关系的。也正是这样,可以获得特征之间的关系,因为特征空间选择得当的话,关联的特征会获得相似的投射。

NLP也使用了类似的思想,但它们用的当然不是我们拍脑袋找个特征空间这样的方法去投射,而是通过上下文来给每一个词赋予特征向量,这就是大名鼎鼎的 Word2Vec,这里https://www.jianshu.com/p/af8f20fe7dd3有详细的说明。

三、embedding实战:在CTR预估中的应用

说明一下,我在这里这么叫它完全是因为这个例子是讲CTR预估的,本质上其实没关系,其他预测任务一样应用。

在著名的DeepFM模型中就使用了embedding层。

https://mp.weixin.qq.com/s?__biz=MzI1MzY0MzE4Mg==&mid=2247483890&idx=1&sn=bd96178202507f9358b17f7c6aa91443&chksm=e9d01133dea7982568ae47e215c670bb86f144d2b47161af5b370471a5fd2a60a0d209aee328&scene=21#wechat_redirect

DeepFM的结构:

这里我们先不关心上面FM层和隐藏层,只关心embedding层,也就是最下面一个的方块和倒数第二个方块之间的连接层。

实现网络结构和参数初始化的代码如下:(代码来自https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-DeepFM-model)

    def _init_graph(self):
        self.graph = tf.Graph()
        with self.graph.as_default():
            tf.set_random_seed(self.random_seed)

            self.feat_index = tf.placeholder(tf.int32,
                                             shape=[None,None],
                                             name='feat_index')
            self.feat_value = tf.placeholder(tf.float32,
                                           shape=[None,None],
                                           name='feat_value')

            self.label = tf.placeholder(tf.float32,shape=[None,1],name='label')
            self.dropout_keep_fm = tf.placeholder(tf.float32,shape=[None],name='dropout_keep_fm')
            self.dropout_keep_deep = tf.placeholder(tf.float32,shape=[None],name='dropout_deep_deep')
            #self.train_phase = tf.placeholder(tf.bool,name='train_phase')

            self.weights = self._initialize_weights()

            # model
            self.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index) # N * F * K
            feat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1])
            self.embeddings = tf.multiply(self.embeddings,feat_value)


            # first order term
            self.y_first_order = tf.nn.embedding_lookup(self.weights['feature_bias'],self.feat_index)
            self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order,feat_value),2)
            self.y_first_order = tf.nn.dropout(self.y_first_order,self.dropout_keep_fm[0])

            # second order term
            # sum-square-part
            self.summed_features_emb = tf.reduce_sum(self.embeddings,1) # None * k
            self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K

            # squre-sum-part
            self.squared_features_emb = tf.square(self.embeddings)
            self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1)  # None * K

            #second orderzz
            self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square,self.squared_sum_features_emb)
            self.y_second_order = tf.nn.dropout(self.y_second_order,self.dropout_keep_fm[1])


            # Deep component
            self.y_deep = tf.reshape(self.embeddings,shape=[-1,self.field_size * self.embedding_size])
            self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[0])

            for i in range(0,len(self.deep_layers)):
                self.y_deep = tf.add(tf.matmul(self.y_deep,self.weights["layer_%d" %i]), self.weights["bias_%d"%i])
                self.y_deep = self.deep_layers_activation(self.y_deep)
                self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[i+1])


            #----DeepFM---------
            if self.use_fm and self.use_deep:
                concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)
            elif self.use_fm:
                concat_input = tf.concat([self.y_first_order, self.y_second_order], axis=1)
            elif self.use_deep:
                concat_input = self.y_deep

            self.out = tf.add(tf.matmul(concat_input,self.weights['concat_projection']),self.weights['concat_bias'])

            # loss
            if self.loss_type == "logloss":
                self.out = tf.nn.sigmoid(self.out)
                self.loss = tf.losses.log_loss(self.label, self.out)
            elif self.loss_type == "mse":
                self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))
            # l2 regularization on weights
            if self.l2_reg > 0:
                self.loss += tf.contrib.layers.l2_regularizer(
                    self.l2_reg)(self.weights["concat_projection"])
                if self.use_deep:
                    for i in range(len(self.deep_layers)):
                        self.loss += tf.contrib.layers.l2_regularizer(
                            self.l2_reg)(self.weights["layer_%d" % i])


            if self.optimizer_type == "adam":
                self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999,
                                                        epsilon=1e-8).minimize(self.loss)
            elif self.optimizer_type == "adagrad":
                self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate,
                                                           initial_accumulator_value=1e-8).minimize(self.loss)
            elif self.optimizer_type == "gd":
                self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
            elif self.optimizer_type == "momentum":
                self.optimizer = tf.train.MomentumOptimizer(learning_rate=self.learning_rate, momentum=0.95).minimize(
                    self.loss)


            #init
            self.saver = tf.train.Saver()
            init = tf.global_variables_initializer()
            self.sess = tf.Session()
            self.sess.run(init)

            # number of params
            total_parameters = 0
            for variable in self.weights.values():
                shape = variable.get_shape()
                variable_parameters = 1
                for dim in shape:
                    variable_parameters *= dim.value
                total_parameters += variable_parameters
            if self.verbose > 0:
                print("#params: %d" % total_parameters)





    def _initialize_weights(self):
        weights = dict()
        #tf.random_uniform((6,6), minval=-0.5,maxval=0.5, dtype=tf.float32)
        #embeddings
        weights['feature_embeddings'] = tf.Variable(
            tf.random_normal([self.feature_size,self.embedding_size],0.0,0.01),
            name='feature_embeddings')
        weights['feature_bias'] = tf.Variable(tf.random_normal([self.feature_size,1],0.0,1.0),name='feature_bias')


        #deep layers
        num_layer = len(self.deep_layers)
        input_size = self.field_size * self.embedding_size
        glorot = np.sqrt(6./(input_size + self.deep_layers[0]))


        weights['layer_0'] = tf.Variable(
            tf.random_normal([input_size,self.deep_layers[0]],0.0,glorot),dtype=tf.float32
        )
        weights['bias_0'] = tf.Variable(
            tf.random_normal([1,self.deep_layers[0]],0.0,glorot),dtype=tf.float32
        )

        for i in range(1,num_layer):
            glorot = np.sqrt(6. / (self.deep_layers[i - 1] + self.deep_layers[i]))
            weights["layer_%d" % i] = tf.Variable(
                np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i - 1], self.deep_layers[i])),
                dtype=np.float32)  # layers[i-1] * layers[i]
            weights["bias_%d" % i] = tf.Variable(
                np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])),
                dtype=np.float32)  # 1 * layer[i]


        # final concat projection layer

        if self.use_fm and self.use_deep:
            input_size = self.field_size + self.embedding_size + self.deep_layers[-1]
        elif self.use_fm:
            input_size = self.field_size + self.embedding_size
        elif self.use_deep:
            input_size = self.deep_layers[-1]

        glorot = np.sqrt(6.0/(input_size + 1))
        weights['concat_projection'] = tf.Variable(np.random.normal(loc=0,scale=glorot,size=(input_size,1)),dtype=np.float32)
        weights['concat_bias'] = tf.Variable(tf.constant(0.01),dtype=np.float32)


        return weights

 

和embedding有关的部分在21-23,119-122行。其中,feature_size是特征维数,也就是所有数值特征按一个算,所有非数值特征有几个标签按几个算出来的总特征个数(这里的处理方式是把离散和非离散特征放到一起做embedding了,也有的方案是分开做的,比如DCN网络);embedding_size是隐向量维数,也就是每个field在one-hot之后被投射到的维数;field_size是训练集的列数,即数值和非数值特征都按一个来算的特征个数,feat_value和feat_index分别是样本集合,对应特征值和特征索引。

在特征初始化函数中,embedding层的输入是feature_size,输出是embedding_size,也就是降维过程,类比上面的将6维降成3维。

在模型构建部分,首先用了一个embedding_lookup函数,从weights['feature_embeddings']中选择索引为feat_index的那些权重。这其实是巧妙的一步。我们知道,虽然one-hot之后的特征有feature_size这么多,但是对于每一个样本,我们输入的就是field_size这么大呀——因为它是训练集的维数。也就是说,每次我们训练的时候,都只考虑非数值列里当前训练样本中的那些非数值标签。我们知道embedding层不止有这些,但是who care?其他的反正都是0。这样,我们就可以一次只更新field_size个参数,而不是feature_size个。

这里,对于embedding层的优化和神经网络的其他优化同步进行,其实完全可以看作全连接层的特殊应用,或者自编码器的扩展实现。毕竟embedding最重要的是对离散特征编码的思想,全连接层是embedding,Word2Vec是embedding,拍脑袋想一个“皇宫”的特征,也是embedding。

四、其他

Embedding也可以升维,这个很容易想明白,https://blog.csdn.net/weixin_42078618/article/details/82999906这里有关于升维的讲解,我就不赘述了。

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值