背景
特征工程是繞不開的話題,巧妙的特征組合也許能夠為模型帶來質的提升。但同時,特征工程耗費的資源也是相當可觀的,對於后期模型特征的維護、模型線上部署不太友好。2016年,微軟提出Deep Crossing模型,旨在解決特征工程中特征組合的難題,降低人力特征組合的時間開銷,通過模型自動學習特征的組合方式,也能達到不錯的效果,且在各種任務中表現出較好的穩定性。
與之前介紹的FNN、PNN不同的是,Deep Crossing並沒有采用顯式交叉特征的方式,而是利用殘差網絡結構挖掘特征間的關系。本文將對DeepCrossing從原理到實現細節進行詳細分析。
分析
1. DeepCrossing模型結構

論文中使用的目標函數為 \(logloss\) :\(logloss=-\frac{1}{N}\sum_{i=1}^{N}(y_ilog(p_i)+(1-y_i)log(1-p_i))\) ,在實際應用中,可以靈活替換為其他目標函數。
下面對各層結構進行分析:
1.1 Embedding & Stacking
Embedding的主要目的是將高維稀疏特征轉化為低維稠密特征,其公式化定義為:\(X_j^O=max(0,W_jX_j^I+b_j)\) ,其中 \(X_j^I\) 代表輸入的第 \(j\) 個特征Field,並且已經過one-hot編碼表示,\(Wb\) 分別表示對應的模型參數。與前幾篇paper介紹的Embedding過程不同的是,DeepCrossing加上了偏置項 \(b\) 。公式中的 \(max\) 操作等價於使用 \(relu\) 激活函數。
盡管可以通過分Field的操作,減少Embedding層的參數量,但是由於某些 高基數特征 的存在,如paper中提到的CampaignID,其對應的 \(W_j\) 仍然十分龐大。為此作者提出,針對這些高基數特征構造衍生特征,具體操作如下。根據CampaignID的歷史點擊率從高到低選擇Top1000個,編號從0到999,將剩余的ID統一編號為1000。同時構建其衍生特征,將所有ID對應的歷史點擊率組合成1001維的稠密矩陣,各個元素分別為對應ID的歷史CTR,最后一個元素為剩余ID的平均CTR。通過降維引入衍生特征的方式,可以有效的減少高基數特征帶來的參數量劇增問題。
經過Embedding之后,直接對所有的 \(X_j^O\) 進行拼接Stacking,\(X^O=[X_0^O,X_1^O,\dots,X_K^O]\) 。作者將特征embedding為256維,但是對於本身維度低於256的特征Field,無需進行Embedding,直接送入Stacking層,如上圖中的 \(Feature\#2\) 所示。
1.2 Residual Unit
殘差的網絡結構如下:

將 \(X^I\) 移項到等式左側,可以看出 \(F\) 函數擬合的是輸入與輸出之間的殘差。對輸入進行全連接變換之后,經過 \(relu\) 激活函數送入第二個全連接層,將輸出結果與原始輸入進行 element-wise add 操作,再經過 \(relu\) 激活輸出。有分析說明,殘差結構能更敏感的捕獲輸入輸出之間的信息差 [2]。
作者通過各種類型各種大小的實驗發現,DeepCrossing具有很好的魯棒性,推測可能是因為殘差結構能起到類似於正則的效果,但是具體原因是如何的並未明確指出,如果有同學了解具體原因,歡迎交流。
1.3 Scoring Layer
使用 \(logloss\) 作為目標函數,可以靈活改用其他函數表示。
2. Early Crossing vs. Late Crossing
在paper中,作者針對特征交叉的時間點先后的問題進行試驗對比。在DeepCrossing中,特征是在Embedding之后就開始進行交叉,但是有一些模型如DSSM,是在各類特征單獨處理完成之后再進行交叉計算,這類模型的結構如下所示:

3. 性能分析
3.1 文本輸入
輸入特征相同,以DSSM作為baseline,根據Table 3可以看出DeepCrossing相對AUC更高。

將生產環境的已有模型Production作為baseline進行對比,雖然DeepCrossing比DSSM表現更好,但稍遜Production。這是因為Production的訓練數據集不同,且有更為豐富的特征。

1)衍生特征Counting Feature的重要性比較
在1.1節中討論過,為了對高基數特征進行降維處理,引入了統計類衍生特征(稱之為Counting Feature)。對比此類特征對於模型的影響,從實驗結果可以看出衍生特征能夠帶來較大提升。

使用Production訓練特征的子集,使用22億條數據進行訓練。最終DeepCrossing表現超過了Production。

實驗
使用 \(MovieLens100K dataset\) ,核心代碼如下。
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 = tf.get_variable(name='residual_b0_%d'%i, shape=[out_node], dtype=tf.float32)
residual = tf.nn.relu(tf.matmul(input, w0) + b0)
w1 = tf.get_variable(name='residual_w1_%d'%i, shape=[out_node, in_node], dtype=tf.float32, regularizer=self.l2_reg)
b1 = tf.get_variable(name='residual_b1_%d'%i, shape=[in_node], dtype=tf.float32)
residual = tf.matmul(residual, w1) + b1
out = tf.nn.relu(residual+x)
return out
def inference(self):
with tf.variable_scope('emb_part'):
emb = [tf.get_variable(name='emb_%d'%i, shape=[self.field_lens[i], self.vec_dim], dtype=tf.float32, regularizer=self.l2_reg) for i in range(self.field_num)]
emb_layer = tf.concat([tf.matmul(self.x[i], emb[i]) for i in range(self.field_num)], axis=1) # (batch, F*K)
x = emb_layer
with tf.variable_scope('residual_part'):
for i in range(self.residual_unit_num):
x = self._residual_unit(x, i)
x = tf.layers.dropout(x, rate=self.dropout_rate, training=self.is_train)
w = tf.get_variable(name='w', shape=[self.field_num*self.vec_dim, 1], dtype=tf.float32, regularizer=self.l2_reg)
b = tf.get_variable(name='b', shape=[1], dtype=tf.float32)
self.y_logits = tf.matmul(x, w) + b
self.y_hat = tf.nn.sigmoid(self.y_logits)
self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
reg_variables = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
if len(reg_variables) > 0:
self.loss += tf.add_n(reg_variables)
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.loss)
reference
[1] Shan, Ying, et al. "Deep crossing: Web-scale modeling without manually crafted combinatorial features." Proceedings of the 22nd ACM SIGKDD international conference on knowledge discovery and data mining. ACM, 2016.
[2] https://zhuanlan.zhihu.com/p/72679537
[3] https://www.zhihu.com/question/20830906/answer/681688041
知識分享
個人知乎專欄:https://zhuanlan.zhihu.com/c_1164954275573858304
歡迎關注微信公眾號:SOTA Lab
專注知識分享,不定期更新計算機、金融類文章