【CTR预估】 xDeepFM模型

xDeepFM 模型看作者邮箱应该中科大、北邮、微软合作发表的,发表在kdd2018
看这个模型的原因是因为最近在写Deep Cross Network的时候感觉总是怪怪的,因为DCN对同一个特征的embedding内部都做了特征的交叉,这个和我们正常直观的特征交叉会有明显的出入,虽然DCN模型在实践中确实会好于正常的wide&deep,说明显式的特征交叉是有意义的,但是有没有办法不对这些自身内部的bit进行交叉,来减少不必要的交叉次数,也可以一定程度上减少自身的冗余对模型效果的影响。搜索来一些发现了这篇文章,所以读了一下。
下载地址《xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems》
作者开源了论文代码: https://github.com/Leavingseason/xDeepFM
我自己写了一个ctr预估相关的git库,复现了一些现在经典的网络,如DIN,ESMM,DIEN等。其中也复现了xDeepFMhttps://github.com/Shicoder/Deep_Rec/tree/master/Deep_Rank

1.Overview

文章提出一个新的模型eXtreme Deep Factorization Machine 。算法提出的目的是为了解决特征交互的问题。DNN有能力学习出任意的函数,但是学习到的特征交互都是隐式的,并且都是基于bit-wise level。所谓的隐式指的是我们并不知道模型所做的特征交叉是在做哪些特征的交叉,所谓的bit-wise level的特征交互,这里作者提出了两个概念,一个是bit-wise level的特征交叉,一个是vector-wise level。意思其实就是字面意思,bit-wise level的特征交叉就是指神经网络中节点之间的交互,如DCN中的crossNet就是bit-wise的交互。vector-wise的特征交互指的是将每个特征embedding后的整个embedding vector作为一个域,然后对域和域之间进行特征交叉,如FMDeepFMPNN中的特征交叉。
那么怎么在神经网络中做特征交互呢?
1.隐式的高阶特征交互其实可以使用DNN模块,而且基本在现在一些经典的基于深度学习的ctr预估模型中都会使用到该部分,如PNN,Wide&Deep,FNN,DeepFM,DCN等。但是DNN虽然可以拟合出任意的函数,却没有理论结论,而且无法知道特征交叉到了什么程度,基于DNN的特征交叉完全是在bit-wise level下进行,和传统的FM基于vector-wise的交叉完全不同。
2.显式的特征交交叉。文章基于DCN算法思想提出了一种新的模型结构CIN用来代替DCN模型中的cross net。使得模型可以显式的学习特征在vector-wise的交叉。并结合隐式的特征交互,提出了一个新模型 xDeepFM而且作者认为DCN的学习交叉能力有限,因为DCN的crossNet每一层的输出其实对最原始输入X_0的一个标量倍数。
如果k为第k层cross net,那么当k=1的时候,有
x1=x0(x0Tw1)+x0=x0(x0Tw1+1)=α1x0\begin{aligned} \mathbf{x}_{1} &=\mathbf{x}_{0}\left(\mathbf{x}_{0}^{T} \mathbf{w}_{1}\right)+\mathbf{x}_{0} \\ &=\mathbf{x}_{0}\left(\mathbf{x}_{0}^{T} \mathbf{w}_{1}+1\right) \\ &=\alpha^{1} \mathbf{x}_{0} \end{aligned}
那么其实标量 α1=x0Tw1+1\alpha^{1}=\mathbf{x}_{0}^{T} \mathbf{w}_{1}+1就是关于x0x_0的一个线性回归。同理,当k=i+1的时候,也能得到
xi+1=x0xiTwi+1+xi=x0((αix0)Twi+1)+αix0=αi+1x0\begin{aligned} \mathbf{x}_{i+1} &=\mathbf{x}_{0} \mathbf{x}_{i}^{T} \mathbf{w}_{i+1}+\mathbf{x}_{i} \\ &=\mathbf{x}_{0}\left(\left(\alpha^{i} \mathbf{x}_{0}\right)^{T} \mathbf{w}_{i+1}\right)+\alpha^{i} \mathbf{x}_{0} \\ &=\alpha^{i+1} \mathbf{x}_{0} \end{aligned}
其中 αi+1=αi(x0Twi+1+1)\alpha^{i+1}=\alpha^{i}\left(\mathbf{x}_{0}^{T} \mathbf{w}_{i+1}+1\right) 也是一个标量。
所以xkx_kx0x_0的一个标量倍数。
但是标量倍数并不意味着xkx_kx0x_0的线性函数,只是模型中特征交叉会相对比较有限。

2.Model Architecture

在这里插入图片描述
首先,模型的结构如图所示。底部是embedding layer。这个现在主流模型都一样,没啥好说的。然后往上走是对embedding layer的特征处理。主要分三块。

2.1 Linear Module

在这里插入图片描述
就是直接把原始特征提取出one-hot特征直接作为模型的输入。

2.2 DNN Module

把embedding layer作为输入做MLP。用来学习高阶隐式特征交叉。
在这里插入图片描述

2.3 算法核心改进的模块(CIN)。

用来学习显式的特征交叉,如图所示。
在这里插入图片描述
下面对CIN模块详细展开描述一下。

3. Compressed Interaction Network(CIN)

CIN模块的目的是做显式的vector-wise特征交互,所以所有的特征交互都是在vector维度进行,所以需要和FM一样将每个特征embedding到相同的维度,假设我们有m个特征,将每个特征映射到D维的embedding向量,那么经过lookup后,我们可以获取到总长度为m*D的embedding向量层。当然我们也可以把这些向量按矩阵的形式写出来,转化成矩阵
X0Rm×D\mathbf{X}^{0} \in \mathbb{R}^{m \times D} 其中每一行表示一个特征,长度为D。
然后CIN层每一层的输出,如第k层的输出也是一个矩阵XkRHk×D\mathrm{X}^{k} \in \mathbb{R}^{H_{k} \times D}。其中HkH_k是第k测光输出的特征向量的个数,也就是XkX_k中输出的一行。另外定义H0=mH_0 = m
那么CIN中XkX_k的每一行的计算公式如下
Xh,k=i=1Hk1j=1mWijk,h(Xi,k1Xj,0)\mathrm{X}_{h, *}^{k}=\sum_{i=1}^{H_{k-1}} \sum_{j=1}^{m} \mathrm{W}_{i j}^{k, h}\left(\mathrm{X}_{i, *}^{k-1} \circ \mathrm{X}_{j, *}^{0}\right)
这里的Xh,k\mathrm{X}_{h, *}^{k}代表的是XkX_k的一行, ◦指的是点积。具体如下:
⟨a1,a2,a3⟩◦⟨b1,b2,b3⟩ = ⟨a1b1,a2b2,a3b3⟩.
可以看到,第k层CIN的输出XkX_k每一行都是需要所有的特征进行交互,然后不同行做相同的交叉,只是他们之间学习出的参数不同。
文章还专门画了图来帮助大家理解。
在这里插入图片描述
图a中的Zk+1Z^{k+1}是计算Xk+1X_{k+1}中一行的一个中间结果。
因为HkH_k和m中的每行都需要交互,所以不做求和的话,其实就是类似一个外积操作。然后再对这个外积形成的矩阵进行按权重W求和。看上去就有点像CNN结构,如图b所示。
这就可以理解为一个CIN的块,那么CIN是怎么输出的呢?
首先CIN是可以正常的堆叠的,但是CIN模块的输出并不只单单使用了最后一个CIN块的输出,而是使用了每个CIN块的输出。每个CIN块的输出XkX_k是个HkDH_{k}*D的矩阵,然后作者这里将每行D维的向量压缩到一维,做了一个pooling操,如
pik=j=1DXi,jkp_{i}^{k}=\sum_{j=1}^{D} \mathrm{X}_{i, j}^{k}
那么每一层的CIN块就可以输出一个向量pk=[p1k,p2k,,pHkk]\mathbf{p}^{k}=\left[p_{1}^{k}, p_{2}^{k}, \ldots, p_{H_{k}}^{k}\right],最后把每一个CIN块的输出concate起来作为CIN结构的输出。类似这样:
在这里插入图片描述
最后就是把上面三块的输出合并在一起输出一个预测值y。如
y^=σ(wlinearTa+wdnnTxdnnk+wcinTp++b)\hat{y}=\sigma\left(\mathbf{w}_{\text {linear}}^{T} \mathbf{a}+\mathbf{w}_{d n n}^{T} \mathbf{x}_{d n n}^{k}+\mathbf{w}_{c i n}^{T} \mathbf{p}^{+}+b\right)

4.Implementation

本文的思路是将Linear,CIN,DNN结合起来形成一个xDeepFM。Linear和DNN没啥好说的,主要是CIN的实现。作者实现采用的是第三节提到的先做外积,然后再利用类似卷积操作来做聚合。我针对作者的代码,自己实现来一下,其中主模型代码如下:

    def _model_fn(self):
        linear_layer = tf.feature_column.linear_model(self.features,self.Linear_Features)
        cin_layer = self.cin_net(self.embedding_layer,direct=False, residual=True)
        dnn_layer = self.fc_net(self.embedding_layer,8)

        linear_logit = linear_layer
        cin_logit = tf.layers.dense(cin_layer,1)
        dnn_logit = tf.layers.dense(dnn_layer,1)

        last_layer = tf.concat([linear_logit, cin_logit, dnn_logit], 1)
        logits = tf.layers.dense(last_layer,1)
        return logits

linear_layercin_layerdnn_layer是底层输入embedding经过LinearCINDNN模块后的输出;再将各自的输出做如下操作:
y^=σ(wlinearTa+wdnnTxdnnk+wcinTp++b)\hat{y}=\sigma\left(\mathbf{w}_{\text {linear}}^{T} \mathbf{a}+\mathbf{w}_{d n n}^{T} \mathbf{x}_{d n n}^{k}+\mathbf{w}_{c i n}^{T} \mathbf{p}^{+}+b\right)

last_layer = tf.concat([linear_logit, cin_logit, dnn_logit], 1)
logits = tf.layers.dense(last_layer,1)

就是模型的输出。
其中主要的实现在cin_net函数,实现如下:

    def cin_net(self,net,direct=True, residual = True):
        '''xDeepFM中的CIN网络'''
        '''CIN'''
        '''final_result存放cin模型的输出'''
        self.final_result = []
        '''dimension对应论文中的参数D,表示embedding的维度'''
        self.dimension = self._check_columns_dimension(self.Deep_Features)
        '''column_num表示的是原始特征的个数,即论文中的m'''
        self.column_num = len(self.Deep_Features)
        print("column_num:{column_num},dimension:{dimension}".format(column_num=self.column_num,dimension=self.dimension))

        x_0 = tf.reshape(net, (-1, self.column_num, self.dimension), "inputs_x0")
        '''这里对所有的x_i都做了split操作,方便做外积'''
        split_x_0 = tf.split(x_0, self.dimension * [1], 2)
        next_hidden = x_0
        for idx,field_num in enumerate(self.field_nums):
            '''cin_block()实现的就是一层的CIN网络结构,split_x_0是原始输入,next_hidden是上一层的输出,field_num表示的是本层输出的向量个数,论文中的H_i'''
            current_out = self.cin_block(split_x_0, next_hidden, 'cross_{}'.format(idx), field_num)
            '''这个是输出是直接pooling,还是各取一半输出和传到下一层,论文中没有提到 '''
            if direct:
                next_hidden = current_out
                current_output = current_out
            else:
                field_num = int(field_num / 2)
                if idx != len(self.field_nums) - 1:
                    next_hidden, current_output = tf.split(current_out, 2 * [field_num], 1)

                else:
                    next_hidden = 0
                    current_output = current_out

            self.final_result.append(current_output)
        result = tf.concat(self.final_result, axis=1)
        result = tf.reduce_sum(result, -1)
        '''最后的输出是否采用残差网络 '''
        if residual:
            exFM_out1 = tf.layers.dense(result, 128, 'relu')
            exFM_in = tf.concat([exFM_out1, result], axis=1, name="user_emb")
            exFM_out = tf.layers.dense(exFM_in, 1)
            return exFM_out
        else:
            exFM_out = tf.layers.dense(result, 1)
            return exFM_out

具体可以看代码中的注释。然后就是每一层的CIN结构实现在cin_block()函数中,如下:

    def cin_block(self,x_0,current_x,name=None,next_field_num=None,reduce_D=False,f_dim=2,bias=True,direct=True):
        ''' 对上一层的输出split,用于计算外积'''
        split_current_x = tf.split(current_x, self.dimension * [1], 2)
        ''' 计算外积'''
        dot_result_m = tf.matmul(x_0, split_current_x, transpose_b=True)
        ''' 获取上一层filed的个数'''
        current_field_num = current_x.get_shape().as_list()[1]
        dot_result_o = tf.reshape(dot_result_m, shape=[self.dimension, -1, self.column_num * current_field_num])
        dot_result = tf.transpose(dot_result_o, perm=[1, 0, 2])
        ''' 是否将每个filter再做一次矩阵分解,分解成两个低秩矩阵'''
        if reduce_D:
            filters0 = tf.get_variable("f0_" + str(name),
                                       shape=[1, next_field_num,  self.column_num, f_dim],
                                       dtype=tf.float32)
            filters_ = tf.get_variable("f__" + str(name),
                                       shape=[1, next_field_num, f_dim, current_field_num],
                                       dtype=tf.float32)
            filters_m = tf.matmul(filters0, filters_)
            filters_o = tf.reshape(filters_m, shape=[1, next_field_num, self.column_num * current_field_num])
            filters = tf.transpose(filters_o, perm=[0, 2, 1])
        else:
            filters = tf.get_variable(name="f_" + str(name),
                                      shape=[1, current_field_num * self.column_num, next_field_num],
                                      dtype=tf.float32)
        ''' 利用一维卷积来实现聚合'''
        curr_out = tf.nn.conv1d(dot_result, filters=filters, stride=1, padding='VALID')
        if bias:
            b = tf.get_variable(name="f_b" + str(name),
                                shape=[next_field_num],
                                dtype=tf.float32,
                                initializer=tf.zeros_initializer())
            curr_out = tf.nn.bias_add(curr_out, b)

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

以上基本就是xDeepFM的全部思路,文章的思路基本解决了我最开始对神经网络中特征交叉的一些疑惑,而且从实验中发现,模型的效果要好于DCN模型。对代码有问题的可以上git「点击这里」去看一下完整的代码实现。
完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章