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等。其中也復現了xDeepFM
: https://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
作爲一個域,然後對域和域之間進行特徵交叉,如FM
,DeepFM
,PNN
中的特徵交叉。
那麼怎麼在神經網絡中做特徵交互呢?
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的時候,有
那麼其實標量 就是關於的一個線性迴歸。同理,當k=i+1的時候,也能得到
其中 也是一個標量。
所以是的一個標量倍數。
但是標量倍數並不意味着是的線性函數,只是模型中特徵交叉會相對比較有限。
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向量層。當然我們也可以把這些向量按矩陣的形式寫出來,轉化成矩陣
其中每一行表示一個特徵,長度爲D。
然後CIN層每一層的輸出,如第k層的輸出也是一個矩陣。其中是第k測光輸出的特徵向量的個數,也就是中輸出的一行。另外定義。
那麼CIN中的每一行的計算公式如下
這裏的代表的是的一行, ◦指的是點積。具體如下:
⟨a1,a2,a3⟩◦⟨b1,b2,b3⟩ = ⟨a1b1,a2b2,a3b3⟩.
可以看到,第k層CIN的輸出每一行都是需要所有的特徵進行交互,然後不同行做相同的交叉,只是他們之間學習出的參數不同。
文章還專門畫了圖來幫助大家理解。
圖a中的是計算中一行的一箇中間結果。
因爲和m中的每行都需要交互,所以不做求和的話,其實就是類似一個外積操作。然後再對這個外積形成的矩陣進行按權重W求和。看上去就有點像CNN結構,如圖b所示。
這就可以理解爲一個CIN的塊,那麼CIN是怎麼輸出的呢?
首先CIN是可以正常的堆疊的,但是CIN模塊的輸出並不只單單使用了最後一個CIN塊的輸出,而是使用了每個CIN塊的輸出。每個CIN塊的輸出是個的矩陣,然後作者這裏將每行D維的向量壓縮到一維,做了一個pooling操,如
那麼每一層的CIN塊就可以輸出一個向量,最後把每一個CIN塊的輸出concate起來作爲CIN結構的輸出。類似這樣:
最後就是把上面三塊的輸出合併在一起輸出一個預測值y。如
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_layer
,cin_layer
,dnn_layer
是底層輸入embedding經過Linear
,CIN
,DNN
模塊後的輸出;再將各自的輸出做如下操作:
即
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「點擊這裏」去看一下完整的代碼實現。
完。