《Field-aware Factorization Machines for CTR Prediction》FFM模型整理及python代码

1 原文(点击下载)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2 FFM模型

FFM(Field-aware Factorization Machine)最初的概念来自Yu-Chin Juan(阮毓钦,毕业于中国台湾大学,现在美国Criteo工作)与其比赛队员,是他们借鉴了来自Michael Jahrer的论文[14]中的field概念提出了FM的升级版模型。通过引入field的概念,FFM把相同性质的特征归于同一个field。以上面的广告分类为例,“Day=26/11/15”、“Day=1/7/14”、“Day=19/2/15”这三个特征都是代表日期的,可以放到同一个field中。同理,商品的末级品类编码生成了550个特征,这550个特征都是说明商品所属的品类,因此它们也可以放到同一个field中。简单来说,同一个categorical特征经过One-Hot编码生成的数值特征都可以放到同一个field,包括用户性别、职业、品类偏好等。在FFM中,每一维特征 x i x_i xi,针对其他特征所在field f j f_j fj,都会学习一个隐向量 v i , f j v_{i,f_j} vi,fj,因此,隐向量不仅与特征相关,也与field相关。也就是说,“Day=26/11/15”这个特征与“Country”特征和“Ad_type"特征进行关联的时候使用不同的隐向量,这“Country”和“Ad_type”的内在差异相符,也是FFM中“field-aware”的由来。

假设样本的 n n n个特征属于 f f f个field,那么FFM的二次项有 n f nf nf个隐向量。而在FM模型中,每一维特征的隐向量只有一个。FM可以看作FFM的特例,是把所有特征都归属到一个field时的FFM模型。根据FFM的field敏感特性,可以导出其模型方程。
在这里插入图片描述
其中 f j f_j fj是第 j个特征所属的field。如果隐向量的长度为 k k k,那么FFM的二次参数有 n f k nfk nfk 个,远多于FM模型的 n k nk nk个。此外,由于隐向量与field相关,FFM二次项并不能够化简,其预测复杂度是 O ( k n 2 ) O(kn^2) O(kn2) 此外,内积 &lt; v i , f j , v j , f i &gt; &lt;v_{i,f_j} ,v_{j,f_i}&gt; <vi,fj,vj,fi>表示让特征 i 与 特征 j j的 field 关联,同时让特征 j 与 i i的 field 关联,由此可见,FM的交叉是针对特征之间的,而FFM是针对特征与 field 之间的。

正是因为FM可以看成FFM中所有特征都属于同一个 field 的特例,故而FM中的每个特征都使用一个隐向量进行表达,而FFM则对每个特征是使用 f 个隐向量表达,因为FFM一共有f个field。另外,值得强调的是,论文中尤其指出,虽然 FFM 相比于 FM,其时间复杂度提升到为 O ( k n 2 ) O(kn^2) O(kn2) ,参数数量提升到 n f k nfk nfk 个,但对每个特征的 f 个 k长度的隐向量表示,其长度 k 相较于FM有了大幅降低,即文中的: k F F M &lt; &lt; k F M k_{FFM} &lt;&lt; k_{FM} kFFM<<kFM,即每个特征表达从 1 个长度 k F M k_{FM} kFM 的表达式变成了 f 个长度为 k F F M k_{FFM} kFFM的表达式

2.1 举个栗子

在这里插入图片描述
这条记录可以编码成5个特征,其中“Genre=Comedy”和“Genre=Drama”属于同一个field,“Price”是数值型,不用One-Hot编码转换。为了方便说明FFM的样本格式,我们将所有的特征和对应的field映射成整数编号。
在这里插入图片描述
FFM组合特征有10项:
在这里插入图片描述
其中,红色是field编号,蓝色是特征编号,绿色是此样本的特征取值。二次项的系数是通过与特征field相关的隐向量点积得到的,二次项共有 n ( n − 1 ) / 2 n(n−1)/2 n(n1)/2 个。

这个公式猛一看显得眼花缭乱,看的时候只关注特征即可,即 &lt; v i , f j , v j , f i &gt; &lt;v_{i,f_j} ,v_{j,f_i}&gt; <vi,fj,vj,fi>中的特征 v下标 i, j​,公式第一行就是 i=1, j=2⋯5​ ,再填补特征 v​的右下角标 fi,fj 。

2.2 优化求解

采用随机梯度下降求解参数,即每次只选一条数据进行训练。

FFM进行二分类时(1, -1),模型的损失函数为logistic loss:
在这里插入图片描述
注意,logistic loss有两种表达式,另一种参见逻辑回归,类别为(1, 0)时的损失函数。

2.3 增加Field信息

对LIBSVM数据组织形式:

Lable feat1:val1 feat2:val2 ...,

其中(feat, val) 对指示特征索引和值,而对于FFM,将数据组织形式扩展为:

label filed1:feat1:val1 field2:feat2:val2 ...

即将相应的域 filed 分配到每个特征。

2.3.1 Categorical Features

对线型模型,分类特征通常转换为多个二进制特征。

Yes P:ESPN A:Nike G:Male

Yes P-ESPN:1 A-Nike:1 G-Male:1

将每个类别作为一个filed,然后以上数据实例就变为:

Yes P:P-ESPN:1 A:A-Nike:1 G:G-Male:1

2.3.2 Numerical Features

考虑以下示例:

AR: accept rate of the conference Hidx: h-index of the author Cite:
number of citations of the author

一共有两个可能方式分配filed,

naive的方式是将每个特征作为dummy filed,生成的数据如下:

Yes AR:AR:45.73 Hidx:Hidx:2 Cite:Cite:3

然后,dummy filed可能不会增加判定信息, 因为它们只是特征的重复。

另一种方式是离散化数值特征为类别,使用如同对待类别特征的设置添加filed信息,生成的数据如下形式:

Yes AR:45:1 Hidex:2:1 Cite:3:1

其中AR特征四舍五入为整数。

缺点:这种方式的缺点是难以判定最佳的离散设置,如到底是将45.73设置为45.7、45、40或者是"int(log(45.73))",此外,离散化可能会损失信息。

2.3.3Single-filed Features

一些数据集上,所有特征属于单个filed,这样将导致增加filed域毫无意义,这种情况多出来在NLP数据集中。考虑以下样本:
在这里插入图片描述

2.4 FFM使用指导原则

FFM包含大量类别型特征的数据集更有效,需要将类别型特征进行dummy编码(转换成了特征稀疏形式);

如果转换后的数据集不足够稀疏,则FFM的field域提升效果不明显(极端示例,NLP中常出现的一个文本特征集对应一个filed,此时field起不到作用);

将FFM适用到数值型特征数据集上没有明显优势(事实上,作者实验验证得到,将FFM适用到全数值特征,当采用dummy filed,即将数值特征当做类别特征进行处理时候,FFM与FM表现近似,filed域起不到作用;而若将数值特征进行离散化处理之后,FFM表现比FM要好,但都差于直接使用dummy filed的效果)。

总结一句话就是FFM,FM均是针对大规模、特征类、稀疏场景(可通过对类别进行dummy编码得到)具有明显优势。

2.5 应用

在DSP的场景中,FFM主要用来预估站内的CTR和CVR,即一个用户对一个商品的潜在点击率和点击后的转化率。

CTR和CVR预估模型都是在线下训练,然后用于线上预测。两个模型采用的特征大同小异,主要有三类:用户相关的特征、商品相关的特征、以及用户-商品匹配特征。用户相关的特征包括年龄、性别、职业、兴趣、品类偏好、浏览/购买品类等基本信息,以及用户近期点击量、购买量、消费额等统计信息。商品相关的特征包括所属品类、销量、价格、评分、历史CTR/CVR等信息。用户-商品匹配特征主要有浏览/购买品类匹配、浏览/购买商家匹配、兴趣偏好匹配等几个维度。

为了使用FFM方法,所有的特征必须转换成“field_id:feat_id:value”格式,field_id代表特征所属field的编号,feat_id是特征编号,value是特征的值。数值型的特征比较容易处理,只需分配单独的field编号,如用户评论得分、商品的历史CTR/CVR等。categorical特征需要经过One-Hot编码成数值型,编码产生的所有特征同属于一个field,而特征的值只能是0或1,如用户的性别、年龄段,商品的品类id等。除此之外,还有第三类特征,如用户浏览/购买品类,有多个品类id且用一个数值衡量用户浏览或购买每个品类商品的数量。这类特征按照categorical特征处理,不同的只是特征的值不是0或1,而是代表用户浏览或购买数量的数值。按前述方法得到field_id之后,再对转换后特征顺序编号,得到feat_id,特征的值也可以按照之前的方法获得。

CTR、CVR预估样本的类别是按不同方式获取的。CTR预估的正样本是站内点击的用户-商品记录,负样本是展现但未点击的记录;CVR预估的正样本是站内支付(发生转化)的用户-商品记录,负样本是点击但未支付的记录。构建出样本数据后,采用FFM训练预估模型,并测试模型的性能。

在这里插入图片描述
由于模型是按天训练的,每天的性能指标可能会有些波动,但变化幅度不是很大。这个表的结果说明,站内CTR/CVR预估模型是非常有效的。

在训练FFM的过程中,有许多小细节值得特别关注。

1、样本归一化。对样本进行归一化,否则容易造成数据溢出,梯度计算失败
2、特征归一化。为了消除不同特征取值范围不同造成的问题,需要对特征进行归一化
3、Early stopping。一定要设置该策略,FFM很容易过拟合
4、省略零值特征。零值特征对模型没有任何贡献,省略零值特征,可以提高FFM模型训练和预测的速度,这也是稀疏样本采用FFM的显著优势

3 python实现

1导入包

import tensorflow as tf
import numpy as np
import pandas as pd
import os

2、设置权重参数

def create2DimensionalWeight(input_x_size, field_size, vector_dimension):
    return tf.Variable(tf.truncated_normal([input_x_size, field_size, vector_dimension]))
def create1DimensionalWeight(input_x_size):
    return tf.Variable(tf.truncated_normal([input_x_size]))
def create0DimensionalWeight():
    return tf.Variable(tf.truncated_normal([1]))

3、预测

def inference(input_x, input_x_field, zero_D_Weights, one_D_Weights, third_D_Weigths):
    secondValue = tf.reduce_sum(tf.multiply(one_D_Weights, input_x))
    firstTwoValue = tf.add(zero_D_Weights, secondValue)
    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(third_D_Weigths, vectorLeft) # 在third_D_weights中查找索引为vectorLeft的值
            weightLeftAfterCut = tf.squeeze(weightLeft) # 删除尺寸为 1  的维度
            vectorRight = tf.convert_to_tensor([[featureIndex2, fieldIndex1, i] for i in range(vector_dimension)])
            weightRight = tf.gather_nd(third_D_Weigths, vectorRight) # 在third_D_weights中查找索引为vectorRight的值
            weightRightAfterCut = tf.squeeze(weightRight) # 删除尺寸为 1  的维度
            tempValue = tf.reduce_sum(tf.multiply(weightLeftAfterCut, weightRightAfterCut)) # 计算两个向量的点积
            xi = tf.squeeze(tf.gather_nd(input_x, indices=[i]))
            xj = tf.squeeze(tf.gather_nd(input_x, indices=[j]))
            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)

4、生成数据, 两类标签, 每个数据input_x_size个特征,属于两个field,每个field10个特征

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

5、设置损失函数,

def Loss(y_, one_D_Weights, third_D_Weigths):
    l2_norm = tf.reduce_sum(
    tf.add(
        tf.multiply(lambda_w, tf.pow(one_D_Weights, 2)),
        tf.reduce_sum(tf.multiply(lambda_v, tf.pow(third_D_Weigths, 2)),axis=[1,2])
        )
    )
    return tf.log(1 + tf.exp(input_y * y_)) + l2_norm

6、训练

def train(loss):
    return tf.train.GradientDescentOptimizer(learning_rate=lr).minimize(loss,  global_step=global_step)

7、运行

if __name__ == '__main__':
    input_x_size = 20 # 20个特征
    field_size = 2 # 两个field
    vector_dimension = 3 # 向量
    total_plan_train_steps = 20
    batch_size = 1 # 使用SGD,每一个样本进行依次梯度下降,更新参数
    all_data_size = 50 # 数据集规模
    lr = 0.01
    MODEL_SAVE_PATH = "FFM_Model"
    MODEL_NAME = "FFM"
    input_x = tf.placeholder(shape=[input_x_size], dtype=tf.float32)
    input_y = tf.placeholder(dtype=tf.float32)
    lambda_w = tf.constant(0.001)
    lambda_v = tf.constant(0.001)
    global_step = tf.Variable(0, trainable=True)  
    zero_D_Weights = create0DimensionalWeight()
    one_D_Weights = create1DimensionalWeight(input_x_size)
    third_D_Weigths = create2DimensionalWeight(input_x_size, field_size, vector_dimension)
    trainx, trainy, trainx_field = generate_data(all_data_size, input_x_size)
    y_ = inference(input_x, trainx_field, zero_D_Weights, one_D_Weights, third_D_Weigths)
    loss = Loss(y_, one_D_Weights, third_D_Weigths)
    train_step = train(loss)
    saver = tf.train.Saver()
    sess = tf.Session()
    init = tf.global_variables_initializer()
    sess.run(init)  
    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=i, 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()

在这里插入图片描述

巨人

1、石晓文的学习日记:https://www.jianshu.com/p/781cde3d5f3d
2、代码: https://github.com/princewen/tensorflow_practice/blob/master/recommendation/recommendation-FFM-Demo/FFM_model.py
3、非常详细的介绍: https://blog.csdn.net/dby_freedom/article/details/84899120
4、3 的 代码: https://github.com/LLSean/data-mining/blob/master/ffm/util.py
5、原文链接: https://dl.acm.org/citation.cfm?id=2959134

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值