DCN:深度交叉网络与显式高阶特征交互

FreeGuideOnline 最新 2026-06-24

引言:为什么需要特征交叉?

在推荐系统与 CTR(点击率)预估任务中,模型的核心能力之一是从原始特征中自动学习有意义的特征交互。传统线性模型(如逻辑回归)依赖于人工特征工程来构造交叉特征。而深度学习模型虽然能够通过多层全连接网络(DNN)隐式地学习高阶特征交互,但这种交互往往是隐式低效的——模型需要大量参数和数据才能捕捉到乘法关系,且无法保证一定学到真正的高阶交叉。

2017 年,Google 提出了 DCN(Deep & Cross Network),在经典的 Wide & Deep 架构基础上,引入了一个全新的 Cross Network(交叉网络),能够以显式可控参数高效的方式学习有限阶的高阶特征交互。本教程将从原理、数学推导、代码实现到调参技巧,带你全面掌握 DCN 及其升级版本 DCN-V2。


一、DCN 核心构思:显式高阶特征交互

1.1 从 Wide & Deep 到 DCN

Wide & Deep 模型的“Wide”部分是一个广义线性模型,需要人工设计交叉特征(如“安装了应用 A 且年龄在 18-25 之间”)。DNN 部分则自动学习隐式交叉。这种混合架构的痛点在于:

  • Wide 部分高度依赖特征工程,无法扩展到没有领域知识的新特征。
  • DNN 部分学习的交叉函数可能过于复杂且难以解释。

DCN 用一个 Cross Network 替代了人工构造的 Wide 部分,该网络能够自动生成有限阶的显式交叉特征,并与并行的 DNN 结合,形成 Deep & Cross Network。其关键创新在于:交叉网络以输入特征的显式叉乘(multiplicative)形式逐层叠加,每一层都显式地学习一个 $x_0 \cdot x_l^T w$ 的残差项,从而保证模型的交叉阶数随层数严格递增。

1.2 Cross Network 的核心特征

  • 显式交叉:每层输出都包含原始输入 $x_0$ 与当前层输入 $x_l$ 的外积,叉乘操作是显式的。
  • 阶数可控:一个具有 $L$ 层的交叉网络,其输出最高包含 $L+1$ 阶的特征交叉。
  • 参数高效:交叉网络的参数量仅为 $2 \times d \times L$($d$ 为输入维度,$L$ 为层数),远少于同等表达能力的 DNN。
  • 保留线性关系:交叉网络的输出可以看作是 $x_0$ 的一个线性函数,其权重由交叉层的非线性变换动态决定。

二、交叉网络的数学原理

2.1 单层交叉运算

设输入向量 $x_0 \in \mathbb{R}^d$ 是所有嵌入拼接后的向量。第 $l$ 层交叉层的公式为:

$$ x_{l+1} = x_0 \cdot (x_l^T w_l) + b_l + x_l $$

其中:

  • $w_l, b_l \in \mathbb{R}^d$ 是第 $l$ 层的权重和偏置参数。
  • $x_0 \cdot (x_l^T w_l)$ 表示标量 $x_l^T w_l$ 乘以向量 $x_0$,本质上是 $x_0$ 与 $x_l$ 的某种双线性交互
  • $+ x_l$ 是残差连接,确保每一层在学习新交叉的同时保留已有信息。

关键解释:$x_l^T w_l$ 把 $x_l$ 投影为一个标量“门控值”,然后用这个值缩放原始输入 $x_0$。因为 $x_0$ 包含了所有一阶特征,所以这一操作等价于将 $x_0$ 中的每个元素与投影后的 $x_l$ 进行乘法交互,产生二阶项;随着层数叠加,会形成更高阶的交叉。

2.2 逐层展开与高阶特性

假设忽略偏置 $b_l$(其作用可被后续层吸收),展开前两层:

  • 初始:$x_0$
  • 第 1 层:$x_1 = x_0 (x_0^T w_0) + x_0 = x_0 (x_0^T w_0 + 1)$
    此时 $x_1$ 包含 $x_0$ 的二阶项(因为 $x_0 \cdot x_0^T$ 产生二阶交叉)。
  • 第 2 层:$x_2 = x_0 (x_1^T w_1) + x_1 = x_0 ((x_0 (x_0^T w_0 + 1))^T w_1) + x_1$
    可以产生 $x_0^3$ 的三阶交叉。

严格的数学归纳可证明:第 $l$ 层的输出 $x_l$ 是 $x_0$ 的 $l+1$ 阶多项式函数。这正是 DCN 能够显式学习有界阶数特征交叉的数学保证。

2.3 与全连接层的对比

一个标准的全连接层 $h_{l+1} = f(W_l h_l + b_l)$ 通过非线性激活函数可以近似任意函数,包括特征叉乘。但要学到 $x_i \cdot x_j$ 这样的乘法关系,DNN 往往需要大量参数将它们映射到隐空间中才能近似。Cross Network 用结构化的设计强制了乘法交互,从而在参数量极少的情况下直接建模二阶、三阶等有限阶交叉,且交叉形式可解释:最终输出可以分解为多个交叉项的线性组合。


三、DCN 的整体模型架构

一个完整的 DCN 模型由四部分组成:

  1. 嵌入层:将高维稀疏类别特征映射为低维稠密向量,并与数值特征拼接,形成 $x_0$。
  2. Cross Network:从 $x_0$ 开始,逐层应用交叉层,输出 $x_{L_c}$($L_c$ 为交叉层数)。
  3. Deep Network:一个标准的多层全连接网络,输入同样的 $x_0$,输出 $h_{L_d}$。
  4. 输出层:将交叉网络和深度网络的输出拼接,经过一个线性变换(或无隐藏层的全连接)得到最终 logit,再通过 sigmoid 输出预测概率。

其公式表示为:

$$ y = \sigma \left( [x_{L_c}^T, h_{L_d}^T] \cdot w_{\text{logit}} + b \right) $$

这种并联结构使得交叉网络专注于特征叉乘,而深度网络则负责学习更一般化的隐式关系,二者互补。


四、DCN 的优势与局限

4.1 主要优势

  • 参数效率极高:交叉部分的参数量仅为 $O(d \cdot L_c)$,在 $d$ 较大时,依然远小于等效的 DNN。
  • 阶数显式可控:通过设定交叉层数 $L_c$,可以精确控制模型学习的最高特征交叉阶数。通常 $L_c=3\sim6$ 即可覆盖大多数有意义的交叉。
  • 保留线性结构:交叉网络可以看作是 $x_0$ 的一种“基展开”,每个展开项是 $x_0$ 的多项式,这使得模型具有较好的记忆能力,特别适合处理那些需要精确记忆特定组合模式的场景。
  • 易于解释:可以可视化交叉项的权重,分析哪些特征组被模型重点关注。

4.2 潜在局限

  • 同阶交互限制:原始 DCN 的交叉网络每一层只引入 $x_0$ 与 $x_l$ 的交互,学到的交叉项本质上都是 $x_0$ 的单项式(例如 $x_i x_j x_k$),即每个特征在交叉项中出现的最高幂次为 1。这种结构被称为“bit-wise”交叉,它无法捕获像 $x_i^2$ 或更具表现力的组合。
  • 对输入顺序敏感:交叉运算不是对称的,特征初始化顺序可能影响学习结果。
  • 容量有限:当数据要求更复杂的非多项式交互时,仅靠交叉网络不够,必须依赖并行的 DNN。

这些局限在后续的 DCN-V2 中得到了改进(用矩阵权重代替向量权重,支持特征分组交叉),我们会在本教程末尾简要介绍。


五、从零实现 Cross Network

下面用 PyTorch 风格伪代码展示交叉网络的实现思路。实际生产环境推荐使用 TensorFlow Recommenders 或 PyTorch 的封装。

import torch
import torch.nn as nn

class CrossLayer(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        # 权重向量 w 和偏置 b
        self.w = nn.Parameter(torch.randn(input_dim, 1))
        self.b = nn.Parameter(torch.randn(input_dim))

    def forward(self, x0, xl):
        # x0: (batch_size, input_dim) 原始输入
        # xl: (batch_size, input_dim) 上一层输出
        # 计算标量: xl * w -> (batch, 1)
        gate = torch.matmul(xl, self.w)  # (batch, 1)
        # 广播乘法: x0 * gate  -> (batch, input_dim)
        interaction = x0 * gate
        # 残差连接
        return interaction + self.b + xl

class CrossNetwork(nn.Module):
    def __init__(self, input_dim, num_layers):
        super().__init__()
        self.layers = nn.ModuleList([CrossLayer(input_dim) for _ in range(num_layers)])

    def forward(self, x0):
        xl = x0
        for layer in self.layers:
            xl = layer(x0, xl)
        return xl