写在前面 xDeepFM(eXtreme DeepFM),这是2018年中科大联合微软在KDD上提出的一个模型,在DeepFM的前面加了一个eXtreme,看这个名字,貌似是DeepFM的加强版,但当我仔细的读完原文之后才发现,如果论血缘关系,这个模型应该离着DCN更近一些,这个模型的改进出发点依然是如何更好的学习特征之间的高阶交互作用,从而挖掘更多的交互信息。而基于这样的动机,作者提出了又一个更powerful的网络来完成特征间的高阶显性交互(DCN的话是一个交叉网络), 这个网络叫做CIN(Compressed Interaction Network),这个网络也是xDeepFM的亮点或者核心创新点了(牛x的地方), 有了这个网络才使得这里的"Deep"变得名副其实。
xDeepFM(eXtreme DeepFM),这是2018年中科大联合微软在KDD上提出的一个模型,在DeepFM的前面加了一个eXtreme,看这个名字,貌似是DeepFM的加强版,但当我仔细的读完原文之后才发现,如果论血缘关系,这个模型应该离着DCN更近一些,这个模型的改进出发点依然是如何更好的学习特征之间的高阶交互作用,从而挖掘更多的交互信息。而基于这样的动机,作者提出了又一个更powerful的网络来完成特征间的高阶显性交互(DCN的话是一个交叉网络), 这个网络叫做CIN(Compressed Interaction Network),这个网络也是xDeepFM的亮点或者核心创新点了(牛x的地方), 有了这个网络才使得这里的"Deep"变得名副其实。而xDeepFM的模型架构依然是w&D结构,更好的理解方式就是用这个CIN网络代替了DCN里面的Cross Network, 这样使得该网络同时能够显性和隐性的学习特征的高阶交互(显性由CIN完成,隐性由DNN完成)。 那么为啥需要同时学习特征的显性和隐性高阶交互呢? 为啥会用CIN代替Cross Network呢? CIN到底有什么更加强大之处呢? xDeepFM与之前的DeepFM以及FM的关系是怎样的呢? 这些问题都会在后面一一揭晓。
这篇文章的逻辑和前面一样,首先依然是介绍xDeepFM的理论部分和论文里面的细节,我觉得这篇文章的创新思路还是非常厉害的,也就是CIN的结构,在里面是会看到RNN和CNN的身影的,又会看到Cross Network的身影。所以这个结构我这次也是花了一些时间去理解,花了一些时间找解读文章看, 但讲真,解读文章真没有论文里面讲的清晰,所以我这次整理也是完全基于原论文加上我自己的理解进行解读。 当然,我水平有限,难免有理解不到位的地方,如果发现有错,也麻烦各位大佬帮我指出来呀。这样优秀的一个模型,不管是工业上还是面试里面,也是非常喜欢用或者考的内容,所以后面依然参考deepctr的代码,进行简化版的复现,重点看看CIN结构的实现过程。最后就是简单介绍和小总。
这篇文章依然比较长,首先是CIN结构本身可能比较难理解,前面需要一定的铺垫任务,比如一些概念(显隐性交叉,bit-wise和vector-wise等), 一些基础模型(FM,FNN,PNN,DNN等),DCN的Cross Network,有了这些铺垫后再理解CIN以及操作会简单些,而CIN本身运算也可能比较复杂,再加上里面时间复杂度和空间复杂度那块的分析,还有后面实验的各个小细节以最后论文还帮助我们串联了各种模型,我想在这篇文章中都整理一下。 再加上模型的复现内容,所以篇幅上还是会很长,各取所需吧还是哈哈。当然这篇文章的重点还是在CIN,这个也是面试里面非常喜欢问的点。
再具体介绍xDeepFM之前,想先整理点铺垫的知识,也是以前的一些内容,是基于原论文的Introduction部分摘抄了一些,算是对前面内容的一些回顾吧,因为这段时间一直忙着找实习,也已经好久没有写这个系列的相关文章了。所以多少还是有点风格和知识上的遗忘哈哈。
首先是在推荐系统里面, 一般原始的特征很难让模型学习到隐藏在数据背后的规律,因为推荐系统中的原始特征往往非常稀疏,且维度非常高。所以如果想得到一个好的推荐系统,我们必须尽可能的制作更多的特征出来,而特征组合往往是比较好的方式,毕竟特征一般都不是独立存在的,那么特征究竟怎么组合呢? 这是一个比较值得研究的难题,并且好多学者在这上面也下足了工夫。 如果你说,特征组合是啥来? 不太清楚了呀,那么文章中这个例子正好能解决你的疑问
起初的时候,是人工特征组合,这个往往在作比赛的时候会遇到,就是特征工程里面自己进行某些特征的交叉与组合来生成新的特征。 这样的方式会有几个问题,作者在论文里面总结了:
所以,让模型自动的进行特征交叉组合探索成了推荐模型里面的比较重要的一个任务,还记得吗? 这个也是模型进化的方向之一,之所以从前面进行引出,是因为本质上这篇的主角xDeepFM也是从这个方向上进行的探索, 那么既然又探索,那也说明了前面模型在这方面还有一定的问题,那么我们就来再综合理一理。
FM模型: 这个模型能够自动学习特征之间的两两交叉,并且比较厉害的地方就是用特征的隐向量内积去表示两两交叉后特征的重要程度,这使得模型在学习交互信息的同时,也让模型有了一定的泛化能力。 But,这个模型也有缺点,首先一般是只能应付特征的两两交叉,再高阶一点的交叉虽然行,但计算复杂,并且作者在论文中提到了高阶交叉的FM模型是不管有用还是无用的交叉都建模,这往往会带来一定的噪声。
DNN模型: 这个非常熟悉了,深度学习到来之后,推荐模型的演化都朝着DNN的时代去了,原因之一就是因为DNN的多层神经网络可以比较出色的完成特征之间的高阶交互,只需要增加网络的层数,就可以轻松的学习交互,这是DNN的优势所在。 比较有代表的模型PNN,DeepCrossing模型等。 But, DNN并不是非常可靠,有下面几个问题。
也就是DNN是否能够真正的有效学习特征之间的高阶交互是个谜!
3. DNN还存在的问题就是学习高阶交互或许是可能,但没法再兼顾低阶交互,也就是记忆能力,所以后面w&D架构在成为了主流架构。
Wide&Deep, DeepFM模型: 这两个模型是既有DNN的深度,也有FM或者传统模型的广度,兼顾记忆能力和泛化能力,也是后面的主流模型。但依然有不足之处,wide&Deep的话不用多讲,首先逻辑回归(宽度部分)仍然需要人工特征交叉,而深度部分的高阶交叉又是一个谜。 DeepFM的话是FM和DNN的组合,用FM替代了逻辑回归,这样至少是模型的自动交叉特征,结合了FM的二阶以及DNN的高阶交叉能力。 但如果DNN这块高度交叉是个谜的话,就有点玄乎了。
DCN网络: 这个模型也是w&D架构,不过宽度那部分使用了一个Cross Network,这个网络的奇妙之处,就是扩展了FM这种只能显性交叉的二阶的模型, 通过交叉网络能真正的显性的进行特征间的高阶交叉。 具体结构后面还会在复习,毕竟这次的xdeepFM主要是在Cross Network的基础上进行的再升级,这也是为啥说论血缘关系,xdeepFM离DCN更近一些的原因。那么Cross network有啥问题呢? 这个想放在后面的2.4去说了,这样能更好的引出本篇主角来。
通过上面的一个梳理,首先是回忆起了前面这几个模型的发展脉络, 其次差不多也能明白xDeepFM到底再干个什么事情了,或者要解决啥问题了,xDeepFM其实简单的说,依然是研究如何自动的进行特征之间的交叉组合,以让模型学习的更好。 从上面这段梳理中,我们至少要得到3个重要信息:
所以xDeepFM的改进动机来了: 更有效的高阶显性交叉特征(CIN),更高的泛化能力(vector-wise), 显性和隐性高阶特征的组合(CIN+DNN), 这就是xDeepFM了, 而这里面的关键,就是CIN网络了。在这之前,还是先把准备工作做足。
这个是为了回顾一下,简单一说,我们拿到的数据往往会有连续型数据和离散型或者叫类别型数据之分。 如果是连续型数据,那个不用多说,一般会归一化或者标准化处理,当然还可能进行一定的非线性化操作,就算处理完了。 而类别型数据,一般需要先LabelEncoder,转成类别型编码,然后再one-hot转成0或者1的编码格式。 比如论文里面的这个例子:
这样的数据往往是高维稀疏的,不利于模型的学习,所以往往在这样数据之后,加一个embedding层,把数据转成低维稠密的向量表示。 关于embedding的原理这里不说了,但这里要注意一个细节,就是如果某个特征每个样本只有一种取值,也就是one-hot里面只有一个地方是1,比如前面3个field。这时候,可以直接拿1所在位置的embedding当做此时类别特征的embedding向量。 但是如果某个特征域每个样本好多种取值,比如interests这个,有好几个1的这种,那么就拿到1所在位置的embedding向量之后求和来代表该类别特征的embedding。这样,经过embedding层之后,我们得到的数据成下面这样了:
这个应该比较好理解,e_i表示的一个向量,一般是D维的(隐向量的维度), 那么假设m表示特征域的个数,那么此时的\mathbf{e}是m\times D的矩阵。
这是特征交互的两种方式,需要了解下,因为论文里面的CIN结构是在vector-wise上完成特征交互的, 这里拿从网上找到的一个例子来解释概念。
假设隐向量的维度是3维, 如果两个特征对应的向量分别是(a_1, b_1, c_1)和(a_2,b_2, c_2)
这个一直没弄明白后者为什么会比前者好,我也在讨论群里问过这个问题,下面是得到的一个伙伴的想法:
对于这个问题有想法的伙伴,也欢迎在下面评论,我自己的想法是bit-wise看上面的定义,仿佛是在元素的级别交叉,然后学习权重, 而vector-wise是在向量的级别交叉,然后学习统一权重,bit-wise具体到了元素级别上,虽然可能学习的更加细致,但这样应该会增加过拟合的风险,失去一定的泛化能力,再联想作者在论文里面解释的bit-wise:
更觉得这个想法会有一定的合理性,就想我在DNN那里解释这个一样,把细节学的太细,就看不到整体了,佛曰:着相了哈哈。如果再联想下FM的设计初衷,FM是一个vector-wise的模型,它进行了显性的二阶特征交叉,却是embedding级别的交互,这样的好处是有一定的泛化能力到看不见的特征交互。 emmm, 我在后面整理Cross Network问题的时候,突然悟了一下, bit-wise最大的问题其实在于违背了特征交互的初衷, 我们本意上其实是让模型学习特征之间的交互,放到embedding的角度,也理应是embedding与embedding的相关作用交互, 但bit-wise已经没有了embedding的概念,以bit为最细粒度进行学习, 这里面既有不同embedding的bit交互,也有同一embedding的bit交互,已经意识不到Field vector的概念。 具体可以看Cross Network那里的解释,分析了Cross Network之后,可能会更好理解些。
这个问题最好是先这样理解或者自己思考下,因为xDeepFM的一个挺大的亮点就是保留了FM的这种vector-wise的特征交互模式,也是作者一直强调的,vector-wise应该是要比bit-wise要好的,否则作者就不会强调Cross Network的弊端之一就是bit-wise,而改进的方法就是设计了CIN,用的是vector-wise。
DNN非常擅长学习特征之间的高阶交互信息,但是隐性的,这个比较好理解了,前面也提到过:
但是问题的话,前面也剖析过了, 简单总结:
谈到显性高阶交互,这里就必须先分析一下我们大名鼎鼎的DCN网络的Cross Network了, 关于这个模型,我在AI上推荐 之 Wide&Deep与Deep&Cross模型文章中进行了一些剖析,这里再复习的话我又参考了一个大佬的文章,因为再把我之前的拿过来感觉没有啥意思,重新再阅读别人的文章很可能会再get新的点,于是乎还真的学习到了新东西,具体链接放到了下面。 这里我们重温下Cross Network,看看到底啥子叫显性高阶交互。再根据论文看看这样子的交互有啥问题。
这里的输入x_0需要提醒下,首先对于离散的特征,需要进行embedding, 对于multi-hot的离散变量, 需要embedding之后再做一个简单的average pooling, 而dense特征,归一化, 然后和embedding的特征拼接到一块作为Cross层和Deep层的输入,也就是Dense特征会在这里进行拼接。 下面回顾Cross Layer。
Cross的目的是一一种显性、可控且高效的方式,自动构造有限高阶交叉特征。 具体的公式如下:
其中\boldsymbol{x}_{l+1}, \boldsymbol{x}_{l}, \boldsymbol{x}_{0} \in \mathbb{R}^{d}。有图有真相:
Cross Layer的巧妙之处全部体现在上面的公式,下面放张图是为了更好的理解,这里我们回顾一些细节。
那么显性交叉到底体会到哪里呢? 还是拿我之前举的那个例子:假设\boldsymbol{x}_{0}=\left[\begin{array}{l}x_{0,1} \\ x_{0,2}\end{array}\right], 为了讨论各层,先令\boldsymbol{b}_i=0, 则
最后得到y_{\text {cross }}=\boldsymbol{x}_{2}^{T} * \boldsymbol{w}_{\text {cross }} \in \mathbb{R}参与到最后的loss计算。 可以看到\boldsymbol{x}_1包含了原始特征x_{0,1},x_{0,2}从一阶导二阶所有可能叉乘组合, 而\boldsymbol{x}_2包含了从一阶导三阶素有可能的叉乘组合, 而显性特征组合的意思,就是最终的结果可以经过一系列转换,得到类似W_{i,j}x_ix_j的形式, 上面这个可以说是非常明显了吧。
这里有一点很值得留意,前面介绍过,文中将dense特征和embedding特征拼接后作为Cross层和Deep层的共同输入。这对于Deep层是合理的,但我们知道人工交叉特征基本是对原始sparse特征进行叉乘,那为何不直接用原始sparse特征作为Cross的输入呢?联系这里介绍的Cross设计,每层layer的节点数都与Cross的输入维度一致的,直接使用大规模高维的sparse特征作为输入,会导致极大地增加Cross的参数量。当然,可以畅想一下,其实直接拿原始sparse特征喂给Cross层,才是论文真正宣称的“省去人工叉乘”的更完美实现,但是现实条件不太允许。所以将高维sparse特征转化为低维的embedding,再喂给Cross,实则是一种trade-off的可行选择。
看下DNN与Cross Network的参数量对比:
初始输入x_0维度是d, Deep和Cross层数分别为L_{cross}和L_{deep}, 为便于分析,设Deep每层神经元个数为m则两部分参数量:
可以看到Cross的参数量随d增大仅呈“线性增长”!相比于Deep部分,对整体模型的复杂度影响不大,这得益于Cross的特殊网络设计,对于模型在业界落地并实际上线来说,这是一个相当诱人的特点。Deep那部分参数计算其实是第一层单算m(d+1), 接下来的L-1层,每层都是m, 再加上b个个数,所以m(m+1)。
好了, Cross的好处啥的都分析完了, 下面得分析点不好的地方了,否则就没法引出这次的主角了。作者直接说:
每一层学习到的是\boldsymbol{x}_0的标量倍,这是啥意思。 这里有一个理论:
这里作者用数学归纳法进行了证明。
当k=1的时候
这里的\alpha^{1}=\mathbf{x}_{0}^{T} \mathbf{w}_{1}+1是x_0的一个线性回归, x_1是x_0的标量倍成立。 假设当k=i的时候也成立,那么k=i+1的时候:
其中\alpha^{i+1}=\alpha^{i}\left(\mathbf{x}_{0}^{T} \mathbf{w}_{i+1}+1\right), 即x_{i+1}依然是x_0的标量倍。
所以作者认为Cross Network有两个缺点:
好了, 如果真正理解了Cross Network,以及上面存在的两个问题,理解xDeepFM的动机就不难了,xDeepFM的动机,正是将FM的vector-wise的思想引入到了Cross部分。
下面主角登场了:
了解了xDeepFM的动机,再强调下xDeepFM的核心,就是提出的一个新的Cross Network(CIN),这个是基于DCN的Cross Network,但是有上面那几点好处。下面的逻辑打算是这样,首先先整体看下xDeepFM的模型架构,由于我们已经知道了这里其实就是用一个CIN网络代替了DCN的Cross Network,那么这里面除了这个网络,其他的我们都熟悉。 然后我们再重点看看CIN网络到底在干个什么样的事情,然后再看看CIN与FM等有啥关系,最后分析下这个新网络的时间复杂度等问题。
首先,我们先看下xDeepFM的架构
这个网络结构名副其实,依然是采用了W&D架构,DNN负责Deep端,学习特征之间的隐性高阶交互, 而CIN网络负责wide端,学习特征之间的显性高阶交互,这样显隐性高阶交互就在这个模型里面体现的淋漓尽致了。不过这里的线性层单拿出来了。
最终的计算公式如下:
这里的\mathbf{a}表示原始的特征,\mathbf{a}_{dnn}^k表示的是DNN的输出, \mathbf{p}^+表示的是CIN的输出。最终的损失依然是交叉熵损失,这里也是做一个点击率预测的问题:
最终的目标函数加了正则化:
这里尝试剖析下本篇论文的主角CIN网络,全称Compressed Interaction Network。这个东西说白了其实也是一个网络,并不是什么高大上的东西,和Cross Network一样,也是一层一层,每一层都是基于一个固定的公式进行的计算,那个公式长这样:
这个公式第一眼看过来,肯定更是懵逼,这是写的个啥玩意?如果我再把CIN的三个核心图放上来:
上面其实就是CIN网络的精髓了,也是它具体的运算过程,只不过直接上图的话,会有些抽象,难以理解,也不符合我整理论文的习惯。下面,我们就一一进行剖析, 先从上面这个公式开始。但在这之前,需要先约定一些符号。要不然不知道代表啥意思。
所以上面的那个公式:
其实就是计算第k层第h个特征向量, 这里的1 \leq h \leq H_{k}, \mathbf{W}^{k, h} \in \mathbb{R}^{H_{k-1} \times m}是第h个特征向量的参数矩阵。 \circ表示的哈达玛积,也就是向量之间对应维度元素相乘(不相加了)。\left\langle a_{1}, a_{2}, a_{3}\right\rangle \circ\left\langle b_{1}, b_{2}, b_{3}\right\rangle=\left\langle a_{1} b_{1}, a_{2} b_{2}, a_{3} b_{3}\right\rangle。通过这个公式也能看到\mathbf{X}^k是通过\mathbf{X}^{k-1}和\mathbf{X}^0计算得来的,也就是说特征的显性交互阶数会虽然网络层数的加深而增加。
那么这个公式到底表示的啥意思呢? 是具体怎么计算的呢?我们往前计算一层就知道了,这里令k=1,也就是尝试计算第一层里面的第h个向量, 那么上面公式就变成了:
这里的\mathbf{W}^{1, h} \in \mathbb{R}^{H_{0} \times m}。这个能看懂吗? 首先这个\mathbf{W}矩阵是H_0行m列, 而前面那两个累加正好也是H_0行m列的参数。m代表的是输入特征的个数, H_0代表的是第0层(k-1层)的神经元的个数, 这个也是m。这个应该好理解,输入层就是第0层。所以这其实就是一个m\times m的矩阵。那么后面这个运算到底是怎么算的呢? 首先对于第i个特征向量, 要依次和其他的m个特征向量做哈达玛积操作,当然也乘以对应位置的权重,求和。对于每个i特征向量,都重复这样的操作,最终求和得到一个D维的向量,这个就是\mathbf{X}_{h, *}^{1}。好吧,这么说。我觉得应该也没有啥感觉,画一下就了然了,现在可以先不用管论文里面是怎么说的,先跟着这个思路走,只要理解了这个公式是怎么计算的,论文里面的那三个图就会非常清晰了。灵魂画手:
这就是上面那个公式的具体过程了,图实在是太难看了, 但应该能说明这个详细的过程了。这样只要给定一个\mathbf{W}^{1,h}之后,就能算出一个相应的\mathbf{X}^1_{h,*}来,这样第一层的H_1个神经元按照这样的步骤就能够都计算出来了。 后面的计算过程其实是同理,无非就是输入是前一层的输出以及\mathbf{X}_0罢了,而这时候,第一个矩阵特征数就不一定是m了,而是一个H_{k-1}行D列的矩阵了。这里的\mathbf{W}^{k,h}就是上面写的H_{k-1}行m列了。
这个过程明白了之后,再看论文后面的内容就相对容易了,首先
CIN里面能看到RNN的身影,也就是当前层的隐藏单元的计算要依赖于上一层以及当前的输入层,只不过这里的当前输入每个时间步都是\mathbf{X}_0。 同时这里也能看到,CIN的计算是vector-wise级别的,也就是向量之间的哈达玛积的操作,并没有涉及到具体向量里面的位交叉。
下面我们再从CNN的角度去看这个计算过程。其实还是和上面一样的计算过程,只不过是换了个角度看而已,所以上面那个只要能理解,下面CNN也容易理解了。首先,这里引入了一个tensor张量\mathbf{Z}^{k+1}表示的是\mathbf{X}^k和\mathbf{X}^0的外积,那么这个东西是啥呢? 上面加权求和前的那个矩阵,是一个三维的张量。
这个可以看成是一个三维的图片,H_{k-1}高,m宽,D个通道。而\mathbf{W}^{k,h}的大小是H_{k-1}\times m的, 这个就相当于一个过滤器,用这个过滤器对输入的图片如果逐通道进行卷积,就会最终得到一个D维的向量,而这个其实就是\mathbf{X}^{k}_{h,*},也就是一张特征图(每个通道过滤器是共享的)。 第k层其实有H_k个这样的过滤器,所以最后得到的是一个H_k\times D的矩阵。这样,在第k个隐藏层,就把了H_{k-1}\times m\times D的三维张量通过逐通道卷积的方式,压缩成了一个H_k\times D的矩阵(H_k张特征图), 这就是第k层的输出\mathbf{X}^k。 而这也就是“compressed"的由来。这时候再看这两个图就非常舒服了:
通过这样的一个CIN网络,就很容易的实现了特征的显性高阶交互,并且是vector-wise级别的,那么最终的输出层是啥呢? 通过上面的分析,首先我们了解了对于第k层输出的某个特征向量,其实是综合了输入里面各个embedding向量显性高阶交互的信息(第k层其实学习的输入embeddingk+1阶交互信息),这个看第一层那个输出就能看出来。第k层的每个特征向量其实都能学习到这样的信息,那么如果把这些向量在从D维度上进行加和,也就是\mathbf{X}^k,这是个H_k\times D的,我们沿着D这个维度加和,又会得到一个H_k的向量,公式如下:
每一层,都会得到一个这样的向量,那么把所有的向量拼接到一块,其实就是CIN网络的输出了。之所以,这里要把中间结果都与输出层相连,就是因为CIN与Cross不同的一点是,在第k层,CIN只包含k+1阶的组合特征,而Cross是能包含从1阶-k+1阶的组合特征的,所以为了让模型学习到从1阶到所有阶的组合特征,CIN这里需要把中间层的结果与输出层建立连接。
这也就是第三个图表示的含义:
这样, 就得到了最终CIN的输出\mathbf{p}^+了:
后面那个维度的意思,就是说每一层是的向量维度是H_i维, 最后是所有时间步的维度之和。 CIN网络的计算过程的细节就是这些了。
CIN和DCN层的设计动机是相似的,Cross层的input也是前一层与输入层,这么做的原因就是可以实现: 有限高阶交互,自动叉乘和参数共享。
但是CIN与Cross的有些地方是不一样的:
这地方没怎么看懂,大体写写吧, 通过对问题进行简化,即假设CIN中不同层的feature map的数量全部一致,均为fields的数量m,并且用[m]表示小于等于m的正整数。CIN中的第一层的第h个feature map表示为x_h^1 \in \mathbb{R}^D,即
因此, 在第一层中通过O(m^2)个参数来建模成对的特征交互关系,相似的,第二层的第h个特征图表示为:
由于第二个\mathbf{W}矩阵在前面一层计算好了,所以第二层的feature map也是只用了O(m^2)个参数就建模出了3阶特征交互关系。
我们知道一个经典的k阶多项式一般是需要O(m^k)个参数的,而我们展示了CIN在一系列feature map中只需要O(k m^2)个参数就可以近似此类多项式。而且paper使用了归纳假设的方法证明了一下,也就是后面那两个公式。具体的没咋看懂证明,不整理了。但得知道两个结论:
sum filter,那么xDeepFM就退化成了FM形式了。一般这种模型的改进,是基于之前模型进行的,也就是简化之后,会得到原来的模型,这样最差的结果,模型效果还是原来的,而不应该会比原来模型的表现差,这样的改进才更有说服力。
所以,既然提到了FM,再考虑下面两个问题理解下CIN设计的合理性。
这一块就是后面实验了,这里作者依然是抛出了三个问题,并通过实验进行了解答。
这里用了三个数据集
评估指标用了两个AUC和Logloss, 这两个是从不同的角度去评估模型。
作者说:
这里作者又梳理了之前的模型,这里就再梳理一遍了
经典推荐系统
深度学习模型
推荐系统数据特点: 稀疏,类别连续特征混合,高维。
关于未来两个方向:
下面看下xDeepFM模型的具体实现, 这样可以从更细节的角度去了解这个模型, 这里我依然是参考的deepctr的代码风格,这种函数式模型编程更清晰一些,当然由于时间原因,我这里目前只完成了一个tf2版本的(pytorch版本的后面有时间会补上)。 这里先看下xDeepFM的全貌:
def xDeepFM(linear_feature_columns, dnn_feature_columns, cin_size=[128, 128]): # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型 dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns+dnn_feature_columns) # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式 # 注意:这里实际的输入预Input层对应,是通过模型输入时候的字典数据的key与对应name的Input层 input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values()) # 线性部分的计算逻辑 -- linear linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_feature_columns) # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型 # 线性层和dnn层统一的embedding层 embedding_layer_dict = build_embedding_layers(linear_feature_columns+dnn_feature_columns, sparse_input_dict, is_linear=False) # DNN侧的计算逻辑 -- Deep # 将dnn_feature_columns里面的连续特征筛选出来,并把相应的Input层拼接到一块 dnn_dense_feature_columns = list(filter(lambda x: isinstance(x, DenseFeat), dnn_feature_columns)) if dnn_feature_columns else [] dnn_dense_feature_columns = [fc.name for fc in dnn_dense_feature_columns] dnn_concat_dense_inputs = Concatenate(axis=1)([dense_input_dict[col] for col in dnn_dense_feature_columns]) # 将dnn_feature_columns里面的离散特征筛选出来,相应的embedding层拼接到一块 dnn_sparse_kd_embed = concat_embedding_list(dnn_feature_columns, sparse_input_dict, embedding_layer_dict, flatten=True) dnn_concat_sparse_kd_embed = Concatenate(axis=1)(dnn_sparse_kd_embed) # DNN层的输入和输出 dnn_input = Concatenate(axis=1)([dnn_concat_dense_inputs, dnn_concat_sparse_kd_embed]) dnn_out = get_dnn_output(dnn_input) dnn_logits = Dense(1)(dnn_out) # CIN侧的计算逻辑, 这里使用的DNN feature里面的sparse部分,这里不要flatten exFM_sparse_kd_embed = concat_embedding_list(dnn_feature_columns, sparse_input_dict, embedding_layer_dict, flatten=False) exFM_input = Concatenate(axis=1)(exFM_sparse_kd_embed) exFM_out = CIN(cin_size=cin_size)(exFM_input) exFM_logits = Dense(1)(exFM_out) # 三边的结果stack stack_output = Add()([linear_logits, dnn_logits, exFM_logits]) # 输出层 output_layer = Dense(1, activation='sigmoid')(stack_output) model = Model(input_layers, output_layer) return model
这种风格最好的一点,就是很容易从宏观上把握模型的整体逻辑。 首先,接收的输入是linear_feature_columns和dnn_feature_columns, 这两个是深度和宽度两侧的特征,具体选取要结合着场景来。 接下来,就是为这些特征建立相应的Input层,这里要分成连续特征和离散的特征,因为后面的处理方式不同, 连续特征的话可以直接拼了, 而离散特征的话,需要过一个embedding层转成低维稠密,这就是第一行代码干的事情了。
接下来, 计算线性部分,从上面xDeepFM的结构里面可以看出, 是分三路走的,线性,CIN和DNN路, 所以get_linear_logits就是线性这部分的计算结果,完成的是w_1x_1+w_2x_2..w_kx_k+b, 这里面依然是连续和离散的不太一样,对于连续特征,直接过一个全连接就实现了这个操作,而离散特征,这里依然过一个embedding,不过这个维度是1,目的是转成了一个连续数值(这个相当于离散特征对应的w值),这样后面进行总的加和操作即可。
接下来是另外两路,DNN这路也比较简单, dnn_feature_columns里面的离散特征过embedding,和连续特征拼接起来,然后过DNN即可。 CIN这路使用的是dnn_feature_columns里面的离散embedding特征,进行显性高阶交叉,这里的输入是[None, field_num, embedding_dim]的维度。这个也好理解,每个特征embedding之后,拼起来即可,注意flatten=False了。 这个输入,过CIN网络得到输出。
这样三路输出都得到,然后进行了一个加和,再连接一个Dense映射到最终输出。这就是整体的逻辑了,关于每个部分的具体细节,可以看代码。 下面主要是看看CIN这个网络是怎么实现的,因为其他的在之前的模型里面也基本是类似的操作,比如前面DIEN,DSIN版本,并且我后面项目里面补充了DCN的deepctr风格版,这个和那个超级像,唯一不同的就是把CrossNet换成了CIN,所以这个如果感觉看不大懂,可以先看那个网络代码。下面说CIN。
再具体代码实现, 我们先简单捋一下CIN网络的实现过程,这里的输入是[None, field_num embed_dim]的维度,在CIN里面,我们知道接下来的话,就是每一层会有H_k个神经元, 而每个神经元的计算要根据上面的那个计算公式,也就是X_0要和前面一层的输出两两embedding,加权求和再求和的方式。 而从CNN的角度来看,这个过程可以是这样,对于每一层的计算,先X_0和X_k进行外积运算(相当于两两embedding),然后采用H_k个过滤器对前面的结果逐通道卷积就能得到每一层的X_k了。 最后的输出是每一层的X_k拼接起来,然后在embedding维度上的求和。 所以依据这个思路,就能得到下面的实现代码:
class CIN(Layer): def __init__(self, cin_size, l2_reg=1e-4): """ :param: cin_size: A list. [H_1, H_2, ....H_T], a list of number of layers """ super(CIN, self).__init__() self.cin_size = cin_size self.l2_reg = l2_reg def build(self, input_shape): # input_shape [None, field_nums, embedding_dim] self.field_nums = input_shape[1] # CIN 的每一层大小,这里加入第0层,也就是输入层H_0 self.field_nums = [self.field_nums] + self.cin_size # 过滤器 self.cin_W = { 'CIN_W_' + str(i): self.add_weight( name='CIN_W_' + str(i), shape = (1, self.field_nums[0] * self.field_nums[i], self.field_nums[i+1]), # 这个大小要理解 initializer='random_uniform', regularizer=l2(self.l2_reg), trainable=True ) for i in range(len(self.field_nums)-1) } super(CIN, self).build(input_shape) def call(self, inputs): # inputs [None, field_num, embed_dim] embed_dim = inputs.shape[-1] hidden_layers_results = [inputs] # 从embedding的维度把张量一个个的切开,这个为了后面逐通道进行卷积,算起来好算 # 这个结果是个list, list长度是embed_dim, 每个元素维度是[None, field_nums[0], 1] field_nums[0]即输入的特征个数 # 即把输入的[None, field_num, embed_dim],切成了embed_dim个[None, field_nums[0], 1]的张量 split_X_0 = tf.split(hidden_layers_results[0], embed_dim, 2) for idx, size in enumerate(self.cin_size): # 这个操作和上面是同理的,也是为了逐通道卷积的时候更加方便,分割的是当一层的输入Xk-1 split_X_K = tf.split(hidden_layers_results[-1], embed_dim, 2) # embed_dim个[None, field_nums[i], 1] feild_nums[i] 当前隐藏层单元数量 # 外积的运算 out_product_res_m = tf.matmul(split_X_0, split_X_K, transpose_b=True) # [embed_dim, None, field_nums[0], field_nums[i]] out_product_res_o = tf.reshape(out_product_res_m, shape=[embed_dim, -1, self.field_nums[0]*self.field_nums[idx]]) # 后两维合并起来 out_product_res = tf.transpose(out_product_res_o, perm=[1, 0, 2]) # [None, dim, field_nums[0]*field_nums[i]] # 卷积运算 # 这个理解的时候每个样本相当于1张通道为1的照片 dim为宽度, field_nums[0]*field_nums[i]为长度 # 这时候的卷积核大小是field_nums[0]*field_nums[i]的, 这样一个卷积核的卷积操作相当于在dim上进行滑动,每一次滑动会得到一个数 # 这样一个卷积核之后,会得到dim个数,即得到了[None, dim, 1]的张量, 这个即当前层某个神经元的输出 # 当前层一共有field_nums[i+1]个神经元, 也就是field_nums[i+1]个卷积核,最终的这个输出维度[None, dim, field_nums[i+1]] cur_layer_out = tf.nn.conv1d(input=out_product_res, filters=self.cin_W['CIN_W_'+str(idx)], stride=1, padding='VALID') cur_layer_out = tf.transpose(cur_layer_out, perm=[0, 2, 1]) # [None, field_num[i+1], dim] hidden_layers_results.append(cur_layer_out) # 最后CIN的结果,要取每个中间层的输出,这里不要第0层的了 final_result = hidden_layers_results[1:] # 这个的维度T个[None, field_num[i], dim] T 是CIN的网络层数 # 接下来在第一维度上拼起来 result = tf.concat(final_result, axis=1) # [None, H1+H2+...HT, dim] # 接下来, dim维度上加和,并把第三个维度1干掉 result = tf.reduce_sum(result, axis=-1, keepdims=False) # [None, H1+H2+..HT] return result
这里主要是解释四点:
[1, self.field_nums[0]*self.field_nums[i], self.field_nums[i+1]的,首先,得明白这个self.field_nums存储的是每一层的神经单元个数,这里包括了输入层,也就是第0层。那么每一层的每个神经元计算都会有一个W^{k,h}, 这个的大小是[H_{k-1},m]维的,而第K层一共H_k个神经元,所以总的维度就是[H_{k-1},m,H_k], 这和上面这个是一个意思,只不过前面扩展了维度1而已。[None, in_width, in_channels]的形式,而对应这里的数据是[None, dim, field_nums[0]*field_nums[i]], 而这里的过滤器大小是[1, field_nums[0]*field_nums[i], field_nums[i+1], 这样进行卷积的话,最后一个维度是卷积核的数量。是沿着dim这个维度卷积,得到的是[None, dim, field_nums[i+1]]的张量,这个就是第i+1层的输出了。和我画的
这个不同的是它把前面这个矩形Flatten了,得到了一个[D,H_{k-1}\times m]的二维矩阵,然后用[1,H_{k-1}\times m]的卷积核沿着D这个维度进行Conv1D, 这样就直接得到了一个D维向量, 而H_k个卷积核,就得到了H_k\times D的矩阵了。
4. 每一层的输出X_k先加入到列表里面,然后在H_i的维度上拼接,再从D这个维度上求和,这样就得到了CIN的最终输出。
关于CIN的代码细节解释到这里啦,剩下的可以看后面链接里面的代码了。
这篇文章主要是介绍了又一个新的模型xDeepFM, 这个模型的改进焦点依然是特征之间的交互信息,xDeepFM的核心就是提出了一个新的CIN结构(这个是重点,面试的时候也喜欢问),将基于Field的vecotr-wise思想引入到了Cross Network中,并保留了Cross高阶交互,自动叉乘,参数共享等优势,模型结构上保留了DeepFM的广深结构。主要有三大优势:
如果说DeepFM只是“Deep & FM”,那么xDeepFm就真正做到了”Deep” Factorization Machine。当然,xDeepFM的时间复杂度比较高,会是工业落地的主要瓶颈,后面需要进行一些优化操作。
这篇论文整体上还是非常清晰的,实验做的也非常丰富,语言描述上也非常地道,建议读读原文呀。
参考: