个性化排序算法实践(五)——DCN算法

wide&deep在个性化排序算法中是影响力比较大的工作了。wide部分是手动特征交叉(负责memorization),deep部分利用mlp来实现高阶特征交叉(负责generalization),wide部分和deep部分joint train。

Deep&Cross Network模型我们下面将简称DCN模型,对比Wide & Deep ,不需要特征工程来获得高阶的交叉特征。对比 FM 系列的模型,DCN 拥有更高的计算效率并且能够提取到更高阶的交叉特征。

一个DCN模型从嵌入和堆积层开始,接着是一个交叉网络和一个与之平行的深度网络,之后是最后的组合层,它结合了两个网络的输出。完整的网络模型如图:

DCN模型

从网络结构上面来看,该模型是非常简单明了的,特征分为类别型与数值型,类别型特征经过 embedding 之后与数值型特征直接拼接作为模型的输入。所有的特征分别经过 cross 和 deep 网络,如果把这两个网络看作特征提取的话,经过提取后的特征向量拼接之后是常规的二分类,如果训练数据是曝光和点击,最后输出的就可以看作点击率了。

  • 离散特征嵌入
    离散特征嵌入这个想法最初来自于 Mikolov 的 word2vec 系列文章。最初解决的问题是词的独热表示过于稀疏,并且不同词之间的向量形式表示完全没有联系。具体思路在此不赘述,最终的实现是将一个上万维的词独热表示嵌入到了只有几百维的稠密向量中。而嵌入的本质其实是构建一张随机初始化的向量查找表,通过我们的训练目标做有监督学习来得到不同词在特定目标下,处于向量空间中的位置。
    将词嵌入的思路推广到其它的离散特征处理中,我们可以用同样的方法将各种类别特征如“用户性别”、“城市”、“日期”嵌入到稠密的向量空间中。经过这样处理之后,自然就解决了原本 FM 遇到的特征稀疏问题。

  • 高阶交叉特征
    在广告场景下,特征交叉的组合与点击率是有显著相关的,例如,“USA”与“Thanksgiving”、“China”与“Chinese New Year”这样的关联特征,对用户的点击有着正向的影响。
    而本文开发了一个新的算子,来得到交叉特征:

DCN

即,
\[ x_{l+1} = x_0 x_l^T w_l + b_l + x_l = f(x_l,w_l,b_l)+x_l \]
考虑 \(x₀\) 为输入的特征及第一层的输入,\(x\) 为 第 \(L\) 层的输入,我们可以看到它的基本思路还是用矩阵乘法来实现特征的组合。

这是个递推形式算子,所以使用它很容易能得到高于二阶的交叉特征;并且该模型还用了残差的思想,解决网络性能退化的问题;此公式还有一个小的优化技巧,三矩阵相乘那个算子,用乘法结合律先计算后面两个矩阵的积,这样可以减少三分之一的计算复杂度。

DCN实现

参考个性化排序算法实践(三)——deepFM算法,算法主框架与其类似,重点讲述DCN的实现步骤。

模型输入

模型的输入主要有下面几个部分:

self.feat_index = tf.placeholder(tf.int32,
                                 shape=[None,None],
                                 name='feat_index')
self.feat_value = tf.placeholder(tf.float32,
                               shape=[None,None],
                               name='feat_value')

self.numeric_value = tf.placeholder(tf.float32,[None,None],name='num_value')

self.label = tf.placeholder(tf.float32,shape=[None,1],name='label')
self.dropout_keep_deep = tf.placeholder(tf.float32,shape=[None],name='dropout_deep_deep')

可以看到,这里与DeepFM相比,一个明显的变化是将离散特征和连续特征分开,连续特征不在转换成embedding进行输入,所以我们的输入共有五部分。
feat_index是离散特征的一个序号,主要用于通过embedding_lookup选择我们的embedding。feat_value是对应离散特征的特征值。numeric_value是我们的连续特征值。label是实际值。还定义了两个dropout来防止过拟合。

权重构建

权重主要包含四部分,embedding层的权重,cross network中的权重,deep network中的权重以及最后链接层的权重,我们使用一个字典来表示:

def _initialize_weights(self):
    weights = dict()

    #embeddings
    weights['feature_embeddings'] = tf.Variable(
        tf.random_normal([self.cate_feature_size,self.embedding_size],0.0,0.01),
        name='feature_embeddings')
    weights['feature_bias'] = tf.Variable(tf.random_normal([self.cate_feature_size,1],0.0,1.0),name='feature_bias')


    #deep layers
    num_layer = len(self.deep_layers)
    glorot = np.sqrt(2.0/(self.total_size + self.deep_layers[0]))

    weights['deep_layer_0'] = tf.Variable(
        np.random.normal(loc=0,scale=glorot,size=(self.total_size,self.deep_layers[0])),dtype=np.float32
    )
    weights['deep_bias_0'] = tf.Variable(
        np.random.normal(loc=0,scale=glorot,size=(1,self.deep_layers[0])),dtype=np.float32
    )


    for i in range(1,num_layer):
        glorot = np.sqrt(2.0 / (self.deep_layers[i - 1] + self.deep_layers[i]))
        weights["deep_layer_%d" % i] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i - 1], self.deep_layers[i])),
            dtype=np.float32)  # layers[i-1] * layers[i]
        weights["deep_bias_%d" % i] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])),
            dtype=np.float32)  # 1 * layer[i]

    for i in range(self.cross_layer_num):

        weights["cross_layer_%d" % i] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(self.total_size,1)),
            dtype=np.float32)
        weights["cross_bias_%d" % i] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(self.total_size,1)),
            dtype=np.float32)  # 1 * layer[i]

    # final concat projection layer
    input_size = self.total_size + self.deep_layers[-1]

    glorot = np.sqrt(2.0/(input_size + 1))
    weights['concat_projection'] = tf.Variable(np.random.normal(loc=0,scale=glorot,size=(input_size,1)),dtype=np.float32)
    weights['concat_bias'] = tf.Variable(tf.constant(0.01),dtype=np.float32)

    return weights

计算网络输入

这一块我们要计算两个并行网络的输入\(X_0\),我们需要将离散特征转换成embedding,同时拼接上连续特征:

# model
self.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index) # N * F * K
feat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1])
self.embeddings = tf.multiply(self.embeddings,feat_value)

self.x0 = tf.concat([self.numeric_value,
                     tf.reshape(self.embeddings,shape=[-1,self.field_size * self.embedding_size])]
                    ,axis=1)

Cross Network

根据论文中的计算公式,一步步计算得到cross network的输出:

# cross_part
self._x0 = tf.reshape(self.x0, (-1, self.total_size, 1))
x_l = self._x0
for l in range(self.cross_layer_num):
    #x_l = tf.tensordot(tf.matmul(self._x0, x_l, transpose_b=True),self.weights["cross_layer_%d" % l],1) + self.weights["cross_bias_%d" % l] + x_l
    # 注意计算顺序,可以加速很多
    x_l = tf.tensordot(tf.reshape(x_l, [-1, 1, self.total_size]), self.weights["cross_layer_%d" % l], 1) * self._x0 + self.weights["cross_bias_%d" % l] + x_l

self.cross_network_out = tf.reshape(x_l, (-1, self.total_size))

Deep Network

这一块就是一个多层全链接神经网络:

self.y_deep = tf.nn.dropout(self.x0,self.dropout_keep_deep[0])

for i in range(0,len(self.deep_layers)):
    self.y_deep = tf.add(tf.matmul(self.y_deep,self.weights["deep_layer_%d" %i]), self.weights["deep_bias_%d"%I])
    self.y_deep = self.deep_layers_activation(self.y_deep)
    self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[i+1])

Combination Layer

最后将两个网络的输出拼接起来,经过一层全链接得到最终的输出:

# concat_part
concat_input = tf.concat([self.cross_network_out, self.y_deep], axis=1)

self.out = tf.add(tf.matmul(concat_input,self.weights['concat_projection']),self.weights['concat_bias'])

定义损失

这里我们可以选择logloss或者mse,并加上L2正则项:

# loss
if self.loss_type == "logloss":
    self.out = tf.nn.sigmoid(self.out)
    self.loss = tf.losses.log_loss(self.label, self.out)
elif self.loss_type == "mse":
    self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))
# l2 regularization on weights
if self.l2_reg > 0:
    self.loss += tf.contrib.layers.l2_regularizer(
        self.l2_reg)(self.weights["concat_projection"])
    for i in range(len(self.deep_layers)):
        self.loss += tf.contrib.layers.l2_regularizer(
            self.l2_reg)(self.weights["deep_layer_%d" % I])
    for i in range(self.cross_layer_num):
        self.loss += tf.contrib.layers.l2_regularizer(
            self.l2_reg)(self.weights["cross_layer_%d" % I])

DCN和同场景模型对比

在deepFM中, 进行了离散特征嵌入的操作,并且还将嵌入前的离散特征加入到了 FM 层;所以该网络可以看作是传统的 FM 、离散特征嵌入之后的 FM 和基本 DNN 三个模型融合的结果。
wide & deep 的思路中,deep 部分的做法和 deepFM 是大相径庭的,关键的 wide 部分其实是离线的特征工程,根据业务场景提前完成了特征交叉等处理,该模型可以看作是 DNN 与离线特征模型的融合结果。
而从 DCN 的网络中我们可以发现,deep 部分网络除了使用离散嵌入特征外,还拼接了数值型特征;cross 部分网络直接完成了特征组合,对比 FM 层它可以学到更高阶的组合特征,对比 wide 网络它不需要做线下的特征工程。

参考:
深度排序模型概述(一)Wide&Deep/xDeepFM
推荐系统遇上深度学习(五)--Deep&Cross Network模型
Github

你可能感兴趣的:(个性化排序算法实践(五)——DCN算法)