个性化排序算法实践(三)——deepFM算法

FM通过对于每一位特征的隐变量内积来提取特征组合,最后的结果也不错,虽然理论上FM可以对高阶特征组合进行建模,但实际上因为计算复杂度原因,一般都只用到了二阶特征组合。对于高阶特征组合来说,我们很自然想到多层神经网络DNN。
DeepFM目的是同时学习低阶和高阶的特征交叉,主要由FM和DNN两部分组成,底部共享同样的输入。模型可以表示为:

\[\hat{y} = sigmoid(y_{FM}+y_{DNN}) \]

DeepFM

这里主要参考了Github上的代码,通过对源码的研究,更加加深了对deepFM理论和应用的了解。
主体部分分为data,fig,output和代码部分。其中,data存储数据集,fig存储训练后保存的结果,output存储测试集prediction,代码详解如下:

Basic-DeepFM-model

config代表了一些基本配置,DataReader.py是一个读取DataFrame格式并进行转换的程序,DeepFM.py是DeepFM算法实现的主程序,main.py是主程序入口,metrics主要保存了gini_norm的实现方法。下面先讲述DeepFM的主方法,然后讲解如何通过main函数实现一个具体的方法。

DeepFM实现

以函数为单位,深度解析DeepFM方法的实现。

初始化函数:

def __init__(self, feature_size, field_size,
                 embedding_size=8, dropout_fm=[1.0, 1.0],
                 deep_layers=[32, 32], dropout_deep=[0.5, 0.5, 0.5],
                 deep_layer_activation=tf.nn.relu,
                 epoch=10, batch_size=256,
                 learning_rate=0.001, optimizer="adam",
                 batch_norm=0, batch_norm_decay=0.995,
                 verbose=False, random_seed=2016,
                 use_fm=True, use_deep=True,
                 loss_type="logloss", eval_metric=roc_auc_score,
                 l2_reg=0.0, greater_is_better=True):

主要包括了一些基础配置,如特征个数,特征域个数,隐向量维度,dropout参数,deep部分的层数,每层神经元个数,激活函数,迭代次数,batch_size,学习率,优化方法,batch_norm参数,代价函数,评估函数,正则化参数的选择等。另外,调用了_init_graph()方法对图初始化。

def _init_graph(self):

构建了deepFM的Tensor图。首先还是初始化权重,重要的几个有:

  • feature_embeddings
    shape为(feature_size,embedding_size),即特征个数(类别特征onehot之后)*embedding维度。
  • feature_bias
    shape为(feature_size,1)
  • deep侧的weight以及bias
    根据DNN的参数设置weight以及bias,其中输入层为field_size*embedding_size,即每一个特征域的embedding向量,输出层前一层为FM层+DNN最后一层,即field_size+embedding_size+deep_layers[-1]。其中field_size+embedding_size参考个性化排序算法实践(一)——FM算法可知是一阶权重与特征embedding的和。权重初始化采用Xavier初始化

Xavier初始化以0为中心的截断正态分布中抽取样本,stddev = sqrt(2 / (fan_in + fan_out)),其中 fan_in 是权重张量的输入单元数,而 fan_out 是权重张量中的输出单位数。

初始化权重之后,便是构建网络层了。网络层的构建就主要分成两部分:FM部分与Deep部分。

FM部分

# FM部分
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)

# 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)
self.y_first_order = tf.nn.dropout(self.y_first_order,self.dropout_keep_fm[0])

# second order term
# 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)
self.y_second_order = tf.nn.dropout(self.y_second_order,self.dropout_keep_fm[1])

同样,根据FM的二次项化简公式,我们可以得到:

\[\hat y(x) = w_0+\sum_{i=1}^n w_i x_i +\sum_{i=1}^n \sum_{j=i+1}^n ⟨vi,vj⟩ x_i x_j \\ =w_0+\sum_{i=1}^n w_i x_i + \frac{1}{2} \sum_{f=1}^{k} {\left \lgroup \left(\sum_{i=1}^{n} v_{i,f} x_i \right)^2 - \sum_{i=1}^{n} v_{i,f}^2 x_i^2\right \rgroup} \qquad \]

这里使用tf.nn.embedding_lookup方法,可以选择出对应的特征与权重相乘求和。具体而言,各个变量含有如下:

  • self.embeddings
    代表\(w_i x_i\),这里的i代表第i个特征域

这里的embedding层对于DNN来说时在提取特征,对于FM来说就是他的2阶特征,并且FM和DNN共享embedding层。

  • self.y_first_order
    代表\(\sum_{i=1}^n w_i x_i\),即一次项的和,这里的i代表第i个特征
  • self.second order
    代表二次项的和,是由\(\frac{1}{2} \sum_{f=1}^{k} {\left \lgroup \left(\sum_{i=1}^{n} v_{i,f} x_i \right)^2 - \sum_{i=1}^{n} v_{i,f}^2 x_i^2\right \rgroup}\)计算得来。

Deep部分

self.y_deep = tf.reshape(self.embeddings,shape=[-1,self.field_size * self.embedding_size])
self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[0])

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])

第一层self.embeddings,之后便是堆叠Deep层了。这里的self.embeddings维度为[-1,特征域个数*embedding维度]。

之后便是最后一层,将FM与Deep结合了,如下:

concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)
self.out = tf.add(tf.matmul(concat_input,self.weights['concat_projection']),self.weights['concat_bias'])
if self.loss_type == "logloss":
    self.out = tf.nn.sigmoid(self.out)
    self.loss = tf.losses.log_loss(self.label, self.out)
elif self.loss_type == "mse":
    self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))

这里concat_input可以认为是最后第二层网络,最后一层输出结果后,如果是分类任务,使用logloss进行代价函数的计算与反向传播,否则使用MSE。

一共多少参数量呢?我们计算下:feature_embeddings是feature_size*embedding_size维,feature_bias是feature_size维,deep层前一层神经元个数\(*\)后一层神经元个数,最后相加即可得:
\(特征个数*embedding维度+特征个数+\sum_{i=1}^{N} layer_{i}+layer_{i}*layer_{i+1}\)

训练

网络训练的主函数为:

def fit(self, Xi_train, Xv_train, y_train,
        Xi_valid=None, Xv_valid=None, y_valid=None,
        early_stopping=False, refit=False):

Xi_train,Xv_train,y_train;Xi_valid,Xv_valid,y_valid分别代表训练和验证的数据集特征域以及特征值。具体含义可见之后的主流程中的具体例子。

loss,opt = self.sess.run([self.loss,self.optimizer],feed_dict=feed_dict)

至于训练的关键语句就是上面一句了。通过feed_dict喂入每一个batch的数据,进行训练和传播。

主流程

这里按照main函数的执行思路进行剖析。
首先,这里提供了一个DataFrame数据集,包括训练集以及测试集。我们首先需要分辨出哪些特征是数值型特征,哪些特征是类别型数值,这是为了之后进行onehot,以及进行特征域的划分。

每一个数值型特征代表一个特征域,每一个类别型特征代表一个特征域,类别型特征进行onehot之后,每个类别特征域下都有若干个特征。

DataReader

DataReader.py文件提供了对DataFrame数据集的初始化以及进一步处理成可以直接训练的数据。这里主要有两个类,FeatureDictionary通过gen_feat_dict()方法,能够得到将所有的特征映射到从0开始的一个特定的数值tc,规则如下:

  • 假如该特征是数值型特征,映射到一个唯一数值tc
  • 假如该特征是类别型特征,每一个类别映射到一个唯一数值,并且每一个类别映射后值tc都加1
  • 每完成一个特征的映射,数值tc+1

这个步骤类似于将类别特征进行onehot,并且得到每一个特征的特征域。
DataParser类则通过对原数据集进一步的处理,得到xi,xv两个列表。这两个列表分别代表原数据集的特征在经过FeatureDictionary后的映射值,以及其原来的数值(如果是类别型特征,xv就令为0)。有如下例子:
假设我们的原有数据集为:

numeric1 numeric2 numeric3 cate1
1.3 3.2 2.0 0
67 3.4 6.7 0
23 4.5 2.4 1
2.6 1.6 5.4 2

经过转化后,xi为:

numeric1 numeric2 numeric3 cate1
1 2 3 4
1 2 3 4
1 2 3 5
1 2 3 6

这里的4,5,6就是类别特征域下不同特征的映射。
xv为:

numeric1 numeric2 numeric3 cate1
1.3 3.2 2.0 1
67 3.4 6.7 1
23 4.5 2.4 1
2.6 1.6 5.4 1

这里的1就是类别特征域下的值。

完成数据集的处理后,通过交叉验证就可以进行训练了。另外,这里使用了特殊的评估函数——gini_norm。
这里将CTR预估问题设定为一个二分类问题,绘制了Gini Normalization来评价不同模型的效果。假设我们有下面两组结果,分别表示预测值和实际值:

predictions = [0.9, 0.3, 0.8, 0.75, 0.65, 0.6, 0.78, 0.7, 0.05, 0.4, 0.4, 0.05, 0.5, 0.1, 0.1]
actual = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

然后我们将预测值按照从小到大排列,并根据索引序对实际值进行排序:

Sorted Actual Values [0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1]

然后,我们可以画出如下的图片:

Gini Normalization1

接下来我们将数据Normalization到0,1之间。并画出45度线。

Gini Normalization2

橙色区域的面积,就是我们得到的Normalization的Gini系数。

这里,由于我们是将预测概率从小到大排的,所以我们希望实际值中的0尽可能出现在前面,因此Normalization的Gini系数越大,分类效果越好。

参考:
FM系列
个性化排序算法实践(一)——FM算法
推荐系统算法学习(二)——DNN与FM DeepFM
Github

posted @ 2019-11-03 12:56  Jamest  阅读(2415)  评论(1编辑  收藏  举报