推荐系统遇上深度学习(二十二):DeepFM升级版XDeepFM模型强势来袭!
今天我們要學習的模型是xDeepFM模型,論文地址為:https://arxiv.org/abs/1803.05170。文中包含我個人的一些理解,如有不對的地方,歡迎大家指正!廢話不多說,我們進入正題!
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中,隱向量是一個單元對象,因此我們將輸入的原特征和神經網絡中的隱層都分別組織成一個矩陣,記為X^0 和 X^k。CIN中每一層的神經元都是根據前一層的隱層以及原特征向量推算而來,其計算公式如下:
?
?
其中點乘的部分計算如下:
?
?
我們來解釋一下上面的過程,第k層隱層含有H_k條神經元向量。隱層的計算可以分成兩個步驟:(1)根據前一層隱層的狀態X^k 和原特征矩陣 X^0,計算出一個中間結果 Z^k+1,它是一個三維的張量,如下圖所示:
?
?
在這個中間結果上,我們用H^k+1 個尺寸為 m*H^k 的卷積核生成下一層隱層的狀態,該過程如圖2所示。這一操作與計算機視覺中最流行的卷積神經網絡大體是一致的,唯一的區別在于卷積核的設計。CIN中一個神經元相關的接受域是垂直于特征維度D的整個平面,而CNN中的接受域是當前神經元周圍的局部小范圍區域,因此CIN中經過卷積操作得到的特征圖(Feature Map)是一個向量,而不是一個矩陣。
?
?
如果你覺得原文中的圖不夠清楚的話,希望下圖可以幫助你理解整個過程:
?
?
CIN的宏觀框架可以總結為下圖:
?
?
可以看出,它的特點是,最終學習出的特征交互的階數是由網絡的層數決定的,每一層隱層都通過一個池化操作連接到輸出層,從而保證了輸出單元可以見到不同階數的特征交互模式。同時不難看出,CIN的結構與循環神經網絡RNN是很類似的,即每一層的狀態是由前一層隱層的值與一個額外的輸入數據計算所得。不同的是,CIN中不同層的參數是不一樣的,而在RNN中是相同的;RNN中每次額外的輸入數據是不一樣的,而CIN中額外的輸入數據是固定的,始終是X^0。
可以看到,CIN是通過(vector-wise)來學習特征之間的交互的,還有一個問題,就是它為什么是顯式的進行學習?我們先從X^1 來開始看,X^1 的第h個神經元向量可以表示成:
?
?
進一步,X^2的第h個神經元向量可以表示成:
?
?
最后,第k層的第h個神經元向量可以表示成:
?
?
因此,我們能夠通過上面的式子對特征交互的形式進行一個很好的表示,它是顯式的學習特征交叉。
2.2 xDeepFM
將CIN與線性回歸單元、全連接神經網絡單元組合在一起,得到最終的模型并命名為極深因子分解機xDeepFM,其結構如下圖:
?
?
集成的CIN和DNN兩個模塊能夠幫助模型同時以顯式和隱式的方式學習高階的特征交互,而集成的線性模塊和深度神經模塊也讓模型兼具記憶與泛化的學習能力。值得一提的是,為了提高模型的通用性,xDeepFM中不同的模塊共享相同的輸入數據。而在具體的應用場景下,不同的模塊也可以接入各自不同的輸入數據,例如,線性模塊中依舊可以接入很多根據先驗知識提取的交叉特征來提高記憶能力,而在CIN或者DNN中,為了減少模型的計算復雜度,可以只導入一部分稀疏的特征子集。
3、Tensorflow充電
在介紹xDeepFM的代碼之前,我們先來進行充電,學習幾個tf的函數以及xDeepFM關鍵過程的實現。
首先我們要實現第一步:
?
?
如何將兩個二維的矩陣,相乘得到一個三維的矩陣?我們首先來看一下tf.split函數的原理:
tf.split(value,num_or_size_splits,axis=0,num=None,name='split' )其中,value傳入的就是需要切割的張量,axis是切割的維度,根據num_or_size_splits的不同形式,有兩種切割方式:
好了,從實際需求出發,我們來體驗一下,假設我們的batch為2,embedding的size是3,field數量為4。我們先來生成兩個這樣的tensor(假設X^k的field也是4 ):
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)生成的矩陣如下:
?
?
在經過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:
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))分割后的結果如下:
?
?
通過結果我們可以看到,我們現在對每一條數據,得到了3個4 * 1的tensor,可以理解為此時的tensor大小為 3(embedding Dimension) * 2(batch) * 4(X^k 或X^0的field數) * 1。
此時我們進行矩陣相乘:
res = tf.matmul(split_arr1,split_arr2,transpose_b=True)這里我理解的,tensorflow對3維及以上矩陣相乘時,矩陣相乘只發生在最后兩維。也就是說,3 * 2 * 4 * 1 和 3 * 2 * 1 * 4的矩陣相乘,最終的結果是3 * 2 * 4 * 4。我們來看看結果:
?
?
可以看到,不僅矩陣的形狀跟我們預想的一樣,同時結果也跟我們預想的一樣。
最后,我們只需要進行transpose操作,把batch轉換到第0維就可以啦。
res = tf.transpose(res,perm=[1,0,2,3])這樣,CIN中的第一步就大功告成了,明白了這一步如何用tensorflow實現,那么代碼你也就能夠順其自然的看懂啦!
這一塊完整的代碼如下:
import tensorflow as tf import numpy as nparr1 = 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_outnext_hidden = curr_outfinal_len += layer_sizefield_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_outnext_hidden = 0final_len += layer_sizefield_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
總結
以上是生活随笔為你收集整理的推荐系统遇上深度学习(二十二):DeepFM升级版XDeepFM模型强势来袭!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 详解分布式协调服务 ZooKeeper
- 下一篇: 【十大经典数据挖掘算法】PageRank