DeepFM升级版XDeepFM模型_xdeepfm模型简介-程序员宅基地

技术标签: 机器学习  深度学习  人工智能  推荐算法  

本文参考自 推荐系统遇上深度学习(二十二)--DeepFM升级版XDeepFM模型强势来袭!,在学习原文的基础上加入一些个人的理解和整理。

 

1、引言

对于预测性的系统来说,特征工程起到了至关重要的作用。特征工程中,挖掘交叉特征是至关重要的。交叉特征指的是两个或多个原始特征之间的交叉组合。例如,在新闻推荐场景中,一个三阶交叉特征为AND(user_organization=msra,item_category=deeplearning,time=monday_morning),它表示当前用户的工作单位为微软亚洲研究院,当前文章的类别是与深度学习相关的,并且推送时间是周一上午。

传统的推荐系统中,挖掘交叉特征主要依靠人工提取,但是现在依旧需要大量人工去做。这种做法主要有以下三种缺点:

1)重要的特征都是与应用场景息息相关的,针对每一种应用场景,工程师们都需要首先花费大量时间和精力深入了解数据的规律之后才能设计、提取出高效的高阶交叉特征,因此人力成本高昂;
2)原始数据中往往包含大量稀疏的特征,例如用户和物品的ID,交叉特征的维度空间是原始特征维度的乘积,因此很容易带来维度灾难的问题;
3)人工提取的交叉特征无法泛化到未曾在训练样本中出现过的模式中。

因此自动学习特征间的交互关系是十分有意义的。目前大部分相关的研究工作是基于因子分解机的框架,利用多层全连接神经网络去自动学习特征间的高阶交互关系,例如FNN、PNN和DeepFM等。其缺点是模型学习出的是隐式的交互特征,其形式是未知的、不可控的;同时它们的特征交互是发生在元素级(bit-wise)而不是特征向量之间(vector-wise),这一点违背了因子分解机的初衷。来自Google的团队在KDD 2017 AdKDD&TargetAD研讨会上提出了DCN模型,旨在显式(explicitly)地学习高阶特征交互,其优点是模型非常轻巧高效,但缺点是最终模型的表现形式是一种很特殊的向量扩张,同时特征交互依旧是发生在元素级上。

我们用下图来回顾一下DCN的实现:

下面是我对文中提到的两个重要概念的理解:

bit-wise VS vector-wise
假设隐向量的维度为3维,如果两个特征(对应的向量分别为(a1,b1,c1)和(a2,b2,c2)的话)在进行交互时,交互的形式类似于f(w1 * a1 * a2, w2 * b1 * b2 , w3 * c1 * c2)的话,此时我们认为特征交互是发生在元素级(bit-wise)上。如果特征交互形式类似于 f(w * (a1 * a2 ,b1 * b2,c1 * c2))的话,那么我们认为特征交互是发生在特征向量级(vector-wise)

explicitly VS implicitly
显式的特征交互和隐式的特征交互。以两个特征为例xi和xj,在经过一系列变换后,我们可以表示成 wij * (xi * xj)的形式,就可以认为是显式特征交互,否则的话,是隐式的特征交互。

微软亚洲研究院社会计算组提出了一种极深因子分解机模型(xDeepFM),不仅能同时以显式和隐式的方式自动学习高阶的特征交互,使特征交互发生在向量级,还兼具记忆与泛化的学习能力。

我们接下来就来看看xDeepFM这个模型是怎么做的吧!

2、xDeepFM模型介绍

2.1 Compressed Interaction Network

为了实现自动学习显式的高阶特征交互,同时使得交互发生在向量级上,文中首先提出了一种新的名为压缩交互网络(Compressed Interaction Network,简称CIN)的神经模型。在CIN中,隐向量是一个单元对象,因此我们将输入的原特征和神经网络中的隐层都分别组织成一个矩阵,记为X0 和 Xk。CIN中每一层的神经元都是根据前一层的隐层以及原特征向量推算而来,其计算公式如下:

其中定义的点乘部分计算如下:

我们来解释一下上面的过程,第k层隐层含有H_k条神经元向量。隐层的计算可以分成两个步骤:(1)根据前一层隐层的状态X_{k} 和原特征矩阵 X_{0},两个向量做点乘(与数学中向量点乘是标量不同)计算出一个中间结果 Z^{k+1},它是一个三维的张量,如下图(这个图更让人混淆)所示:

在这个中间结果上,我们用H^k+1 个尺寸为 m*Hk 的卷积核生成下一层隐层的状态,该过程如图2所示。这一操作与计算机视觉中最流行的卷积神经网络大体是一致的,唯一的区别在于卷积核的设计。CIN中一个神经元相关的接受域是垂直于特征维度D的整个平面,而CNN中的接受域是当前神经元周围的局部小范围区域,因此CIN中经过卷积操作得到的特征图(Feature Map)是一个向量,而不是一个矩阵。

Feature map1中一个D维向量,第一个元素是由一组权重1*第一层所有元素得到的,第二个元素是由一组权重1*第二层所有元素得到的,以此类推得到整个D维向量;因为我们有多组不同的权重1、权重2、权重3、权重4.... 所以再重复上面的计算得到Feature map2、Feature map3...Feature mapHk+1,所有这些D维向量构成一个矩阵。

如果你觉得原文中的图不够清楚的话,希望下图可以帮助你理解整个过程:

H_{k-1}和 H_{k} 是不相等的,即X^{k}X^{k-1} 中D维向量的个数是不同的。

CIN(压缩交互网络) 的原理:

        第一步:m个特征,每个特征有d维,构成一个m*d的二维矩阵(张量)X0,X0(m*d)与X_k-1 (H_k-1 *m)做点乘得到一个三维矩阵Z1;

        第二步:这个三维矩阵在通过H_k组权重参数H_k*(H_k-1 * m)将该三维矩阵压缩成H_k个d维向量, 得到X_k(H_k*d)的二维矩阵。运算过程中都是向量界别的运算,所以是显示特征交叉。

        CIN的宏观框架可以总结为下图,在做pooling时 考虑到向量中所有数据都是有用的,所以不会选择max pooling(会将大部分数据丢掉,比如应用在图像处理中,只提取出最明显的特征),而是选择sum pooling,每一个交叉特征向量经过sum pooling之后变成一个值,所有交叉特征经过pooling之后变成一个向量,再接一个全连接层得到CIN的输出一个数。CIN的输出(一个值) + 一次项(一个值)  +DNN(一个值)  直接加和得到输出,再通过sigmoid 得到做最后的输出

可以看出,它的特点是,最终学习出的特征交互的阶数是由网络的层数决定的,每一层隐层都通过一个池化操作连接到输出层,从而保证了输出单元可以见到不同阶数的特征交互模式。同时不难看出,CIN的结构与循环神经网络RNN是很类似的,即每一层的状态是由前一层隐层的值与一个额外的输入数据计算所得。不同的是,CIN中不同层的参数是不一样的,而在RNN中是相同的;RNN中每次额外的输入数据是不一样的,而CIN中额外的输入数据是固定的,始终是X0。

可以看到,CIN是通过(vector-wise)来学习特征之间的交互的,还有一个问题,就是它为什么是显式的进行学习?我们先从X1 来开始看,X1 的第h个神经元向量可以表示成下式,其中h是固定的,i=1~m, j=1~m,  (X_{i}^{0} \circ X_{j}^{0})的结果就是一个三维矩阵,通过权重系数将三维矩阵压缩成一个D维向量。

进一步,X^2的第h个神经元向量可以表示成:

最后,第k层的第h个神经元向量可以表示成:

因此,我们能够通过上面的式子对特征交互的形式进行一个很好的表示,它是显式的学习特征交叉。

2.2 xDeepFM

CIN与线性回归单元、全连接神经网络单元组合在一起,得到最终的模型并命名为极深因子分解机xDeepFM,其结构如下图:xDeepFM觉得只有wide model 记忆模型and deep model学习模型,deep model做的是隐示的特征交叉??,并不知道那些特征做了交叉,只知道对不同的特征*不同的权重然后继续下一层,还不够,希望有一个方式能显示的对Xi,Xj以某种方式做交叉和组合,我还要选出其中一部分特征完成某种形式的交叉,就有了CIN。  其实在推荐或CTR预估的模型都是在反复的做一件事情,就是将特征以显示或隐示的方式做特征交叉或特征组装。 

集成的CIN和DNN两个模块能够帮助模型同时以显式和隐式的方式学习高阶的特征交互,而集成的线性模块和深度神经模块也让模型兼具记忆与泛化的学习能力。值得一提的是,为了提高模型的通用性,xDeepFM中不同的模块共享相同的输入数据。而在具体的应用场景下,不同的模块也可以接入各自不同的输入数据,例如,线性模块中依旧可以接入很多根据先验知识提取的交叉特征来提高记忆能力,而在CIN或者DNN中,为了减少模型的计算复杂度,可以只导入一部分稀疏的特征子集。

3、Tensorflow充电

在介绍xDeepFM的代码之前,我们先来进行充电,学习几个tf的函数以及xDeepFM关键过程的实现。

tf.split
首先我们要实现第一步:

如何将两个二维的矩阵,相乘得到一个三维的矩阵?我们首先来看一下tf.split函数的原理:

tf.split(
    value,
    num_or_size_splits,
    axis=0,
    num=None,
    name='split'
)

其中,value传入的就是需要切割的张量,axis是切割的维度,根据num_or_size_splits的不同形式,有两种切割方式:

  1. 如果num_or_size_splits传入的是一个整数,这个整数代表这个张量最后会被切成几个小张量。此时,传入axis的数值就代表切割哪个维度(从0开始计数)。调用tf.split(my_tensor, 2,0)返回两个10 * 30 * 40的小张量。
  2. 如果num_or_size_splits传入的是一个向量,那么向量有几个分量就分成几份,切割的维度还是由axis决定。比如调用tf.split(my_tensor, [10, 5, 25], 2),则返回三个张量分别大小为 20 * 30 * 10、20 * 30 * 5、20 * 30 * 25。很显然,传入的这个向量各个分量加和必须等于axis所指示原张量维度的大小 (10 + 5 + 25 = 40)。

好了,从实际需求出发,我们来体验一下,假设我们的batch为2,embedding的size是3,field数量为4。我们先来生成两个这样的tensor(假设X^k的field也是4 ):

整体流程:(1)  先生成两个2*4*3的tensor,(2)然后按axis=2切分得到3*2*4*1,  (3)tensor与自身的转置相乘 3*2*4*1 3*2*1*4 = 3*2*4*4, (4)将tensor的shape进行转换 3*2*4*4  --> 2*3*4*4

arr1 = tf.convert_to_tensor(np.arange(1,25).reshape(2,4,3),dtype=tf.int32) # 2*4*3
arr2 = tf.convert_to_tensor(np.arange(1,25).reshape(2,4,3),dtype=tf.int32)

生成的矩阵2*4*3如下:

在经过CIN的第一步之后,我们目标的矩阵大小应该是2(batch) * 3(embedding Dimension) * 4(X^k的field数) * 4(X^0的field数)。如果只考虑batch中第一条数据的话,应该形成的是 1 * 3 * 4 * 4 的矩阵。忽略第0维,想像成一个长宽为4,高为3的长方体,长方体横向切割,第一个横截面对应的数字应该如下:

那么想要做到这样的结果,我们首先按输入数据的axis=2进行split:2*4*3 --> 3*2*4*1

split_arr1 = tf.split(arr1,[1,1,1],2)  # 3*2*4*1
split_arr2 = tf.split(arr2,[1,1,1],2)
print(split_arr1)
print(sess.run(split_arr1))
print(sess.run(split_arr2))

分割后的结果如下:

通过结果我们可以看到,我们现在对每一条数据,得到了3个4*1的tensor,可以理解为此时的tensor大小为 3(embedding Dimension) * 2(batch) * 4(X^k 或X^0的field数) * 1。

此时我们进行矩阵相乘: tf.multiply() 两个矩阵中对应元素各自相乘;tf.matmul() 将矩阵a乘以矩阵b,生成a * b。

res = tf.matmul(split_arr1,split_arr2,transpose_b=True)  transpose_b:如果 True,第二个矩阵 在乘法之前转置.
 

这里我理解的,tensorflow对3维及以上矩阵相乘时,矩阵相乘只发生在最后两维。也就是说,3 * 2 * 4 * 1 和 3 * 2 * 1 * 4的矩阵相乘,最终的结果是3 * 2 * 4 * 4。我们来看看结果:

res 的第一个矩阵就是就是通过下图的4*1向量*1*4向量得到的。

split_arr1,split_arr2 是两个相同的2*4*1 三维tensor ,

  

可以看到,不仅矩阵的形状跟我们预想的一样,同时结果也跟我们预想的一样。

最后,我们只需要进行transpose操作,把batch转换到第0维就可以啦。

res = tf.transpose(res,perm=[1,0,2,3])

这样,CIN中的第一步就大功告成了,明白了这一步如何用tensorflow实现,那么代码你也就能够顺其自然的看懂啦!

这一块完整的代码如下:

import tensorflow as tf
import numpy as np

arr1 = tf.convert_to_tensor(np.arange(1,25).reshape(2,4,3),dtype=tf.int32)
arr2 = tf.convert_to_tensor(np.arange(1,25).reshape(2,4,3),dtype=tf.int32)


with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    split_arr1 = tf.split(arr1,[1,1,1],2)
    split_arr2 = tf.split(arr2,[1,1,1],2)
    print(split_arr1)
    print(sess.run(split_arr1))
    print(sess.run(split_arr2))
    res = tf.matmul(split_arr1,split_arr2,transpose_b=True)
    print(sess.run(res))
    res = tf.transpose(res,perm=[1,0,2,3])
    print(sess.run(res))

4、XDeepFM的TF实现  接下来的还没看!!!

本文的代码来自github地址:https://github.com/Leavingseason/xDeepFM
而我的github库中也偷偷把这里面的代码加进去啦:https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-XDeepFM-Demo

真的是写的非常好的一段代码,希望大家可以比着自己敲一敲,相信你会有所收获。

具体的代码细节我们不展开进行讨论,我们只说一下数据的问题吧:
1、代码中的数据按照ffm的格式存储,格式如下:filed:n th dimension:value,即这个特征属于第几个field,在所有特征全部按one-hot展开后的第几维(而不是在这个field中是第几维)以及对应的特征值。
2、代码中使用到的数据属于多值的离散特征。

关于代码实现细节,我们这里只说一下CIN的实现:

由于X^0 在每一层都有用到,所以我们先对 X^0 进行一个处理:

nn_input = tf.reshape(nn_input, shape=[-1, int(field_num), hparams.dim])
split_tensor0 = tf.split(hidden_nn_layers[0], hparams.dim * [1], 2)

在计算X^k 时,我们需要用到 X^k-1 的数据,代码中用hidden_nn_layers保存这些数据。对X^k-1 进行和X^0 同样的处理:

split_tensor = tf.split(hidden_nn_layers[-1], hparams.dim * [1], 2)

接下来就是我们之前讲过的,对两个split之后的tensor进行相乘再转置的过程啦:

dot_result_m = tf.matmul(split_tensor0, split_tensor, transpose_b=True)
dot_result_o = tf.reshape(dot_result_m, shape=[hparams.dim, -1, field_nums[0]*field_nums[-1]])
dot_result = tf.transpose(dot_result_o, perm=[1, 0, 2])

接下来,我们需要进行CIN的第二步,先回顾一下:

这里我们用1维卷积实现,假设X^K的field的数量我们起名为layer_size:

filters = tf.get_variable(name="f_"+str(idx),
                     shape=[1, field_nums[-1]*field_nums[0], layer_size],
                     dtype=tf.float32)

curr_out = tf.nn.conv1d(dot_result, filters=filters, stride=1, padding='VALID')

此时我们curr_out的大小就是 Batch * Embedding Size * Layer size,我们需要进行一下转置:

curr_out = tf.transpose(curr_out, perm=[0, 2, 1])

接下来就是最后一步,进行sumpooling,如下图:

代码中有两种选择方式,direct方式和非direct方式,direct方式,直接把完整curr_out作为最后输出结果的一部分,同时把完整的curr_out作为计算下一个隐藏层向量的输入。非direct方式,把curr_out按照layer_size进行均分,前一半作为计算下一个隐藏层向量的输入,后一半作为最后输出结果的一部分。


if direct:
    hparams.logger.info("all direct connect")
    direct_connect = curr_out
    next_hidden = curr_out
    final_len += layer_size
    field_nums.append(int(layer_size))

else:
    hparams.logger.info("split connect")
    if idx != len(hparams.cross_layer_sizes) - 1:
        next_hidden, direct_connect = tf.split(curr_out, 2 * [int(layer_size / 2)], 1)
        final_len += int(layer_size / 2)
    else:
        direct_connect = curr_out
        next_hidden = 0
        final_len += layer_size
    field_nums.append(int(layer_size / 2))

final_result.append(direct_connect)
hidden_nn_layers.append(next_hidden)

最后 ,经过sum_pooling操作,再拼接一个输出层,我们就得到了CIN部分的输出:

result = tf.concat(final_result, axis=1)
result = tf.reduce_sum(result, -1)

hparams.logger.info("no residual network")
w_nn_output = tf.get_variable(name='w_nn_output',
                              shape=[final_len, 1],
                              dtype=tf.float32)
b_nn_output = tf.get_variable(name='b_nn_output',
                              shape=[1],
                              dtype=tf.float32,
                              initializer=tf.zeros_initializer())
self.layer_params.append(w_nn_output)
self.layer_params.append(b_nn_output)
exFM_out = tf.nn.xw_plus_b(result, w_nn_output, b_nn_output)

5、总结

我们今天介绍的xDeepFM模型,由linear、DNN、CIN三部分组成,其中CIN实现了自动学习显式的高阶特征交互,同时使得交互发生在向量级上。该模型在几个数据集上都取得了超过DeepFM模型的效果。

参考文献

1、论文:https://arxiv.org/abs/1803.05170
2、特征交互:一种极深因子分解机模型(xDeepFM):https://www.xianjichina.com/news/details_81731.html
3、https://blog.csdn.net/SangrealLilith/article/details/80272346
4、https://github.com/Leavingseason/xDeepFM

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qfikh/article/details/103976790

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签