NFM
0.結(jié)論
- NFM是FM的神經(jīng)網(wǎng)絡(luò)化嘗試
- NFM用神經(jīng)網(wǎng)絡(luò)代替FM中二階隱向量交叉的操作了赵,相比于FM竞穷,NFM的表達(dá)能力和特征交叉能力更強
- 局限性在于結(jié)構(gòu)與PNN近似孕惜,特征工程層面的優(yōu)化嘗試幾乎窮盡,模型進(jìn)一步提升的空間非常小
1. 動機
NFM(Neural Factorization Machines)是2017年由新加坡國立大學(xué)的何向南教授等人在SIGIR會議上提出的一個模型戒洼,傳統(tǒng)的FM模型僅局限于線性表達(dá)和二階交互燕垃, 無法勝任生活中各種具有復(fù)雜結(jié)構(gòu)和規(guī)律性的真實數(shù)據(jù)枢劝, 針對FM的這點不足, 作者提出了一種將FM融合進(jìn)DNN的策略利术,通過引進(jìn)了一個特征交叉池化層的結(jié)構(gòu),使得FM與DNN進(jìn)行了完美銜接低矮,這樣就組合了FM的建模低階特征交互能力和DNN學(xué)習(xí)高階特征交互和非線性的能力印叁,形成了深度學(xué)習(xí)時代的神經(jīng)FM模型(NFM)。
那么NFM具體是怎么做的呢军掂? 首先看一下NFM的公式:
我們對比FM轮蜕, 就會發(fā)現(xiàn)變化的是第三項,前兩項還是原來的蝗锥, 因為我們說FM的一個問題跃洛,就是只能到二階交叉, 且是線性模型终议, 這是他本身的一個局限性汇竭, 而如果想突破這個局限性, 就需要從他的公式本身下點功夫穴张, 于是乎细燎,作者在這里改進(jìn)的思路就是用一個表達(dá)能力更強的函數(shù)來替代原FM中二階隱向量內(nèi)積的部分。
而這個表達(dá)能力更強的函數(shù)呢皂甘, 我們很容易就可以想到神經(jīng)網(wǎng)絡(luò)來充當(dāng)玻驻,因為神經(jīng)網(wǎng)絡(luò)理論上可以擬合任何復(fù)雜能力的函數(shù), 所以作者真的就把這個換成了一個神經(jīng)網(wǎng)絡(luò)偿枕,當(dāng)然不是一個簡單的DNN璧瞬, 而是依然底層考慮了交叉户辫,然后高層使用的DNN網(wǎng)絡(luò), 這個也就是我們最終的NFM網(wǎng)絡(luò)了:
這個結(jié)構(gòu)嗤锉,如果前面看過了PNN的伙伴會發(fā)現(xiàn)渔欢,這個結(jié)構(gòu)和PNN非常像,只不過那里是一個product_layer档冬, 而這里換成了Bi-Interaction Pooling了膘茎, 這個也是NFM的核心結(jié)構(gòu)了。這里注意酷誓, 這個結(jié)構(gòu)中披坏,忽略了一階部分,只可視化出來了盐数, 我們還是下面從底層一點點的對這個網(wǎng)絡(luò)進(jìn)行剖析棒拂。
2. 模型結(jié)構(gòu)與原理
2.1 Input 和Embedding層
輸入層的特征為了方便,文章指定了稀疏離散特征(當(dāng)然在實際場景的應(yīng)用中玫氢,分為數(shù)值特征與分類特征)帚屉。Embedding層(在該模型中其實就是一個全連接層)將高維的稀疏特征轉(zhuǎn)化為低維的密集特征表示。
2.2 Bi-Interaction Pooling layer
在Embedding層和神經(jīng)網(wǎng)絡(luò)之間加入了特征交叉池化層是本網(wǎng)絡(luò)的核心創(chuàng)新了漾峡,正是因為這個結(jié)構(gòu)攻旦,實現(xiàn)了FM與DNN的無縫連接, 組成了一個大的網(wǎng)絡(luò)生逸,且能夠正常的反向傳播牢屋。假設(shè)是所有特征embedding的集合, 那么在特征交叉池化層的操作:
表示兩個向量的元素積操作槽袄,即兩個向量對應(yīng)維度相乘得到的元素積向量(可不是點乘呀)烙无,其中第維的操作:
這便定義了在embedding空間特征的二階交互,這個不仔細(xì)看會和感覺FM的最后一項很像遍尺,但是不一樣截酷,一定要注意這個地方不是兩個隱向量的內(nèi)積,而是元素積乾戏,也就是這一個交叉完了之后k個維度不求和迂苛,最后會得到一個維向量,而FM那里內(nèi)積的話最后得到一個數(shù)鼓择, 在進(jìn)行兩兩Embedding元素積之后灾部,對交叉特征向量取和, 得到該層的輸出向量惯退, 很顯然赌髓, 輸出是一個維的向量。
注意, 之前的FM到這里其實就完事了锁蠕, 上面就是輸出了夷野,而這里很大的一點改進(jìn)就是加入特征池化層之后, 把二階交互的信息合并荣倾, 且上面接了一個DNN網(wǎng)絡(luò)悯搔, 這樣就能夠增強FM的表達(dá)能力了, 因為FM只能到二階舌仍, 而這里的DNN可以進(jìn)行多階且非線性妒貌,只要FM把二階的學(xué)習(xí)好了, DNN這塊學(xué)習(xí)來會更加容易铸豁, 作者在論文中也說明了這一點灌曙,且通過后面的實驗證實了這個觀點。
如果不加DNN节芥, NFM就退化成了FM在刺,所以改進(jìn)的關(guān)鍵就在于加了一個這樣的層,組合了一下二階交叉的信息头镊,然后又給了DNN進(jìn)行高階交叉的學(xué)習(xí)蚣驼,成了一種“加強版”的FM。
Bi-Interaction層不需要額外的模型學(xué)習(xí)參數(shù)相艇,更重要的是它在一個線性的時間內(nèi)完成計算颖杏,和FM一致的,即時間復(fù)雜度為坛芽,為embedding向量的數(shù)量留储。
2.3 隱藏層
即由多個全連接層構(gòu)成。不過在此之前靡馁,作者還采用了Dropout****和Batch Normalization欲鹏,原因如下:
Dropout:是神經(jīng)網(wǎng)絡(luò)的正則化技術(shù)机久,為了防止過擬合臭墨;
Batch Normalization:該層是對輸入的每個小批量(min-batch)標(biāo)準(zhǔn)化為零均值的單位方差的高斯分布(zero-mean unit-variance Gaussian distribution)。作者使用BN膘盖,是為了避免embedding向量的更新將輸入層的分布更改為隱藏層或輸出層胧弛;
2.4 預(yù)測層
通過邏輯回歸將隱藏層的向量轉(zhuǎn)變?yōu)樽詈箢A(yù)測的結(jié)果。
NFM相比較于其他模型的核心創(chuàng)新點是特征交叉池化層侠畔,基于它结缚,實現(xiàn)了FM和DNN的無縫連接,使得DNN可以在底層就學(xué)習(xí)到包含更多信息的組合特征软棺,這時候红竭,就會減少DNN的很多負(fù)擔(dān),只需要很少的隱藏層就可以學(xué)習(xí)到高階特征信息。NFM相比之前的DNN茵宪, 模型結(jié)構(gòu)更淺最冰,更簡單,但是性能更好稀火,訓(xùn)練和調(diào)參更容易暖哨。集合FM二階交叉線性和DNN高階交叉非線性的優(yōu)勢,非常適合處理稀疏數(shù)據(jù)的場景任務(wù)凰狞。在對NFM的真實訓(xùn)練過程中篇裁,也會用到像Dropout和BatchNormalization這樣的技術(shù)來緩解過擬合和在過大的改變數(shù)據(jù)分布。
下面通過代碼看下NFM的具體實現(xiàn)過程赡若, 學(xué)習(xí)一些細(xì)節(jié)达布。
3. 代碼實現(xiàn)
下面我們看下NFM的代碼復(fù)現(xiàn),這里主要是給大家說一下這個模型的設(shè)計邏輯斩熊,參考了deepctr的函數(shù)API的編程風(fēng)格往枣, 具體的代碼以及示例大家可以去參考后面的GitHub,里面已經(jīng)給出了詳細(xì)的注釋粉渠, 這里主要分析模型的邏輯這塊分冈。關(guān)于函數(shù)API的編程式風(fēng)格,我們還給出了一份文檔霸株, 大家可以先看這個雕沉,再看后面的代碼部分,會更加舒服些去件。下面開始:
這里主要說一下NFM模型的總體運行邏輯坡椒, 這樣可以讓大家從宏觀的層面去把握模型的設(shè)計過程躏尉, 該模型所使用的數(shù)據(jù)集是criteo數(shù)據(jù)集涯保,具體介紹參考后面的GitHub。 數(shù)據(jù)集的特征會分為dense特征(連續(xù))和sparse特征(離散)空郊, 所以模型的輸入層接收這兩種輸入宫莱。但是我們這里把輸入分成了linear input和dnn input兩種情況丈攒,而每種情況都有可能包含上面這兩種輸入。因為我們后面的模型邏輯會分這兩部分走授霸,這里有個細(xì)節(jié)要注意巡验,就是光看上面那個NFM模型的話,是沒有看到它線性特征處理的那部分的碘耳,也就是FM的前半部分公式那里圖里面是沒有的显设。但是這里我們要加上。
所以模型的邏輯我們分成了兩大部分辛辨,這里我分別給大家解釋下每一塊做了什么事情:
- linear part: 這部分是有關(guān)于線性計算捕捂,也就是FM的前半部分的計算瑟枫。對于這一塊的計算,我們用了一個get_linear_logits函數(shù)實現(xiàn)指攒,后面再說力奋,總之通過這個函數(shù),我們就可以實現(xiàn)上面這個公式的計算過程幽七,得到linear的輸出
- dnn part: 這部分是后面交叉特征的那部分計算景殷,F(xiàn)M的最后那部分公式f(x)。 這一塊主要是針對離散的特征澡屡,首先過embedding猿挚, 然后過特征交叉池化層,這個計算我們用了get_bi_interaction_pooling_output函數(shù)實現(xiàn)驶鹉, 得到輸出之后又過了DNN網(wǎng)絡(luò)绩蜻,最后得到dnn的輸出
模型的最后輸出結(jié)果,就是把這兩個部分的輸出結(jié)果加和(當(dāng)然也可以加權(quán))室埋,再過一個sigmoid得到办绝。所以NFM的模型定義就出來了:
def NFM(linear_feature_columns, dnn_feature_columns):
"""
搭建NFM模型,上面已經(jīng)把所有組塊都寫好了姚淆,這里拼起來就好
:param linear_feature_columns: A list. 里面的每個元素是namedtuple(元組的一種擴(kuò)展類型孕蝉,同時支持序號和屬性名訪問組件)類型,表示的是linear數(shù)據(jù)的特征封裝版
:param dnn_feature_columns: A list. 里面的每個元素是namedtuple(元組的一種擴(kuò)展類型腌逢,同時支持序號和屬性名訪問組件)類型降淮,表示的是DNN數(shù)據(jù)的特征封裝版
"""
# 構(gòu)建輸入層,即所有特征對應(yīng)的Input()層搏讶, 這里使用字典的形式返回佳鳖, 方便后續(xù)構(gòu)建模型
# 構(gòu)建模型的輸入層,模型的輸入層不能是字典的形式媒惕,應(yīng)該將字典的形式轉(zhuǎn)換成列表的形式
# 注意:這里實際的輸入與Input()層的對應(yīng)系吩,是通過模型輸入時候的字典數(shù)據(jù)的key與對應(yīng)name的Input層
dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns+dnn_feature_columns)
input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
# 線性部分的計算 w1x1 + w2x2 + ..wnxn + b部分,dense特征和sparse兩部分的計算結(jié)果組成妒蔚,具體看上面細(xì)節(jié)
linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_feature_columns)
# DNN部分的計算
# 首先穿挨,在這里構(gòu)建DNN部分的embedding層,之所以寫在這里面睛,是為了靈活的遷移到其他網(wǎng)絡(luò)上絮蒿,這里用字典的形式返回
# embedding層用于構(gòu)建FM交叉部分以及DNN的輸入部分
embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)
# 過特征交叉池化層
pooling_output = get_bi_interaction_pooling_output(sparse_input_dict, dnn_feature_columns, embedding_layers)
# 加個BatchNormalization
pooling_output = BatchNormalization()(pooling_output)
# dnn部分的計算
dnn_logits = get_dnn_logits(pooling_output)
# 線性部分和dnn部分的結(jié)果相加尊搬,最后再過個sigmoid
output_logits = Add()([linear_logits, dnn_logits])
output_layers = Activation("sigmoid")(output_logits)
model = Model(inputs=input_layers, outputs=output_layers)
return model
有了上面的解釋叁鉴,這個模型的宏觀層面相信就很容易理解了。關(guān)于這每一塊的細(xì)節(jié)佛寿,這里就不解釋了幌墓,在我們給出的GitHub代碼中但壮,我們已經(jīng)加了非常詳細(xì)的注釋,大家看那個應(yīng)該很容易看明白常侣, 為了方便大家的閱讀蜡饵,我們這里還給大家畫了一個整體的模型架構(gòu)圖,幫助大家更好的了解每一塊以及前向傳播胳施。
下面是一個通過keras畫的模型結(jié)構(gòu)圖溯祸,為了更好的顯示,數(shù)值特征和類別特征都只是選擇了一小部分舞肆,畫圖的代碼也在github中焦辅。
4. 思考題
- NFM中的特征交叉與FM中的特征交叉有何異同,分別從原理和代碼實現(xiàn)上進(jìn)行對比分析
答:
從原理上來看:
NFM則是將FM的二次交叉后的向量作為DNN的輸入,即引入了非線性變換來提升模型非線性表達(dá)能力椿胯,又學(xué)習(xí)到高階的組合特征筷登。
從代碼上來看
- 采用特征pooling的方式代替DeepFM中二階特征向量橫向連接操作,得到的結(jié)果向量由n*k維優(yōu)化為k維哩盲,大大減少訓(xùn)練參數(shù)數(shù)目前方。(n為特征域個數(shù),k為embedding向量維度)
- 采用sum pooling的方式綜合二階特征信息廉油,可能會有信息損失
FM layer樣例:
class FM_Layer(Layer):
def __init__(self):
super(FM_Layer, self).__init__()
def call(self, inputs):
# 優(yōu)化后的公式為: 0.5 * 求和(和的平方-平方的和) =>> B x 1
concated_embeds_value = inputs # B x n x k
square_of_sum = tf.square(tf.reduce_sum(concated_embeds_value, axis=1, keepdims=True)) # B x 1 x k
sum_of_square = tf.reduce_sum(concated_embeds_value * concated_embeds_value, axis=1, keepdims=True) # B x1 xk
cross_term = square_of_sum - sum_of_square # B x 1 x k
cross_term = 0.5 * tf.reduce_sum(cross_term, axis=2, keepdims=False) # B x 1
return cross_term
def compute_output_shape(self, input_shape):
return (None, 1)
NFM Bi-Interaction Layer樣例
class BiInteractionPooling(Layer):
def __init__(self):
super(BiInteractionPooling, self).__init__()
def call(self, inputs):
# 優(yōu)化后的公式為: 0.5 * (和的平方-平方的和) =>> B x k
concated_embeds_value = inputs # B x n x k
square_of_sum = tf.square(tf.reduce_sum(concated_embeds_value, axis=1, keepdims=False)) # B x k
sum_of_square = tf.reduce_sum(concated_embeds_value * concated_embeds_value, axis=1, keepdims=False) # B x k
cross_term = 0.5 * (square_of_sum - sum_of_square) # B x k
return cross_term
def compute_output_shape(self, input_shape):
return (None, input_shape[2])
5. 參考資料
王喆 - 《深度學(xué)習(xí)推薦系統(tǒng)》