推薦系統系列(五):Deep Crossing理論與實踐
推薦系統系列(五):Deep Crossing理論與實踐
Dadada 日拱一卒無有盡,功不唐捐終入海背景
特徵工程是繞不開的話題,巧妙的特徵組合也許能夠為模型帶來質的提升。但同時,特徵工程耗費的資源也是相當可觀的,對於後期模型特徵的維護、模型線上部署不太友好。2016年,微軟提出Deep Crossing模型,旨在解決特徵工程中特徵組合的難題,降低人力特徵組合的時間開銷,通過模型自動學習特徵的組合方式,也能達到不錯的效果,且在各種任務中表現出較好的穩定性。
與之前介紹的FNN、PNN不同的是,Deep Crossing並沒有採用顯式交叉特徵的方式,而是利用殘差網路結構挖掘特徵間的關係。本文將對DeepCrossing從原理到實現細節進行詳細分析。
分析
1. DeepCrossing模型結構
整個模型包含四種結構:Embedding,Stacking,Residual Unit,Scoring Layer。
論文中使用的目標函式為:,在實際應用中,可以靈活替換為其他目標函式。
下面對各層結構進行分析:
1.1 Embedding & Stacking
Embedding的主要目的是將高維稀疏特徵轉化為低維稠密特徵,其公式化定義為: ,其中 代表輸入的第 個特徵Field,並且已經過one-hot編碼表示, 分別表示對應的模型引數。與前幾篇paper介紹的Embedding過程不同的是,DeepCrossing加上了偏置項 。公式中的 操作等價於使用 啟用函式。
儘管可以通過分Field的操作,減少Embedding層的引數量,但是由於某些高基數特徵的存在,如paper中提到的CampaignID,其對應的 仍然十分龐大。為此作者提出,針對這些高基數特徵構造衍生特徵,具體操作如下。根據CampaignID的歷史點選率從高到低選擇Top1000個,編號從0到999,將剩餘的ID統一編號為1000。同時構建其衍生特徵,將所有ID對應的歷史點選率組合成1001維的稠密矩陣,各個元素分別為對應ID的歷史CTR,最後一個元素為剩餘ID的平均CTR。通過降維引入衍生特徵的方式,可以有效的減少高基數特徵帶來的引數量劇增問題。
經過Embedding之後,直接對所有的 進行拼接Stacking, 。作者將特徵embedding為256維,但是對於本身維度低於256的特徵Field,無需進行Embedding,直接送入Stacking層,如上圖中的 所示。
1.2 Residual Unit
殘差的網路結構如下:
公式定義為:
將 移項到等式左側,可以看出 函式擬合的是輸入與輸出之間的殘差。對輸入進行全連線變換之後,經過 啟用函式送入第二個全連線層,將輸出結果與原始輸入進行 element-wise add 操作,再經過 啟用輸出。有分析說明,殘差結構能更敏感的捕獲輸入輸出之間的資訊差 [2]。
作者通過各種型別各種大小的實驗發現,DeepCrossing具有很好的魯棒性,推測可能是因為殘差結構能起到類似於正則的效果,但是具體原因是如何的並未明確指出,如果有同學瞭解具體原因,歡迎交流。
1.3 Scoring Layer
使用 作為目標函式,可以靈活改用其他函式表示。
2. Early Crossing vs. Late Crossing
在paper中,作者針對特徵交叉的時間點先後的問題進行試驗對比。在DeepCrossing中,特徵是在Embedding之後就開始進行交叉,但是有一些模型如DSSM,是在各類特徵單獨處理完成之後再進行交叉計算,這類模型的結構如下所示:
文中提到,DSSM更擅長文字處理,設計文字處理相關實驗,DeepCrossing比DSSM表現更優異。作者認為,DeepCrossing表現優異主要來源於:1)殘差結構;2)及早的特徵交叉處理;
3. 效能分析
3.1 文字輸入
輸入特徵相同,以DSSM作為baseline,根據Table 3可以看出DeepCrossing相對AUC更高。
將生產環境的已有模型Production作為baseline進行對比,雖然DeepCrossing比DSSM表現更好,但稍遜Production。這是因為Production的訓練資料集不同,且有更為豐富的特徵。
3.2 其他對比
1)衍生特徵Counting Feature的重要性比較
在1.1節中討論過,為了對高基數特徵進行降維處理,引入了統計類衍生特徵(稱之為Counting Feature)。對比此類特徵對於模型的影響,從實驗結果可以看出衍生特徵能夠帶來較大提升。
2)與生產環境模型Production進行比較
使用Production訓練特徵的子集,使用22億條資料進行訓練。最終DeepCrossing表現超過了Production。
實驗
使用 ,核心程式碼如下。
class DeepCrossing(object):
def __init__(self, vec_dim=None, field_lens=None, lr=None, residual_unit_num=None, residual_w_dim=None, dropout_rate=None, lamda=None):
self.vec_dim = vec_dim
self.field_lens = field_lens
self.field_num = len(field_lens)
self.lr = lr
self.residual_unit_num = residual_unit_num
self.residual_w_dim = residual_w_dim
self.dropout_rate = dropout_rate
self.lamda = float(lamda)
self.l2_reg = tf.contrib.layers.l2_regularizer(self.lamda)
self._build_graph()
def _build_graph(self):
self.add_input()
self.inference()
def add_input(self):
self.x = [tf.placeholder(tf.float32, name='input_x_%d'%i) for i in range(self.field_num)]
self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
self.is_train = tf.placeholder(tf.bool)
def _residual_unit(self, input, i):
x = input
in_node = self.field_num*self.vec_dim
out_node = self.residual_w_dim
w0 = tf.get_variable(name='residual_w0_%d'%i, shape=[in_node, out_node], dtype=tf.float32, regularizer=self.l2_reg)
b0