DeepFM升級版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的宏觀框架可以總結爲下圖,在做pooling時 考慮到向量中所有數據都是有用的,所以不會選擇max pooling(會將大部分數據丟掉,比如應用在圖像處理中,只提取出最明顯的特徵),而是選擇sum pooling: 

可以看出,它的特點是,最終學習出的特徵交互的階數是由網絡的層數決定的,每一層隱層都通過一個池化操作連接到輸出層,從而保證了輸出單元可以見到不同階數的特徵交互模式。同時不難看出,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

 

發佈了120 篇原創文章 · 獲贊 134 · 訪問量 60萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章