Wide&Deep的进阶---Deep&Cross(DCN)模型理解与分析,附TF2.x复现

Deep & Cross Network for Ad Click Predictions

Deep&Cross模型是2017年由斯坦福大学和谷歌在ADKDD会议上联合提出的,该模型是对Wide&Deep模型的一种改进。由于Wide&Deep模型的Wide部分的特征交互需要特征工程,而手工设计特征工程非常的繁琐【虽然好的特征工程是在比赛中很多模型成功的关键因素之一】。2阶的FM模型在线性的时间复杂度中自动进行特征交互,但是这些特征交互的表现能力并不够,并且随着阶数的上升,模型复杂度会大幅度提高。所以作者对Wide部分进行更改,提出了一个Cross Network来自动进行特征之间的交叉,并且网络的时间和空间复杂度都是线性的。通过与Deep部分相结合,构成了深度交叉网络(Deep & Cross Network),简称DCN。

微信文章:

Deep & Cross

模型的结构非常简洁,从下往上依次为:Embedding和Stacking层、Cross网络层与Deep网络层并列、输出合并层,得到最终的预测结果。

Embedding and Stacking Layer

根据模型结构图我们发现,输入的特征分为密集型【连续】特征和稀疏离散型特征。但大部分特征都为分类的稀疏特征,为了输入到网络中,一般将进行one-hot编码操作,但这样会导致过高的维度特征空间。因此需要Embedding操作将高维稀疏特征转化为低维密集型特征:

\mathbf{x}_{embed, i}=W_{embed,i}\mathbf{x}_i \\

其中对于某一类稀疏分类特征(如id),\mathbf{x}_{embed, i}是第i个分类值(id序号)的embedding向量。W_{embed,i}\in \mathbb{R}^{n_e\times n_v}表示该类特征的embedding矩阵,n_e表示embedding的隐藏单元,n_v表示该类特征的数量(如id的总数)。x_i \in \mathbb{R}^{n_v \times 1}表示某个样本在该特征的二元稀疏向量(如id=1的one-hot向量)。【实质上就是在训练得到的Embedding参数矩阵中找到属于当前样本对应的Embedding向量】

---->Group-wise Embedding

其实觉大多数基于深度学习的推荐模型都需要Embedding操作,参数学习是通过神经网络进行训练

最后,该层需要将所有的密集型特征与通过embedding转换后的特征进行联合(Stacking):

x_0=[x_{embed,1}^T,x_{embed,2}^T,...,x_{embed,k}^T,x_{dense}^T] \\

Cross Network

这是本文最大的创新点---Cross网络(Cross Network),设计该网络的目的是增加特征之间的交互力度。交叉网络由多个交叉层组成,假设第l层交叉层的输出向量为\mathbf{x}_l ,那么对于第l+1层交叉层输出向量\mathbf{x}_{l+1}为:

\mathbf{x}_{l+1}=\mathbf{x}_0\mathbf{x}_{l}^T\mathbf{w}_l+\mathbf{b}_l+\mathbf{x}_l=f(\mathbf{x}_l,\mathbf{w}_l,\mathbf{b}_l)+\mathbf{x}_l \\

​ 其中\mathbf{x}_{l},\mathbf{x}_{l+1} \in \mathbb{R}^d是第ll+1交叉层的输出向量,\mathbf{w}_l,\mathbf{b}_l\in \mathbb{R}^d是权重参数和偏置。并且定义了一个映射函数f来拟合残差\mathbf{x}_{l+1}-\mathbf{x}_{l}【这里发现和残差网络的思想有些类似】。可视化结果如下:

其实文章最关键的部分是对Cross网络的一个分析,不过这部分的内容没怎么看懂,所以就通过一个实例进行一下分析:

举例:令x_0=[x_0^1,x_0^2]^Tb_i=0则:

\begin{array}{l}\\ \mathbf{x}_1 &=\mathbf{x}_0*\mathbf{x}_0^T*\mathbf{w}_0+\mathbf{x}_0\\ &=[x_0^1,x_0^2]^T*[x_0^1,x_0^2]*[w_0^1,w_0^2]+[x_0^1,x_0^2]^T\\ &=[w_0^1(x_0^1)^2+w_0^2x_0^1x_0^2+x_0^1,w_0^1x_0^1x_0^2+w_0^2(x_0^2)^2+x_0^2]^T\\ \mathbf{x}_2 &=\mathbf{x}_0*\mathbf{x}_1^T*\mathbf{w}_1+\mathbf{x}_1\\ &=[w_1^1x_0^1x_1^1+w_1^2x_0^1x_1^2+x_1^1,w_1^1x_0^2x_1^1+w_1^2x_0^2x_1^2+x_1^2]^T\\ &=[w_1^1x_0^1(w_0^1(x_0^1)^2+w_0^2x_0^1x_0^2+x_0^1)+w_1^2x_0^1(w_0^1x_0^1x_0^2+w_0^2(x_0^2)^2+x_0^2)+w_0^1(x_0^1)^2+w_0^2x_0^1x_0^2+x_0^1,......]^T \end{array} \\

观察交叉网络结构并结合上述例子,可以得到以下结论:

  1. \mathbf{x}_1中包含了包含了所有的\mathbf{x}_0的1、2阶特征交互,\mathbf{x}_2包含了所有\mathbf{x}_0,\mathbf{x}_1的1、2、3阶特征交互,那么\mathbf{x}_{l+1}包含了所有的\mathbf{x}_0,\mathbf{x}_1,...,\mathbf{x}_{l}1~l+2阶特征交互。因此,交叉网络层的叉乘阶数是有限的,l层特征对应的最高的叉乘阶数为l+1
  2. Cross网络的参数是共享的。
  3. 计算交叉网络的参数数量。假设交叉层的数量为L_c,特征x的维度为d,那么总共的参数数量为:

d\times L_c \times2 \\

  1. 并且交叉网络的时间和空间复杂度是线性的。相对于深度学习网络,交叉网络的复杂性可以忽略不计。

FM的泛化

关于第2点,Cross网络的参数共享,文章是将它与FM结合进行分析,认为Cross网络是FM的泛化形式:

  1. 在FM模型中,特征x_i的权重为v_i,那么特征交叉项x_ix_j 的权重应该为<v_i,v_j>。在DCN中,x_i的权重为\{W_K^{(i)}\}_{k=1}^l,交叉项x_ix_j的权重是参数\{W_K^{(i)}\}_{k=1}^l\{W_K^{(j)}\}_{k=1}^l的乘积。因此两个模型都各自学习了独立于其他特征的一些参数,并且交叉项的权重是相应参数的某种组合。
  2. 参数共享不仅使模型更有效,而且使模型能够泛化到看不见的特征交互作用,并且对噪声更具有鲁棒性。例如对于两个稀疏特征x_i,x_j,它们在数据中几乎从不发生交互,那么学习x_ix_j的权重对于预测没有任何的意义。
  3. FM只局限于阶为2的特征交互(其实FM可以扩展到n阶特征交互,但是复杂度太高了,一般只进行2阶特征交互),DCN可以构建高阶的特征交互,阶数由网络深度决定,并且交叉网络的参数只依据输入的维度线性增长。

Deep Network

该部分由一个全连接的神经网络构成,即MLP。

Combination Layer

将两个网络的输出进行拼接,并通过简单的Logistic回归完成最后的预测:

p=sigmoid([x_{L_1}^T,h_{L_2}^T]w_{logits}) \\

其中x_{L_1}^T,h_{L_2}^T表示交叉网络和深度网络的输出。

最后二元分类的代价函数为:

\operatorname{loss}=-\frac{1}{N} \sum_{i=1}^{N} y_{i} \log \left(p_{i}\right)+\left(1-y_{i}\right) \log \left(1-p_{i}\right)+\lambda \sum_{l}\left\|\mathbf{w}_{l}\right\|^{2} \\

代码复现

该模型的复现比较简单,关键在于Cross网络的构造,这个地方卡了比较长的时间,主要是Tensorflow中(None,1, d),(1,d)^T, (d,1)的内积相乘,后来参考了deepctr的代码,了解了tf.tensordot函数:

class CrossNetwork(Layer):
    """
    Cross Network
    """
    def __init__(self, layer_num):
        """
        :param layer_num: the deep of cross network
        """
        self.layer_num = layer_num

        super(CrossNetwork, self).__init__()

    def build(self, input_shape):
        dim = int(input_shape[-1])
        self.cross_weights = [
            self.add_weight(shape=(dim, 1),
                            initializer='random_uniform',
                            name='w_' + str(i))
            for i in range(self.layer_num)]
        self.cross_bias = [
            self.add_weight(shape=(dim, 1),
                            initializer='random_uniform',
                            name='b_'+str(i))
            for i in range(self.layer_num)]

    def call(self, inputs, **kwargs):
        x_0 = tf.expand_dims(inputs, axis=2)
        x_l = x_0
        for i in range(self.layer_num):
            x_l1 = tf.tensordot(x_l, self.cross_weights[i], axes=[1, 0])
            x_l = tf.matmul(x_0, x_l1) + self.cross_bias[i] + x_l
        x_l = tf.squeeze(x_l, axis=2)
        return x_l

整个模型:

class DCN(keras.Model):
    """
    Deep&Cross Network model
    """
    def __init__(self, feature_columns, hidden_units):
        """
        :param feature_columns: dense_feature_columns + sparse_feature_columns
        :param hidden_units: a list of neural network hidden units
        """
        super(DCN, self).__init__()
        self.dense_feature_columns, self.sparse_feature_columns = feature_columns
        self.layer_num = len(hidden_units)
        self.embed_layers = {
            'embed_' + str(i): Embedding(input_dim=feat['feat_num'], input_length=1,
                                             output_dim=feat['embed_dim'], embeddings_initializer='random_uniform')
            for i, feat in enumerate(self.sparse_feature_columns)
        }
        self.cross_network = CrossNetwork(self.layer_num)
        self.dnn_network = [Dense(units=unit, activation='relu') for unit in hidden_units]
        self.concat = Concatenate(axis=-1)
        self.dense_final = Dense(1)

    def call(self, inputs):
        dense_inputs, sparse_inputs = inputs
        x = dense_inputs
        for i in range(sparse_inputs.shape[1]):
            embed_i = self.embed_layers['embed_{}'.format(i)](sparse_inputs[:, i])
            x = tf.concat([x, embed_i], axis=-1)
        cross_x = self.cross_network(x)
        dnn_x = x
        for dense in self.dnn_network:
            dnn_x = dense(dnn_x)
        x = self.concat([cross_x, dnn_x])
        outputs = tf.nn.sigmoid(self.dense_final(x))
        return outputs

模型的输入分为:密集型特征和稀疏型特征。

具体代码:

总结

DCN建立在Wide&Deep模型的基础上,对Wide部分进行了修改,构造了能够自动进行特征交叉的Cross网络,提高了特征交互的能力

编辑于 2021-01-03 14:04