赞
踩
理论部分请参照:推荐系统遇上深度学习(三)--DeepFM模型理论和实践 - 简书,这里主要针对源码进行解读,填一些坑。
1.DeepFM可以看做是从FM基础上衍生的算法,将Deep与FM相结合,用FM做特征间低阶组合,用Deep NN部分做特征间高阶组合,通过并行的方式组合两种方法,使得最终的架构具有以下特点。
- (1) 不需要预训练 FM 得到隐向量;
- (2) 不需要人工特征工程;
- (3)能同时学习低阶和高阶的组合特征;
- (4)FM 模块和 Deep 模块共享 Feature Embedding 部分,可以更快的训练,以及更精确的训练学习。
2.整体的架构体系如下,左侧为FM的结构层,右侧为Deep部分的结构层,两者公用相同的特征输入,其实我刚看到下面的图也有点懵逼,两部分分离的架构都好理解,但是那些红线和黑线都表示什么?为什么有Field的标识,从输入层网上传播时候为什么有embedding层,而且还分成不同的块? 这都是我刚看到如下图产生的疑问,经过阅读源码后,才有了较为清晰的理解,接下来进行一一的解答。
1.咱们从下往上开始看吧,首先是最底下这一部分,可以看到field这个名词,这是因为在处理特征时候,我们需要对离散型数据进行one-hot转化,经过one-hot之后一列会变成多列,这样会导致特征矩阵变得非常稀疏。
为了应对这一问题,我们把进行one-hot之前的每个特征都看作一个field,这样做的原因是为了在进行矩阵存储时候,我们可以把对一个大稀疏矩阵的存储,转换成对两个小矩阵 和一个字典的存储。 (这个是deepFM最难理解的地方)
*小字典的形式如下,为离散型特征每个特征值都添加索引标识,对连续数据只给予一个特征索引,这样的作用用于我们不必存储离散特征的onehot矩阵,而只需要存储 离散特征值对应在 字典中的 编号就行了,用编号作为特征标识。
{'missing_feat': 0, 'ps_car_01_cat': {10: 1, 11: 2, 7: 3, 6: 4, 9: 5, 5: 6, 4: 7, 8: 8, 3: 9, 0: 10, 2: 11, 1: 12, -1: 13}, 'ps_car_02_cat': {1: 14, 0: 15}
*第一个小矩阵是特征索引矩阵,长度是 样本长度,每个列表示样本特征值所对应 在字典中的索引值,如下是对其中两样本特征索引的打印,可以发现这样的区别在于 离散特征的位置,举个例子,180可能表示‘性别为男’、181表示‘性别为女’的含义,如果是以往的方式,下面每行的特征索引值都是一致的(但是对性别one-hot这个要用两列表示),但是这里主要是考虑针对one-hot带来的稀疏问题,使用如下一行代替多行的形式(可以用labelencoder进行理解、只不过这里用索引是因为后面要用所以做权值抽取),只保存离散特征所代表的值对应的索引。
- [180, 186, 200, 202, 205, 213, 215, 217, 219, 221, 223, 225, 227, 229, 235, 248, 250, 251, 253, 254, 255, 3, 14, 16, 19, 30, 40, 50, 54, 56, 61, 147, 66, 172, 173, 175, 176, 0, 174]
- [181, 185, 190, 202, 205, 213, 216, 217, 220, 221, 223, 225, 227, 229, 242, 248, 250, 251, 253, 254, 255, 7, 14, 16, 19, 31, 33, 50, 54, 55, 61, 79, 66, 172, 173, 175, 176, 0, 174]
*第二个小矩阵是特征值矩阵,这里的特征值是和上面的特征索引矩阵相对应的,存储对应索引位置含义的特征值,以下是两个样本的特征值的例子,可以发现在离散特征值都是为1的,这是因为我们只记录了样本对应的离散特征的索引,所以肯定是为1的,不可能为0,这点需要多加思考,是理解 当前矩阵分解表示的关键。
- [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.3, 0.6103277807999999, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.316227766, 0.6695564092, 0.3521363372, 3.4641016150999997, 2.0, 0.4086488773474527],
- [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.5, 0.7713624309999999, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.316227766, 0.6063200202000001, 0.3583294573, 2.8284271247, 1.0, 0.4676924847454411]
经过以上过程,我们可以将原样本都转换成上面的 两个小矩阵,并保留了特征索引字典,这样相对于对原始特征做了one-hot的转换,只不过用了一种特征抗稀疏化的方式,原来特征的长度是field_size个,现在one-hot后的特征长度也是field_size个,只不过可以假定为 feature_size个(应该的one-hot过的长度),因为我们之后会对每个离散特征值 跟 连续特征一起做处理。
对应看图中,每个连续型特征都可以对应为一个点,而每个离散特征(成为一个field),因为要做one-hot话,则每个field根据 不同值的数量n可以对应n个点。
2.接下来,从架构上进行观察,其实主要经过了以下步骤:
(1)对 feature的embeding转换,包括两部分的embedding, 其中可以看到1是针对不同特征做的embedding【FM的二阶两两交互计算部分和 deep部分是共享这个embedding结果的】,2是FM的一阶计算部分【使用权重weights['feature_bias'] 、直接对原始特征做的一阶计算】
3是对应FM的二阶计算阶段,对经过weights['feature_embeddings']权重embedding的结果做二阶交叉计算,4是deep部分的全连接计算部分,使用神经网络权重weights["layer_n]和 weights["bias_n]进行计算。
(2)FM计算的过程,其中左部分FM阶段对应的公式如下,可以看到2部分对应着左侧一阶的公式,3部分对应右侧二阶的公式。
(3)深度部分可以对应序号4,架构图是下面的形式。
3.具体计算时,主要涉及四个权重(embedding_size是进行embeding转化时指定的大小)
* weights['feature_embeddings'] :维度是[self.feature_size,self.embedding_size]。这里表示存储对 每个feature的embedng表示,这里embedding转换就相当于是一个全连接层、feature_size是总的特征数量(可以看出离散特征的每个离散值都有单独的embedding表示的)。
* weights['feature_bias']:维度是 [self.feature_size,1]。这里存储对特征的一维交叉计算,对应FM公式中的一维计算部分,对每个特征都会附加长度为1的权重。
* weights['layer_0']:维度是(上一层神经元树量,当前层神经元数量),对应deep部分神经元的权重部分
* weights['bias_0']:维度是(1,当前层神经元数量),对应深度计算的偏置部分。
(1)首先进行 样本embedding结果的获取,针对每个样本,会根据其具有的特征索引列表feat_index,获取这些特征对应在weights['feature_embeddings'] 矩阵中所存储的embeding表达形式。 之后我们样本原值, 利用embeding表达形式【维度 是 field长度 * embeding长度】 * 样本原值【维度是field长度】 得到当前样本的 embedding转换结果 【维度是 field长度 * embeding长度】,这里样本的特征长度都是field长度,权重矩阵中的特征长度则是feature_size,这点要明白,有点小弯。
- 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)
(2)进行FM部分的计算,FM部分可以分为一阶计算 和 二阶计算两部分。首先是一阶计算阶段, 其直接使用W *x计算结果即可,没做embeding.
- 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])
在二阶计算部分,对应如下的对FM转化的公式(优化后公式简单相当多),这里的u其实就是weights['feature_embeddings']隐向量矩阵,u*x已经在第一部分做embedding时候做过了,剩余的就是 对两两内积结果的组内相加等操作
对应代码如下,每个样本都是对应下面的过程,两部分的相减。
- # second order term 这整体区间代表FM公式中的二次项计算
- # sum-square-part 在公式中 相减的前一部分。 先加后平方 这里的1表示维度内相加,对应公式中的,对所有的u*x的结果相加
- 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])
(3)最后是深度deep计算,这部分就是典型的DNN的方式,从embedidng结果开始【尺寸是field_size * embedding_size】,定义几层全连接计算即可。
- # Deep component 将Embedding part的输出再经过几层全链接层
- 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])
至此已经完成了核心的计算过程。
5.以上是 主要的模型架构部分,整体的算法过程是这样的:
(1)数据的加载,训练集和测试集的分装。
(2)配置离散特征集、连续特征集,构建特征字典,构建每个样本的特征索引矩阵 和特征值矩阵, 完成对原特征矩阵 one-hot形式的转换。
(3)搭建网络架构,初始化网络参数,对网络进行批次训练,最后进行效果验证。
该链接是我自己添加注释后的代码,地址:https://github.com/isthegoal/recommendation_model_master/tree/master/Basic-DeepFM-model
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。