推荐系统之从Wide&Deep到DeepFM

目录

一、ctr预估之从Wide&Deep到DeepFM

二、推荐系统遇上深度学习(三)--DeepFM模型理论和实践

1、背景

2、DeepFM模型

3、相关知识

4、代码解析

参考资料


一、ctr预估之从Wide&Deep到DeepFM

转载:ctr预估之Wide&Deep和DeepFM__zhang_bei_的博客-CSDN博客

分享两个ctr预估算法。一个是Wide&Deep,是谷歌16年提的算法《Wide & Deep Learning for Recommender Systems》,链接https://arxiv.org/pdf/1606.07792.pdf,这个算法是用在谷歌应用商店做排序的。另外一个是DeepFM,来自于华为17年提出的《DeepFM: A Factorization-Machine based Neural Network for CTR Prediction》,链接https://arxiv.org/pdf/1703.04247.pdf,这个是华为用在应用商店做排序的。这两个算法既可以在广告中做ctr预估,也可以在推荐系统中做排序。

ctr预估算法中最经典的莫过于逻辑回归LR,优势是速度快,便于加特征,可解释。缺点也很明显,就是无法直接拟合高阶特征,无法做特征交叉,因此需要大量的人工特征工程和专家经验来做特征交叉。后来出现的因子分解机FM,可以解决二阶特征交叉问题,但是不能解决高阶特征交叉。比如年轻男性喜欢玩射击游戏,就包括<年龄,性别,游戏>3阶特征。最近几年随着深度学习的快速发展,工程师引入深度学习解决高阶特征交叉问题,并提出了一批算法,比如FNN,AFM,NFM,DeepCrossing以及Wide&Deep和DeepFM等,私以为最后两篇文章比较经典。

最早看这两篇算法的时候,我的感觉是这两个算法有什么差别,不是一样的吗?最近重新细读了一遍,才理解其中的不同。先来介绍一下Wide&Deep,模型结构如下图所示。所谓Wide&Deep就是整个模型结构由wide部分和deep部分共同组成,图中左边是wide模型,一个逻辑回归,右边是deep模型。假设输入的都是类别特征,deep模型的输入是用one-hot表示的Sparse Features。比如,对手机型号这个特征而言,市面上可能存在数千个手机型号,但是每个用户只对应一个手机型号,那么这个稀疏向量有几千个维度,只有一个位置为1,其余位置为0。将每个特征以embedding表示,embedding是一个低维稠密向量。将高维稀疏特征,映射到对应的embedding上去,将所有特征的embedding水平拼接起来,生成图中的Dense Embeddings,最后通过若干层神经网络得到输出。将wide和deep部分组合起来,就是图中间的wide&deep模型。

在这里插入图片描述

谷歌应用商店实际使用的模型结构如下图所示。最右边的Cross Product Transformation就是wide部分,是曝光app和用户已安装app的交叉特征。剩下的就是deep部分,类别特征都用embedding表示,维度是32。连续特征包括年龄,安装app数量等,经过预处理之后,和所有的embedding水平拼接在一起,作为DNN的输入,总维度是1200。值得一提的是,wide部分用带L1正则化的FTRL优化,deep部分用AdaGrad做优化,两个部分联合训练,惊叹还有这样的骚操作。

在这里插入图片描述

再来介绍一下DeepFM,模型结构如下图所示。黑色线表示带有权重的连接,红色线表示权重为1的连接,蓝色线表示one-hot映射到对应的embedding。图中的Field表示一个特征类别,比如手机型号是一个field,性别是一个field,地理位置是一个field。每个field用one-hot表示,构成最下面的Sparse Features。稀疏特征有两部分输出,一部分是加权求和得到FM Layer中的第一个节点,另一部分索引对应的embedding得到Dense Embeddings。Dense Embeddings也有两部分输出,一部分是两两做点积输出到FM Layer,另外一部分是拼接起来作为Hidden Layer的输入。

在这里插入图片描述


图画的比较复杂,直接看公式反而更简单,公式在下面。也是两部分组成,一部分是因子分解机FM,一部分是DNN,DNN和上面介绍的deep部分是相同的。需要注意的是,FM和DNN共享embedding。论文中做过试验,将FM和DNN的embedding相互独立,效果要差。关于DeepFM论文给出的物理意义是,FM部分负责一阶特征和二阶交叉特征,而DNN部分负责二阶以上的高阶特征交叉。

论文中对DeepFM的超参做了调试,并基于他们给出的数据集给出了最优的超参:1)激活函数relu效果好于tanh,这与wide&deep的线上模型一致。2)每个隐层的unit在200到400之间效果最佳,相比wide&deep的unit数量要少一些。3)3层网络效果最佳,这与wide&deep的隐层数量相同。4)在总的神经单元数量固定的情况下,constant结构效果最佳,constant表示每个隐层的unit数量相同;而wide&deep采用的是塔式结构,越往上unit数量越少。5)文中没有说真实场景中embedding维度是多少,但是试验中的embedding维度设置为10,wide&deep中是32维,两个都不大。

总结下wide&deep和DeepFM的异同:

1)两者的DNN部分模型结构相同;

2)wide&deep需要做特征工程,二阶特征交叉需要靠特征工程来实现,通过wide部分发挥作用;

3)DeepFM完全不需要做特征工程,直接输入原始特征即可,二阶特征交叉靠FM来实现,并且FM和DNN共享embedding;

4)从试验结果来看DeepFM效果优于wide&deep。

注:field的定义,每一个特征类别叫做field,与列对应。有多少列特征,就有多少field

推荐系统遇上深度学习系列:
推荐系统遇上深度学习(一)--FM模型理论和实践:https://www.jianshu.com/p/152ae633fb00
推荐系统遇上深度学习(二)--FFM模型理论和实践:https://www.jianshu.com/p/781cde3d5f3d

二、推荐系统遇上深度学习(三)--DeepFM模型理论和实践

转载:https://www.jianshu.com/p/6f1c2643d31b

1、背景

特征组合的挑战
对于一个基于CTR预估的推荐系统,最重要的是学习到用户点击行为背后隐含的特征组合。在不同的推荐场景中,低阶组合特征或者高阶组合特征可能都会对最终的CTR产生影响。

之前介绍的因子分解机(Factorization Machines, FM)通过对于每一维特征的隐变量内积来提取特征组合。最终的结果也非常好。但是,虽然理论上来讲FM可以对高阶特征组合进行建模,但实际上因为计算复杂度的原因一般都只用到了二阶特征组合。

那么对于高阶的特征组合来说,我们很自然的想法,通过多层的神经网络即DNN去解决。

DNN的局限
下面的图片来自于张俊林教授在AI大会上所使用的PPT。

我们之前也介绍过了,对于离散特征的处理,我们使用的是将特征转换成为one-hot的形式,但是将One-hot类型的特征输入到DNN中,会导致网络参数太多:

如何解决这个问题呢,类似于FFM中的思想,将特征分为不同的field:

再加两层的全链接层,让Dense Vector进行组合,那么高阶特征的组合就出来了

但是低阶和高阶特征组合隐含地体现在隐藏层中,如果我们希望把低阶特征组合单独建模,然后融合高阶特征组合。

即将DNN与FM进行一个合理的融合:

二者的融合总的来说有两种形式,一是串行结构,二是并行结构

而我们今天要讲到的DeepFM,就是并行结构中的一种典型代表。

2、DeepFM模型

我们先来看一下DeepFM的模型结构:

DeepFM包含两部分:神经网络部分与因子分解机部分,分别负责低阶特征的提取和高阶特征的提取。这两部分共享同样的输入。DeepFM的预测结果可以写为:

FM部分

FM部分的详细结构如下:

FM部分是一个因子分解机。关于因子分解机可以参阅文章[Rendle, 2010] Steffen Rendle. Factorization machines. In ICDM, 2010.。因为引入了隐变量的原因,对于几乎不出现或者很少出现的隐变量,FM也可以很好的学习。

FM的输出公式为:

注:如上公式第一个d实际只能取到d-1,因为取d的时候,j2已经等于d+1了,显然不符合。

采用的是FM化简之后的内积公式,最终的维度是:field_size + embedding_size

这里分别展开解释下维度的两部分是怎么来的,对于理解模型还是很重要的:

第一、field_size对应的是

这里的X是one-hot之后的,one-hot之后,我们认为X的每一列都是一个单独的维度的特征。

这里我们表达的是X的1阶特征,说白了就是单独考虑X的每个特征,他们对最终预测的影响是多少。

是多少那?是W!W对应的就是这些维度特征的权重。假设one-hot之后特征数量是feature_size,那么W的维度就是 (feature_size, 1)。

这里 是把X和W每一个位置对应相乘相加。由于X是one-hot之后的,所以相当于是进行了一次Embedding!X在W上进行一次嵌入,或者说是一次选择,选择的是W的行,按照X中不为0的那些特征对应的index,选择W中row=index的行。

这里解释下Embedding: W是一个矩阵,每一行对应X的一个维度的特征(这里是one-hot之后的维度,一定要注意)。W的列数为1,表示嵌入之后的维度是1。

W的每一行对应一个特征,相当于是我们拿输入Xi作为一个index, Xi的任意一个Field i中只有1个为1,其余的都是0。哪个位置的特征值为1,那么就选中W中对应的行,作为嵌入后这个Field i对应的新的特征表示。

对于每一个Field都执行这样的操作,就选出来了X_i Embedding之后的表示。注意到,每个Field都肯定会选出且仅选出W中的某一行(想想为什么?),因为W的列数是固定的,每一个Field都选出W.cols作为对应的新特征。

把每个Field选出来的这些W的行,拼接起来就得到了X Embedding后的新的表示:维度是num(Field) * num(W.cols)。虽然每个Field的长度可能不同,但是都是在W中选择一行,所以选出来的长度是相同的。

这也是Embedding的一个特性:虽然输入的Field长度不同,但是Embedding之后的长度是相同的。

什么?Embedding这么复杂,怎么实现的?非常简单!直接把X和W做内积即可。是的,你没看错,就是这么简单(tensoflow中封装了下,改成了tf.nn.embedding_lookup(embeddings, index),原理就是不做乘法,直接选出对应的行)。

自己写个例子试试:X.shape=(1, feature_size), W.shape = (feture_size, embedding_size)就知道为什么选出1对应W的行和做内积结果是一样的是怎么回事了。

所以:FM模块图中,黑线部分是一个全连接!W就是里面的权重。把输入X和W相乘就得到了输出。至于Addition Unit,我们就不纠结了,这里并没有做什么加法,就把他当成是反应1阶特征对输出的影响就行了。

FM论文中给出了化简后的公式:

这里最后的结果中是在[1,K]上的一个求和。 K就是W的列数,就是Embedding后的维度,也就是embedding_size。也就是说,在DeepFM的FM模块中,最后没有对结果从[1,K]进行求和。而是把这K个数拼接起来形成了一个K维度的向量。

FM Component总结:

  1. FM模块实现了对于1阶和2阶组合特征的建模。
  2. 没有使用预训练
  3. 没有人工特征工程
  4. embedding矩阵的大小是:特征数量 * 嵌入维度。 然后用一个index表示选择了哪个特征

需要训练的有两部分:

  1. input_vector和Addition Unit相连的全连接层,也就是1阶的Embedding矩阵
  2. Sparse Feature到Dense Embedding的Embedding矩阵,中间也是全连接的,要训练的是中间的权重矩阵,这个权重矩阵也就是隐向量V

深度部分

深度部分是一个前馈神经网络。与图像或者语音这类输入不同,图像语音的输入一般是连续而且密集的,然而用于CTR的输入一般是及其稀疏的。因此需要重新设计网络结构。具体实现中为,在第一层隐含层之前,引入一个嵌入层来完成将输入向量压缩到低维稠密向量。

嵌入层(embedding layer)的结构如上图所示。当前网络结构有两个有趣的特性,1)尽管不同field的输入长度不同,但是embedding之后向量的长度均为K。2)在FM里得到的隐变量Vik现在作为了嵌入层网络的权重。

这里的第二点如何理解呢,假设我们的k=5,首先,对于输入的一条记录,同一个field 只有一个位置是1,那么在由输入得到dense vector的过程中,输入层只有一个神经元起作用,得到的dense vector其实就是输入层到embedding层该神经元相连的五条线的权重,即vi1,vi2,vi3,vi4,vi5。这五个值组合起来就是我们在FM中所提到的Vi。在FM部分和DNN部分,这一块是共享权重的,对同一个特征来说,得到的Vi是相同的。

有关模型具体如何操作,我们可以通过代码来进一步加深认识。

3、相关知识

我们先来讲两个代码中会用到的相关知识吧,代码是参考的github上星数最多的DeepFM实现代码。

Gini Normalization
代码中将CTR预估问题设定为一个二分类问题,绘制了Gini Normalization来评价不同模型的效果。这个是什么东西,不太懂,百度了很多,发现了一个比较通俗易懂的介绍。

假设我们有下面两组结果,分别表示预测值和实际值:

predictions = [0.9, 0.3, 0.8, 0.75, 0.65, 0.6, 0.78, 0.7, 0.05, 0.4, 0.4, 0.05, 0.5, 0.1, 0.1]
actual = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

然后我们将预测值按照从小到大排列,并根据索引序对实际值进行排序:

Sorted Actual Values [0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1]

然后,我们可以画出如下的图片:

接下来我们将数据Normalization到0,1之间。并画出45度线。

橙色区域的面积,就是我们得到的Normalization的Gini系数。

这里,由于我们是将预测概率从小到大排的,所以我们希望实际值中的0尽可能出现在前面,因此Normalization的Gini系数越大,分类效果越好。

embedding_lookup
在tensorflow中有个embedding_lookup函数,我们可以直接根据一个序号来得到一个词或者一个特征的embedding值,那么他内部其实是包含一个网络结构的,如下图所示:

假设我们想要找到2的embedding值,这个值其实是输入层第二个神经元与embedding层连线的权重值。

之前有大佬跟我探讨word2vec输入的问题,现在也算是有个比较明确的答案,输入其实就是one-hot Embedding,而word2vec要学习的是new Embedding。

4、代码解析

好,一贯的风格,先来介绍几个地址:
原代码地址:https://github.com/ChenglongChen/tensorflow-DeepFM
本文代码地址:https://github.com/princewen/tensorflow_practice/tree/master/Basic-DeepFM-model
数据下载地址:https://www.kaggle.com/c/porto-seguro-safe-driver-prediction

好了,话不多说,我们来看看代码目录吧,接下来,我们将主要对网络的构建进行介绍,而对数据的处理,流程的控制部分,相信大家根据代码就可以看懂。

项目结构
项目结构如下:

其实还应该有一个存放data的路径。config.py保存了我们模型的一些配置。DataReader对数据进行处理,得到模型可以使用的输入。DeepFM是我们构建的模型。main是项目的入口。metrics是计算normalized gini系数的代码。

模型输入

模型的输入主要有下面几个部分:

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')

feat_index是特征的一个序号,主要用于通过embedding_lookup选择我们的embedding。feat_value是对应的特征值,如果是离散特征的话,就是1,如果不是离散特征的话,就保留原来的特征值。label是实际值。还定义了两个dropout来防止过拟合。

权重构建
权重的设定主要有两部分,第一部分是从输入到embedding中的权重,其实也就是我们的dense vector。另一部分就是深度神经网络每一层的权重。第二部分很好理解,我们主要来看看第一部分:

#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')

weights['feature_embeddings'] 存放的每一个值其实就是FM中的vik,所以它是F * K的。其中,F代表feture的大小(将离散特征转换成one-hot之后的特征总量),K代表dense vector的大小。

weights['feature_bias']是FM中的一次项的权重。

Embedding part
这个部分很简单啦,是根据feat_index选择对应的weights['feature_embeddings']中的embedding值,然后再与对应的feat_value相乘就可以了:

# 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)

FM part
首先来回顾一下我们之前对FM的化简公式,之前去今日头条面试还问到过公式的推导。

所以我们的二次项可以根据化简公式轻松的得到,再加上我们的一次项,FM的part就算完了。同时更为方便的是,由于权重共享,我们这里可以直接用Embedding part计算出的embeddings来得到我们的二次项:

# 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 order
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])

DNN part
DNNpart的话,就是将Embedding part的输出再经过几层全链接层:

# 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])

最后,我们要将DNN和FM两部分的输出进行结合:

concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)

损失及优化器
我们可以使用logloss(如果定义为分类问题),或者mse(如果定义为预测问题),以及多种的优化器去进行尝试,这些根据不同的参数设定得到:

# 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)

模型效果
前面提到了,我们用logloss作为损失函数去进行模型的参数更新,但是代码中输出了模型的 Normalization 的 Gini值来进行模型评价,我们可以对比一下(记住,Gini值越大越好呦):

好啦,本文只是提供一个引子,有关DeepFM更多的知识大家可以更多的进行学习呦。

参考资料

1、http://www.360doc.com/content/17/0315/10/10408243_637001469.shtml
2、https://blog.csdn.net/u010665216/article/details/78528261

 TensorFlow Wide And Deep 模型详解与应用(一) - 知乎一、论文概述Wide and deep 模型是 TensorFlow 在 2016 年 6 月左右发布的一类用于分类和回归的模型,并应用到了 Google Play 的应用推荐中 [1]。 wide and deep 模型的核心思想是结合线性模型的记忆能力(memoriz…https://zhuanlan.zhihu.com/p/126722504

 TensorFlow Wide And Deep 模型 - 知乎之前对 Wide And Deep 模型看过一点文章,但是没有深入了解,这两天抽出时间来仔细看了下相关代码和资料,然后写点初步的总结,总体来说还是很有意思的想法,把深度学习突破了在图像和语言领域的限制,用到了以往…https://zhuanlan.zhihu.com/p/43328492

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值