推荐系统Field-aware Factorization Machines(FFM)
Posted 天泽28
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了推荐系统Field-aware Factorization Machines(FFM)相关的知识,希望对你有一定的参考价值。
推荐系统(四)Field-aware Factorization Machines(FFM)
推荐系统系列博客:
上一篇博客介绍了FM模型,这篇博客来介绍下FM模型的改进版FFM模型,看名字也能窥探一二,FFM模型相比较FM模型改进点在于“F”,这个F指的是Field-aware,作者也在论文中提到这个idea受rendle大佬的PITF文章[文献2]启发。这篇文章的核心思想点理解起来可能会比较绕,因为这篇博客重点的笔墨都会放在这个idea的理解上。在看这篇博客之前,请务必已经对FM模型非常熟悉,关于FM模型可以参考我的上一篇博客:推荐系统(三)Factorization Machines(FM)。
为了方便下面对FFM核心思想的讲解,我们先来造个数据集,假设我们有个数据集如下:
性别 | 年龄 | 教育水平 | label |
---|---|---|---|
男 | 45 | 初中 | 0 |
男 | 25 | 研究生 | 1 |
基于上面这个数据集,我们来对比下FM和FFM,这样大家看起来也一目了然。
FM思想: 为每个特征取值训练一个embedding向量(比如性别=男),这样在做特征二价交叉的时候(比如性别和年龄的交叉,男#45)只需要分别拿出“男”的embedding向量和“45”的embedding向量,然后做个向量内积就可以了。这样做的好处就是即使交叉的两个特征从来没在数据集中出现过,也能得到embedding向量,从而学到对应的参数。
FFM思想: 我们考虑两个二阶交叉特征:[男#45、男#初中]。在FM模型中,男#45=embedding(男)
∗
*
∗ embedding(45),男#初中=embedding(男)
∗
*
∗ embedding(初中),能够发现这两个交叉特征中,因为都有“性别=男”这个特征,在计算这两个交叉特征时,对于“性别=男”这个特征的embedding向量也是用的一样的,都是embedding(男)。 而FFM认为 [男#45、男#初中] 虽然都是“男”,但因为一个是和年龄交叉,一个是和教育水平交叉,因此如果如果用相同的embedding(男)向量是会导致信息有损。 因此FFM引入了field的概念,即对于“性别=男”这个特征取值,不再只有一个embedding向量(FM中只有一个),而是有
f
f
f个(这个
f
f
f是field的个数,比如上面这个数据集有[性别,年龄,教育水平]三个field的,因此
f
=
3
f=3
f=3),扩展来说,对于每个特征取值都不再只有一个embedding向量,都是有
f
f
f个。举个例子,还是交叉特征[男#45],因为男的filed是性别,45的field是年龄,因此,男#45=embedding(男,年龄)
∗
*
∗ embedding(45,性别) 。上面这个公式一定要理解,这是整个FFM的核心,并且对于field是双向的,也就是不能只有embedding(男,年龄),还要有embedding(45,性别)。
注:对于[性别,年龄,教育水平] 我们通常称呼为特征,为了和论文中称呼统一,在这篇博客里,我们称为field。
讲完上面这个核心思想,我们再来看看FM和FFM形式化公式的区别:
- FM
y ^ ( x ) = w 0 + ∑ i = 1 n w i x i + ∑ i = 1 n ∑ j = i + 1 n < v i , v j > x i , x j (1) \\haty(x) = w_0 + \\sum_i=1^nw_ix_i + \\sum_i=1^n\\sum_j=i+1^n<v_i,v_j>x_i,x_j \\tag1 y^(x)=w0+i=1∑nwixi+i=1∑nj=i+1∑n<vi,vj>xi,xj(1) - FFM
y ^ ( x ) = w 0 + ∑ i = 1 n w i x i + ∑ i = 1 n ∑ j = i + 1 n < v i , f j , v j , f i > x i , x j (2) \\haty(x) = w_0 + \\sum_i=1^nw_ix_i + \\sum_i=1^n\\sum_j=i+1^n<v_i,f_j,v_j,f_i>x_i,x_j \\tag2 y^(x)=w0+i=1∑nwixi+i=1∑nj=i+1∑n<vi,fj,vj,fi>xi,xj(2)
从公式2和公式1的对比中,也能发现,FFM相比较FM,仅在二阶交叉部分引入了field信息(实际上就是个side information)。从时间复杂度上来看,FM的时间复杂度可以简化至 O ( k n ) O(kn) O(kn),而FFM时间复杂度 O ( k n 2 ) O(kn^2) O(kn2),这也是FFM在工业界用的比较少的原因。从二阶交叉项系数个数来看,FM为 n k nk nk,而FFM为 n f k nfk nfk, k k k为embedding维度。因此,虽然FFM添加了field information后,相比较FM刻画的更加精细,由此也带来时间复杂度上升和过拟合问题,至于过拟合问题,论文中给出了两种解决办法:1. 添加正则项,2. 早停。
对于FFM的使用,作者在论文中也给出了建议:
- FFMs should be effective for data sets that contain categorical features and are transformed to binary features.
- If the transformed set is not sparse enough, FFMs seem to bring less benefit.
- It is more difficult to apply FFMs on numerical data sets.
翻译下也就是:
- 对于含有类别特征的数据集,需要对特征进行二值化处理,这样FFM才会比较有效
- 数据集越稀疏,FFM越有优势,也就是FFM在高维稀疏的数据集上表现比较好。
- 如果一个数据集只有连续值,则不适用于FFM。
最后来看看实现,作者给出了一个C++版本:LIBFFM,这里就不多介绍,有兴趣的可以自己看看。paddlepaddle官方也给出了paddle版本的实现 ffm,基于的数据集是Display Advertising Challenge所用的Criteo数据集,这个数据集共有13个连续值特征,26个类别型特征。我们一起来看下paddle的实现,官方的代码并没有一些维度的注释,这让人看起来着实难受,我这里给加了一些维度的注释:
class FFM(nn.Layer):
def __init__(self, sparse_feature_number, sparse_feature_dim,
dense_feature_dim, sparse_num_field):
super(FFM, self).__init__()
self.sparse_feature_number = sparse_feature_number
self.sparse_feature_dim = sparse_feature_dim
self.dense_feature_dim = dense_feature_dim
self.dense_emb_dim = self.sparse_feature_dim # 9
self.sparse_num_field = sparse_num_field
self.init_value_ = 0.1
# sparse part coding
# [1000001, 1]
self.embedding_one = paddle.nn.Embedding(
sparse_feature_number, # 1000001
1,
sparse=True,
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.TruncatedNormal(
mean=0.0,
std=self.init_value_ /
math.sqrt(float(self.sparse_feature_dim)))))
# [1000001, 9*39]
self.embedding = paddle.nn.Embedding(
self.sparse_feature_number,
self.sparse_feature_dim * self.sparse_num_field,
sparse=True,
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.TruncatedNormal(
mean=0.0,
std=self.init_value_ /
math.sqrt(float(self.sparse_feature_dim)))))
# dense part coding w
# shape(13,)
# Tensor(shape=[13], dtype=float32, place=CPUPlace, stop_gradient=False,
# [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
self.dense_w_one = paddle.create_parameter(
shape=[self.dense_feature_dim],
dtype='float32',
default_initializer=paddle.nn.initializer.Constant(value=1.0))
# shape(1, 13, 9*39)
self.dense_w = paddle.create_parameter(
shape=[
1, self.dense_feature_dim,
self.dense_emb_dim * self.sparse_num_field # 13, 9*39
],
dtype='float32',
default_initializer=paddle.nn.initializer.Constant(value=1.0))
def forward(self, sparse_inputs, dense_inputs):
"""
one sample example:
[array([0]), array([737395]), array([210498]), array([903564]), array([286224]), array([286835]),
array([906818]), array([906116]), array([67180]), array([27346]), array([51086]), array([142177]),
array([95024]), array([157883]), array([873363]), array([600281]), array([812592]), array([228085]),
array([35900]), array([880474]), array([984402]), array([100885]), array([26235]), array([410878]),
array([798162]), array([499868]), array([306163]),
array([0. , 0.00497512, 0.05 , 0.08 , 0.20742187, 0.028, 0.35 , 0.08 , 0.082 , 0.,
0.4 , 0. , 0.08 ], dtype=float32)]
:param sparse_inputs: list[array], 26 len
[array([0]), array([737395]), array([210498]), array([903564]), array([286224]), array([286835]),
array([906818]), array([906116]), array([67180]), array([27346]), array([51086]), array([142177]),
array([95024]), array([157883]), array([873363]), array([600281]), array([812592]), array([228085]),
array([35900]), array([880474]), array([984402]), array([100885]), array([26235]), array([410878]),
array([798162]), array([499868]), array([306163]),
:param dense_inputs: list 13 len
array([0. , 0.00497512, 0.05 , 0.08 , 0.20742187,
0.028 , 0.35 , 0.08 , 0.082 , 0. ,
0.4 , 0. , 0.08 ]
:return:
"""
# -------------------- first order term --------------------
# sparse_inputs, list, length 26, [Tensor(shape=[2, 1]),...,]
# [[[737395],[715353]],...] feature_name* batch_size*1
# sparse_inputs_concat, Tensor(shape=[2, 26]) ---> batch_size=2, shape[batch_size, 26]
# [[737395, 210498, 903564, 286224, 286835, 906818, 906116, 67180 , 27346 , 51086 ,
# 142177, 95024 , 157883, 873363, 600281, 812592, 228085, 35900 , 880474, 984402,
# 100885, 26235 , 410878, 798162, 499868, 306163],[]]
sparse_inputs_concat = paddle.concat(sparse_inputs, axis=1)
# shape=[batch_size, 26, 1]
# [[[-0.00620287],
# [-0.01724204],
# [-0.02544647],
# [ 0.01982319],
# [-0.03302126],
# [ 0.00377966],...,], [[],..[]]]
sparse_emb_one = self.embedding_one(sparse_inputs_concat)
# dense_inputs: shape=[batch_size, 13]
# dense_w_one: shape=[13]
# 点乘
# Tensor(shape=[2, 13], dtype=float32, place=CPUPlace, stop_gradient=False,
# [[0., 0.00497512, 0.05000000, 0.08000000, 0.20742187, 0.02800000, 0.34999999,
# 0.08000000, 0.08200000, 0., 0.40000001, 0., 0.08000000],
# [0., 0.93200666, 0.02000000, 0.14000000, 0.03956250, 0.32800001, 0.98000002,
# 0.12000000, 1.88600004, 0. , 1.79999995, 0., 0.14000000]]))
dense_emb_one = paddle.multiply(dense_inputs, self.dense_w_one) # shape=[batch_size, 13]
# shape=[batch_size, 13, 1]
# [[ [0. ],
# [0.00497512],
# [0.05000000],
# [0.08000000],
# [0.20742187],
# [0.02800000],
# [0.34999999],
# [0.08000000],
# [0.08200000],
# [0. ],
# [0.40000001],
# [0. ],
# [0.08000000]],
#
# [[0. ],
# [0.93200666],
# [0.02000000],
# [0.14000000],
# [0.03956250],
# [0.32800001],
# [0.98000002],
# [0.12000000],
# [1.88600004],
# [0. ],
# [1.79999995],
# [0. ],
# [0.14000000]]]
dense_emb_one = paddle.unsqueeze(dense_emb_one, axis=2) # shape=[batch_size, 13, 1]
# paddle.sum(sparse_emb_one, 1) --->shape=[2, 1], [[-0.13885814],[-0.21163476]]
# paddle.sum(dense_emb_one, 1) --->shape=[2, 1], [[-0.13885814], [-0.21163476]]
y_first_order = paddle.sum(sparse_emb_one, 1) + paddle.sum(
dense_emb_one, 1) # [batch_size, 1]
# -------------------Field-aware second order term --------------------
# shape=[batch_size, 26, 351]
sparse_embeddings = self.embedding(sparse_inputs_concat)
# shape=[batch_size, 13, 1], batch_size=2
dense_inputs_re = paddle.unsqueeze(dense_inputs, axis=2)
# shape=[batch_size, 13, 351]
print("==========dense_inputs_re========", dense_inputs_re)
print("=============dense_w============", self.dense_w)
dense_embeddings = paddle.multiply(dense_inputs_re, self.dense_w) # [2,13,1]*[1,13,351]=[2,13,351]
print("=============dense_embeddings============", dense_embeddings)
# shape=[batch_size, 39, 351]
feat_embeddings = paddle.concat([sparse_embeddings, dense_embeddings], 1)
# shape=[batch_size, 39, 39, 9]
field_aware_feat_embedding = paddle.reshape(
feat_embeddings,
shape=[-1, self.sparse_num_field, self.sparse_num_field, self.sparse_feature_dim])
field_aware_interaction_list = []
for i in range(self.sparse_num_field): # 39个特征,26个离散值特征+13个连续值特征
for j in range(i + 1, self.sparse_num_field): # 39
field_aware_interaction_list.append(
# sum后维度shape=[2, 1],
# [
# [0.00212428],
# [0.00286741]
# ]
# 对应着FFM二阶部分,embedding(x_i, f_j) * embedding(x_j, f_i)
paddle.sum(field_aware_feat_embedding[:, i, j, :] * # shape=[2, 9], 对应元素相乘
field_aware_feat_embedding[:, j, i, :], 1, keepdim=True))
# shape=[2, 1]
y_field_aware_second_order = paddle.add_n(field_aware_interaction_list)
return y_first_order, y_field_aware_second_order
参考文献
[1]: Juan Y, Zhuang Y, Chin W S, et al. Field-aware factorization machines for CTR prediction[C]//Proceedings of the 10th ACM conference on recommender systems. 2016: 43-50.
[2]: Rendle S, Schmidt-Thieme L. Pairwise interaction tensor factorization for personalized tag recommendation[C]//Proceedings of the third ACM international conference on Web search and data mining. 2010: 81-90.
[3]: 推荐系统Field-aware Factorization Machines(FFM)
推荐系统(十六)多任务学习:腾讯PLE模型(Progressive Layered Extraction model)
推荐系统(十六)多任务学习:腾讯PLE模型(Progressive Layered Extraction model)