推荐系统遇上深度学习 (七)--NFM 模型理论和实践


本文地址:http://www.6aiq.com/article/1547742643318
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出

原文发布于微信公众号 - 小小挖掘机(wAIsjwj)
原文发表时间:2018-05-05

1、引言

在 CTR 预估中,为了解决稀疏特征的问题,学者们提出了 FM 模型来建模特征之间的交互关系。但是 FM 模型只能表达特征之间两两组合之间的关系,无法建模两个特征之间深层次的关系或者说多个特征之间的交互关系,因此学者们通过 Deep Network 来建模更高阶的特征之间的关系。

因此 FM 和深度网络 DNN 的结合也就成为了 CTR 预估问题中主流的方法。有关 FM 和 DNN 的结合有两种主流的方法,并行结构和串行结构。两种结构的理解以及实现如下表所示:
16f0d0a34f64462480153187d33bbcfb.png

今天介绍的 NFM 模型 (Neural Factorization Machine),便是串行结构中一种较为简单的网络模型。

2、NFM 模型介绍

我们首先来回顾一下 FM 模型,FM 模型用 n 个隐变量来刻画特征之间的交互关系。这里要强调的一点是,n 是特征的总数,是 one-hot 展开之后的,比如有三组特征,两个连续特征,一个离散特征有 5 个取值,那么 n=7 而不是 n=3.

c712aa76f2de475aa4c25a3f825a4366.png

顺便回顾一下化简过程:
1f34a3ccd0824c6aaba80dde8cd8f6f3.png

可以看到,不考虑最外层的求和,我们可以得到一个 K 维的向量。

对于 NFM 模型,目标值的预测公式变为:
0f239c592be44f35b95dc02685cb1047.png

其中,f(x) 是用来建模特征之间交互关系的多层前馈神经网络模块,架构图如下所示:
a3f330909a544b1f9270b89062e987dc.png

Embedding Layer和我们之间几个网络是一样的,embedding 得到的 vector 其实就是我们在 FM 中要学习的隐变量 v。

Bi-Interaction Layer名字挺高大上的,其实它就是计算 FM 中的二次项的过程,因此得到的向量维度就是我们的 Embedding 的维度。最终的结果是:
a465caaba2c4494db264f0de6c21f5ae.png
Hidden Layers 就是我们的 DNN 部分,将 Bi-Interaction Layer 得到的结果接入多层的神经网络进行训练,从而捕捉到特征之间复杂的非线性关系。

在进行多层训练之后,将最后一层的输出求和同时加上一次项和偏置项,就得到了我们的预测输出:
8da32fdd6b244ce39a5c3daff88974d3.png

是不是很简单呢,哈哈。

3、代码实战

终于到了激动人心的代码实战环节了,本文的代码有不对的的地方或者改进之处还望大家多多指正。

本文的 github 地址为:



https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-NFM-Demo

本文的代码根据之前 DeepFM 的代码进行改进,我们只介绍模型的实现部分,其他数据处理的细节大家可以参考我的 github 上的代码.

模型输入

模型的输入主要有下面几个部分:

self.feat_index = tf.placeholder(tf.int32,
                                 shape=[None,None],
                                 name='feat_index')
self.feat_value = tf.placeholder(tf.float32,
                               shape=[None,None],
                               name='feat_value')

self.label = tf.placeholder(tf.float32,shape=[None,1],name='label')
self.dropout_keep_deep = tf.placeholder(tf.float32,shape=[None],name='dropout_deep_deep')

feat_index是特征的一个序号,主要用于通过 embedding_lookup 选择我们的 embedding。feat_value是对应的特征值,如果是离散特征的话,就是 1,如果不是离散特征的话,就保留原来的特征值。label 是实际值。还定义了 dropout 来防止过拟合。

权重构建

权重主要分以下几部分,偏置项,一次项权重,embeddings,以及 DNN 的权重

def _initialize_weights(self):
 weights = dict()

 #embeddings
 weights['feature_embeddings'] = tf.Variable(
 tf.random_normal([self.feature_size,self.embedding_size],0.0,0.01),
 name='feature_embeddings')
 weights['feature_bias'] = tf.Variable(tf.random_normal([self.feature_size,1],0.0,1.0),name='feature_bias')
 weights['bias'] = tf.Variable(tf.constant(0.1),name='bias')

 #deep layers
 num_layer = len(self.deep_layers)
 input_size = self.embedding_size
 glorot = np.sqrt(2.0/(input_size + self.deep_layers[0]))

 weights['layer_0'] = tf.Variable(
 np.random.normal(loc=0,scale=glorot,size=(input_size,self.deep_layers[0])),dtype=np.float32
 )
 weights['bias_0'] = tf.Variable(
 np.random.normal(loc=0,scale=glorot,size=(1,self.deep_layers[0])),dtype=np.float32
 )

 for i in range(1,num_layer):
 glorot = np.sqrt(2.0 / (self.deep_layers[i - 1] + self.deep_layers[i]))
 weights["layer_%d" % i] = tf.Variable(
 np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i - 1], self.deep_layers[i])),
 dtype=np.float32) # layers[i-1] * layers[i]
 weights["bias_%d" % i] = tf.Variable(
 np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])),
 dtype=np.float32) # 1 * layer[i]

 return weights

Embedding Layer
这个部分很简单啦,是根据 feat_index 选择对应的 weights[‘feature_embeddings’] 中的 embedding 值,然后再与对应的 feat_value 相乘就可以了:

# Embeddings
self.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index) # N * F * K
feat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1])
self.embeddings = tf.multiply(self.embeddings,feat_value) # N * F * K

Bi-Interaction Layer
我们直接根据化简后的结果进行计算,得到一个 K 维的向量:

# sum-square-part
self.summed_features_emb = tf.reduce_sum(self.embeddings, 1) # None * k
self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K

# squre-sum-part
self.squared_features_emb = tf.square(self.embeddings)
self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1) # None * K

# second order
self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square, self.squared_sum_features_emb)

Deep Part
将 Bi-Interaction Layer 层得到的结果经过一个多层的神经网络,得到交互项的输出:

self.y_deep = self.y_second_order
for i in range(0, len(self.deep_layers)):
 self.y_deep = tf.add(tf.matmul(self.y_deep, self.weights["layer_%d" % i]), self.weights["bias_%d" % i])
 self.y_deep = self.deep_layers_activation(self.y_deep)
 self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[i + 1])

得到预测输出
为了得到预测输出,我们还需要两部分,分别是偏置项和一次项:

# first order term
self.y_first_order = tf.nn.embedding_lookup(self.weights['feature_bias'], self.feat_index)
self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order, feat_value), 2)

# bias
self.y_bias = self.weights['bias'] * tf.ones_like(self.label)

而我们的最终输出如下:

# out
self.out = tf.add_n([tf.reduce_sum(self.y_first_order,axis=1,keep_dims=True),
 tf.reduce_sum(self.y_deep,axis=1,keep_dims=True),
 self.y_bias])

剩下的代码就不介绍啦!

好啦,本文只是提供一个引子,有关 NFM 的知识大家可以更多的进行学习呦。

4、小结

NFM 模型将 FM 与神经网络结合以提升 FM 捕捉特征间多阶交互信息的能力。根据论文中实验结果,NFM 的预测准确度相较 FM 有明显提升,并且与现有的并行神经网络模型相比,复杂度更低。

NFM 本质上还是基于 FM,FM 会让一个特征固定一个特定的向量,当这个特征与其他特征做交叉时,都是用同样的向量去做计算。这个是很不合理的,因为不同的特征之间的交叉,重要程度是不一样的。因此,学者们提出了 AFM 模型(Attentional factorization machines),将 attention 机制加入到我们的模型中,关于 AFM 的知识,我们下一篇来一探究竟。


本文地址:http://www.6aiq.com/article/1547742643318
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出