個性化排序算法實踐(三)——deepFM算法


FM通過對於每一位特征的隱變量內積來提取特征組合,最后的結果也不錯,雖然理論上FM可以對高階特征組合進行建模,但實際上因為計算復雜度原因,一般都只用到了二階特征組合。對於高階特征組合來說,我們很自然想到多層神經網絡DNN。
DeepFM目的是同時學習低階和高階的特征交叉,主要由FM和DNN兩部分組成,底部共享同樣的輸入。模型可以表示為:

\[\hat{y} = sigmoid(y_{FM}+y_{DNN}) \]

DeepFM

這里主要參考了Github上的代碼,通過對源碼的研究,更加加深了對deepFM理論和應用的了解。
主體部分分為data,fig,output和代碼部分。其中,data存儲數據集,fig存儲訓練后保存的結果,output存儲測試集prediction,代碼詳解如下:

Basic-DeepFM-model

config代表了一些基本配置,DataReader.py是一個讀取DataFrame格式並進行轉換的程序,DeepFM.py是DeepFM算法實現的主程序,main.py是主程序入口,metrics主要保存了gini_norm的實現方法。下面先講述DeepFM的主方法,然后講解如何通過main函數實現一個具體的方法。

DeepFM實現

以函數為單位,深度解析DeepFM方法的實現。

初始化函數:

def __init__(self, feature_size, field_size,
                 embedding_size=8, dropout_fm=[1.0, 1.0],
                 deep_layers=[32, 32], dropout_deep=[0.5, 0.5, 0.5],
                 deep_layer_activation=tf.nn.relu,
                 epoch=10, batch_size=256,
                 learning_rate=0.001, optimizer="adam",
                 batch_norm=0, batch_norm_decay=0.995,
                 verbose=False, random_seed=2016,
                 use_fm=True, use_deep=True,
                 loss_type="logloss", eval_metric=roc_auc_score,
                 l2_reg=0.0, greater_is_better=True):

主要包括了一些基礎配置,如特征個數,特征域個數,隱向量維度,dropout參數,deep部分的層數,每層神經元個數,激活函數,迭代次數,batch_size,學習率,優化方法,batch_norm參數,代價函數,評估函數,正則化參數的選擇等。另外,調用了_init_graph()方法對圖初始化。

def _init_graph(self):

構建了deepFM的Tensor圖。首先還是初始化權重,重要的幾個有:

  • feature_embeddings
    shape為(feature_size,embedding_size),即特征個數(類別特征onehot之后)*embedding維度。
  • feature_bias
    shape為(feature_size,1)
  • deep側的weight以及bias
    根據DNN的參數設置weight以及bias,其中輸入層為field_size*embedding_size,即每一個特征域的embedding向量,輸出層前一層為FM層+DNN最后一層,即field_size+embedding_size+deep_layers[-1]。其中field_size+embedding_size參考個性化排序算法實踐(一)——FM算法可知是一階權重與特征embedding的和。權重初始化采用Xavier初始化

Xavier初始化以0為中心的截斷正態分布中抽取樣本,stddev = sqrt(2 / (fan_in + fan_out)),其中 fan_in 是權重張量的輸入單元數,而 fan_out 是權重張量中的輸出單位數。

初始化權重之后,便是構建網絡層了。網絡層的構建就主要分成兩部分:FM部分與Deep部分。

FM部分

# FM部分
self.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index) # N * F * K
feat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1])
self.embeddings = tf.multiply(self.embeddings,feat_value)

# first order term
self.y_first_order = tf.nn.embedding_lookup(self.weights['feature_bias'],self.feat_index)
self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order,feat_value),2)
self.y_first_order = tf.nn.dropout(self.y_first_order,self.dropout_keep_fm[0])

# second order term
# sum-square-part
self.summed_features_emb = tf.reduce_sum(self.embeddings,1) # None * k
self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K

# squre-sum-part
self.squared_features_emb = tf.square(self.embeddings)
self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1)  # None * K

#second order
self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square,self.squared_sum_features_emb)
self.y_second_order = tf.nn.dropout(self.y_second_order,self.dropout_keep_fm[1])

同樣,根據FM的二次項化簡公式,我們可以得到:

\[\hat y(x) = w_0+\sum_{i=1}^n w_i x_i +\sum_{i=1}^n \sum_{j=i+1}^n ⟨vi,vj⟩ x_i x_j \\ =w_0+\sum_{i=1}^n w_i x_i + \frac{1}{2} \sum_{f=1}^{k} {\left \lgroup \left(\sum_{i=1}^{n} v_{i,f} x_i \right)^2 - \sum_{i=1}^{n} v_{i,f}^2 x_i^2\right \rgroup} \qquad \]

這里使用tf.nn.embedding_lookup方法,可以選擇出對應的特征與權重相乘求和。具體而言,各個變量含有如下:

  • self.embeddings
    代表\(w_i x_i\),這里的i代表第i個特征域

這里的embedding層對於DNN來說時在提取特征,對於FM來說就是他的2階特征,並且FM和DNN共享embedding層。

  • self.y_first_order
    代表\(\sum_{i=1}^n w_i x_i\),即一次項的和,這里的i代表第i個特征
  • self.second order
    代表二次項的和,是由\(\frac{1}{2} \sum_{f=1}^{k} {\left \lgroup \left(\sum_{i=1}^{n} v_{i,f} x_i \right)^2 - \sum_{i=1}^{n} v_{i,f}^2 x_i^2\right \rgroup}\)計算得來。

Deep部分

self.y_deep = tf.reshape(self.embeddings,shape=[-1,self.field_size * self.embedding_size])
self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[0])

for i in range(0,len(self.deep_layers)):
    self.y_deep = tf.add(tf.matmul(self.y_deep,self.weights["layer_%d" %i]), self.weights["bias_%d"%i])
    self.y_deep = self.deep_layers_activation(self.y_deep)
    self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[i+1])

第一層self.embeddings,之后便是堆疊Deep層了。這里的self.embeddings維度為[-1,特征域個數*embedding維度]。

之后便是最后一層,將FM與Deep結合了,如下:

concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)
self.out = tf.add(tf.matmul(concat_input,self.weights['concat_projection']),self.weights['concat_bias'])
if self.loss_type == "logloss":
    self.out = tf.nn.sigmoid(self.out)
    self.loss = tf.losses.log_loss(self.label, self.out)
elif self.loss_type == "mse":
    self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))

這里concat_input可以認為是最后第二層網絡,最后一層輸出結果后,如果是分類任務,使用logloss進行代價函數的計算與反向傳播,否則使用MSE。

一共多少參數量呢?我們計算下:feature_embeddings是feature_size*embedding_size維,feature_bias是feature_size維,deep層前一層神經元個數\(*\)后一層神經元個數,最后相加即可得:
\(特征個數*embedding維度+特征個數+\sum_{i=1}^{N} layer_{i}+layer_{i}*layer_{i+1}\)

訓練

網絡訓練的主函數為:

def fit(self, Xi_train, Xv_train, y_train,
        Xi_valid=None, Xv_valid=None, y_valid=None,
        early_stopping=False, refit=False):

Xi_train,Xv_train,y_train;Xi_valid,Xv_valid,y_valid分別代表訓練和驗證的數據集特征域以及特征值。具體含義可見之后的主流程中的具體例子。

loss,opt = self.sess.run([self.loss,self.optimizer],feed_dict=feed_dict)

至於訓練的關鍵語句就是上面一句了。通過feed_dict喂入每一個batch的數據,進行訓練和傳播。

主流程

這里按照main函數的執行思路進行剖析。
首先,這里提供了一個DataFrame數據集,包括訓練集以及測試集。我們首先需要分辨出哪些特征是數值型特征,哪些特征是類別型數值,這是為了之后進行onehot,以及進行特征域的划分。

每一個數值型特征代表一個特征域,每一個類別型特征代表一個特征域,類別型特征進行onehot之后,每個類別特征域下都有若干個特征。

DataReader

DataReader.py文件提供了對DataFrame數據集的初始化以及進一步處理成可以直接訓練的數據。這里主要有兩個類,FeatureDictionary通過gen_feat_dict()方法,能夠得到將所有的特征映射到從0開始的一個特定的數值tc,規則如下:

  • 假如該特征是數值型特征,映射到一個唯一數值tc
  • 假如該特征是類別型特征,每一個類別映射到一個唯一數值,並且每一個類別映射后值tc都加1
  • 每完成一個特征的映射,數值tc+1

這個步驟類似於將類別特征進行onehot,並且得到每一個特征的特征域。
DataParser類則通過對原數據集進一步的處理,得到xi,xv兩個列表。這兩個列表分別代表原數據集的特征在經過FeatureDictionary后的映射值,以及其原來的數值(如果是類別型特征,xv就令為0)。有如下例子:
假設我們的原有數據集為:

numeric1 numeric2 numeric3 cate1
1.3 3.2 2.0 0
67 3.4 6.7 0
23 4.5 2.4 1
2.6 1.6 5.4 2

經過轉化后,xi為:

numeric1 numeric2 numeric3 cate1
1 2 3 4
1 2 3 4
1 2 3 5
1 2 3 6

這里的4,5,6就是類別特征域下不同特征的映射。
xv為:

numeric1 numeric2 numeric3 cate1
1.3 3.2 2.0 1
67 3.4 6.7 1
23 4.5 2.4 1
2.6 1.6 5.4 1

這里的1就是類別特征域下的值。

完成數據集的處理后,通過交叉驗證就可以進行訓練了。另外,這里使用了特殊的評估函數——gini_norm。
這里將CTR預估問題設定為一個二分類問題,繪制了Gini Normalization來評價不同模型的效果。假設我們有下面兩組結果,分別表示預測值和實際值:

predictions = [0.9, 0.3, 0.8, 0.75, 0.65, 0.6, 0.78, 0.7, 0.05, 0.4, 0.4, 0.05, 0.5, 0.1, 0.1]
actual = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

然后我們將預測值按照從小到大排列,並根據索引序對實際值進行排序:

Sorted Actual Values [0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1]

然后,我們可以畫出如下的圖片:

Gini Normalization1

接下來我們將數據Normalization到0,1之間。並畫出45度線。

Gini Normalization2

橙色區域的面積,就是我們得到的Normalization的Gini系數。

這里,由於我們是將預測概率從小到大排的,所以我們希望實際值中的0盡可能出現在前面,因此Normalization的Gini系數越大,分類效果越好。

參考:
FM系列
個性化排序算法實踐(一)——FM算法
推薦系統算法學習(二)——DNN與FM DeepFM
Github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM