zoukankan      html  css  js  c++  java
  • 推荐系统遇上深度学习

    推荐系统遇上深度学习(一)--FM模型理论和实践

     https://www.jianshu.com/p/152ae633fb00

    1、FM背景

    在计算广告和推荐系统中,CTR预估(click-through rate)是非常重要的一个环节,判断一个商品的是否进行推荐需要根据CTR预估的点击率来进行。在进行CTR预估时,除了单特征外,往往要对特征进行组合。对于特征组合来说,业界现在通用的做法主要有两大类:FM系列与Tree系列。今天,我们就来讲讲FM算法。

    2、one-hot编码带来的问题

    FM(Factorization Machine)主要是为了解决数据稀疏的情况下,特征怎样组合的问题。已一个广告分类的问题为例,根据用户与广告位的一些特征,来预测用户是否会点击广告。数据如下:(本例来自美团技术团队分享的paper)

     
     

    clicked是分类值,表明用户有没有点击该广告。1表示点击,0表示未点击。而country,day,ad_type则是对应的特征。对于这种categorical特征,一般都是进行one-hot编码处理。

    将上面的数据进行one-hot编码以后,就变成了下面这样 :

     
     

    因为是categorical特征,所以经过one-hot编码以后,不可避免的样本的数据就变得很稀疏。举个非常简单的例子,假设淘宝或者京东上的item为100万,如果对item这个维度进行one-hot编码,光这一个维度数据的稀疏度就是百万分之一。由此可见,数据的稀疏性,是我们在实际应用场景中面临的一个非常常见的挑战与问题。

    one-hot编码带来的另一个问题是特征空间变大。同样以上面淘宝上的item为例,将item进行one-hot编码以后,样本空间有一个categorical变为了百万维的数值特征,特征空间一下子暴增一百万。所以大厂动不动上亿维度,就是这么来的。

    3、对特征进行组合

    普通的线性模型,我们都是将各个特征独立考虑的,并没有考虑到特征与特征之间的相互关系。但实际上,大量的特征之间是有关联的。最简单的以电商为例,一般女性用户看化妆品服装之类的广告比较多,而男性更青睐各种球类装备。那很明显,女性这个特征与化妆品类服装类商品有很大的关联性,男性这个特征与球类装备的关联性更为密切。如果我们能将这些有关联的特征找出来,显然是很有意义的。

    一般的线性模型为:

     
     

    从上面的式子很容易看出,一般的线性模型压根没有考虑特征间的关联。为了表述特征间的相关性,我们采用多项式模型。在多项式模型中,特征xi与xj的组合用xixj表示。为了简单起见,我们讨论二阶多项式模型。具体的模型表达式如下:

     
     

    上式中,n表示样本的特征数量,xi表示第i个特征。
    与线性模型相比,FM的模型就多了后面特征组合的部分。

    4、FM求解

    从上面的式子可以很容易看出,组合部分的特征相关参数共有n(n−1)/2个。但是如第二部分所分析,在数据很稀疏的情况下,满足xi,xj都不为0的情况非常少,这样将导致ωij无法通过训练得出。

    为了求出ωij,我们对每一个特征分量xi引入辅助向量Vi=(vi1,vi2,⋯,vik)。然后,利用vivj^T对ωij进行求解。

     
     

    那么ωij组成的矩阵可以表示为:

     
     

    那么,如何求解vi和vj呢?主要采用了公式:

     
     

    具体过程如下:

     
     

    上面的式子中有同学曾经问我第一步是怎么推导的,其实也不难,看下面的手写过程(大伙可不要嫌弃字丑哟)

     
     

    经过这样的分解之后,我们就可以通过随机梯度下降SGD进行求解:

     
     

    5、tensorflow代码详解

    代码参考地址:https://github.com/babakx/fm_tensorflow/blob/master/fm_tensorflow.ipynb
    上面的代码使用的是python2编码,在python3下运行会出错,所以如果大家使用的是python3的话,可以参考我写的,其实就是修复了几个bug啦,哈哈。
    我的github地址:
    https://github.com/princewen/tensorflow_practice/tree/master/recommendation-FM-demo

    本文使用的数据是MovieLens100k Datase,数据包括四列,分别是用户ID,电影ID,打分,时间。

     
     

    输入变换

    要使用FM模型,我们首先要将数据处理成一个矩阵,矩阵的大小是用户数 * 电影数。如何根据现有的数据进行处理呢?使用的是scipy.sparse中的csr.csr_matrix,理解这个函数真的费了不少功夫呢,不过还是在下面博客(https://blog.csdn.net/u012871493/article/details/51593451)的帮助下理解了函数的原理。盗用博客中的一张图来帮助大家理解这个函数的输入:

     
     

    函数形式如下:

    csr_matrix((data, indices, indptr)
    

    可以看到,函数接收三个参数,第一个参数是数值,第二个参数是每个数对应的列号,第三个参数是每行的起始的偏移量,举上图的例子来说,第0行的起始偏移是0,第0行有2个非0值,因此第一行的起始偏移是2,第1行有两个非0值,因此第二行的起始偏移是4,依次类推。

    下面的代码是如何将原始的文件输入转换成我们的矩阵:

    def vectorize_dic(dic,ix=None,p=None,n=0,g=0):
        """
        dic -- dictionary of feature lists. Keys are the name of features
        ix -- index generator (default None)
        p -- dimension of featrure space (number of columns in the sparse matrix) (default None)
        """
        if ix==None:
            ix = dict()
    
        nz = n * g
    
        col_ix = np.empty(nz,dtype = int)
    
        i = 0
        for k,lis in dic.items():
            for t in range(len(lis)):
                ix[str(lis[t]) + str(k)] = ix.get(str(lis[t]) + str(k),0) + 1
                col_ix[i+t*g] = ix[str(lis[t]) + str(k)]
            i += 1
    
        row_ix = np.repeat(np.arange(0,n),g)
        data = np.ones(nz)
        if p == None:
            p = len(ix)
    
        ixx = np.where(col_ix < p)
        return csr.csr_matrix((data[ixx],(row_ix[ixx],col_ix[ixx])),shape=(n,p)),ix
    
    cols = ['user','item','rating','timestamp']
    
    train = pd.read_csv('data/ua.base',delimiter='	',names = cols)
    test = pd.read_csv('data/ua.test',delimiter='	',names = cols)
    
    x_train,ix = vectorize_dic({'users':train['user'].values,
                                'items':train['item'].values},n=len(train.index),g=2)
    
    
    x_test,ix = vectorize_dic({'users':test['user'].values,
                               'items':test['item'].values},ix,x_train.shape[1],n=len(test.index),g=2)
    
    
    y_train = train['rating'].values
    y_test = test['rating'].values
    
    x_train = x_train.todense()
    x_test = x_test.todense()
    

    如果不做处理,函数返回的矩阵是按如下的格式保存的:

     
     

    使用todense变换后,变成如下样式:

     
     

    估计值计算
    得到我们的输入之后,我们使用tensorflow来设计我们的模型,其实很简单啦,我们模型的估计值由两部分构成,原始的可以理解为线性回归的部分,以及交叉特征的部分,交叉特征直接使用我们最后推导的形式即可,再回顾一遍:

     
     

    因此,我们需要定义三个placeholder,分别是输入的x,输入的y,以及我们的 用户数*电影数大小的待学习的fm矩阵:

    n,p = x_train.shape
    
    k = 10
    
    x = tf.placeholder('float',[None,p])
    
    y = tf.placeholder('float',[None,1])
    
    w0 = tf.Variable(tf.zeros([1]))
    w = tf.Variable(tf.zeros([p]))
    
    v = tf.Variable(tf.random_normal([k,p],mean=0,stddev=0.01))
    
    #y_hat = tf.Variable(tf.zeros([n,1]))
    
    linear_terms = tf.add(w0,tf.reduce_sum(tf.multiply(w,x),1,keep_dims=True)) # n * 1
    pair_interactions = 0.5 * tf.reduce_sum(
        tf.subtract(
            tf.pow(
                tf.matmul(x,tf.transpose(v)),2),
            tf.matmul(tf.pow(x,2),tf.transpose(tf.pow(v,2)))
        ),axis = 1 , keep_dims=True)
    
    
    y_hat = tf.add(linear_terms,pair_interactions)
    
    

    定义损失函数

    这里我们定义的损失函数除了平方损失外,还加了l2正则项,并使用梯度下降法进行参数的更新:

    lambda_w = tf.constant(0.001,name='lambda_w')
    lambda_v = tf.constant(0.001,name='lambda_v')
    
    l2_norm = tf.reduce_sum(
        tf.add(
            tf.multiply(lambda_w,tf.pow(w,2)),
            tf.multiply(lambda_v,tf.pow(v,2))
        )
    )
    
    error = tf.reduce_mean(tf.square(y-y_hat))
    loss = tf.add(error,l2_norm)
    
    
    train_op = tf.train.GradientDescentOptimizer(learning_rate=0.01).minimize(loss)
    
    

    模型训练
    接下来就是训练啦,这段代码比较好理解:

    epochs = 10
    batch_size = 1000
    
    # Launch the graph
    init = tf.global_variables_initializer()
    with tf.Session() as sess:
        sess.run(init)
    
        for epoch in tqdm(range(epochs), unit='epoch'):
            perm = np.random.permutation(x_train.shape[0])
            # iterate over batches
            for bX, bY in batcher(x_train[perm], y_train[perm], batch_size):
                _,t = sess.run([train_op,loss], feed_dict={x: bX.reshape(-1, p), y: bY.reshape(-1, 1)})
                print(t)
    
    
        errors = []
        for bX, bY in batcher(x_test, y_test):
            errors.append(sess.run(error, feed_dict={x: bX.reshape(-1, p), y: bY.reshape(-1, 1)}))
            print(errors)
        RMSE = np.sqrt(np.array(errors).mean())
        print (RMSE)
    

    参考文章:

    1、http://blog.csdn.net/bitcarmanlee/article/details/52143909
    2、https://blog.csdn.net/u012871493/article/details/51593451

     


    作者:石晓文的学习日记
    链接:https://www.jianshu.com/p/152ae633fb00
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
     

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

    https://www.jianshu.com/p/781cde3d5f3d

    1、FFM理论

    在CTR预估中,经常会遇到one-hot类型的变量,one-hot类型变量会导致严重的数据特征稀疏的情况,为了解决这一问题,在上一讲中,我们介绍了FM算法。这一讲我们介绍一种在FM基础上发展出来的算法-FFM(Field-aware Factorization Machine)。

    FFM模型中引入了类别的概念,即field。还是拿上一讲中的数据来讲,先看下图:

     
     

    在上面的广告点击案例中,“Day=26/11/15”、“Day=1/7/14”、“Day=19/2/15”这三个特征都是代表日期的,可以放到同一个field中。同理,Country也可以放到一个field中。简单来说,同一个categorical特征经过One-Hot编码生成的数值特征都可以放到同一个field,包括用户国籍,广告类型,日期等等。

    在FFM中,每一维特征 xi,针对其它特征的每一种field fj,都会学习一个隐向量 v_i,fj。因此,隐向量不仅与特征相关,也与field相关。也就是说,“Day=26/11/15”这个特征与“Country”特征和“Ad_type"特征进行关联的时候使用不同的隐向量,这与“Country”和“Ad_type”的内在差异相符,也是FFM中“field-aware”的由来。

    假设样本的 n个特征属于 f个field,那么FFM的二次项有 nf个隐向量。而在FM模型中,每一维特征的隐向量只有一个。FM可以看作FFM的特例,是把所有特征都归属到一个field时的FFM模型。根据FFM的field敏感特性,可以导出其模型方程。

     
     

    可以看到,如果隐向量的长度为 k,那么FFM的二次参数有 nfk 个,远多于FM模型的 nk个。此外,由于隐向量与field相关,FFM二次项并不能够化简,其预测复杂度是 O(kn^2)。

    下面以一个例子简单说明FFM的特征组合方式。输入记录如下:

     
     

    这条记录可以编码成5个特征,其中“Genre=Comedy”和“Genre=Drama”属于同一个field,“Price”是数值型,不用One-Hot编码转换。为了方便说明FFM的样本格式,我们将所有的特征和对应的field映射成整数编号。

     
     

    那么,FFM的组合特征有10项,如下图所示。

     
     

    其中,红色是field编号,蓝色是特征编号。

    2、FFM实现细节

    这里讲得只是一种FFM的实现方式,并不是唯一的。

    损失函数
    FFM将问题定义为分类问题,使用的是logistic loss,同时加入了正则项

     
     

    什么,这是logisitc loss?第一眼看到我是懵逼的,逻辑回归的损失函数我很熟悉啊,不是长这样的啊?其实是我目光太短浅了。逻辑回归其实是有两种表述方式的损失函数的,取决于你将类别定义为0和1还是1和-1。大家可以参考下下面的文章:https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/6340129.html。当我们将类别设定为1和-1的时候,逻辑回归的损失函数就是上面的样子。

    随机梯度下降

    训练FFM使用的是随机梯度下降方法,即每次只选一条数据进行训练,这里还有必要补一补梯度下降的知识,梯度下降是有三种方式的,截图取自参考文献3:

     
     

    总给人一种怪怪的感觉。batch为什么是全量的数据呢,哈哈。

    3、tensorflow实现代码

    本文代码的github地址:
    https://github.com/princewen/tensorflow_practice/tree/master/recommendation-FFM-Demo

    这里我们只讲解一些细节,具体的代码大家可以去github上看:

    生成数据
    这里我没有找到合适的数据,就自己产生了一点数据,数据涉及20维特征,前十维特征是一个field,后十维是一个field:

    def gen_data():
        labels = [-1,1]
        y = [np.random.choice(labels,1)[0] for _ in range(all_data_size)]
        x_field = [i // 10 for i in range(input_x_size)]
        x = np.random.randint(0,2,size=(all_data_size,input_x_size))
        return x,y,x_field
    

    定义权重项
    在ffm中,有三个权重项,首先是bias,然后是一维特征的权重,最后是交叉特征的权重:

    def createTwoDimensionWeight(input_x_size,field_size,vector_dimension):
        weights = tf.truncated_normal([input_x_size,field_size,vector_dimension])
    
        tf_weights = tf.Variable(weights)
    
        return tf_weights
    
    def createOneDimensionWeight(input_x_size):
        weights = tf.truncated_normal([input_x_size])
        tf_weights = tf.Variable(weights)
        return tf_weights
    
    def createZeroDimensionWeight():
        weights = tf.truncated_normal([1])
        tf_weights = tf.Variable(weights)
        return tf_weights
    

    计算估计值
    估计值的计算这里不能项FM一样先将公式化简再来做,对于交叉特征,只能写两重循环,所以对于特别多的特征的情况下,真的计算要爆炸呀!

    def inference(input_x,input_x_field,zeroWeights,oneDimWeights,thirdWeight):
        """计算回归模型输出的值"""
    
        secondValue = tf.reduce_sum(tf.multiply(oneDimWeights,input_x,name='secondValue'))
    
        firstTwoValue = tf.add(zeroWeights, secondValue, name="firstTwoValue")
    
        thirdValue = tf.Variable(0.0,dtype=tf.float32)
        input_shape = input_x_size
    
        for i in range(input_shape):
            featureIndex1 = I
            fieldIndex1 = int(input_x_field[I])
            for j in range(i+1,input_shape):
                featureIndex2 = j
                fieldIndex2 = int(input_x_field[j])
                vectorLeft = tf.convert_to_tensor([[featureIndex1,fieldIndex2,i] for i in range(vector_dimension)])
                weightLeft = tf.gather_nd(thirdWeight,vectorLeft)
                weightLeftAfterCut = tf.squeeze(weightLeft)
    
                vectorRight = tf.convert_to_tensor([[featureIndex2,fieldIndex1,i] for i in range(vector_dimension)])
                weightRight = tf.gather_nd(thirdWeight,vectorRight)
                weightRightAfterCut = tf.squeeze(weightRight)
    
                tempValue = tf.reduce_sum(tf.multiply(weightLeftAfterCut,weightRightAfterCut))
    
                indices2 = [I]
                indices3 = [j]
    
                xi = tf.squeeze(tf.gather_nd(input_x, indices2))
                xj = tf.squeeze(tf.gather_nd(input_x, indices3))
    
                product = tf.reduce_sum(tf.multiply(xi, xj))
    
                secondItemVal = tf.multiply(tempValue, product)
    
                tf.assign(thirdValue, tf.add(thirdValue, secondItemVal))
    
        return tf.add(firstTwoValue,thirdValue)
    

    定义损失函数
    损失函数我们就用逻辑回归损失函数来算,同时加入正则项:

    lambda_w = tf.constant(0.001, name='lambda_w')
    lambda_v = tf.constant(0.001, name='lambda_v')
    
    zeroWeights = createZeroDimensionWeight()
    
    oneDimWeights = createOneDimensionWeight(input_x_size)
    
    thirdWeight = createTwoDimensionWeight(input_x_size,  # 创建二次项的权重变量
                                           field_size,
                                           vector_dimension)  # n * f * k
    
    y_ = inference(input_x, trainx_field,zeroWeights,oneDimWeights,thirdWeight)
    
    l2_norm = tf.reduce_sum(
        tf.add(
            tf.multiply(lambda_w, tf.pow(oneDimWeights, 2)),
            tf.reduce_sum(tf.multiply(lambda_v, tf.pow(thirdWeight, 2)),axis=[1,2])
        )
    )
    
    loss = tf.log(1 + tf.exp(input_y * y_)) + l2_norm
    
    train_step = tf.train.GradientDescentOptimizer(learning_rate=lr).minimize(loss)
    

    训练
    接下来就是训练了,每次只用喂一个数据就好:

    input_x_batch = trainx[t]
    input_y_batch = trainy[t]
    predict_loss,_, steps = sess.run([loss,train_step, global_step],
                             feed_dict={input_x: input_x_batch, input_y: input_y_batch})
    

    跑的是相当的慢,我们来看看效果吧:

     
     

    参考文章

    1、https://tech.meituan.com/deep-understanding-of-ffm-principles-and-practices.html
    2、https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/6340129.html
    3、https://www.cnblogs.com/pinard/p/5970503.html



    作者:石晓文的学习日记
    链接: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的输出公式为:

     
     

    深度部分

     
     

    深度部分是一个前馈神经网络。与图像或者语音这类输入不同,图像语音的输入一般是连续而且密集的,然而用于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

     


    作者:石晓文的学习日记
    链接:https://www.jianshu.com/p/6f1c2643d31b
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    常用模块的作业
    一些常用的模块
    软件开发的目录规范

    匿名函数作业
    模块
    算法,面向过程与匿名函数
    生成器表达式作业
    C. Perfect Keyboard
    C. Ehab and Path-etic MEXs
  • 原文地址:https://www.cnblogs.com/DjangoBlog/p/10749104.html
Copyright © 2011-2022 走看看