推荐系统 - 基于FFM算法的协同召回算法

理论参考:https://www.jianshu.com/p/781cde3d5f3d

https://www.cnblogs.com/zhangchaoyang/articles/8157893.html

说明

1.在之前的文章中,已经讲述过FM的概念,而FFM是在FM基础上做了一些小的改进得到的,引入了Field(域)的概念,域表示特征所属的类型。域和特征之间是一种1:n的关系,多个特征可以同属一个域,是一种抽象的概念,比如一些one-hot生成的列名 性别=男 和性别=女,也属于同一性别field,引用别人博客中的图,使用Field相当于引入一种分而治之的思想,这样大大减少多阶关联挖掘的参数。

    对于离散特征,采用one-hot编码,同一种属性的归到一个Field,不论是连续特征还是离散特征,它们都有一个共同点:同一个field下只有一个feature的值不是0,其他feature的值都是0。

 

                                          

2.通过引入域的概念后,我们可以在FM进行特征间二阶关系挖掘基础上,加上特征与域之间的特征挖掘,从而可以将原来的FM式子改成如下的形式,主要的改变在最后的权重上,权重原来的n*k变成了n*f*k,之前存储一个隐向量矩阵,而现在要存储f个图隐向量矩阵,其中n是特征数量,f是域的数量,k是隐变量的长度,现在的vivi不仅跟xixi有关系,还跟与xixi相乘的xjxj所属的Field有关系。                      

                                                

3.通过以上改进,FFM实际相比FM做了更多的信息挖掘,而且可以对不同特点、不同数据类型的特征进行区别对待,之前的FM可以看做只有一个Field的个例,但是因为增加了权值的数量,所以FFM相对FM,速度稍慢一些。

源码

该代码中数据集是自己生成的,设定生成

       特征数量是20,  field的数量是2, 隐变量v的长度是3

程序主要思路:

       (1) 生成数据集,指定 field特征标识匹配

       (2)定义在FFM公式中起主要作用的三个变量,三个权重项,首先是bias,然后是一维特征的权重,最后是交叉特征的权重。 三者分别定义 模型方程中的 w0、wi 和最后相乘的那几个部分。可详见链接中的公式y(x) 这里在最后一部分部分会牵扯到隐向量长度k,隐向量作用是针对每一维特征 xi,针对其它特征的每一种field fj, 都会学习一个隐向量 v_i,fj, ,共有n*f个长度为k的隐变量,第三部分参数是牵扯到nfk的。

       (3)进行网络的训练阶段,使用输入 数据和 定义的权重进行FFM公式的计算,主要对应在inference的计算中。估计值的计算 这里不能像FM一样先将公式化简再来做,对于交叉特征,只能写两重循环。【核心部分吧,对变量和输入参数的组织】             

       (4)定义损失函数,并定义正则性,进行网络外部循环的训练。

代码:

import tensorflow as tf
import pandas as pd
import numpy as np
import os
'''
   原版代码解读:https://www.jianshu.com/p/781cde3d5f3d (好吧,估计这几个模型,全都是这一系列下对应的源码)
   
   在 FM基础上 引入了  将特征归属不同field的性质,这样  就相当于在之前FM式中加上了field
   field跟特征之间是  1:n的关系,也就是多个特征是属于同一个field的,我们在做特征交互时,也要考虑特征域所带来的隐向量作用关系



    看了下推荐系统遇上深度学习(二)--FFM模型理论和实践  就明白了,链接和这个代码是配套的。
    
    首先是(1) 生成数据集,指定 field匹配
          (2)定义在FFM公式中起主要作用的三个变量,三个权重项,首先是bias,然后是一维特征的权重,最后是交叉特征的权重。
          
                三者分别定义  模型方程中的   w0、wi   和最后相乘的那几个部分。可详见链接中的公式y(x)
                
                这里在最后一部分部分会牵扯到隐向量长度k,隐向量作用是针对每一维特征 xi,针对其它特征的每一种field fj,
                都会学习一个隐向量 v_i,fj, ,共有n*f个长度为k的隐变量,第三部分参数是牵扯到nfk的。
          (3)进行网络的训练阶段,使用输入 数据和  定义的权重进行FFM公式的计算,主要对应在inference的计算中。估计值的计算
          这里不能像FM一样先将公式化简再来做,对于交叉特征,只能写两重循环。【核心部分吧,对变量和输入参数的组织】
          (4)定义损失函数,并定义正则性,进行网络外部循环的训练。
          
    
    可以看出其核心就是 在FM公式基础上,加上了Field的概念,不同的特征可以归属同一个Field,这样进行进一步Field划分的原因是因为细化不同
    类型含义间特征的区别意义,   其相对于FM模型的特点是FM可以看作FFM的特例,是把所有特征都归属到一个field时的FFM模型。根据FFM的field敏感特性,
    可以导出其模型方程。
    
'''

#这里  定义有两个不用不同的field,  部分特征属于一种field,另一部分特征属于其他field.
#三个重要度维度的定义, 这用于进行权重的定义, 其中20是特征数量,  2是field的数量, 3是指 隐变量v的长度
input_x_size = 20
field_size = 2
vector_dimension = 3

total_plan_train_steps = 1000
# 使用SGD,每一个样本进行依次梯度下降,更新参数
batch_size = 1

all_data_size = 1000

lr = 0.01

MODEL_SAVE_PATH = "TFModel"
MODEL_NAME = "FFM"

def createTwoDimensionWeight(input_x_size,field_size,vector_dimension):
    #对 第三部分矩阵的定义,这个是最复杂的部分, 对应在公式中,隐变量长度是k个,FFM二次参数是nfk个, n表示特征数、f表示域数量、k表示假定的隐向量长度
    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

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
    #可以严格按照公式去看,  外面的加是 i,内部循环是 i+1
    for i in range(input_shape):
        featureIndex1 = i
        fieldIndex1 = int(input_x_field[i])
        for j in range(i+1,input_shape):

            ########################第一部分  vi vj部分
            featureIndex2 = j
            fieldIndex2 = int(input_x_field[j])

            #Vi,f计算   注意看到公式中和下面计算上的区别,[featureIndex1,fieldIndex2,i]两个是反着的,表示不同的对应向量。
            vectorLeft = tf.convert_to_tensor([[featureIndex1,fieldIndex2,i] for i in range(vector_dimension)])
            weightLeft = tf.gather_nd(thirdWeight,vectorLeft) #允许在多维上进行索引,提取出矩阵。这是按照vectorLeft指定的index位置提取矩阵数据
            weightLeftAfterCut = tf.squeeze(weightLeft)#该函数返回一个张量,这个张量是将原始input中所有维度为1的那些维都删掉的结果
            # Vj,f计算
            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))

            #######################内部训练的 xi * xj 的部分    会获取不同的用于交叉的特征值。
            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)
            # 对第三部分结果的累加,thirdValue  是不断进行积累的。  assign就是一个赋值
            tf.assign(thirdValue, tf.add(thirdValue, secondItemVal))

    return tf.add(firstTwoValue,thirdValue)

def gen_data():
    '''
    这点其实有点不太好, 数据都是随机生成的,不太好捕捉 field所起到的不同feature作用
    '''
    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))

    print('生成的特征:',x.shape,x)
    print('随机生成的分类标签:',len(y),y)
    print('生成的每个特征所属的field,没有具体特征含义就可以这么任性:',x_field)
    return x,y,x_field


if __name__ == '__main__':
    global_step = tf.Variable(0,trainable=False)
    trainx,trainy,trainx_field = gen_data()
    #
    input_x = tf.placeholder(tf.float32,[input_x_size ])
    input_y = tf.placeholder(tf.float32)
    #

    lambda_w = tf.constant(0.001, name='lambda_w')
    lambda_v = tf.constant(0.001, name='lambda_v')

    ### 这三个Weights是用来干嘛的呢
    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)

    saver = tf.train.Saver()
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())

        #共进行了1000轮对 FFM网络的训练。
        for i in range(total_plan_train_steps):
            for t in range(all_data_size):
                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})

                print("After  {step} training   step(s)   ,   loss    on    training    batch   is  {predict_loss} "
                      .format(step=steps, predict_loss=predict_loss))

                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME), global_step=steps)
                writer = tf.summary.FileWriter(os.path.join(MODEL_SAVE_PATH, MODEL_NAME), tf.get_default_graph())
                writer.close()
        #

















 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值