以前總結了PNN,NFM,AFM這類兩兩向量乘積的方式,這一節咱們換新的思路來看特徵交互。DeepCrossing是最先在CTR模型中使用ResNet的前輩,DCN在ResNet上進一步創新,爲高階特徵交互提供了新的方法並支持任意階數的特徵交叉。html
如下代碼針對Dense輸入更容易理解模型結構,針對spare輸入的代碼和完整代碼 👇
https://github.com/DSXiangLi/CTRpython
Deep Crossing結構比較簡單,和最原始的Embedding+MLP的模型結果相比,差別在於以後跟的不是全鏈接層而是殘差層。模型結構以下git
簡單說說殘差網絡,基本的網絡結構以下github
殘差網絡解決了什麼,爲何有效?這篇博客講得很清楚,核心是解決網絡退化的問題,既隨着網絡深度增長,網絡的表現先是逐漸增長至飽和,而後迅速降低。這裏的降低並不是指過擬合。理論上若是20層的網絡是最優解,那30層的網絡會包含20層的網絡,後面10層只需作恆等映射\(a^{l} = a^{l-1}\)便可,所以更多懷疑是MLP不易擬合恆等映射。而上述殘差網絡由於作了identity mapping,當\(F(a^{l-1}, w^l)=0\)時,就直接沿用上一層數據也就是進行了恆等變換。網絡
那把ResNet放到CTR模型裏又有什麼特殊的優點呢?老實說感受像是把那個時期比較牛的框架直接拿來用。。。不過能想到的一種是MLP學習的是高階泛化特徵,而ResNet作的identity mapping會保留更多的原始低階特徵信息,有點相似Wide&Deep又不徹底是,由於輸入已是Embedding而不是原始的離散特徵了。真棒又強行解釋了一波。。。app
def residual_layer(x0, unit, dropout_rate, batch_norm, mode): # f(x): input_size -> unit -> input_size # output = relu(f(x) + x) input_size = x0.get_shape().as_list()[-1] # input_size -> unit x1 = tf.layers.dense(x0, units = unit, activation = 'relu') if batch_norm: x1 = tf.layers.batch_normalization( x1, center=True, scale=True, trainable=True, training=(mode == tf.estimator.ModeKeys.TRAIN) ) if dropout_rate > 0: x1 = tf.layers.dropout( x1, rate=dropout_rate, training=(mode == tf.estimator.ModeKeys.TRAIN) ) # unit -> input_size x2 = tf.layers.dense(x1, units = input_size ) # stack with original input and apply relu output = tf.nn.relu(tf.add(x2, x0)) return output @tf_estimator_model def model_fn(features, labels, mode, params): dense_feature = build_features() dense = tf.feature_column.input_layer(features, dense_feature) # stacked residual layer with tf.variable_scope('Residual_layers'): for i, unit in enumerate(params['hidden_units']): dense = residual_layer( dense, unit, dropout_rate = params['dropout_rate'], batch_norm = params['batch_norm'], mode = mode) add_layer_summary('residual_layer{}'.format(i), dense) with tf.variable_scope('output'): y = tf.layers.dense(dense, units=1) add_layer_summary( 'output', y ) return y
Deep&Cross帶着Wide&Deep的風格,在保留全聯接的Deep部分的同時,Deep&Cross借鑑了上述ResNet的思路,創新了顯式的高階特徵交互方式。以前的模型要麼像DeepFM直接依賴全鏈接層來捕捉高階特徵交互,要麼像PNN,NFM,AFM先基於向量兩兩作內/外/element-wise乘積學習二階交互特徵,再依賴全聯接層來學習更高階的交互信息。兩兩交互式的方法很難擴展到更高階,由於會存在維度爆炸的問題。框架
DCN的輸入是Embedding和連續特徵拼接而成的Dense輸入,由於不像PNN,AFM等須要兩兩向量內積,所以對每一個特徵Embedding的維度是否一致沒有要求,而後Cross部分和Deep部分共享輸入,進行聯合訓練,最終把兩個part進行拼接後預測ctr。模型結構以下dom
Deep部分沒啥好說的和DeepFM,Wide&Deep同樣就是多個全聯接層用來學習泛化特徵。Cross部分由多層的cross_layer組成,輸入有N個特徵,爲簡化Embedding維度統一是爲K,每層cross_layer的計算以下ide
1. 特徵共享:控制複雜度
特徵共享的存在,保證了Cross每增長一層,新增的參數都是\(O(NK)\)學習
FM視角(式4): FM是每一個離散特徵共享一個隱向量v,向量交互的權重爲隱向量內積,但這種操做只侷限於兩兩交互。而Cross是Embedding的每個元素和其他全部元素交互時共享一個權重w。(這裏感受cross直接用原始的one-hot也是能夠的,只不過用Embedding能夠進一步下降複雜度)
OPNN視角(式5): OPNN兩兩向量作外積獲得\(N^2\)個\(K^2\)外積矩陣,拼在一塊兒其實就是Cross不區分Field直接作外積獲得的大外積矩陣。不過不像OPNN採用簡單粗暴的sum_pooling來解決維度爆炸的問題,Cross採用每行共享一個權重的方式來降維。保留更多信息的同時保證了Cross-layer的複雜度不會隨層數上升而上升, 每層的維度都是最初的\(NK\), 複雜度也是\(O(NK)\)
2. 多項式內核:任意階數特徵交互
爲簡化咱們先忽略截距項,看下兩層的cross-layer
會發現ResNet加上cross,相似於對輸入向量進行了多項式計算,Cross的部分每深一層,就能夠捕捉更高一階的特徵交互信息。所以高級特徵交互信息的捕捉再也不簡單依賴MLP而是人爲可控。同時ResNet的存在也保證了不會隨着Cross的加深而致使模型過於泛化,由於最初的輸入特徵始終保留。
DCN已經很優秀,只能想到能夠吐槽的點
在上面參數共享討論的兩種視角,恰好對應到cross layer的兩種計算方式。按照原始順序Embedding先作外積再加權求和(特徵共享中的OPNN視角),會須要存儲巨大的臨時矩陣,代碼以下
def cross_op_raw(xl, x0, weight, feature_size): # (x0 * xl) * w # (batch,feature_size) - > (batch, feature_size * feature_size) outer_product = tf.matmul(tf.reshape(x0, [-1, feature_size,1]), tf.reshape(xl, [-1, 1, feature_size]) ) # (batch,feature_size*feature_size) ->(batch, feature_size) interaction = tf.tensordot(outer_product, weight, axes=1) return interaction
而經過調整向量乘積的順序\((x_0 * x_l) *w \to x_0 * (x_l * w)\)咱們能夠避免外積矩陣的運算(特徵共享中的FM視角),也就是paper中提到的利用\(x_0x_l^T\)是秩爲1的矩陣特性。
def cross_op_better(xl, x0, weight, feature_size): # x0 * (xl * w) # (batch, 1, feature_size) * (feature_size) -> (batch,1) transform = tf.tensordot( tf.reshape( xl, [-1, 1, feature_size] ), weight, axes=1 ) # (batch, feature_size) * (batch, 1) -> (batch, feature_size) interaction = tf.multiply( x0, transform ) return interaction
完整代碼以下
def cross_layer(x0, cross_layers, cross_op = 'better'): xl = x0 if cross_op == 'better': cross_func = cross_op_better else: cross_func = cross_op_raw with tf.variable_scope( 'cross_layer' ): feature_size = x0.get_shape().as_list()[-1] # feature_size = n_feature * embedding_size for i in range( cross_layers): weight = tf.get_variable( shape=[feature_size], initializer=tf.truncated_normal_initializer(), name='cross_weight{}'.format( i ) ) bias = tf.get_variable( shape=[feature_size], initializer=tf.truncated_normal_initializer(), name='cross_bias{}'.format( i ) ) interaction = cross_func(xl, x0, weight, feature_size) xl = interaction + bias + xl # add back original input -> (batch, feature_size) add_layer_summary( 'cross_{}'.format( i ), xl ) return xl @tf_estimator_model def model_fn_dense(features, labels, mode, params): dense_feature = build_features() dense_input = tf.feature_column.input_layer(features, dense_feature) # deep part dense = stack_dense_layer(dense_input, params['hidden_units'], params['dropout_rate'], params['batch_norm'], mode, add_summary = True) # cross part xl = cross_layer(dense_input, params['cross_layers'], params['cross_op']) with tf.variable_scope('stack'): x_stack = tf.concat( [dense, xl], axis=1 ) with tf.variable_scope('output'): y = tf.layers.dense(x_stack, units =1) add_layer_summary( 'output', y ) return y
https://github.com/DSXiangLi/CTR
CTR學習筆記&代碼實現1-深度學習的前奏LR->FFM
CTR學習筆記&代碼實現2-深度ctr模型 MLP->Wide&Deep
CTR學習筆記&代碼實現3-深度ctr模型 FNN->PNN->DeepFM
CTR學習筆記&代碼實現4-深度ctr模型 NFM/AFM
資料