参考:

源码:https://github.com/ChenglongChen/tensorflow-DeepFM

原文下载:https://arxiv.org/abs/1703.04247

参看原文我们可以发现,deepfm由两部分组成:FM、deep,两部分共享一套输入,下面的介绍也从这两个方面展开。

1.FM部分

fm部分我的上篇文章https://blog.csdn.net/guanbai4146/article/details/80139806已经介绍,这里简单提一下。

从上面公式(这里提一下w_{i},w_{ij}是两套不同的参数,对应于下面的weights["feature_bias"]和weights["feature_embeddings"])可以看到,原始fm由两部分组成,一阶原始特征+二阶特征组合+偏置项,强调的是特征组合的影响,有个点在于如何训练参数。

1.1 稀疏特征输入如何训练参数

首先,数值型特征可以直接输入模型(除必要的变换操作外),fm主要解决的是稀疏特征的问题,这里指的主要是类别型特征,比如性别。这类特征输入模型的时候需要做特征展开,也就是一列展开成两列了。具体可以参考上一篇博客的题外话。

这里要提一下这个点,因为这贯穿整个实现过程。这里性别就是一个field,而展开后的两列就是feature(下面会用特征域(field)和特征(feature)表示)。

1.2 FM结构

上图截自原文。

主要分四个部分:sparse feature层、dense embedding层、fm层、输出层,图中不同的符号和线段表示了不同的计算逻辑。

虽然在上一篇文章已经介绍了,但这里还需要提一下,fm的特点是引入了隐向量的概念

假设有三个变量x_{1},x_{2},x_{3},那么他们交叉就有三个组合w_{12}x_{1}x_{2},w_{13}x_{1}x_{3},w_{23}x_{2}x_{3},其中w是组合特征域的权重参数,这里我直接用特征域来表述,方便理解。如果这个这三个特征域都是类别特征,很容易出现什么现象?

比如:x_{1}=[1,0,0],x_{2}=[0,1,0]==>x_{1}*x_{2}=0,向量內积为0那么使用梯度下降寻参的时候就没办法训练得到w_{12}的参数值,原本输入特征就非常稀疏,导致问题更加严峻。所以这里引入隐向量(latent vector)的概念解决稀疏性问题,

具体来说,令w_{12}=v _{1} * v _{2}(这里v _{1},v _{2}都是向量,维度为K,K的大小超参设定),那么即使x _{1},x _{2}正交v _{1}也可以在后面x_{1} * x_{3}中得到训练,极大缓解了数据稀疏带来的问题。

所以,这里的特征参数都会用一个K维的隐向量表示,了解了上述介绍后我们再看怎么实现(以下所有代码均来自源码DeepFM文件中,这里对每行代码做注释和个人理解的说明):

首先声明两个占位符,这些占位符会在训练的时候由数据填充

# None * F, None * 特征域大小,特征索引占位符
self.feat_index = tf.placeholder(tf.int32, shape=[None, None],name="feat_index")
# None * F, 特征值占位符(输入的样本数据)
self.feat_value = tf.placeholder(tf.float32, shape=[None, None],name="feat_value") 

这里重点说明这两个占位符,前面我们知道特征域(field)会被展开拉直成特征(features)集,比如现在有两个特征域,年龄和性别,会被展开成【年龄、男、女】,对应类别特征值用01表示,看下面例子

年龄 性别
25
26
24

那么这里field有2个【年龄、性别】,feature有三个值【年龄、男、女】,预处理部分会构建一个特征索引表【年龄:0,男:1,女:2】

先说feat_index数据形式,在预处理中会将原始样本值处理成索引,所以每个样本表的索引表就是:

年龄 性别
0 1
0 2
0 1

feat_value就是输入的样本值(类别特征数值化,对应的feature上取值设为1):

年龄
26 1  
26   1
24 1  

注意:这里每行样本都是2列(和域维度一样),虽然类别型特征对应的feature不同,比如第一行样本第二列表示男这个特征,而第二行第二列表示女这个特征。而他们的权重会通过feat_index的每行的索引去关联。

接下来介绍各层代码的开发。

1.2.1从sparse feature层到dense embedding层:

# None * F * K  样本数 * 特征域个数 * 隐向量长,最后得到的embeddings维度(none, none, 8)
self.embeddings = tf.nn.embedding_lookup(self.weights["feature_embeddings"], self.feat_index)
# feat_value:(?, 39, 1)输入特征值的维度
feat_value = tf.reshape(self.feat_value, shape=[-1, self.field_size, 1])
# (?, 39, 8) multiply两个矩阵的对应元素各自相乘(参数权重和对应的特征值相乘)w * x
self.embeddings = tf.multiply(self.embeddings, feat_value)

注:解释的时候都以一条样本输入说明,代码注释中的?和None都表示样本数

第一行解释:

self.weights["feature_embeddings"]是特征的权重参数,注意这里是特征的权重,参看之前那篇文章关于fm和ffm区别的叙述中,原始的fm模型对每个特征都是有一个特征权重的(注意是特征不是特征域)。具体定义如下
# feature_size:特征长度,embedding_size:隐向量长度
weights["feature_embeddings"] = tf.Variable(tf.random_normal([self.feature_size, self.embedding_size], 0.0, 0.01),name="feature_embeddings")
embedding_lookup(params, ids)表示按后面ids从params里面选择对应索引的值,也就是说从按照特征索引(feat_index)从特征权重集(weights[feature_embeddings])中选择对应的特征权重。仍然以上面的例子说明,

weights[feature_embeddings]就是【年龄(0)的权重,男(1)的权重,女(2)的权重】,以第一个样本索引【0,1】为例,就是选出了【年龄(0)的权重,男(1)的权重】

第二到三行解释:

第二行主要是二维向三维的转换,变成:样本数 * 特征域数 * 1,以一条样本为例就是上面的feat_value的第一行【25,0】

第三行是矩阵对应元素相乘,也就是每个特征权重乘以样本特征值,仍然以第一条样本为例就是【25 * 年龄的权重,0 * 男的权重】

从图中也能看出这两层之间也就是权重连接的关系,维度大小并没有变化。

1.2.2 FM层:

从结构图中我们也可以看到fm的数据源来自前面两层,sparse和dense都有数据输入。

从前文和公式我们也能知道,fm分为一阶项(w_{i} * x_{i})和二阶项(w_{ij} * x_i * x_j)两部分,

先说一阶项:

# None * 特征域数 * 1
self.y_first_order = tf.nn.embedding_lookup(self.weights["feature_bias"], self.feat_index)
# None * 特征域数 axis=2最内层元素维度的加和
self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order, feat_value), 2)
# None * 特征域数
# 第一层dropout
self.y_first_order = tf.nn.dropout(self.y_first_order, self.dropout_keep_fm[0])

第一行和上面类似,就是选择对应输入特征的权重值(w_{i} * x_{i}中的w_{i}),不做赘述,其中weights["feature_bias"]的定义如下:

weights["feature_bias"] = tf.Variable(tf.random_uniform([self.feature_size, 1], 0.0, 1.0), name="feature_bias")

接下来第二行和第三行就是做对应的运算(w_{i} * x_{i})和添加dropout层。

二阶项:

# sum_square part  和平方,None * K 就是w_1 * x_1... + w_n * x_n...
self.summed_features_emb = tf.reduce_sum(self.embeddings, 1)
self.summed_features_emb_square = tf.square(self.summed_features_emb)  # None * K  所有特征加和平方 (w_1 * x_1 + w_n * x_n...)^2# square_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 (和平方-平方和) / 2对应元素相减,得到(x_1 * y_1 + ... + x_n * y_n)二阶特征交叉,二阶项本质是各向量交叉后求和的值,所以维度是K
self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square, self.squared_sum_features_emb)  # None * K
self.y_second_order = tf.nn.dropout(self.y_second_order, self.dropout_keep_fm[1])  # None * K fm第二层的dropout

代码就是实现下面的计算逻辑:

\sum _{i=1}^{n-1}\sum _{j=i+1}^n<v_{i},v_{j}>x_{i}x_{j} =...=0.5*\sum _{f=1}^{k}((\sum _{i=1}^{n}v_{i,f}x_{i})^2-(\sum _{i=1}^{n}v_{i,f}^{2}x_{i}^{2}))

2. Deep部分

模型结构如下:

原始的deepfm深度部分使用的就是普通的dnn结构,如下代码:

# None * (F*K) (样本数, 特征域长度 * 隐向量长度)
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]) # None * layer[i] * 1 (wx+b)if self.batch_norm:  # 对参数批正则化self.y_deep = self.batch_norm_layer(self.y_deep, train_phase=self.train_phase, scope_bn="bn_%d" %i) # None * layer[i] * 1self.y_deep = self.deep_layers_activation(self.y_deep)  # 激活层# dropout at each Deep layer 每层都有一个dropout参数self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[1+i])

首先将dense层输出进行reshape维度变换(将特征域对应的向量拉直平铺),添加dropout层

深度结构一共两层,循环部分分别添加以下步骤:w*x+b、批正则化(可选)、激活层、dropout层,正常的dnn结构。

上面使用的权重参数(w,b),声明如下:

# num_layer:dnn层数
num_layer = len(self.deep_layers)
# 特征域个数 * 隐向量长度
input_size = self.field_size * self.embedding_size
# 标准差glorot设定参数的标准,标准差=sqrt(2/输入维度+输出维度)
glorot = np.sqrt(2.0 / (input_size + self.deep_layers[0]))
# layer_0是和dense层输出做计算,所以参数维度是[特征域个数*隐向量长]
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)  # 1 * layers[0]for i in range(1, num_layer):glorot = np.sqrt(2.0 / (self.deep_layers[i-1] + self.deep_layers[i]))# layer_1第2层 32*32, layers[i-1] * 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)# 1 * layer[i]weights["bias_%d" % i] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])), dtype=np.float32)

按空行分界,先说空行上半部分

deep_layer列表存储的是两层dnn的输出维度,glorot是参数初始化的一种理论,这里不赘述。

深度部分的输入就是dense embedding层的输出,第一层计算的时候进行了reshape拉直处理,所以参数w的输入大小(input_size)=特征域个数 * 隐向量长度,这里就是分别声明w权重和偏置项。

再说空行下半部分,这里声明的时候range是从1开始的,所以这里是从dnn的第二层开始声明对应的参数变量(w,b,每一层输出维度的超参存储在deep_layer中)。

以上,fm部分和dnn部分都已经解释清楚了,接下来就是收尾。

3.FM和Deep的组合收尾

先看下整体的结构图(以上结构图均截自原文):

最后这部分就是将fm和deep部分组合到一起,也就是低阶特征和高阶特征的组合,最后计算输出,先看下代码:

# fm的一阶项、二阶项、深度输出项拼接,变成 样本数 * 79
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"])

第一行就是一阶项+二阶项(fm部分)、deep部分拼接(拉直铺平)

第二行就是最后的全连接层,计算逻辑就是w*x+b。至此,deepfm结构实现完毕。

4.总结

deepfm已经在工业界得到了普遍的应用,写这篇分享的目的也是希望自己能够从理论到实践有一个更深入的理解,不当之处还望多多指正。

Tensorflow实现DeepFM(代码分析)相关推荐

  1. tensorflow笔记:多层CNN代码分析

    tensorflow笔记系列:  (一) tensorflow笔记:流程,概念和简单代码注释  (二) tensorflow笔记:多层CNN代码分析  (三) tensorflow笔记:多层LSTM代 ...

  2. tensorflow笔记:多层LSTM代码分析

    tensorflow笔记系列:  (一) tensorflow笔记:流程,概念和简单代码注释  (二) tensorflow笔记:多层CNN代码分析  (三) tensorflow笔记:多层LSTM代 ...

  3. Resnet论文解读与TensorFlow代码分析

    残差网络Resnet论文解读 1.论文解读 博客地址:https://blog.csdn.net/loveliuzz/article/details/79117397 2.理解ResNet结构与Ten ...

  4. GraphSAGE NIPS 2017 代码分析(Tensorflow版)

    文章目录 数据集 ppi数据集信息 toy-ppi-G.json 图的信息 toy-ppi-class_map.json toy-ppi-id_map.json toy-ppi-walks.txt t ...

  5. 理解ResNet结构与TensorFlow代码分析

    该博客主要以TensorFlow提供的ResNet代码为主,但是我并不想把它称之为代码解析,因为代码和方法,实践和理论总是缺一不可.  github地址,其中: resnet_model.py为残差网 ...

  6. 独家 | 手把手教TensorFlow(附代码)

    上一期我们发布了"一文读懂TensorFlow(附代码.学习资料)",带领大家对TensorFlow进行了全面了解,并分享了入门所需的网站.图书.视频等资料,本期文章就来带你一步步 ...

  7. 开源项目kcws代码分析--基于深度学习的分词技术

    http://blog.csdn.net/pirage/article/details/53424544 分词原理 本小节内容参考待字闺中的两篇博文: 97.5%准确率的深度学习中文分词(字嵌入+Bi ...

  8. TensorFlow-CIFAR10 CNN代码分析

    CIFAR 代码组织 代码分析 cifar10_trainpy cifar10py cifar10_evalpy Reference 根据TensorFlow 1.2.1,改了官方版本的报错. CIF ...

  9. Vision Transformer(VIT)代码分析——保姆级教程

    目录 前言 一.代码分析 1.1.DropPath模块 1.2.Patch Embeding 1.3.Multi-Head Attention 1.4.MLP 1.5.Block 1.6.Vision ...

最新文章

  1. python可视化案例书籍推荐_这5款Python可视化神器,总有一款适合你!
  2. areas where akka is being deployed into production
  3. JavaScript知识总结
  4. NYOJ 737 合并石子(一)
  5. Javascript高级程序设计第二版第七章匿名函数--笔记
  6. 使用序列化反序列化实现学生管理系统
  7. SYBASE数据导入技巧
  8. 电击图片弹出无边自定义窗口
  9. 网购成瘾是一种精神障碍?将导致抑郁和破坏社交 网友:没钱即可根治
  10. tf.gradients ---错误FetchargumentNonehasinvalidtype
  11. Windows系统服务器中安装Redis服务
  12. 透析澳大利亚大学计算机硕士课程 .转
  13. android 触摸屏部分失灵,触摸屏失灵修复小技巧
  14. word恢复忘记保存的文档
  15. 胃溃疡 ---- 四药同治(丽珠得乐、奥美拉唑、阿莫西林 + 克林霉素)
  16. android-ProGuard混淆
  17. 某程序员因准点下班没加班,被劝退!网友:还有没有天理?
  18. 运用审查元素下载网页视频
  19. 网站变灰,首页变灰怎么实现?
  20. 机器学习——科学计算库(Numpy,Matplotlib,Pandas)

热门文章

  1. 发送writely和orkut邀请
  2. 新海诚没有参与制作的作品_超豪华的制作团队只有这一次——新海诚《你的名字》访谈(下)...
  3. Ecshop实现注册页面手机号唯一的验证
  4. Unity URP DOTS Animator
  5. cudnn.benchmark = True什么意思
  6. R语言结果输出到word-stargazer
  7. DOL魔盘存储网络管理系统试用心得(转)
  8. BAT批处理批量修改文件后缀名
  9. 搭建客服知识库,轻松解决售后服务问题
  10. cocos2dx 更改精灵图片方法