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) 此外,内积
<
v
i
,
f
j
,
v
j
,
f
i
>
<v_{i,f_j} ,v_{j,f_i}>
<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 < < k F M k_{FFM} << 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(n−1)/2 个。
这个公式猛一看显得眼花缭乱,看的时候只关注特征即可,即 < v i , f j , v j , f i > <v_{i,f_j} ,v_{j,f_i}> <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