dEEPFM 论文阅读
附上论文连接:DeepFM: A Factorization-Machine based Neural Network for CTR Prediction
学习复杂的功能交互背后的用户行为是至关重要的对于最大化CTR推荐系统。现存的模型对于低阶或高阶的特征交互有很强的依赖,需要专家做大量的特征工程。在这篇文章中,我们同时注重低阶和高阶的特征交互的端到端的学习模型是可能的。DeepFM结合了因子分解机的推荐能力以及深度学习的特征学习能力,形成了一种新的神经网络结构,比较谷歌的"Wide&Deep model,我们不需要对原始数据做特征工程。综合所有实验,验证了DeepFM模型在CTR预测中的有效性核高效性。
1 Introduction
预测点击率对于推荐系统是至关重要的,推荐系统根据用户点击的概率来进行推荐商品。大多数推荐系统的目标是最大化点击数量,因此商品的推荐应该根据CTR来评估;在其他应用场景,例如提高广告收入也是非常重要的,所以广告排序的策略应该根据CTR x bid 来决定,这里的“bid”
是当用户点击后系统得到的奖励(租金),无论是哪种场景,很明显,最重要的是评估CTR的正确性。
学习隐藏在用户点击行为背后的特征对于CTR的预测是非常重要的。例如我们发现,在午餐时间,食物相关的APP经常会被下载,APP的类别和时间戳的交互就可以看为CTR模型的第二阶特征。另外我们发现男性的青年喜欢射击和RPG游戏,因此年龄和性别和APP种类的交互可以堪为第三阶特征。通常这种隐藏在行为背后的特征的交互是非常复杂的,低阶与高阶交互扮演着重要的角色。根据 Wide & Deep model [Cheng et al., 2016]同时考虑低阶和高阶特征交互比单独只考虑任一种情况会带来额外的改进。
模型性能的关键取决于特征的交互。一些特征交互比较好理解,比如之前提到的特征,但是大多数特征交互是隐藏在数据背后很难被发现的(比如“尿布”和“啤酒”的关系是从数据中挖掘的,而非专家发现的),这只能通过机器学习的方法自动挖掘。即使是一些容易理解的特征,专家也不可能花大量时间把所有的特征都做出来。
尽管一些简单的广义线性模型,例如FTRL [McMahan et al., 2013],在实际中有良好的表现。但是线性模型缺乏学习交互特征的能力,通常的做法是手动地在特征向量中加入成对的特征交互,这种方法很难推广到高阶特征交互的模型中,也很难推广到从未或很少出现在训练数据中的高阶特征交互模型中, FM因子分解机,将两两特征相互作用作为特征间潜在向量的内积,得到了很好的结果。虽然理论上FM可以对高阶特征交互进行建模,但在实际中由于其复杂性,通常只考虑二阶特征交互.
深度神经网络作为一种强大的特征表示学习方法,具有学习复杂特征交互的潜力。所以将CNN和RNN用于CTR 的预测有一些想法 ,但是基于cnn的模型偏向于相邻特征之间的交互,而基于rnnn的模型更适合于具有顺序相关性的点击数据。[Zhang等人,2016]研究了特征表示,提出了因子化-机器支持神经网络(Factorization-machine support Neural Network, FNN)。该模型在应用DNN之前对FM进行了预训练,因而受到FM能力的限制。[Qu et al., 2016]对特征交互进行了研究,在嵌入层和全连通层之间引入了一个product层,特征之间的乘积,提出了基于乘积的神经网络(PNN)。如[Cheng et al., 2016]所述,PNN和FNN与其他深度模型一样,捕捉到的少数低阶特征的交互,这对CTR预测也至关重要。为了对低阶和高阶特征交互进行建模,[Cheng et al., 2016]提出了一种有趣的混合网络结构 (Wide &Deep) 结合了线性(宽)模型和深度模型。在该模型中,宽部和深部分别需要两种不同的输入,而宽部的输入仍然依赖于专家特征工程。
可以看出,现有的模型偏向于低阶或高阶的特征交互,或者依赖于特征工程。在本文中,证明了可以推导出一个学习模型,该模型能够以端到端的方式学习所有阶的特征交互,除了原始特征外,不需要任何特征工程。我们的主要贡献总结如下:
- 提出了一种新的神经网络模型DeepFM(图1)融合了FM和deep neural networks (DNN)的架构。它对低阶特征交互(如FM)和高阶特征交互(如DNN)进行建模。与宽深模型不同[Cheng et al., 2016], DeepFM可以在不需要任何特征工程的情况下进行端到端的训练.
- DeepFM可以被有效地训练,因为它的宽部分和深部分,不像[Cheng et al., 2016],共享相同的输入和嵌入向量。在[Cheng et al., 2016]中,输入向量可以是巨大的,因为它在其宽部分的输入向量中包含了人工设计的成对特征交互,这也大大增加了其复杂性
- 对DeepFM的基准数据和商业数据进行了评估,结果显示,与现有的CTR预测模型相比,DeepFM的预测结果有了持续的改进
2 Approach
通常训练数据n个样本为
,
是用户和商品的
数据,
∈ {0,1},1表示点击,0表示未点击。X包括多个领域的特征,例如离散的特征表示为one-hot编码,连续特征为自己本身值或者离散化的one-hot向量。所以
,一个域的数据表示为
通常情况
是高维且稀疏的,我们的目标就是
以此评估用户在众多APP中点击某个APP的可能性。
如图1所示,DeepFM主要分为两部分,FM部分和Deep部分。对于特征i
,用标量
来衡量它的一阶特征权重,一个latent vector
潜在向量
用来衡量与其他特征的交互影响,
加入到二阶特征以及Deep的交互特征中。
,
均是可训练的参数,预测模型的表达式可写成:
FM Component
如图2所示,FM的输出是一个Addition和若干Inner Product 内积的和:
除了特征间的线性(一阶)相互作用外,FM还将特征间的成对(二阶)相互作用,作为各自特征潜在向量的内积进行建模.这里你会发现,图二和图三一样也包含embedding层,这在公式(2)的体现就是
,V就相当于对输入的稀疏数据做了编码的操作。
Deep Component
深度分量是一个前馈神经网络,用于学习高阶特征交互。如图3所示,具体来说,用于CTR预测的原始特征输入向量通常是高度稀疏的、超高维数的、分类连续混合的,并按字段(如性别、位置、年龄)分组。这就需要一个嵌入层将输入向量压缩成一个低维的、密集的实值向量,然后再将其输入到第一个隐层中,否则网络将无法进行训练。
Embedding component:
K是潜在向量维度,一般自己根据数据维度设定。
- 输入数据的嵌入大小相同K维
- 利用FM中的二阶特征中的潜在特征向量(V)作为网络权值,对其进行学习,将输入向量压缩为嵌入向量。
在[Zhang et al., 2016]中,V通过FM进行预训练,并作为初始化。在这项工作中,我们没有像[Zhang et al., 2016]那样使用FM的潜在特征向量来初始化网络,而是将FM模型作为我们整体学习架构的一部分,以及其他DNN模型
因此,我们不需要FM的预培训,而是以端到端方式联合培训整个网络。表示嵌入层的输出为: 这里 为输入字段的词嵌入,m表示输入域的数量。前向传播的过程:
这里l表示层的深度,最终将所有dense后的特征输入到sigmoid函数中,得到CET预测: 这里|H|表示隐藏层的数量。
值得指出的是,FM组件和deep组件具有相同的特征嵌入,这带来了两个重要的好处: - 1)从原始特征中学习低阶和高阶特征交互;
- 2)不需要像Width& Deep [Cheng et al., 2016]一样有专门的特征工程的输入
后面的和其他模型的对比就不做过多介绍了,作者通过实验,说明了DeepFM比其他模型例如FM,FNN,PNN,DNN等都表现的更好。
其他模型的结构图,分别的FNN,PNN,Width&Deep。
思考:
DeepFM考虑了特征之间的交互,结合了神经网络,对于非线性的关系有更好的挖掘。不过我觉得,以上这些模型都是一种ensemble的形式,并没有像FM那样有开创性的不同。对于用户信息,大多数还是字符特征,如何从文本类的信息中,挖掘出强表征,这个应该属于NLP领域的问题。包括后面的CIN,也借鉴了NLP中的注意力机制。当用户的标签啊,评论啊这些文本信息,通过词向量能够很好的表征用户的喜好和特点,在结合视频(商品)的高表征,比如这个用户得到的表征向量,能很好的反应它是个宅男,二次元,动漫甚至精确到动漫名,人物名这些信息,而当一个视频或者APP和动漫甚至动漫名相关,它能很好地被表征出来,那么这两部分的特征向量的距离就非常接近,从而就能充分预测用户的点击这个视频的概率非常大。
keras尝试搭建的模型:
参考这篇用Keras实现一个DeepFM
我整合简化了部分:
from keras.layers import *
from keras.models import Model
import tensorflow as tf
class MySumLayer(Layer):
def __init__(self, axis, **kwargs):
self.supports_masking = True
self.axis = axis
super(MySumLayer, self).__init__(**kwargs)
def compute_mask(self, input, input_mask=None):
# do not pass the mask to the next layers
return None
def call(self, x, mask=None):
if mask is not None:
# mask (batch, time)
mask = K.cast(mask, K.floatx())
if K.ndim(x) != K.ndim(mask):
mask = K.repeat(mask, x.shape[-1])
mask = tf.transpose(mask, [0, 2, 1])
x = x * mask
if K.ndim(x) == 2:
x = K.expand_dims(x)
return K.sum(x, axis=self.axis)
else:
if K.ndim(x) == 2:
x = K.expand_dims(x)
return K.sum(x, axis=self.axis)
def compute_output_shape(self, input_shape):
output_shape = []
for i in range(len(input_shape)):
if i != self.axis:
output_shape.append(input_shape[i])
if len(output_shape) == 1:
output_shape.append(1)
return tuple(output_shape)
class MyMeanPool(Layer):
def __init__(self, axis, **kwargs):
self.supports_masking = True
self.axis = axis
super(MyMeanPool, self).__init__(**kwargs)
def compute_mask(self, input, input_mask=None):
# need not to pass the mask to next layers
return None
def call(self, x, mask=None):
if mask is not None:
if K.ndim(x)!=K.ndim(mask):
mask = K.repeat(mask, x.shape[-1])
mask = tf.transpose(mask, [0,2,1])
mask = K.cast(mask, K.floatx())
x = x * mask
return K.sum(x, axis=self.axis) / K.sum(mask, axis=self.axis)
else:
return K.mean(x, axis=self.axis)
def compute_output_shape(self, input_shape):
output_shape = []
for i in range(len(input_shape)):
if i!=self.axis:
output_shape.append(input_shape[i])
return tuple(output_shape)
class MyFlatten(Layer):
def __init__(self, **kwargs):
self.supports_masking = True
super(MyFlatten, self).__init__(**kwargs)
def compute_mask(self, inputs, mask=None):
if mask==None:
return mask
return K.batch_flatten(mask)
def call(self, inputs, mask=None):
return K.batch_flatten(inputs)
def compute_output_shape(self, input_shape):
return (input_shape[0], np.prod(input_shape[1:]))
class my_Fm():
# 创建输入:
def __init__(self, spare_feat, dense_feat, varlenSpare_feat,latent):
'''
:param spare_feat:[{'name':name,'n_dims':4},...]
:param dense_feat:[{'name':name}]
:param varlenSpare_feat:[{'name':name,'max_len':10,'n_dims':1000}]
:param laten: 二次项和DNN中的编码域维度
'''
self.spare_feat=spare_feat # 离散特征
self.varlenSpare_feat=varlenSpare_feat # 变长离散特征
self.dense_feat=dense_feat
self.spare_input_dict ={} # 保存离散特征输入向量
self.varlenSpare_dict={} # 保存变长特征输入向量
self.dense_input_dict = {} # 保存连续特征
self.latent=latent # 二次项的编码域维度
for items in spare_feat:
self.spare_input_dict[items['name']]=Input(shape=[1], name=items['name'])
for items in dense_feat:
self.dense_input_dict[items['name']]=Input(shape=[1], name=items['name'])
for items in varlenSpare_feat:
self.varlenSpare_dict[items['name']]=Input(shape=[items['max_len']],name=items['name'])
def build(self):
'''First Order Embeddings'''
first_emb=[]
# 连续型数值feat
numeric = Concatenate()([inputs for inputs in self.dense_input_dict.values()]) # None*2
dense_numeric = Dense(1,name='fmFirst_dense')(numeric) # None*1
first_emb.append(dense_numeric)
# 离散型数值feat
for items in self.spare_feat:
first_emb.append(Reshape([1])(
Embedding(items['n_dims'],1,name=items['name']+'_fmFirst_embding')(self.spare_input_dict[items['name']]))) # 从None*1*1 to None*1
# 变长离散型feat
for items in self.varlenSpare_feat:
first_emb.append(MyMeanPool(axis=1)(Embedding(items['n_dims'], 1, mask_zero=True,name=items['name']+'_fmFirst_embding'
)(self.varlenSpare_dict[items['name']]))) # None*max_len*1 to None*1
y_first_order=Add()(first_emb) # 得到一次项
'''Second Order Embeddings'''
latent = 8
second_emb=[]
# 连续型
for items in self.dense_feat:
second_emb.append(RepeatVector(1)(
Dense(self.latent,name=items['name']+'fmSecond_dense')(self.dense_input_dict[items['name']]))) # None * 1 * latent
# 离散型数值feat
for items in self.spare_feat:
second_emb.append(Embedding(items['n_dims'], self.latent)(
self.spare_input_dict[items['name']]))# None * 1 * latent
# 变长离散型feat
for items in self.varlenSpare_feat:
second_emb.append(RepeatVector(1)(MyMeanPool(axis=1)(
Embedding(items['n_dims'], self.latent, mask_zero=True)(
self.varlenSpare_dict[items['name']]))))# None * max_len * latent to None * 1 * latent
emb = Concatenate(axis=1)(second_emb) # None * n * latent
'''compute'''
summed_features_emb = MySumLayer(axis=1)(emb) # None * K
summed_features_emb_square = Multiply()([summed_features_emb, summed_features_emb]) # None * K
squared_features_emb = Multiply()([emb, emb]) # None * 9 * K
squared_sum_features_emb = MySumLayer(axis=1)(squared_features_emb) # Non * K
sub = Subtract()([summed_features_emb_square, squared_sum_features_emb]) # None * K
sub = Lambda(lambda x: x * 0.5)(sub) # None * K
y_second_order = MySumLayer(axis=1)(sub) # None * 1
'''deep parts'''
# 这部分可以自由发挥
y_deep = MyFlatten()(emb) # None*(6*K)
y_deep = Dropout(0.5)(Dense(128, activation='relu')(y_deep))
y_deep = Dropout(0.5)(Dense(64, activation='relu')(y_deep))
y_deep = Dropout(0.5)(Dense(32, activation='relu')(y_deep))
y_deep = Dropout(0.5)(Dense(1, activation='relu')(y_deep))
'''deepFM'''
y = Concatenate(axis=1)([y_first_order, y_second_order, y_deep])
y = Dense(1, activation='sigmoid')(y)
# 所有输入汇总
inputs_list=[{**self.spare_input_dict,**self.dense_input_dict,**self.varlenSpare_dict}[
items['name']] for items in self.spare_feat+self.dense_feat+self.varlenSpare_feat]
model = Model(inputs=inputs_list,
outputs=[y])
model.summary()
return model
if __name__ == '__main__':
spare_feat=[{'name': 'age', 'n_dims': 3},{'name':'version','n_dims': 10}]
dense_feat=[{'name':'score'},{'name':'valuse'}]
varlenSpare_feat=[{'name': 'app', 'max_len':128, 'n_dims': 1000},{'name': 'user', 'max_len':260, 'n_dims': 12454}]
FM=my_Fm(spare_feat=spare_feat,dense_feat=dense_feat,varlenSpare_feat=varlenSpare_feat,latent=64)
model=FM.build()