深度学习 - 37.TF x Keras Deep & Cross Network DCN 实现

本文介绍了深度交叉网络(DCN)模型,用于点击率(CTR)预测,强调了其在特征交叉学习上的优势。DCN通过叠加层实现特征的自动学习,避免手动特征工程,同时在模型精度和内存使用上优于传统算法。模型包括嵌入层、交叉网络和组合输出层,其中交叉网络通过多层权重矩阵或向量实现特征交互。文章还提供了DCN模型的TensorFlow实现代码,并展示了简单的训练过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一.引言

二.模型简介

1.Embedding and stacking layer

2.Cross Network

2.1 模型架构分析

2.2 计算逻辑

2.3 W 权重由 Vector 转换为 Matrix

3.Combination output layer

三.模型实践

1.DCN Model

2.代码测试

四.模型思考

1.多项式近似

2.FM 交叉泛化


一.引言

从前面 FM 家族的实现可以看出,特征工程已经成为 CTR 预估的关键,如何实现自动有效的特征交叉并不断挖掘数据潜在的组合成为模型预测效果的关键。DNN 能够自动学习特征交互;然而,它们隐式地生成所有交互,并不是学习所有交叉特征类型的有效方法。深度交叉网络(Deep & Cross Network, DCN) 通过叠加所有类型的输入来实现自动特征学习,同样可以在每一层应用特征交叉,不需要手动的特征工程,并且对 DNN 模型增加的额外复杂性可以忽略不计。实验结果表明,在CTR预测数据集和密集分类数据集上,该算法在模型精度和内存使用方面均优于现有算法。

二.模型简介

DCN 模型从嵌入层和叠加层开始,然后是并行的交叉网络和深度网络,最后将两个网络的输出组合通过 Sigmoid 输出 CTR 预估值。

1.Embedding and stacking layer

这一层主要负责原始的数据处理,将 Dense 特征与 Embedding 化的稀疏特征 Concat 起来,得到一个 None x FeatNum 的输入。

def get_deep_order(feat_index, args):
    embedding = tf.nn.embedding_lookup(args, feat_index)[:, :, :]
    embedding_flatten = layers.Flatten()(embedding)
    return embedding_flatten

这里假定 args 是 Field x EmbedingDim 的参数矩阵,feat_index 为命中的索引 id,首先 lookup 获取对应 embedding,如果是 multi_hot 特征,可以通过 lookup_sparse 将多个 embedding 合在一起,然后通过 Flatten 将所有 embedding concat 在一起,得到模型输入。

2.Cross Network

2.1 模型架构分析

第二层由 Cross Network 和 Deep Network 组成,这里 DNN 不再赘述了,主要分析下 Cross Network:

x_1=x_0x_0^Tw_{c,0}+b_{c,0}+x_0

广义表达式为:

x_{l+1}=x_0x_l^Tw_{l}+b_{l}+x_l=f(x_l,w_l,b_l) +x_l

Cross Network 通过一层一层叠加实现特征交叉方式,假设实现常见的 3 层叠加:

\\ x_1=x_0x_0^Tw_{0}+b_{0}+x_0 \\ x_2=x_0x_1^Tw_{1}+b_{1}+x_1 \\ x_3=x_0x_2^Tw_{2}+b_{2}+x_2

这里将 x_1 带入到 x_2 中看看:

x_2=w_0w_1x_0^2x_0^T+w_1x_0^2+w_0x_0x_0^T+(w_0+w_1+1)b_1+x_0+b_0

DNN 学到的更多的是隐式和高度非线性的特征,而 DCN 则类似多项式一样学习特征的交叉,持续的叠加 layer,多项式的项数也会越来越高。

2.2 计算逻辑

这里计算逻辑也比较清晰,首先叠加几层就会初始化几个 W 和 B:

- 输入 X0  None x Dim x 1

- 转置 xT 为  None x 1 x Dim

- 参数 W 为 Dim x 1

- 偏置 b 为 Dim x 1

则计算时维度变化为:

x_0x^Tw - None x Dim x 1 + b - Dim x 1 + x - None x Dim x 1 => None x Dim x 1

所以 Cross Network 一直叠加,但输入输出维度一直保持不变,即 None x Dim x 1。

def get_cross_net(input, layer_num, kernelType, kernels, bias):
    # None x dim => None x dim x 1
    x_0 = tf.expand_dims(input, axis=2)
    x_l = x_0

    if kernelType == "vector":
        for i in range(layer_num):
            # None x 32 x 1 => None x 1 x 32 x 32 x 1 => None x 1 x 1
            xl_w = tf.tensordot(x_l, kernels[i], axes=(1, 0))
            # x_0: None x 32 x 1 xl_w: None x 1 x 1
            dot_ = tf.matmul(x_0, xl_w)
            # dot: None x 32 x 1 + bias: 32 x 1
            x_l = dot_ + bias[i] + x_l

    re = tf.squeeze(x_l, axis=2)
    return re

这里代码实现中具体标识了维度,由于输入层的维度为 None x Dim,为了参与后续矩阵计算需要 expand_dim 在末尾添加一维,经过 layer_num 次累加后输出 None x Dim x 1,再调用 tf.squeeze 将最后的 1 去掉,得到一个 None x Dim 继续后续的计算。

2.3 W 权重由 Vector 转换为 Matrix

这里初始化了 Dim x 1 的 vector 权重,参考 FwFM 该权重也可以使用 matrix 即 dim x dim 的矩阵形式,具体的计算逻辑会稍有不同:

    elif kernelType == "matrix":
        for i in range(layer_num):
            # w: (dim, dim) x_l (None, dim, 1) => (None, dim, 1)
            xl_w = tf.einsum('ij,bjk->bik', kernels[i], x_l)  # W * xi (bs, dim, 1)
            dot_ = xl_w + bias[i]  # W * xi + b
            x_l = x_0 * dot_ + x_l  # x0 · (W * xi + b) +xl  Hadamard-product

由于矩阵维度为 dim x dim,所以最后一步计算修改为对位相乘即哈达玛积,最后输出的也是 None x Dim x 1,仍然需要使用 tf.squeeze 转换为 None x Dim 供后续计算使用。

3.Combination output layer

概率 P:

这一步主要将 Cross Network 输出的 None x Dim 与 DNN 输出的 None x Dim2 concat 起来然后输出到最终的 Sigmoid 作为预估概率值,和前面提到的 DeepFwFM、DeepFmFM 类似。

损失函数:

损失函数采用 Log Loss,较低的对数损失值意味着更好的预测,同时加入了 L2 正则化系数。

三.模型实践

1.DCN Model

#!/usr/bin/python
# -*- coding: UTF-8 -*-

from tensorflow.keras import layers, Model
from tensorflow.keras.layers import Layer, Activation
import tensorflow as tf
from tensorflow.python.keras.backend import relu
from GetTrainAndTestData import genSamples
from Handel import get_deep_order


def get_cross_net(input, layer_num, kernelType, kernels, bias):
    # None x dim => None x dim x 1
    x_0 = tf.expand_dims(input, axis=2)
    x_l = x_0

    if kernelType == "vector":
        for i in range(layer_num):
            # None x 32 x 1 => None x 1 x 32 x 32 x 1 => None x 1 x 1
            xl_w = tf.tensordot(x_l, kernels[i], axes=(1, 0))
            # x_0: None x 32 x 1 xl_w: None x 1 x 1
            dot_ = tf.matmul(x_0, xl_w)
            # dot: None x 32 x 1 + bias: 32 x 1
            x_l = dot_ + bias[i] + x_l
    elif kernelType == "matrix":
        for i in range(layer_num):
            # w: (dim, dim) x_l (None, dim, 1) => (None, dim, 1)
            xl_w = tf.einsum('ij,bjk->bik', kernels[i], x_l)  # W * xi (bs, dim, 1)
            dot_ = xl_w + bias[i]  # W * xi + b
            x_l = x_0 * dot_ + x_l  # x0 · (W * xi + b) +xl  Hadamard-product
    else:
        raise ValueError("kernelType should be 'vector' or 'matrix'")

    re = tf.squeeze(x_l, axis=2)
    return re


class DCN(Layer):

    def __init__(self, feature_num, embedding_dim, layer_num=3, dense1_dim=128, dense2_dim=64,
                 kernelType='matrix', **kwargs):
        self.feature_num = feature_num
        self.embedding_dim = embedding_dim
        self.dense1_dim = dense1_dim
        self.dense2_dim = dense2_dim
        self.activation = Activation(relu)

        # DCN 参数形式与层数
        self.kernelType = kernelType
        self.layer_num = layer_num

        super().__init__(**kwargs)

    # 定义模型初始化 根据特征数目
    def build(self, input_shape):
        # create a trainable weight variable for this layer
        dim = int(input_shape[-1]) * self.embedding_dim

        self.embedding = self.add_weight(name="embedding",
                                         shape=(self.feature_num, self.embedding_dim),
                                         initializer='he_normal',
                                         trainable=True)

        # DNN Dense1
        self.dense1 = self.add_weight(name='dense1',
                                      shape=(input_shape[1] * self.embedding_dim, self.dense1_dim),
                                      initializer='he_normal',
                                      trainable=True)

        # DNN Bias1
        self.bias1 = self.add_weight(name='bias1',
                                     shape=(self.dense1_dim,),
                                     initializer='he_normal',
                                     trainable=True)

        # DNN Dense2
        self.dense2 = self.add_weight(name='dense2',
                                      shape=(self.dense1_dim, self.dense2_dim),
                                      initializer='he_normal',
                                      trainable=True)

        # DNN Bias1
        self.bias2 = self.add_weight(name='bias2',
                                     shape=(self.dense2_dim,),
                                     initializer='he_normal',
                                     trainable=True)

        if self.kernelType == 'vector':
            self.kernels = [self.add_weight(name='kernel' + str(i),
                                            shape=(dim, 1),
                                            initializer="he_normal",
                                            trainable=True) for i in range(self.layer_num)]
        elif self.kernelType == 'matrix':
            self.kernels = [self.add_weight(name='kernel' + str(i),
                                            shape=(dim, dim),
                                            initializer="he_normal",
                                            trainable=True) for i in range(self.layer_num)]
        else:  # error
            raise ValueError("kernelType should be 'vector' or 'matrix'")
        self.bias = [self.add_weight(name='bias4dcn' + str(i),
                                     shape=(dim, 1),
                                     initializer="he_normal",
                                     trainable=True) for i in range(self.layer_num)]

        # Be sure to call this at the end
        super(DCN, self).build(input_shape)

    def call(self, inputs, **kwargs):

        # None x 32
        deep_order = get_deep_order(inputs, self.embedding)
        # Cross [None x (Filed x K) => None x (Field x K)]
        cross_order = get_cross_net(deep_order, self.layer_num, self.kernelType, self.kernels, self.bias)

        # Deep None x Dim2
        deep_order = self.activation(tf.matmul(deep_order, self.dense1) + self.bias1)
        deep_order = self.activation(tf.matmul(deep_order, self.dense2) + self.bias2)

        # Concat Cross + DNN
        concat_order = tf.concat([deep_order, cross_order], axis=-1)
        return concat_order

init 函数主要分两个方面参数,一方面是 DNN 隐层的参数,这里定义 128 和 64 的两个全连接层,另一方面是 DCN 的参数,主要是参数 kernel 核类型与 layer_num 即叠加次数。

2.代码测试

if __name__ == '__main__':
    train, labels = genSamples()

    # 构建模型
    input = layers.Input(shape=4, name='input', dtype='int32')
    model_layer = DCN(400, 8)(input)
    output = layers.Dense(1, activation='sigmoid')(model_layer)
    DCNModel = Model(input, output, name="DCN")

    # 模型编译
    DCNModel.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics='accuracy')
    DCNModel.summary()

    # 模型训练
    DCNModel.fit(train, labels, epochs=10, batch_size=128)

模型 Summary:

训练流程:

这里未加入正则化且使用简单随机构造的数据,具体场景大家可以更换数据与 DCN 参数进行适配。

Epoch 1/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6945 - accuracy: 0.5012
Epoch 2/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6926 - accuracy: 0.5160
Epoch 3/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6903 - accuracy: 0.5303
Epoch 4/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6866 - accuracy: 0.5472
Epoch 5/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6812 - accuracy: 0.5634
Epoch 6/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6742 - accuracy: 0.5809
Epoch 7/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6687 - accuracy: 0.5916
Epoch 8/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6638 - accuracy: 0.5983
Epoch 9/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6587 - accuracy: 0.6067
Epoch 10/10
469/469 [==============================] - 1s 2ms/step - loss: 0.6528 - accuracy: 0.6129

四.模型思考

1.多项式近似

通过 Weierstrass 逼近定理,在一定平滑性假设下,任何函数都可以用一个多项式逼近到任意精度。因此,我们从多项式逼近的角度来分析交叉网络。特别是,交叉网络以一种有效的、有表现力的和更广泛地推广到现实数据集的方式逼近相同程度的多项式类。就像我们前面写到的一样,随着迭代次数的加深,x0 的项数也越来越大:

x_2=w_0w_1x_0^2x_0^T+w_1x_0^2+w_0x_0x_0^T+(w_0+w_1+1)b_1+x_0+b_0

2.FM 交叉泛化

DCN Cross Network 继承了 FM 模型的参数共享精神,并进一步向更深层次的结构扩展。在 FM 模型中,特征 xi 与权值 vector vi 相关联,交叉项 wij 的权值由< vi, vj >计算。在DCN中,xi 与标量 w 相关联,xi xj 的权值是 w 参数的乘积。两个模型中每个特征都学习了一些独立于其他特征的参数,交叉项的权值是相应参数的一定组合。参数共享不仅提高了模型的效率,而且使模型能够泛化到看不见的特征交互,对噪声的鲁棒性更强。FM是一个浅结构,仅限于表示2次的交叉项。相比之下,DCN 可以构造所有的交叉项 x_1^{\alpha _1},x_2^{\alpha _2} ..., x_d^{\alpha _d},交叉阶 α 由 layer_num 决定。因此,交叉网络将参数共享的思想从单层扩展到多层和高度交叉项。注意,与高阶 FMs 不同,交叉网络中的参数数量仅随输入维数线性增长,而传统的 FFM 参数则出现指数增长的情况。

参考:

Deep & Cross Network for Ad Click Predictions

DCN V2: Improved Deep & Cross Network and Practical Lessons for Web-scale Learning to Rank Systems

更多推荐算法相关深度学习:深度学习导读专栏  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BIT_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值