背景与动机

DeepFM 的局限

DeepFM = FM (二阶) + DNN (隐式高阶)

问题:

  • FM 只显式建模二阶交互
  • DNN 虽然能学高阶,但隐式、不可解释
  • xDeepFM 的 CIN 虽然显式,但计算复杂、参数量大

DCN 的创新

核心思想: 用 Cross Network 显式建模特征交叉

DCN = Deep (线性) + Cross Network (显式交叉) + DNN (隐式高阶)

三大组件:

  1. Deep 部分 - 线性模型,记忆低阶特征
  2. Cross Network - 显式建模特征交叉
  3. Tower 部分 - 捕捉非线性

演进历史

FM (2010)
   ↓ 只能建模二阶交互
DeepFM (2017)
   ↓ 加入 DNN 隐式高阶
xDeepFM (2018)
   ↓ CIN 显式高阶,但复杂
DCN (2019) ⭐
   ↓ Cross Network 显式交叉,更简单

核心创新:Cross Network

Cross Network 是什么?

Cross Network = 显式建模特征交叉的网络

类似于 CIN,但计算更简单、更高效。

Cross Layer 的计算公式

数学公式:

xₗ₊₁ = xₗ ⊙ wₖ₊₁ + xₗ + bₖ₊₁

其中:
- xₗ: 第 l 层的输入
- wₖ₊₁: 第 l+1 层的权重矩阵
- bₖ₊₁: 第 l+1 层的偏置
- ⊙: 逐元素相乘

逐元素相乘 (Hadamard Product):

xₗ = [a₁, a₂, a₃]
w =  [w₁, w₂, w₃]

xₗ ⊙ w = [a₁×w₁, a₂×w₂, a₃×w₃]

Cross Network 的层叠

Cross Layer 1:
  输入: 原始特征
  输出: 第 1 层交叉特征

Cross Layer 2:
  输入: 原始特征 + 第 1 层交叉特征
  输出: 第 2 层交叉特征

...

Cross Network vs CIN 对比

维度 CIN Cross Network
计算公式 复杂的压缩 简单的线性
参数量 n × n × k n × k × L
表达能力 更强 足够强
计算效率 较慢 更快

为什么 DCN 更流行?

  1. 计算简单:线性公式,无压缩
  2. 参数适中:比 CIN 少
  3. 显式交叉:保持可解释性
  4. 性能好:训练和推理速度快

DCN vs xDeepFM/DeepFM 对比

核心区别

模型 Deep 部分 高阶部分
DeepFM FM 隐向量内积 DNN 隐式
xDeepFM - CIN 显式
DCN 线性模型 Cross Network 显式

显式交叉对比

模型 交叉网络 计算复杂度
xDeepFM CIN (复杂)
DCN Cross Network (简单)

参数量对比

假设:

  • 特征数 n = 5
  • 隐向量维度 k = 8
  • 交叉层数 L = 3
模型 交叉网络参数 总参数量
DeepFM 0 ~5K
xDeepFM CIN: L × n × n × k ~15K
DCN L × n × k ~120

模型架构

整体结构

                    输入特征 (离散索引)
                            ↓
                    Embedding 层 (稀疏→稠密)
                            ↓
        ┌───────────────────┼───────────────────┐
        ↓                   ↓                   ↓
    Deep 部分      Cross Network         Tower 部分
   (线性记忆)      (显式交叉)         (隐式非线性)
        ↓                   ↓                   ↓
    Deep 输出      Cross 输出            Tower 输出
        └───────────────────┴───────────────────┘
                            ↓
                        最终输出层

各组件详解

1. Embedding 层

将离散特征索引映射到稠密向量

用户 123 → [0.23, 0.15, ...]  # k 维
广告 45  → [0.67, 0.32, ...]
2. Deep 部分

线性模型,记忆低阶特征

Deep 输出 = Σᵢ wᵢxᵢ + b₀

类似 LR,但共享 Embedding。

3. Cross Network 部分

多层 Cross Layer,显式建模特征交叉

单层 Cross Layer:

xₗ₊₁ = xₗ ⊙ wₖ₊₁ + xₗ + bₖ₊₁

多层堆叠:

Cross Layer 1: x₁ = x₀ ⊙ w₁ + x₀ + b₁
Cross Layer 2: x₂ = x₀ ⊙ w₂ + x₁ + b₂
Cross Layer 3: x₃ = x₀ ⊙ w₃ + x₂ + b₃

关键: 每层都连接到原始输入 x₀

4. Tower 部分

隐式学习复杂的高阶非线性关系

Tower 输出 = MLP(Deep 输出 + Cross 输出)

参数量计算

假设:

  • 特征数 n = 5
  • 隐向量维度 k = 8
  • Cross 层数 L = 3
  • Tower 隐藏层 [64, 32]

各部分参数量:

Embedding:     Σ(feature_dim × k)
Deep:         n × k
Cross Network: L × n × k
Tower:         适当的参数

代码实现

Cross Layer 实现

class CrossLayer(nn.Module):
    """
    Cross Layer: 显式建模特征交叉

    公式:
        xₗ₊₁ = xₗ ⊙ wₖ₊₁ + xₗ + bₖ₊₁
    """

    def __init__(self, num_features, embedding_dim=8):
        super().__init__()

        self.num_features = num_features
        self.embedding_dim = embedding_dim

        # ==================== 权重矩阵 ====================
        # 形状: (num_features, embedding_dim)
        self.w = nn.Parameter(torch.randn(num_features, embedding_dim) * 0.01)

        # 偏置: (embedding_dim,)
        self.b = nn.Parameter(torch.zeros(embedding_dim))

    def forward(self, x):
        """
        Args:
            x: (batch_size, num_features, embedding_dim) 前一层的输出

        Returns:
            output: (batch_size, num_features, embedding_dim) 当前层输出
        """
        # ==================== Cross 操作 ====================
        # x₀ ⊙ w: (batch_size, num_features, embedding_dim)
        cross = x * self.w.unsqueeze(0)

        # ==================== Residual 连接 ====================
        # x₀ ⊙ w + x₀: (batch_size, num_features, embedding_dim)
        output = cross + x + self.b

        return output

DCN 实现

import torch
import torch.nn as nn


class DCN(nn.Module):
    """
    Deep & Cross Network (DCN)

    核心创新:
        Cross Network 显式建模特征交叉

    模型结构:
        Deep (线性) + Cross Network (显式交叉) + Tower (隐式非线性)
    """

    def __init__(self, feature_dims, embedding_dim=8,
                 num_cross_layers=3, hidden_dims=[64, 32]):
        """
        Args:
            feature_dims: 每个特征的可能取值数列表
            embedding_dim: embedding 向量维度
            num_cross_layers: Cross Network 的层数
            hidden_dims: Tower 的隐藏层维度列表
        """
        super().__init__()

        self.feature_dims = feature_dims
        self.num_features = len(feature_dims)
        self.embedding_dim = embedding_dim

        # ==================== Embedding 层 ====================
        self.embeddings = nn.ModuleList([
            nn.Embedding(dim, embedding_dim) for dim in feature_dims
        ])

        # ==================== Deep 部分 ====================
        # 线性模型: Σᵢ wᵢxᵢ + b₀
        self.deep = nn.Linear(self.num_features, 1)

        # ==================== Cross Network ====================
        self.cross_layers = nn.ModuleList()

        # 创建多层 Cross Layer
        for _ in range(num_cross_layers):
            self.cross_layers.append(
                CrossLayer(self.num_features, embedding_dim)
            )

        # ==================== Tower 部分 ====================
        # Tower 输入维度: num_features × embedding_dim × (num_cross_layers + 1)
        # (原始 + 各层 Cross 输出)
        tower dnn_input_dim = self.num_features * embedding_dim * (num_cross_layers + 1)

        # 构建 Tower 层
        tower_layers = []
        for hidden_dim in hidden_dims:
            tower_layers.append(nn.Linear(tower_input_dim, hidden_dim))
            tower_layers.append(nn.ReLU())
            tower_layers.append(nn.BatchNorm1d(hidden_dim))
            tower_input_dim = hidden_dim

        # Tower 输出层
        tower_layers.append(nn.Linear(tower_input_dim, 1))

        self.tower = nn.Sequential(*tower_layers)

    def forward(self, x):
        """
        Args:
            x: (batch_size, num_features) 离散特征索引

        Returns:
            logits: (batch_size, 1) 预测分数
        """
        batch_size = x.shape[0]

        # ==================== Embedding ====================
        embedded_features = []
        for i, emb in enumerate(self.embeddings):
            emb_i = emb(x[:, i])
            embedded_features.append(emb_i)

        all_embeddings = torch.cat(embedded_features, dim=1)
        all_embeddings = all_embeddings.view(
            batch_size, self.num_features, self.embedding_dim
        )

        # ==================== Cross Network ====================
        # 保存各层输出,包括原始 embedding
        cross_outputs = [all_embeddings]

        for cross in self.cross_layers:
            # 每层都连接到原始输入
            layer_output = cross(all_embeddings)
            cross_outputs.append(layer_output)

        # ==================== Deep 部分 ====================
        deep_output = self.deep(x.float())

        # ==================== Tower 部分 ====================
        # 拼接 Deep 输出和所有 Cross 输出
        # (原始 + 各层 Cross 输出)
        tower_input = torch.cat([all_embeddings] + cross_outputs, dim=1)
        tower_input = tower_input.view(batch_size, -1)

        # 通过 Tower 网络
        tower_output = self.tower(tower_input)

        # ==================== 合并输出 ====================
        # DCN = Deep + Tower
        output = deep_output + tower_output

        return output


# ==================== 使用示例 ====================
if __name__ == '__main__':
    # 特征定义
    feature_dims = [1000, 500, 5, 4, 10]

    # 创建 DCN 模型
    model = DCN(
        feature_dims=feature_dims,
        embedding_dim=8,
        num_cross_layers=3,
        hidden_dims=[64, 32]
    )

    print('=== DCN 模型结构 ===')
    print(model)

    # ==================== 参数量分析 ====================
    total_params = sum(p.numel() for p in model.parameters())

    embedding_params = sum(p.numel() for p in model.embeddings.parameters())
    deep_params = sum(p.numel() for p in model.deep.parameters())
    cross_params = sum(p.numel() for p in model.cross_layers.parameters())
    tower_params = sum(p.numel() for p in model.tower.parameters())

    print(f'\n参数量分析:')
    print(f'  总参数量: {total_params:,}')
    print(f'  Embedding: {embedding_params:,}')
    print(f'  Deep 部分: {deep_params:,}')
    print(f'  Cross Network: {cross_params:,}')
    print(f'  Tower 部分: {tower_params:,}')

    # ==================== 生成训练数据 ====================
    batch_size = 32

    x = torch.tensor([
        [torch.randint(0, dim, size=(1,)).item() for dim in feature_dims]
        for _ in range(batch_size)
    ])

    y = torch.randint(0, 2, (batch_size, 1), dtype=torch.float32)

    # ==================== 训练配置 ====================
    criterion = nn.BCEWithLogitsLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    print(f'\n=== 开始训练 ===')
    print(f'batch_size: {batch_size}')
    print(f'特征数: {model.num_features}')
    print(f'Cross 层数: {len(model.cross_layers)}')

    # ==================== 训练循环 ====================
    for epoch in range(100):
        pred = model(x)
        loss = criterion(pred, y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 20 == 0:
            print(f'Epoch {epoch + 1:3d}, Loss: {loss.item():.6f}')

    # ==================== 预测示例 ====================
    model.eval()

    with torch.no_grad():
        test_x = torch.tensor([[
            torch.randint(0, dim, size=(1,)).item() for dim in feature_dims
        ]])

        logits = model(test_x)
        click_prob = torch.sigmoid(logits)

        print(f'\n=== 预测结果 ===')
        print(f'模型输出 (logits): {logits.item():.4f}')
        print(f'点击概率 (sigmoid): {click_prob.item():.4f}')

    # ==================== Cross Network 工作原理示例 ====================
    print(f'\n=== Cross Network 工作原理示例 ===')
    print('第 1 层:')
    print('x₁ = x₀ ⊙ w₁ + x₀ + b₁')
    print('其中: x₀ ⊙ w₁ 是显式特征交叉')
    print('      + x₀ 是 residual 连接')
    print('\n第 2 层:')
    print('x₂ = x₀ ⊙ w₂ + x₁ + b₂')
    print('    ^^^ 连接到原始输入,而非上一层')
    print('\n优势: 简单的计算公式 + Residual 连接 + 显式交叉')

面试常见问题

Q1: DCN 的核心优势是什么?

A:

  1. 简单高效:Cross Network 公式简单,计算快
  2. 显式交叉:可解释的特征交叉
  3. 参数适中:比 xDeepFM 少,比 DeepFM 多
  4. 性能好:训练和推理速度快

Q2: Cross Network 和 CIN 的区别?

A:

对比维度 CIN Cross Network
计算方式 压缩 线性逐元素相乘
公式 复杂 简单
参数量 L × n × n × k L × n × k
表达能力 更强 足够强

Q3: 为什么每层都连接到原始输入?

A:

第 1 层: x₁ = f(x₀)
第 2 层: x₂ = f(x₀, x₁)  # 连接到 x₀
第 3 层: x₃ = f(x₀, x₂)  # 连接到 x₀

原因:

  1. 防止深层网络退化
  2. 保留原始特征信息
  3. 类似 ResNet 的思想

Q4: Deep 部分的作用?

A:
类似 Linear/Logistic Regression,记忆低阶特征的影响。

公式:

Deep 输出 = Σᵢ wᵢxᵢ + b₀

Q5: Tower 部分的作用?

A:
隐式学习复杂的高阶非线性关系。

类似 DeepFM 的 DNN 部分,但输入更丰富(包含 Cross 输出)。

Q6: DCN 的适用场景?

A:

场景 是否推荐 原因
需要显式交叉 ✅ 推荐 Cross Network 提供
计算效率重要 ✅ 推荐 比 xDeepFM 快
数据量大 ✅ 推荐 参数适中
简单场景 ❌ 不推荐 DeepFM 足够
最强表达力 ❌ 不推荐 xDeepFM 更强

Q7: 如何调优 Cross 层数?

A:

层数 效果 参数量
1 层 二阶交叉
2 层 三阶交叉
3+ 层 更高阶

建议:

  • 从 2-3 层开始
  • 根据验证集效果调整
  • 层数过多容易过拟合

Q8: DCN 的主要优势总结?

A:

  1. Cross Network:显式、简单、高效的特征交叉
  2. Deep 部分:线性记忆
  3. Tower 部分:隐式高阶非线性
  4. 参数量:适中,比 xDeepFM 少
  5. 性能:计算快,训练和推理快

模型对比总结

FM (2010)
   ↓ 二阶交互
DeepFM (2017)
   ↓ FM + DNN
xDeepFM (2018)
   ↓ CIN 显式高阶(复杂)
DCN (2019) ⭐
   ↓ Cross Network 显式(简单)
AutoInt (2020)
   ↓ 自动交互阶数

快速检查清单

理解 DCN,你应该能回答:

  • 解释 DCN 的三大组件
  • 说明 Cross Network 的计算公式
  • 理解逐元素相乘
  • 了解 Residual 连接的作用
  • 比较 DCN vs xDeepFM
  • 计算 DCN 的参数量
  • 知道如何调优 Cross 层数
  • 能从零实现简单的 DCN

参考资料

  • DCN 原始论文: https://arxiv.org/abs/1708.00144
  • Cross Network: https://arxiv.org/abs/1708.00144
  • 推荐系统模型库: https://github.com/shenweichen/DeepCTR

实现案例(CTR预测)

import torch
import torch.nn as nn


class CrossLayer(nn.Module):
    """
    Cross Layer: 显式建模特征交叉
    """

    def __init__(self, num_features, embedding_dim=8):
        super().__init__()

        # 权重矩阵: (embedding_dimkt, embedding_dim)
        self.w = nn.Parameter(torch.randn(embedding_dim, embedding_dim) * 0.01)
        self.b = nn.Parameter(torch.zeros(embedding_dim))

    def forward(self, x0, xl):
        """
        x0: (batch_size, num_features, embedding_dim) - 原始输入
        xl: (batch_size, num_features, embedding_dim) - 当前层输入
        output: (batch_size, num_features, embedding_dim)
        """
        batch_size, num_features, embedding_dim = x0.shape

        # Reshape for computation: (batch_size * num_features, embedding_dim)
        xl_flat = xl.view(-1, embedding_dim)

        # Compute xl^T * W * xl: (batch_size * num_features, embedding_dim)
        cross_term = torch.matmul(xl_flat, self.w)

        # Add bias and reshape back
        cross_term = cross_term + self.b
        cross_term = cross_term.view(batch_size, num_features, embedding_dim)

        # Element-wise multiply with original input: x0 * (cross_term + b) + x0
        output = x0 * cross_term + xl

        return output


class DCN(nn.Module):
    """
    Deep & Cross Network (DCN)

    架构: Deep(线性) + Cross Network(显式交叉) + Tower(高阶)
    """

    def __init__(self, feature_dims, embedding_dim=8,
                 num_cross_layers=3, hidden_dims=[64, 32]):
        super().__init__()

        self.feature_dims = feature_dims
        self.num_features = len(feature_dims)
        self.embedding_dim = embedding_dim

        # Embedding 层
        self.embeddings = nn.ModuleList([
            nn.Embedding(dim, embedding_dim) for dim in feature_dims
        ])

        # Deep 部分
        self.deep = nn.Linear(self.num_features, 1)

        # Cross Network
        self.cross_layers = nn.ModuleList()
        for _ in range(num_cross_layers):
            self.cross_layers.append(
                CrossLayer(self.num_features, embedding_dim)
            )

        # Tower 部分
        tower_input_dim = self.num_features * embedding_dim
        tower_layers = []
        for hidden_dim in hidden_dims:
            tower_layers.append(nn.Linear(tower_input_dim, hidden_dim))
            tower_layers.append(nn.ReLU())
            tower_layers.append(nn.BatchNorm1d(hidden_dim))
            tower_input_dim = hidden_dim
        tower_layers.append(nn.Linear(tower_input_dim, 1))

        self.tower = nn.Sequential(*tower_layers)

    def forward(self, x):
        """
        x: (batch_size, num_features)
        output: (batch_size, 1)
        """
        batch_size = x.shape[0]

        # Embedding
        embedded_features = []
        for i, emb in enumerate(self.embeddings):
            emb_i = emb(x[:, i])
            embedded_features.append(emb_i)

        all_embeddings = torch.cat(embedded_features, dim=1)
        all_embeddings = all_embeddings.view(
            batch_size, self.num_features, self.embedding_dim
        )

        # Cross Network
        x0 = all_embeddings
        xl = x0
        for cross in self.cross_layers:
            xl = cross(x0, xl)
        cross_output = xl

        # Flatten for Tower
        cross_output = cross_output.view(batch_size, -1)

        # Deep 部分 - 使用平均池化的embeddings
        deep_output = self.deep(all_embeddings.mean(dim=2))

        # Tower 部分
        tower_output = self.tower(cross_output)

        # 合并输出
        output = deep_output + tower_output

        return output


if __name__ == '__main__':
    # 特征定义
    feature_dims = [1000, 500, 5, 4, 10]

    model = DCN(
        feature_dims=feature_dims,
        embedding_dim=8,
        num_cross_layers=3,
        hidden_dims=[64, 32]
    )

    print('=== DCN 模型结构 ===')
    print(model)

    # 参数量分析
    total_params = sum(p.numel() for p in model.parameters())

    embedding_params = sum(p.numel() for p in model.embeddings.parameters())
    deep_params = sum(p.numel() for p in model.deep.parameters())
    cross_params = sum(p.numel() for p in model.cross_layers.parameters())
    tower_params = sum(p.numel() for p in model.tower.parameters())

    print(f'\n参数量分析:')
    print(f'  总参数量: {total_params:,}')
    print(f'  Embedding: {embedding_params:,}')
    print(f'  Deep 部分: {deep_params:,}')
    print(f'  Cross Network: {cross_params:,}')
    print(f'  Tower 部分: {tower_params:,}')

    # 生成训练数据
    batch_size = 32
    x = torch.tensor([
        [torch.randint(0, dim, size=(1,)).item() for dim in feature_dims]
        for _ in range(batch_size)
    ])
    y = torch.randint(0, 2, (batch_size, 1), dtype=torch.float32)

    # 训练
    criterion = nn.BCEWithLogitsLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    print(f'\n=== 开始训练 ===')
    print(f'batch_size: {batch_size}')
    print(f'特征数: {model.num_features}')
    print(f'Cross 层数: {len(model.cross_layers)}')

    for epoch in range(4000):
        pred = model(x)
        loss = criterion(pred, y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 20 == 0:
            print(f'Epoch {epoch + 1:3d}, Loss: {loss.item():.6f}')

    # 预测
    model.eval()
    with torch.no_grad():
        test_x = torch.tensor([[
            torch.randint(0, dim, size=(1,)).item() for dim in feature_dims
        ]])

        logits = model(test_x)
        click_prob = torch.sigmoid(logits)

        print(f'\n=== 预测结果 ===')
        print(f'模型输出 (logits): {logits.item():.4f}')
        print(f'点击概率 (sigmoid): {click_prob.item():.4f}')

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐