如何用keras实现deepFM
實現基本完全基于文末列出的deepFM 原文(還有幾處或者更多地方可以優化,比如二次項多值輸入的處理,樣本編碼等等)
文末參考的文章用Keras實現一個DeepFM?是我們初期學習和搭建deepFM 的主要參考。然后下面我們的實現會比參考內容更簡單而且有一些處理上的差異。同時在我們的業務數據集上,下面我們自己的實現方式得到的測試 auc 大約都比按照上面文章的實現測試 auc 高約 0~0.01 左右。(當然這里可能有各種原因導致的差異,并不能說下面的實現是絕對優于參考文章的)
下面的內容完全是個人行為,有錯漏希望多指教
實現這個 deepFM 需要掌握的內容
Keras 的使用,包括如果使用 Sequential 搭建模型,以及如何使用函數式 API 搭建較簡單模型
Dense, Embedding, Reshape, Concatenate, Add, Substract, Lambda 這幾個 Layer 的使用方式
自定義簡單的 Layer
FM 的基本原理
另外一些零散又沒法繞過的內容(優化器,激活函數,損失函數,正則化),幸運的是這些內容大部分框架幫我們處理好了,我們暫時只需要調參即可,甚至不會調參的化,copy一下別人的配置個人覺得也無傷大雅(畢竟我也只會copy)。這里的參數只要不是太過分,參數變化對對模型結果應該起不到決定性作用。
deepFM 說起來結構還是比較簡單,包含了左邊的 FM 和右邊的 deep 部分,每個神經元進行了什么操作也在圖中表示得很清楚。需要注意的是,圖中的連線有紅線和黑線的區別,紅線表示權重為 1,黑線表示有需要訓練的權重連線。
Addition 普通的線性加權相加,就是 w*x
Inner Product 內積操作,就是 FM 的二次項隱向量兩兩相乘的部分
Sigmoid 激活函數,即最后整合兩部分輸出合并進入 sigmoid 激活函數得到的輸出結果
Activation Function,這里為激活函數用的就是線性整流器 relu 函數
這里不著重描述 FM 是什么,FM 由如下公式表示(只討論二階組合的情況)
同樣是線性公式,和 LR 的唯一區別,就在于后面的二次項,該二次項表示各個特征交叉相乘,即相當于我們在機器學習中的組合特征。
FM 的這部分能力,解決了 LR 只能對一階特征做學習的局限性。
LR 如果要使用組合特征,必須手動做特征組合,這一步需要經驗。FM 的二次項可以自動對特征做組合。
同時 FM 的公式可以化為如下,v 表示的就是對應的特征 x 的隱向量。上面的公式還能進一步轉換成這個公式的優點在于,上一個公式要訓練組合權重 w,需要兩個組合特征的樣本值同時有值才能使 w 得到訓練,但是組合特征原本樣本就較少,這樣的訓練方式很難使權重 w 得到充分訓練。
通過因式分解機,可以使用一個長度為 k 的隱向量來表達每一個輸入的特征值 x,標記為 v,并且通過兩個特征的 v 值求內積,其結果可以等同于特征交叉項的權重 w。
通過隱向量 v 表示特征的方式好處是,交叉項不需要保證兩個特征均有值才能使 v 得到訓練,每一個包含有值特征 x 的樣本,都能使之對應的隱向量 v 得到訓練。
這里圈一下重點:
LR 的升級版
有個二階項(也可以有更高階項,但付出的計算代價也更大)
通過引入隱向量,訓練時二階項組合特征無需同時有值就可以得到訓練
樣本保存格式
每個訓練樣本都會有自己的保存格式,libsvm 或者 tfrecord 或者其他什么形式。
我們的樣本格式為:
單值離散特征而是直接輸入index
多值離散特征也是輸入 index,但是是輸入一串對應的 index 值,如 [5,9,11]
如果有沒有維表的字符串特征,我們通過哈希轉換成某個范圍內的數字,這個轉換是確定的,比如 “hello” 恒轉換成 10,即變成了 1 情況里描述的單值離散特征 (哈希是會出現一定概率碰撞的,這里需要將維度冗余大約10倍使碰撞率低于5%,目前這樣處理在我們的場景下模型效果無差異)
連續值直接輸入即可。(如果是較大的連續值,需在特征工程部分先做歸一化,或者考慮先做離散化處理成離散值)
最后得到的樣本形如 1,5,10,3,6,0.5,0.4,100,[5,9,11]。這樣的話,線上的 TFserving 除了最后的 [5,9,11] 部分因為是變長,還是必須轉換成 one-hot 形式。其余部分線上交給 embedding 層處理,就無需拼接 one-hot 向量輸入,節省輸入樣本長度。
特征索引
當然如果保存后的樣本是上面些的 1,5,10,3,6,0.5,0.4,100,[5,9,11],我們還需要知道每個值是什么特征,維度是多少,以及訓練時如何轉換成可以使用的樣本。
所以需要有一行特征索引和每一條樣本的每個值一一對應。假設可以用以下形式存儲索引。
1-age-100, 1-gender-3, 2-ads_weight-1, 3-game-50... 對應的每個表示是 類型-特征名-維度左滑查看完整代碼,下同總之只要有一個對應方式,通過查詢索引找到特征的信息即可,我們后面的輸入樣本就需要根據這些信息來轉換,并且喂給模型做訓練。
模型輸入
后續對于模型的輸入,我們根據不同特征定義了對應不同的 Input。所以最后輸入的訓練格式要注意。訓練輸入應該長相如下,
train_x = [np.array([...]), np.array([...]), np.array([...])] label = np.array([0, 1, 0 ...])實現 FM 部分談到具體如何實現模型。下圖是 deepFM 網絡的 FM 部分。我們看到上圖有紅色的連線和黑色的連線
第一層到第三層的黑色的連線部分就是原始輸入通過線性加權,得到模型的一次項。
第二層到第三層的紅色連線則指的是原始特征通過各自的隱向量來表達后,根據公式兩兩做內積,得到一堆內積結果
最后第三層到第四層的一次項和二次項通過紅色連線相加,得到最后的 FM 輸出
按步驟實現,就是需要實現一次項和二次項兩部分,然后相加得到 FM 這部分的輸出。
一次項部分
FM 的一次項部分這一部分思路很簡單
連續值,通過 dense(1) 得到 input*weight 的輸出。
單值離散特征,通過 embedding(dim,1) 得到一維輸出, embedding(dim,1) 可以認為就是 input*weight 得到的一個輸出
多值離散特征,通過 dense(1) 得到一個輸出值,比如 [1,5,7] 實際進入訓練時是 [0,1,0,0,0,1,0,1](假設最大特征就是7)dense(1) 得到的就是對應 1 值乘以 weight并且相加得到的結果。
最后把上面1,2,3得到的單位輸出全部 Add 相加,得到的就是上述一次項結果。
上述過程可以簡單通過代碼表達為
continuous = Input(shape=(1, ), name='single_continuous') single_discrete = Input(shape=(1, ), name='single_discrete') multi_discrete = Input(shape=(8, ), name='multi_discrete') continuous_dense = Dense(1)(continuous) single_embedding = Reshape([1])(Embedding(10, 1)(single_discrete)) multi_dense = Dense(1)(multi_discrete) first_order_sum = Add()([continuous_dense, single_embedding, multi_dense])二次項部分
FM 的二次項部分等價于這一部分主要做的事情,就是需要得到一個表示各 field 的隱向量,而且不管每個特征 field 長度是多少,最后得到的隱向量長度都為 k(這里 k 由開發者自己指定)。我們來分析一下如何處理這部分。
連續值,要得到 k 長度輸出,我們直接使用 dense(k)
單值離散特征,同樣通過 Embedding(dim,k) 得到 k 長度輸出
多值離散特征,使用 Dense(k) 得到 k 長度輸出(這一部分為了簡化,直接使用全連接層,包括后面的二次項操作)。
承接上面的代碼,這一部分的代碼表示為
continuous_k = Dense(3)(continuous) single_k = Reshape([3])(Embedding(10, 3)(single_discrete)) multi_k = Dense(3)(multi_discrete)最后得到如下圖的輸出,我們這里假設 k=3,下列每一種類型的 3 位輸出,最后 concate 在一起,要作為后面說到的 deep 層的輸入。再看一次上面二次項部分的公式,我們使用其實相當于兩部分內容這一部分是先相加后平方這一部分是先平方后相加
從上一張圖我們看到一個信息,每個 k=3 的時候,每個輸出節點,其實就相當于 Xi*Vil。
多值離散特征的 k=3 的每個輸出其實等于 XiVil+XjVjl,因為他還是同一個 field 的多個特征值,為了簡化,我們認為這個結果近似等于
所以不管是要先相加后平方,或者先平方后相加,最后等同于上面的 3 個 3維神經元相加起來,對應得到的數就等于求和部分。如下圖。先相加后平方
所以這里每個 k=3 的輸出,都是一個 Xi*Vil。先相加后平方的一項,利用 Lambda 層對每個元素做一次平方處理,接上面的代碼得到
sum_square_layer = Lambda(lambda x: x**2)(Add()([continuous_k, single_k, multi_k]))先平方后相加
跟上一步類似,我們得到
continuous_square = Lambda(lambda x:x**2)(continuous_k) single_square = Lambda(lambda x:x**2)(single_k) multi_square = Lambda(lambda x:x**2)(multi_k) square_sum_layer = Add()([continuous_square, single_square, multi_square])二次項的最后輸出
最后結合上面兩部分,得到二次項的最后輸出為上面兩項相減,乘以二分之一后,再對 k=3 的三個值相加。
substract_layer = Lambda(lambda x:x*0.5)(Subtract()([sum_square_layer, square_sum_layer])) # 要實現單層的各個值相加,目前 Keras 似乎沒有這樣的操作 # 可以通過自定義一個簡單層來簡單實現我們需要的功能 class SumLayer(Layer): def __init__(self, **kwargs): super(SumLayer, self).__init__(**kwargs) def call(self, inputs): inputs = K.expand_dims(inputs) return K.sum(inputs, axis=1) def compute_output_shape(self, input_shape): return tuple([input_shape[0], 1]) # 最后相加 k 維輸出,結果等于 second_order_sum = SumLayer()(substract_layer)FM部分的最后輸出
我們再回顧一下要注意到最開始的deepFM論文中的原圖,FM 部分最后連接到outpu units的 FM 部分,是紅色的線,weight-1 connection。也就是說,FM 部分最后相當于需要把一次項和二次項的輸出值相加得到一個單值輸出,然后再跟 deep 部分的輸出相加,進入 sigmoid 激活函數。所以 FM 部分我們最后的輸出為
fm_output = Add()([first_order_sum, second_order_sum])實現 deep 部分[ deep部分 ]
deep 部分全是黑色連線,所以實現很簡單,只需要把上面 FM 部分的二次項 k 維輸出 concate 后作為輸入,然后進入幾層全連接層,最后得到的單值輸出和 FM 部分的單值輸出 concate,再經過一次 Dense(1),進入 sigmoid 函數即可。
可以直接看代碼如何實現這部分。
deep_input = Concatenate()([continuous_k, single_k, multi_k]) deep_layer_0 = Dropout(0.5)(Dense(64, activation='relu')(deep_input)) deep_layer_1 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_0)) deep_layer_2 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_1)) deep_output = Dropout(0.5)(Dense(1, activation='relu')(deep_layer_2))最后輸出部分concat_layer = Concatenate()([fm_output, deep_output]) y = Dense(1, activation='sigmoid')(concat_layer)到此模型的代碼就完成了,剩余的就是樣本的處理,以及各自如何把樣本喂入模型的代碼。這些代碼應該是根據各自業務,各自樣本格式需要做對應處理。
deepFM 結果對比根據上面的方法實現了模型之后,我們用自己的業務的幾份數據集做了離線測試。
完整deepFM模型
只有deep的模型,deep部分的輸入是特征簡單embedding等處理后的輸入,即上面的[continuous_dense, single_embedding, multi_dense] concat后的輸入
只有 FM 的模型,即把 deep 部分去掉,最后進入 Dense(1) 的只有fm_output
在目前我們個業務的幾份不同日期的數據集合上測試,得到的AUC 結果如下。
| 數據集 | deepFM | FM | deep |
A1 | 0.86661 | 0.86581 | 0.85166 |
A2 | 0.86125 | 0.86121 | 0.84789 |
A3 | 0.84842 | 0.84841 | 0.80581 |
A4 | 0.84170 | 0.84083 | 0.82848 |
有一個大致經驗,deepFM 在有效特征更多,特征工程處理更好,數據更干凈,數據更有區分度的數據集上,得到的對比結果差異會更大。
反之,如果數據特征和 label 本身的關聯性不高,數據本身無法很好的區分樣本時,對比結果差異會很小。
FM 有時候表現已經和 deepFM 幾乎無差別。猜測的原因是數據本身并不需要復雜規則就能得到很好的模型區分,所以比 FM 多出來的這部分 deep 能力顯得并不太重要。
同時因為 deep 部分明顯效果都不如前兩者,所以可能可以驗證上一步猜測。
預計需要的是更多在特征工程上做優化,以及挖掘更多有效特征。
最后附上代碼demo以上面的代碼為例,附上完整的實現代碼。這個demo是直接可運行的。
import numpy as np from keras.layers import * from keras.models import Model from keras import backend as K from keras import optimizers from keras.engine.topology import Layer # 樣本和標簽,這里需要對應自己的樣本做處理 train_x = [ np.array([0.5, 0.7, 0.9]), np.array([2, 4, 6]), np.array([[0, 1, 0, 0, 0, 1, 0, 1], [0, 1, 0, 0, 0, 1, 0, 1], [0, 1, 0, 0, 0, 1, 0, 1]]) ] label = np.array([0, 1, 0]) # 輸入定義 continuous = Input(shape=(1, ), name='single_continuous') single_discrete = Input(shape=(1, ), name='single_discrete') multi_discrete = Input(shape=(8, ), name='multi_discrete') # FM 一次項部分 continuous_dense = Dense(1)(continuous) single_embedding = Reshape([1])(Embedding(10, 1)(single_discrete)) multi_dense = Dense(1)(multi_discrete) # 一次項求和 first_order_sum = Add()([continuous_dense, single_embedding, multi_dense]) # FM 二次項部分 k=3 continuous_k = Dense(3)(continuous) single_k = Reshape([3])(Embedding(10, 3)(single_discrete)) multi_k = Dense(3)(multi_discrete) # 先相加后平方 sum_square_layer = Lambda(lambda x: x**2)( Add()([continuous_k, single_k, multi_k])) # 先平方后相加 continuous_square = Lambda(lambda x: x**2)(continuous_k) single_square = Lambda(lambda x: x**2)(single_k) multi_square = Lambda(lambda x: x**2)(multi_k) square_sum_layer = Add()([continuous_square, single_square, multi_square]) substract_layer = Lambda(lambda x: x * 0.5)( Subtract()([sum_square_layer, square_sum_layer])) # 定義求和層 class SumLayer(Layer): def __init__(self, **kwargs): super(SumLayer, self).__init__(**kwargs) def call(self, inputs): inputs = K.expand_dims(inputs) return K.sum(inputs, axis=1) def compute_output_shape(self, input_shape): return tuple([input_shape[0], 1]) # 二次項求和 second_order_sum = SumLayer()(substract_layer) # FM 部分輸出 fm_output = Add()([first_order_sum, second_order_sum]) # deep 部分 deep_input = Concatenate()([continuous_k, single_k, multi_k]) deep_layer_0 = Dropout(0.5)(Dense(64, activation='relu')(deep_input)) deep_layer_1 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_0)) deep_layer_2 = Dropout(0.5)(Dense(64, activation='relu')(deep_layer_1)) deep_output = Dropout(0.5)(Dense(1, activation='relu')(deep_layer_2)) concat_layer = Concatenate()([fm_output, deep_output]) y = Dense(1, activation='sigmoid')(concat_layer) model = Model(inputs=[continuous, single_discrete, multi_discrete], outputs=y) Opt = optimizers.Adam( lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False) model.compile( loss='binary_crossentropy', optimizer=Opt, metrics=['acc']) model.fit( train_x, label, shuffle=True, epochs=1, verbose=1, batch_size=1024, validation_split=None)????
參考內容deepFM 原文?
https://arxiv.org/pdf/1703.04247.pdf
用Keras實現一個DeepFM
https://blog.csdn.net/songbinxu/article/details/80151814
keras 文檔?
https://keras.io/zh/layers/core/
- CTR 模型最全演化圖譜https://www.infoq.cn/article/TySwhPNlckijh8Q_vdyO
總結
以上是生活随笔為你收集整理的如何用keras实现deepFM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【小程序开发者专享】腾讯云联手多家科技企
- 下一篇: 腾讯物联网操作系统正式开源,最小体积仅1