继续创作,加速生长!这是我参加「日新方案 10 月更文应战」的第1天,点击检查活动详情
之前总结了PNN,NFM,AFM这类两两向量乘积的方式,这一节咱们换新的思路来看特征交互。DeepCrossing是最早在CTR模型中运用ResNet的长辈,DCN在ResNet进步一步立异,为高阶特征交互提供了新的方法并支撑恣意阶数的特征交叉。
以下代码针对Dense输入更容易理解模型结构,针对spare输入的代码和完好代码 github.com/DSXiangLi/C…
Deep Crossing
Deep Crossing结构比较简略,和最原始的Embedding+MLP的模型结果相比,差异在于之后跟的不是全衔接层而是残差层。模型结构如下
简略说说残差网络,基本的网络结构如下
残差网络处理了什么,为什么有用?这篇博客讲得很清楚,核心是处理网络退化的问题,既跟着网络深度添加,网络的表现先是逐步添加至饱和,然后敏捷下降。这儿的下降并非指过拟合。理论上如果20层的网络是最优解,那30层的网络会包括20层的网络,后面10层只需做恒等映射al=al−1a^{l} = a^{l-1}即可,因而更多怀疑是MLP不易拟合恒等映射。而上述残差网络由于做了identity mapping,当F(al−1,wl)=0F(a^{l-1}, w^l)=0时,就直接沿用上一层数据也便是进行了恒等改换。
那把ResNet放到CTR模型里又有什么特别的优势呢?老实说感觉像是把那个时期比较牛的结构直接拿来用。。。不过能想到的一种是MLP学习的是高阶泛化特征,而ResNet做的identity mapping会保留更多的原始低阶特征信息,有点相似Wide&Deep又不完全是,由于输入已经是Embedding而不是原始的离散特征了。真棒又强行解说了一波。。。
代码实现
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
Deep&Cross带着Wide&Deep的风格,在保留全联接的Deep部分的同时,Deep&Cross学习了上述ResNet的思路,立异了显式的高阶特征交互方式。之前的模型要么像DeepFM直接依靠全衔接层来捕捉高阶特征交互,要么像PNN,NFM,AFM先根据向量两两做内/外/element-wise乘积学习二阶交互特征,再依靠全联接层来学习更高阶的交互信息。两两交互式的方法很难扩展到更高阶,由于会存在维度爆炸的问题。
模型细节
DCN的输入是Embedding和接连特征拼接而成的Dense输入,由于不像PNN,AFM等需要两两向量内积,因而对每个特征Embedding的维度是否共同没有要求,然后Cross部分和Deep部分同享输入,进行联合训练,最终把两个part进行拼接后预测ctr。模型结构如下
Deep部分没啥好说的和DeepFM,Wide&Deep相同便是多个全联接层用来学习泛化特征。Cross部分由多层的cross_layer组成,输入有N个特征,为简化Embedding维度统一是为K,每层cross_layer的计算如下
1. 特征同享:操控复杂度 特征同享的存在,确保了Cross每添加一层,新增的参数都是O(NK)O(NK)
-
FM视角(式4): FM是每个离散特征同享一个隐向量v,向量交互的权重为隐向量内积,但这种操作只局限于两两交互。而Cross是Embedding的每一个元素和其余所有元素交互时同享一个权重w。(这儿感觉cross直接用原始的one-hot也是能够的,只不过用Embedding能够进一步降低复杂度)
-
OPNN视角(式5): OPNN两两向量做外积得到N2N^2个K2K^2外积矩阵,拼在一起其实便是Cross不区别Field直接做外积得到的大外积矩阵。不过不像OPNN选用简略粗暴的sum_pooling来处理维度爆炸的问题,Cross选用每行同享一个权重的方式来降维。保留更多信息的同时确保了Cross-layer的复杂度不会随层数上升而上升, 每层的维度都是最初的NKNK, 复杂度也是O(NK)O(NK)
2. 多项式内核:恣意阶数特征交互 为简化咱们先忽略截距项,看下两层的cross-layer
会发现ResNet加上cross,相似于对输入向量进行了多项式计算,Cross的部分每深一层,就能够捕捉更高一阶的特征交互信息。因而高档特征交互信息的捕捉不再简略依靠MLP而是人为可控。同时ResNet的存在也确保了不会跟着Cross的加深而导致模型过于泛化,由于最初的输入特征一直保留。
DCN已经很优秀,只能想到能够吐槽的点
- 对记忆信息的学习或许会有缺乏,虽然有ResNet但输入已经是Embedding特征,多少已经是泛化后的特征表达,不知道再参加Wide部分是不是会有提高。
代码实现
在上面参数同享评论的两种视角,刚好对应到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
而通过调整向量乘积的顺序(x0∗xl)∗w→x0∗(xl∗w)(x_0 * x_l) *w \to x_0 * (x_l * w)咱们能够防止外积矩阵的运算(特征同享中的FM视角),也便是paper中说到的利用x0xlTx_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
材料
- Gang Fu,Mingliang Wang, 2017, Deep & Cross Network for Ad Click Predictions
- Ying Shan, T. Ryan Hoens, 2016, Deep Crossing: Web-Scale Modeling without Manually Crafted Combinatorial Features
- blog.csdn.net/Dby_freedom…
- zhuanlan.zhihu.com/p/80226180