【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「點擊這裏」去看一下完整的代碼實現。
完。

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