FM通過對於每一位特征的隱變量內積來提取特征組合,最后的結果也不錯,雖然理論上FM可以對高階特征組合進行建模,但實際上因為計算復雜度原因,一般都只用到了二階特征組合。對於高階特征組合來說,我們很自然想到多層神經網絡DNN。
DeepFM目的是同時學習低階和高階的特征交叉,主要由FM和DNN兩部分組成,底部共享同樣的輸入。模型可以表示為:
這里主要參考了Github上的代碼,通過對源碼的研究,更加加深了對deepFM理論和應用的了解。
主體部分分為data,fig,output和代碼部分。其中,data存儲數據集,fig存儲訓練后保存的結果,output存儲測試集prediction,代碼詳解如下:
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的二次項化簡公式,我們可以得到:
這里使用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]
然后,我們可以畫出如下的圖片:
接下來我們將數據Normalization到0,1之間。並畫出45度線。
橙色區域的面積,就是我們得到的Normalization的Gini系數。
這里,由於我們是將預測概率從小到大排的,所以我們希望實際值中的0盡可能出現在前面,因此Normalization的Gini系數越大,分類效果越好。
參考:
FM系列
個性化排序算法實踐(一)——FM算法
推薦系統算法學習(二)——DNN與FM DeepFM
Github