从FM推演各深度学习CTR预估模型
本文的PDF版本、代碼實現和數據可以在我的github取到。
1.引言
點擊率(click-through rate, CTR)是互聯網公司進行流量分配的核心依據之一。比如互聯網廣告平臺,為了精細化權衡和保障用戶、廣告、平臺三方的利益,準確的CTR預估是不可或缺的。CTR預估技術從傳統的邏輯回歸,到近兩年大火的深度學習,新的算法層出不窮:DeepFM, NFM, DIN, AFM, DCN……?
然而,相關的綜述文章不少,但碎片羅列的居多,模型之間內在的聯系和演化思路如何揭示?怎樣才能迅速get到新模型的創新點和適用場景,快速提高新論文速度,節約理解、復現模型的成本?這些都是亟待解決的問題。?
我們認為,從FM及其與神經網絡的結合出發,能夠迅速貫穿很多深度學習CTR預估網絡的思路,從而更好地理解和應用模型。
2.本文的思路與方法
3.FM:降維版本的特征二階組合
CTR預估本質是一個二分類問題,以移動端展示廣告推薦為例,依據日志中的用戶側的信息(比如年齡,性別,國籍,手機上安裝的app列表)、廣告側的信息(廣告id,廣告類別,廣告標題等)、上下文側信息(渠道id等),去建模預測用戶是否會點擊該廣告。?
FM出現之前的傳統的處理方法是人工特征工程加上線性模型(如邏輯回歸Logistic Regression)。為了提高模型效果,關鍵技術是找到到用戶點擊行為背后隱含的特征組合。如男性、大學生用戶往往會點擊游戲類廣告,因此“男性且是大學生且是游戲類”的特征組合就是一個關鍵特征。但這本質仍是線性模型,其假設函數表示成內積形式一般為:
?
ylinear=σ(?w??,x???)ylinear=σ(?w→,x→?)
?
其中x??x→為特征向量,w??w→為權重向量,σ()σ()為sigmoid函數。
但是人工進行特征組合通常會存在諸多困難,如特征爆炸、特征難以被識別、組合特征難以設計等。為了讓模型自動地考慮特征之間的二階組合信息,線性模型推廣為二階多項式(2d?Polynomial2d?Polynomial)模型:
?
ypoly=σ(?w??,x???+∑i=1n∑j=1nwij?xi?xj)ypoly=σ(?w→,x→?+∑i=1n∑j=1nwij?xi?xj)
?
其實就是對特征兩兩相乘(組合)構成新特征(離散化之后其實就是“且”操作),并對每個新特征分配獨立的權重,通過機器學習來自動得到這些權重。將其寫成矩陣形式為:
?
ypoly=σ(w??T?x??+x??T?W(2)?x??)ypoly=σ(w→T?x→+x→T?W(2)?x→)
?
其中W(2)W(2)為二階特征組合的權重矩陣,是對稱矩陣。而這個矩陣參數非常多,為O(n2)O(n2)。為了降低該矩陣的維度,可以將其因子分解(FactorizationFactorization)為兩個低維(比如n?kn?k)矩陣的相乘。則此時WW矩陣的參數就大幅降低,為O(nk)O(nk)。公式如下:
?
W(2)=WT?WW(2)=WT?W
?
這就是RendleRendle等在2010年提出因子分解機(Factorization Machines,FM)的名字的由來。FM的矩陣形式公式如下:
?
yFM=σ(w??T?x??+x??T?WT?W?x??)yFM=σ(w→T?x→+x→T?WT?W?x→)
?
將其寫成內積的形式:?
yFM=σ(?w??,x???+?W?x??,W?x???)yFM=σ(?w→,x→?+?W?x→,W?x→?)
利用?∑ni=1ai→,∑ni=1ai→?=∑ni=1∑nj=1?ai→,aj→??∑i=1nai→,∑i=1nai→?=∑i=1n∑j=1n?ai→,aj→?,可以將上式進一步改寫成求和式的形式:?
yFM=σ(?w??,x???+∑i=1n∑j=1n?xi?v??i,xj?v??j?)yFM=σ(?w→,x→?+∑i=1n∑j=1n?xi?v→i,xj?v→j?)
其中vi→vi→向量是矩陣WW的第ii列。為了去除重復項與特征平方項,上式可以進一步改寫成更為常見的FM公式:?
yFM=σ(?w??,x???+∑i=1n∑j=i+1n?v??i,v??j?xi?xj)yFM=σ(?w→,x→?+∑i=1n∑j=i+1n?v→i,v→j?xi?xj)
對比二階多項式模型,FM模型中特征兩兩相乘(組合)的權重是相互不獨立的,它是一種參數較少但表達力強的模型。
?
此處附上FM的TensorFlow代碼實現,完整數據和代碼請戳這里。注意FM通過內積進行無重復項與特征平方項的特征組合過程使用了一個小trick,就是:?
∑i=1n∑j=i+1nxixj=1/2×[(∑i=1nxi)2?∑i=1nx2i]∑i=1n∑j=i+1nxixj=1/2×[(∑i=1nxi)2?∑i=1nxi2]
?
class FM(Model):def __init__(self, input_dim=None, output_dim=1, factor_order=10, init_path=None, opt_algo='gd', learning_rate=1e-2,l2_w=0, l2_v=0, random_seed=None):Model.__init__(self)# 一次、二次交叉、偏置項init_vars = [('w', [input_dim, output_dim], 'xavier', dtype),('v', [input_dim, factor_order], 'xavier', dtype),('b', [output_dim], 'zero', dtype)]self.graph = tf.Graph()with self.graph.as_default():if random_seed is not None:tf.set_random_seed(random_seed)self.X = tf.sparse_placeholder(dtype)self.y = tf.placeholder(dtype)self.vars = init_var_map(init_vars, init_path)w = self.vars['w']v = self.vars['v']b = self.vars['b']# [(x1+x2+x3)^2 - (x1^2+x2^2+x3^2)]/2# 先計算所有的交叉項,再減去平方項(自己和自己相乘)X_square = tf.SparseTensor(self.X.indices, tf.square(self.X.values), tf.to_int64(tf.shape(self.X)))xv = tf.square(tf.sparse_tensor_dense_matmul(self.X, v))p = 0.5 * tf.reshape(tf.reduce_sum(xv - tf.sparse_tensor_dense_matmul(X_square, tf.square(v)), 1),[-1, output_dim])xw = tf.sparse_tensor_dense_matmul(self.X, w)logits = tf.reshape(xw + b + p, [-1])self.y_prob = tf.sigmoid(logits)self.loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=self.y)) + \l2_w * tf.nn.l2_loss(xw) + \l2_v * tf.nn.l2_loss(xv)self.optimizer = get_optimizer(opt_algo, learning_rate, self.loss)#GPU設定config = tf.ConfigProto()config.gpu_options.allow_growth = Trueself.sess = tf.Session(config=config)# 圖中所有variable初始化tf.global_variables_initializer().run(session=self.sess)- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
4.用神經網絡的視角看FM:嵌入后再進行內積
我們觀察FM公式的矩陣內積形式:
?
yFM=σ(?w??,x???+?W?x??,W?x???)yFM=σ(?w→,x→?+?W?x→,W?x→?)
?
發現W?x??W·x→部分就是將離散系數特征通過矩陣乘法降維成一個低維稠密向量。這個過程對神經網絡來說就叫做嵌入(embedding)。所以用神經網絡視角來看:
其示意圖如下。為了表述清晰,我們繪制的是神經網絡計算圖而不是網絡結構圖——在網絡結構圖中增加了權重WW的位置。?
5.FM的實際應用:考慮領域信息
廣告點擊率預估模型中的特征以分領域的離散特征為主,如:廣告類別、用戶職業、手機APP列表等。由于連續特征比較好處理,為了簡化起見,本文只考慮同時存在不同領域的離散特征的情形。處理離散特征的常見方法是通過獨熱(one-hot)編碼轉換為一系列二值特征向量。然后將這些高維稀疏特征通過嵌入(embedding)轉換為低維連續特征。前面已經說明FM中間的一個核心步驟就是嵌入,但這個嵌入過程沒有考慮領域信息。這使得同領域內的特征也被當做不同領域特征進行兩兩組合了。?
其實可以將特征具有領域關系的特點作為先驗知識加入到神經網絡的設計中去:同領域的特征嵌入后直接求和作為一個整體嵌入向量,進而與其他領域的整體嵌入向量進行兩兩組合。而這個先嵌入后求和的過程,就是一個單領域的小離散特征向量乘以矩陣的過程。此時FM的過程變為:對不同領域的離散特征分別進行嵌入,之后再進行二階特征的向量內積。其計算圖圖如下所示:?
這樣考慮其實是給FM增加了一個正則:考慮了領域內的信息的相似性。而且還有一個附加的好處,這些嵌入后的同領域特征可以拼接起來作為更深的神經網絡的輸入,達到降維的目的。接下來我們將反復看到這種處理方式。?
此處需要注意,這與“基于領域的因子分解機”(Field-aware Factorization Machines,FFM)有區別。FFM也是FM的另一種變體,也考慮了領域信息。但其不同點是同一個特征與不同領域進行特征組合時,其對應的嵌入向量是不同的。本文不考慮FFM的作用機制。?
經過這些改進的FM終究還是淺層網絡,它的表現力仍然有限。為了增加模型的表現力(model capacity),一個自然的想法就是將該淺層網絡不斷“深化”。
?
6.embedding+MLP:深度學習CTR預估的通用框架
embedding+MLP是對于分領域離散特征進行深度學習CTR預估的通用框架。深度學習在特征組合挖掘(特征學習)方面具有很大的優勢。比如以CNN為代表的深度網絡主要用于圖像、語音等稠密特征上的學習,以W2V、RNN為代表的深度網絡主要用于文本的同質化、序列化高維稀疏特征的學習。CTR預估的主要場景是對離散且有具體領域的特征進行學習,所以其深度網絡結構也不同于CNN與RNN。?
具體來說, embedding+MLP的過程如下:
其示意圖如下:?
embedding+MLP的缺點是只學習高階特征組合,對于低階或者手動的特征組合不夠兼容,而且參數較多,學習較困難。
7.FNN:FM與MLP的串聯結合
Weinan Zhang等在2016年提出的因子分解機神經網絡(Factorisation Machine supported Neural Network,FNN)將考FM與MLP進行了結合。它有著十分顯著的特點:
其計算圖如下所示:?
通過觀察FFN的計算圖可以看出其與embedding+MLP確實非常像。不過此處省略了FNN的FM部分的線性模塊。這種省略為了更好地進行兩個模型的對比。接下來的計算圖我們都會省略線性模塊。
此處附上FNN的代碼實現,完整數據和代碼請戳這里。:
class FNN(Model):def __init__(self, field_sizes=None, embed_size=10, layer_sizes=None, layer_acts=None, drop_out=None,embed_l2=None, layer_l2=None, init_path=None, opt_algo='gd', learning_rate=1e-2, random_seed=None):Model.__init__(self)init_vars = []num_inputs = len(field_sizes)for i in range(num_inputs):init_vars.append(('embed_%d' % i, [field_sizes[i], embed_size], 'xavier', dtype))node_in = num_inputs * embed_sizefor i in range(len(layer_sizes)):init_vars.append(('w%d' % i, [node_in, layer_sizes[i]], 'xavier', dtype))init_vars.append(('b%d' % i, [layer_sizes[i]], 'zero', dtype))node_in = layer_sizes[i]self.graph = tf.Graph()with self.graph.as_default():if random_seed is not None:tf.set_random_seed(random_seed)self.X = [tf.sparse_placeholder(dtype) for i in range(num_inputs)]self.y = tf.placeholder(dtype)self.keep_prob_train = 1 - np.array(drop_out)self.keep_prob_test = np.ones_like(drop_out)self.layer_keeps = tf.placeholder(dtype)self.vars = init_var_map(init_vars, init_path)w0 = [self.vars['embed_%d' % i] for i in range(num_inputs)]xw = tf.concat([tf.sparse_tensor_dense_matmul(self.X[i], w0[i]) for i in range(num_inputs)], 1)l = xw#全連接部分for i in range(len(layer_sizes)):wi = self.vars['w%d' % i]bi = self.vars['b%d' % i]print(l.shape, wi.shape, bi.shape)l = tf.nn.dropout(activate(tf.matmul(l, wi) + bi,layer_acts[i]),self.layer_keeps[i])l = tf.squeeze(l)self.y_prob = tf.sigmoid(l)self.loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=l, labels=self.y))if layer_l2 is not None:self.loss += embed_l2 * tf.nn.l2_loss(xw)for i in range(len(layer_sizes)):wi = self.vars['w%d' % i]self.loss += layer_l2[i] * tf.nn.l2_loss(wi)self.optimizer = get_optimizer(opt_algo, learning_rate, self.loss)config = tf.ConfigProto()config.gpu_options.allow_growth = Trueself.sess = tf.Session(config=config)tf.global_variables_initializer().run(session=self.sess)- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
8.DeepFM: FM與MLP的并聯結合
針對FNN需要預訓練的問題,Huifeng Guo等提出了深度因子分解機模型(Deep Factorisation Machine, DeepFM, 2017)。該模型的特點是:
其計算圖如下所示:?
通過觀察DeepFM的計算圖可以看出紅色虛線以上部分其實就是FM部分,虛線以下就是MLP部分。
此處附上DeepFM的代碼實現,完整數據和代碼請戳這里。:
def model_fn(features, labels, mode, params):"""Bulid Model function f(x) for Estimator."""#------超參數的設定----field_size = params["field_size"]feature_size = params["feature_size"]embedding_size = params["embedding_size"]l2_reg = params["l2_reg"]learning_rate = params["learning_rate"]#batch_norm_decay = params["batch_norm_decay"]#optimizer = params["optimizer"]layers = map(int, params["deep_layers"].split(','))dropout = map(float, params["dropout"].split(','))#------權重------FM_B = tf.get_variable(name='fm_bias', shape=[1], initializer=tf.constant_initializer(0.0))FM_W = tf.get_variable(name='fm_w', shape=[feature_size], initializer=tf.glorot_normal_initializer())# FFM_V = tf.get_variable(name='fm_v', shape=[feature_size, embedding_size], initializer=tf.glorot_normal_initializer())# F * E #------build feaure-------feat_ids = features['feat_ids']feat_ids = tf.reshape(feat_ids,shape=[-1,field_size]) # None * f/K * Kfeat_vals = features['feat_vals']feat_vals = tf.reshape(feat_vals,shape=[-1,field_size]) # None * f/K * K#------build f(x)------with tf.variable_scope("First-order"):feat_wgts = tf.nn.embedding_lookup(FM_W, feat_ids) # None * f/K * Ky_w = tf.reduce_sum(tf.multiply(feat_wgts, feat_vals),1)with tf.variable_scope("Second-order"):embeddings = tf.nn.embedding_lookup(FM_V, feat_ids) # None * f/K * K * Efeat_vals = tf.reshape(feat_vals, shape=[-1, field_size, 1]) # None * f/K * K * 1 ?embeddings = tf.multiply(embeddings, feat_vals) #vij*xi sum_square = tf.square(tf.reduce_sum(embeddings,1)) # None * K * Esquare_sum = tf.reduce_sum(tf.square(embeddings),1)y_v = 0.5*tf.reduce_sum(tf.subtract(sum_square, square_sum),1) # None * 1with tf.variable_scope("Deep-part"):if FLAGS.batch_norm:#normalizer_fn = tf.contrib.layers.batch_norm#normalizer_fn = tf.layers.batch_normalizationif mode == tf.estimator.ModeKeys.TRAIN:train_phase = True#normalizer_params = {'decay': batch_norm_decay, 'center': True, 'scale': True, 'updates_collections': None, 'is_training': True, 'reuse': None}else:train_phase = False#normalizer_params = {'decay': batch_norm_decay, 'center': True, 'scale': True, 'updates_collections': None, 'is_training': False, 'reuse': True}else:normalizer_fn = Nonenormalizer_params = Nonedeep_inputs = tf.reshape(embeddings,shape=[-1,field_size*embedding_size]) # None * (F*K)for i in range(len(layers)):#if FLAGS.batch_norm:# deep_inputs = batch_norm_layer(deep_inputs, train_phase=train_phase, scope_bn='bn_%d' %i)#normalizer_params.update({'scope': 'bn_%d' %i})deep_inputs = tf.contrib.layers.fully_connected(inputs=deep_inputs, num_outputs=layers[i], \#normalizer_fn=normalizer_fn, normalizer_params=normalizer_params, \weights_regularizer=tf.contrib.layers.l2_regularizer(l2_reg), scope='mlp%d' % i)if FLAGS.batch_norm:deep_inputs = batch_norm_layer(deep_inputs, train_phase=train_phase, scope_bn='bn_%d' %i) #放在RELU之后 https://github.com/ducha-aiki/caffenet-benchmark/blob/master/batchnorm.md#bn----before-or-after-reluif mode == tf.estimator.ModeKeys.TRAIN:deep_inputs = tf.nn.dropout(deep_inputs, keep_prob=dropout[i]) #Apply Dropout after all BN layers and set dropout=0.8(drop_ratio=0.2)#deep_inputs = tf.layers.dropout(inputs=deep_inputs, rate=dropout[i], training=mode == tf.estimator.ModeKeys.TRAIN)y_deep = tf.contrib.layers.fully_connected(inputs=deep_inputs, num_outputs=1, activation_fn=tf.identity, \weights_regularizer=tf.contrib.layers.l2_regularizer(l2_reg), scope='deep_out')y_d = tf.reshape(y_deep,shape=[-1])#sig_wgts = tf.get_variable(name='sigmoid_weights', shape=[layers[-1]], initializer=tf.glorot_normal_initializer())#sig_bias = tf.get_variable(name='sigmoid_bias', shape=[1], initializer=tf.constant_initializer(0.0))#deep_out = tf.nn.xw_plus_b(deep_inputs,sig_wgts,sig_bias,name='deep_out')with tf.variable_scope("DeepFM-out"):#y_bias = FM_B * tf.ones_like(labels, dtype=tf.float32) # None * 1 warning;這里不能用label,否則調用predict/export函數會出錯,train/evaluate正常;初步判斷estimator做了優化,用不到label時不傳y_bias = FM_B * tf.ones_like(y_d, dtype=tf.float32) # None * 1y = y_bias + y_w + y_v + y_dpred = tf.sigmoid(y)predictions={"prob": pred}export_outputs = {tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: tf.estimator.export.PredictOutput(predictions)}# Provide an estimator spec for `ModeKeys.PREDICT`if mode == tf.estimator.ModeKeys.PREDICT:return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions,export_outputs=export_outputs)#------bulid loss------loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=y, labels=labels)) + \l2_reg * tf.nn.l2_loss(FM_W) + \l2_reg * tf.nn.l2_loss(FM_V) #+ \ l2_reg * tf.nn.l2_loss(sig_wgts)# Provide an estimator spec for `ModeKeys.EVAL`eval_metric_ops = {"auc": tf.metrics.auc(labels, pred)}if mode == tf.estimator.ModeKeys.EVAL:return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions,loss=loss,eval_metric_ops=eval_metric_ops)#------bulid optimizer------if FLAGS.optimizer == 'Adam':optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate, beta1=0.9, beta2=0.999, epsilon=1e-8)elif FLAGS.optimizer == 'Adagrad':optimizer = tf.train.AdagradOptimizer(learning_rate=learning_rate, initial_accumulator_value=1e-8)elif FLAGS.optimizer == 'Momentum':optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.95)elif FLAGS.optimizer == 'ftrl':optimizer = tf.train.FtrlOptimizer(learning_rate)train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())# Provide an estimator spec for `ModeKeys.TRAIN` modesif mode == tf.estimator.ModeKeys.TRAIN:return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions,loss=loss,train_op=train_op)- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
9.NFM:通過逐元素乘法延遲FM的實現過程
我們再回到考慮領域信息的FM,它仍有改進的空間。因為以上這些網絡的FM部分都是只進行嵌入向量的兩兩內積后直接求和,沒有充分利用二階特征組合的信息。Xiangnan He等在2017年提出了神經網絡因子分解機(Neural Factorization Machines,NFM)對此作出了改進。其計算圖如下所示:?
?
NFM的基本特點是:
此處附上NFM的代碼實現,完整數據和代碼請戳這里:
def model_fn(features, labels, mode, params):"""Bulid Model function f(x) for Estimator."""#------hyperparameters----field_size = params["field_size"]feature_size = params["feature_size"]embedding_size = params["embedding_size"]l2_reg = params["l2_reg"]learning_rate = params["learning_rate"]#optimizer = params["optimizer"]layers = map(int, params["deep_layers"].split(','))dropout = map(float, params["dropout"].split(','))#------bulid weights------Global_Bias = tf.get_variable(name='bias', shape=[1], initializer=tf.constant_initializer(0.0))Feat_Bias = tf.get_variable(name='linear', shape=[feature_size], initializer=tf.glorot_normal_initializer())Feat_Emb = tf.get_variable(name='emb', shape=[feature_size,embedding_size], initializer=tf.glorot_normal_initializer())#------build feaure-------feat_ids = features['feat_ids']feat_ids = tf.reshape(feat_ids,shape=[-1,field_size])feat_vals = features['feat_vals']feat_vals = tf.reshape(feat_vals,shape=[-1,field_size])#------build f(x)------with tf.variable_scope("Linear-part"):feat_wgts = tf.nn.embedding_lookup(Feat_Bias, feat_ids) # None * F * 1y_linear = tf.reduce_sum(tf.multiply(feat_wgts, feat_vals),1)with tf.variable_scope("BiInter-part"):embeddings = tf.nn.embedding_lookup(Feat_Emb, feat_ids) # None * F * Kfeat_vals = tf.reshape(feat_vals, shape=[-1, field_size, 1])embeddings = tf.multiply(embeddings, feat_vals) # vij * xisum_square_emb = tf.square(tf.reduce_sum(embeddings,1))square_sum_emb = tf.reduce_sum(tf.square(embeddings),1)deep_inputs = 0.5*tf.subtract(sum_square_emb, square_sum_emb) # None * Kwith tf.variable_scope("Deep-part"):if mode == tf.estimator.ModeKeys.TRAIN:train_phase = Trueelse:train_phase = Falseif mode == tf.estimator.ModeKeys.TRAIN:deep_inputs = tf.nn.dropout(deep_inputs, keep_prob=dropout[0]) # None * Kfor i in range(len(layers)):deep_inputs = tf.contrib.layers.fully_connected(inputs=deep_inputs, num_outputs=layers[i], \weights_regularizer=tf.contrib.layers.l2_regularizer(l2_reg), scope='mlp%d' % i)if FLAGS.batch_norm:deep_inputs = batch_norm_layer(deep_inputs, train_phase=train_phase, scope_bn='bn_%d' %i) #放在RELU之后 https://github.com/ducha-aiki/caffenet-benchmark/blob/master/batchnorm.md#bn----before-or-after-reluif mode == tf.estimator.ModeKeys.TRAIN:deep_inputs = tf.nn.dropout(deep_inputs, keep_prob=dropout[i]) #Apply Dropout after all BN layers and set dropout=0.8(drop_ratio=0.2)#deep_inputs = tf.layers.dropout(inputs=deep_inputs, rate=dropout[i], training=mode == tf.estimator.ModeKeys.TRAIN)y_deep = tf.contrib.layers.fully_connected(inputs=deep_inputs, num_outputs=1, activation_fn=tf.identity, \weights_regularizer=tf.contrib.layers.l2_regularizer(l2_reg), scope='deep_out')y_d = tf.reshape(y_deep,shape=[-1])with tf.variable_scope("NFM-out"):#y_bias = Global_Bias * tf.ones_like(labels, dtype=tf.float32) # None * 1 warning;這里不能用label,否則調用predict/export函數會出錯,train/evaluate正常;初步判斷estimator做了優化,用不到label時不傳y_bias = Global_Bias * tf.ones_like(y_d, dtype=tf.float32) # None * 1y = y_bias + y_linear + y_dpred = tf.sigmoid(y)predictions={"prob": pred}export_outputs = {tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: tf.estimator.export.PredictOutput(predictions)}# Provide an estimator spec for `ModeKeys.PREDICT`if mode == tf.estimator.ModeKeys.PREDICT:return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions,export_outputs=export_outputs)#------bulid loss------loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=y, labels=labels)) + \l2_reg * tf.nn.l2_loss(Feat_Bias) + l2_reg * tf.nn.l2_loss(Feat_Emb)# Provide an estimator spec for `ModeKeys.EVAL`eval_metric_ops = {"auc": tf.metrics.auc(labels, pred)}if mode == tf.estimator.ModeKeys.EVAL:return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions,loss=loss,eval_metric_ops=eval_metric_ops)#------bulid optimizer------if FLAGS.optimizer == 'Adam':optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate, beta1=0.9, beta2=0.999, epsilon=1e-8)elif FLAGS.optimizer == 'Adagrad':optimizer = tf.train.AdagradOptimizer(learning_rate=learning_rate, initial_accumulator_value=1e-8)elif FLAGS.optimizer == 'Momentum':optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.95)elif FLAGS.optimizer == 'ftrl':optimizer = tf.train.FtrlOptimizer(learning_rate)train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())# Provide an estimator spec for `ModeKeys.TRAIN` modesif mode == tf.estimator.ModeKeys.TRAIN:return tf.estimator.EstimatorSpec(mode=mode,predictions=predictions,loss=loss,train_op=train_op)- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
10.AFM: 對簡化版NFM進行加權求和
NFM的主要創新點是在FM過程中添加了逐元素相乘的運算來增加模型的復雜度。但沒有在此基礎上添加更復雜的運算過程,比如對加權求和。Jun Xiao等在2017年提出了注意力因子分解模型(Attentional Factorization Machine,AFM)就是在這個方向上的改進。其計算圖如下所示:?
?
AFM的特點是:
11.PNN:通過改進向量乘法運算延遲FM的實現過程
再回到FM。既然AFM、NFM可以通過添加逐元素乘法的運算來增加模型的復雜度,那向量乘法有這么多,可否用其他的方法增加FM復雜度?答案是可以的。Huifeng Guo等在2016年提出了基于向量積的神經網絡(Product-based Neural Networks,PNN)就是一個典型例子。其簡化計算圖如下所示:?
對比之前模型的計算圖,我們可以發現PNN的基本特點是:
?
此處分別附上PNN的內積與外積形式代碼,完整數據和代碼請戳這里。
class PNN1(Model):def __init__(self, field_sizes=None, embed_size=10, layer_sizes=None, layer_acts=None, drop_out=None,embed_l2=None, layer_l2=None, init_path=None, opt_algo='gd', learning_rate=1e-2, random_seed=None):Model.__init__(self)init_vars = []num_inputs = len(field_sizes)for i in range(num_inputs):init_vars.append(('embed_%d' % i, [field_sizes[i], embed_size], 'xavier', dtype))num_pairs = int(num_inputs * (num_inputs - 1) / 2)node_in = num_inputs * embed_size + num_pairs# node_in = num_inputs * (embed_size + num_inputs)for i in range(len(layer_sizes)):init_vars.append(('w%d' % i, [node_in, layer_sizes[i]], 'xavier', dtype))init_vars.append(('b%d' % i, [layer_sizes[i]], 'zero', dtype))node_in = layer_sizes[i]self.graph = tf.Graph()with self.graph.as_default():if random_seed is not None:tf.set_random_seed(random_seed)self.X = [tf.sparse_placeholder(dtype) for i in range(num_inputs)]self.y = tf.placeholder(dtype)self.keep_prob_train = 1 - np.array(drop_out)self.keep_prob_test = np.ones_like(drop_out)self.layer_keeps = tf.placeholder(dtype)self.vars = init_var_map(init_vars, init_path)w0 = [self.vars['embed_%d' % i] for i in range(num_inputs)]xw = tf.concat([tf.sparse_tensor_dense_matmul(self.X[i], w0[i]) for i in range(num_inputs)], 1)xw3d = tf.reshape(xw, [-1, num_inputs, embed_size])row = []col = []for i in range(num_inputs-1):for j in range(i+1, num_inputs):row.append(i)col.append(j)# batch * pair * kp = tf.transpose(# pair * batch * ktf.gather(# num * batch * ktf.transpose(xw3d, [1, 0, 2]),row),[1, 0, 2])# batch * pair * kq = tf.transpose(tf.gather(tf.transpose(xw3d, [1, 0, 2]),col),[1, 0, 2])p = tf.reshape(p, [-1, num_pairs, embed_size])q = tf.reshape(q, [-1, num_pairs, embed_size])ip = tf.reshape(tf.reduce_sum(p * q, [-1]), [-1, num_pairs])# simple but redundant# batch * n * 1 * k, batch * 1 * n * k# ip = tf.reshape(# tf.reduce_sum(# tf.expand_dims(xw3d, 2) *# tf.expand_dims(xw3d, 1),# 3),# [-1, num_inputs**2])l = tf.concat([xw, ip], 1)for i in range(len(layer_sizes)):wi = self.vars['w%d' % i]bi = self.vars['b%d' % i]l = tf.nn.dropout(activate(tf.matmul(l, wi) + bi,layer_acts[i]),self.layer_keeps[i])l = tf.squeeze(l)self.y_prob = tf.sigmoid(l)self.loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=l, labels=self.y))if layer_l2 is not None:self.loss += embed_l2 * tf.nn.l2_loss(xw)for i in range(len(layer_sizes)):wi = self.vars['w%d' % i]self.loss += layer_l2[i] * tf.nn.l2_loss(wi)self.optimizer = get_optimizer(opt_algo, learning_rate, self.loss)config = tf.ConfigProto()config.gpu_options.allow_growth = Trueself.sess = tf.Session(config=config)tf.global_variables_initializer().run(session=self.sess)- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
12.DCN:高階FM的降維實現
以上的FM推廣形式,主要是對FM進行二階特征組合。高階特征組合是通過MLP實現的。但這兩種實現方式是有很大不同的,FM更多是通過向量embedding之間的內積來實現,而MLP則是在向量embedding之后一層一層進行權重矩陣乘法實現。可否直接將FM的過程在高階特征組合上進行推廣?答案是可以的。Ruoxi Wang等在2017提出的深度與交叉神經網絡(Deep & Cross Network,DCN)就是在這個方向進行改進的。DCN的計算圖如下:?
DCN的特點如下:
?
13.Wide&Deep: DeepFM與DCN的基礎框架
開篇已經提到,本文思路有兩條主線。到此為止已經將基于FM的主線介紹基本完畢。接下來將串講從embedding+MLP自身的演進特點的CTR預估模型主線,而這條思路與我們之前的FM思路同樣有千絲萬縷的聯系。?
Google在2016年提出的寬度與深度模型(Wide&Deep)在深度學習CTR預估模型中占有非常重要的位置,它奠定了之后基于深度學習的廣告點擊率預估模型的框架。?
Wide&Deep將深度模型與線性模型進行聯合訓練,二者的結果求和輸出為最終點擊率。其計算圖如下:?
我們將Wide&Deep的計算圖與之前的模型進行對比可知:
?
此處附上DeepFM的代碼實現,完整數據和代碼請戳這里:
def get_model(model_type, model_dir):print("Model directory = %s" % model_dir)# 對checkpoint去做設定runconfig = tf.contrib.learn.RunConfig(save_checkpoints_secs=None,save_checkpoints_steps = 100,)m = None# 寬模型if model_type == 'WIDE':m = tf.contrib.learn.LinearClassifier(model_dir=model_dir,feature_columns=wide_columns)# 深度模型if model_type == 'DEEP':m = tf.contrib.learn.DNNClassifier(model_dir=model_dir,feature_columns=deep_columns,hidden_units=[100, 50, 25])# 寬度深度模型if model_type == 'WIDE_AND_DEEP':m = tf.contrib.learn.DNNLinearCombinedClassifier(model_dir=model_dir,linear_feature_columns=wide_columns,dnn_feature_columns=deep_columns,dnn_hidden_units=[100, 70, 50, 25],config=runconfig)print('estimator built')return m- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
14.Deep Cross: DCN由其殘差網絡思想進化
由K. He等提出的深度殘差網絡能夠大大加深神經網絡的深度,同時不會引起退化的問題,顯著提高了模型的精度。Ying Shan等將該思路應用到廣告點擊率預估模型中,提出深度交叉模型(DeepCross,2016DeepCross,2016)。Deep Cross的計算圖如下:?
將Deep Cross與之前的模型對比,可以發現其特點是:
?
15.DIN:對同領域歷史信息引入注意力機制的MLP
以上神經網絡對同領域離散特征的處理基本是將其嵌入后直接求和,這在一般情況下沒太大問題。但其實可以做得更加精細。比如對于歷史統計類特征。以用戶歷史瀏覽的商戶id為例,假設用戶歷史瀏覽了10個商戶,這些商戶id的常規處理方法是作為同一個領域的特征嵌入后直接求和得到一個嵌入向量。但這10個商戶只有一兩個商戶與當前被預測的廣告所在的商戶相似,其他商戶關系不大。增加這兩個商戶在求和過程中的權重,應該能夠更好地提高模型的表現力。而增加求和權重的思路就是典型的注意力機制思路。?
由?Bahdanau et al. (2015)?引入的現代注意力機制,本質上是加權平均(權重是模型根據數據學習出來的),其在機器翻譯上應用得非常成功。受注意力機制的啟發,Guorui Zhou等在2017年提出了深度興趣網絡(Deep Interest Network,DIN)。DIN主要關注用戶在同一領域的歷史行為特征,如瀏覽了多個商家、多個商品等。DIN可以對這些特征分配不同的權重進行求和。其網絡結構圖如下:?
?
16.多任務視角:信息的遷移與補充
對于數據驅動的解決方案而言,數據和模型同樣重要,數據(特征)通常決定了效果的上限,各式各樣的模型會以不同的方式去逼近這個上限。而所有算法應用的老司機都知道很多場景下,如果有更多的數據進行模型訓練,效果一般都能顯著得到提高。廣告也是一樣的場景,在很多電商的平臺上會有很多不同場景的廣告位,每個場景蘊含了用戶的不同興趣的表達,這些信息的匯總與融合可以帶來最后效果的提升。但是將不同場景的數據直接進行合并用來訓練(ctr/cvr)模型,結果很多時候并不是很樂觀,仔細想想也是合理的,不同場景下的樣本分布存在差異,直接對樣本累加會影響分布導致效果負向。?
而深度學習發展,使得信息的融合與應用有了更好的進展,用Multi?taskMulti?task?learning(MTL)learning(MTL)的方式可以很漂亮的解決上面提到的問題。我們不直接對樣本進行累加和訓練,而是像上圖所示,把兩個場景分為兩個task,即分為兩個子網絡。對單個網絡而言,底層的embedding層的表達受限于單場景的數據量,很可能學習不充分。而上圖這樣的網絡結合,使得整個訓練過程有了表示學習的共享(Shared Lookup Table),這種共享有助于大樣本的子任務幫助小樣本的子任務,使得底層的表達學習更加充分。?DeepFM和DCN也用到了這個思路!只是它們是對同一任務的不同模型進行結合,而多任務學習是對不同任務的不同模型進行結合。而且,我們可以玩得更加復雜。?
Multi-task learning(MTL)整個結構的上層的不同的task的子網絡是不一樣的,這樣每個子網絡可以各自去擬合自己task對應的概念分布。并且,取決于問題與場景的相似性和復雜度,可以把底層的表達學習,從簡單的共享embedding到共享一些層次的表達。極端的情況是我們可以直接共享所有的表達學習(representation learning)部分,而只接不同的網絡head來完成不一樣的任務。這樣帶來的另外一個好處是,不同的task可以共享一部分計算,從而實現計算的加速。?
值得一提的另一篇paper是阿里媽媽團隊提出的“完整空間多任務模型”(Entire Space Multi-Task Model,ESMM),也是很典型的多任務學習和信息補充思路,這篇paper解決的問題不是ctr(點擊率)預估而是cvr(轉化率)預估,傳統CVR預估模型會有比較明顯的樣本選擇偏差(sample selection bias)和訓練數據過于稀疏(data sparsity )的問題,而ESMM模型利用用戶行為序列數據,在完整的樣本數據空間同時學習點擊率和轉化率(post-view clickthrough&conversion rate,CTCVR),在一定程度上解決了這個問題。?
在電商的場景下,用戶的決策過程很可能是這樣的,在觀察到系統展現的推薦商品列表后,點擊自己感興趣的商品,進而產生購買行為。所以用戶行為遵循這樣一個決策順序:impression → click → conversion。CVR模型旨在預估用戶在觀察到曝光商品進而點擊到商品詳情頁之后購買此商品的概率,即pCVR = p(conversion|click,impression)。?
預估點擊率pCTR,預估點擊下單率pCVR和預估點擊與下單率pCTCVR關系如下。?
?
傳統的CVR預估任務通常采用類似于CTR預估的技術進行建模。但是不同于CTR預估任務的是,這個場景面臨一些特有的挑戰:1) 樣本選擇偏差;2) 訓練數據稀疏;3) 延遲反饋等。?
ESMM模型提出了下述的網絡結構進行問題建模?
EMMS的特點是:
?
注意到pCTR 和pCTCVR是在整個樣本空間上建模得到的,pCVR 只是一個中間變量。因此,ESMM模型是在整個樣本空間建模,而不像傳統CVR預估模型那樣只在點擊樣本空間建模。
17.各種模型的對比和總結
前面介紹了各種基于深度學習的廣告點擊率預估算法模型,針對不同的問題、基于不同的思路,不同的模型有各自的特點。各個模型具體關系比較如下表1所示:?
表 1. 各模型對比
?
| Wide&Deep | √ | × | × |
| FNN | √ | √ | × |
| DeepFM | √ | √ | × |
| NFM | √ | √ | × |
| DIN | √ | × | √ |
| AFM | × | √ | √ |
| Deep Cross | √ | × | × |
| DCN | √ | √ | × |
本文從開篇就說明這些模型推演的核心思路是“通過設計網絡結構進行組合特征的挖掘”,其在各個模型的實現方式如下:
當然,廣告點擊率預估深度學習模型還有很多,比如Jie Zhu提出的基于決策樹的神經網絡(Deep Embedding Forest)將深度學習與樹型模型結合起來。如果數據特征存在圖像或者大量文本相關特征,傳統的卷積神經網絡、循環神經網絡均可以結合到廣告點擊率預估的場景中。各個深度模型都有相應的特點,限于篇幅,我們就不再贅述了。
18.后記
目前深度學習的算法層出不窮,看論文確實有些應接不暇。我們的經驗有兩點:要有充分的生產實踐經驗,同時要有扎實的算法理論基礎。很多論文的亮點其實是來自于實際做工程的經驗。也辛虧筆者一直都在生產一線并帶領算法團隊進行工程研發(當然也因此荒廢了近2年的博客,T△T ),積淀了一些特征工程、模型訓練的經驗,才勉強跟得上新論文。比如DIN“對用戶側的某領域歷史特征基于廣告側的同領域特征進行加權求和”的思想,其實與傳統機器學習對強業務相關特征進行針對性特征組合的特征工程思路比較相似。另一方面,對深度學習的經典、前沿方法的熟悉也很重要。從前面我們的串講也能夠看出,CTR預估作為一個業務特點很強的場景,在應用深度學習的道路上,也充分借鑒了注意力機制、殘差網絡、聯合訓練、多任務學習等經典的深度學習方法。了解博主的朋友也知道我們一直推崇理論與實踐相結合的思路,我們自身對這條經驗也非常受用。當然,計算廣告是一個很深的領域,自己研究尚淺,串講難免存在紕漏。歡迎大家指出問題,共同交流學習。
參考文獻
J. Xiao, H. Ye, X. He, H. Zhang, F. Wu, and T.-S. Chua. Attentional factorization machines: Learning the weight of feature interactions via attention networks. In IJCAI, 2017.
Ying Shan, T Ryan Hoens, Jian Jiao, Haijing Wang, Dong Yu, and JC Mao. 2016. Deep Crossing: Web-Scale Modeling without Manually Cra ed Combinatorial Features. In Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining. ACM, 255–262.
總結
以上是生活随笔為你收集整理的从FM推演各深度学习CTR预估模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spark RDD使用详解5--Acti
- 下一篇: 程序员面试、算法研究、编程艺术、红黑树、