Deep Learning over Multi-field Categorical Data – A Case Study on User Response Prediction
前言
该篇论文于2016年发表,提出了基于FM预训练获取离散特征embedding表示,结合DNN来进行CTR的预估,因为思想比较简洁,放在2019年来看已经不算特别新奇了,因此简单地过一下论文内容,然后再做一下简单的实现。
FNN结构
FNN(Factorisation Machine supported Neural Network)的模型结构如下:
![](https://i-blog.csdnimg.cn/blog_migrate/92292f937f5ba48c2ad685d0ac2de3c6.png)
这个结构和现在比较流行的embedding+mlp的结构基本保持一致,所不同的在于,通常embedding matrix我们都是通过随机初始化并通过joint train来一起训练的,而FNN是利用FM来学习embedding的嵌入表达后作为embedding matrix的初始化,通过fine tuning 来调整参数,相当于给embedding加上了先验知识,可以更快地收敛,当然这不代表其效果就比随机初始化好…
文章中作者叙述模型的输入输出还是很详细的,这里我就直接截图了:
![](https://i-blog.csdnimg.cn/blog_migrate/804fe1e74a90688545c14b3ba5f6e446.png)
最后一层的输出使用sigmoid进行激活,其余层的激活函数均使用tanh,tanh的函数图像和sigmoid一直,只是其经过(0,0)而不是(0,0.5)点,即tanh是关于原点对称的函数图像,在许多试验下其效果都好于sigmoid,用tanh还是16年的工作,现在relu、prelu、leakeyrelu、dice等等激活函数层出不穷,实际使用可选余地就很多了。
这里着重看一下z即embedding后的层:
![](https://i-blog.csdnimg.cn/blog_migrate/4e1472fd4d702c6d28078ba8a5d4dada.png)
FM模型会为每一个特征都单独学习一个隐向量,通过隐向量之间的内积表示两个特征之间的交叉权重,这里将FM训练后的特征提取出来作为FNN的embedding的初始化。
假设输入是n个离散特征,onehot后则只有n个1(这里假设没有多值特征),假设FM的embedding_size为k,通过而言我们会直接拼接这个n个embedding vector以及一个偏置biases,FNN则是每个embedding vector都会拼接一个偏置,从而生成k+1维向量,然后将n个embedding vector拼接,长度则为n*(k+1),最后concat个偏置,长度则为1+n*(k+1),剩下的就是拼接若干层全连接了,损失使用logloss。
实践环节
数据还是和之前一样使用的是movielen数据集,剔除了多值特征,这里姑且假设FM已经训练完毕,直接进入FNN的环节。
提取出FM模型训练的embedding矩阵
embedding_matrix = E.eval()
直接打印tensor是不会转为ndarray的,使用.eval()可以将tensor输出为ndarray
将输入转为index
之前输入是onehot的值,需要将每一行记录转为由该值的索引组成的列表,有点绕,假设一共有4个field,即onehot后只有4个1,那么将这4个1的索引位置保存下来用于提取embedding,如下:
def transform_data2index(data):
res = []
for line in data.values:
res.append(np.argwhere(line).reshape([-1]))
return np.array(res)
转换前:
转化后:
![](https://i-blog.csdnimg.cn/blog_migrate/eb1666342ee8e2990ef623325eba83f7.png)
定义FNN模型结构
# fnn model parameter
# hidden layers
fnn_layers = [32,48,32]
n,f = x_train.shape
fields = 4
embedding_size = 20
# define the model structure
fnn_input = tf.placeholder(tf.int32,[None,fields])
fnn_output = tf.placeholder(tf.float32,[None,1])
fnn_embedding_matrix = tf.Variable(initial_value=embedding_matrix)
output = tf.reshape(tf.nn.embedding_lookup(fnn_embedding_matrix,fnn_input),[-1,fields*embedding_matrix.shape[1]])
first_layer = tf.Variable(tf.random_normal([fields*embedding_matrix.shape[1],fnn_layers[0]],stddev=0.01))
first_biases = tf.Variable(tf.zeros([fnn_layers[0]]))
output = tf.nn.relu(tf.add(tf.matmul(output,first_layer),first_biases))
second_layer = tf.Variable(tf.random_normal([fnn_layers[0],fnn_layers[1]],stddev=0.01))
second_biases = tf.Variable(tf.zeros([fnn_layers[1]]))
output = tf.nn.relu(tf.add(tf.matmul(output,second_layer),second_biases))
third_layer = tf.Variable(tf.random_normal([fnn_layers[1],fnn_layers[2]],stddev=0.01))
third_biases = tf.Variable(tf.zeros([fnn_layers[2]]))
output = tf.nn.relu(tf.add(tf.matmul(output,third_layer),third_biases))
output_layer = tf.Variable(tf.random_normal([fnn_layers[2],1],stddev=0.01))
output_biases = tf.Variable(tf.zeros([1]))
output = tf.nn.sigmoid(tf.add(tf.matmul(output,output_layer),output_biases))
这里实现和论文中不太一样,我是选择直接将多个embedding进行concat后直接喂入下一层,而不是增加k+1个偏置后喂入MLP,而且我使用的是RELU作为激活函数,而不是使用tanh作为激活函数,总体流程还是差不多的。
定义loss以及优化器
# logloss
fnn_loss = -tf.reduce_mean(
tf.multiply(fnn_output,tf.log(output))
+tf.multiply((1-fnn_output),tf.log(1-output)))
# define the optimization of fnn
fnn_optimizer = tf.train.AdamOptimizer().minimize(fnn_loss)
初始化参数并训练
# initialize the varibale
epochs = 20
batch_size = 1024
init = tf.global_variables_initializer()
# start the epochs of fnn
sess.run(init)
for epoch in range(epochs):
for x_,y_ in get_next_batch(fnn_train_pd,y_train.values.reshape([-1,1]),batch_size):
sess.run(fnn_optimizer,feed_dict={fnn_input:x_,fnn_output:y_})
if epoch%5==0:
print("========={}/{},train_loss is {}==============".format(epoch,epochs,sess.run(fnn_loss,feed_dict={fnn_input:fnn_train_pd,fnn_output:y_train.values.reshape([-1,1])})))
print("========={}/{},test_loss is {}==============".format(epoch,epochs,sess.run(fnn_loss,feed_dict={fnn_input:fnn_test_pd,fnn_output:y_test.values.reshape([-1,1])})))
# fnn_auc
train_predict_val = sess.run(output,feed_dict={fnn_input:fnn_train_pd})
test_predict_val = sess.run(output,feed_dict={fnn_input:fnn_test_pd})
print("the train auc of fnn is {},test auc is {}"
.format(roc_auc_score(y_train.values.reshape(-1,1),train_predict_val),roc_auc_score(y_test.values.reshape(-1,1),test_predict_val)))
结果对比
the train auc of fm is 0.807176239234,test auc is 0.774158085472
the train auc of fnn is 0.809782081153,test auc is 0.776368069515
总结
FNN的优点在于使用DNN进行训练,充分利用DNN强大的学习能力来进行ctr预估,并且利用FM模型来进行embedding的预训练,从而加快DNN的训练速度,利用FM的先验知识来进行模型参数的初始化也使得即使隐层比较多也能很快地收敛因为最前面层的参数已经获得了较好的初始化不会被梯度消失的问题影响很大,但是不足之处也在于,需要FM来进行预训练,并且对于FM的挖掘仅限于获取FM的副产物而放弃了FM本身的学习能力,倘若当时想到FM和DNN可以共用一份隐向量并且可以放在一起使用同一个loss进行train的话,效果可能会更好。