
做 CTR 预估的同学应该都有这种体会模型结构翻来覆去但线上真正见效的往往不是网络深了多少而是特征交叉做对了。DeepCross Network 是我在 CTR 建模里用得最多的基线模型之一也是我建议每个做推荐、广告、搜索排序的同学认真吃透的一个结构。它和 DeepFM、xDeepFM 几乎同时期出现但设计思路完全不同——不是靠更深的网络而是靠一个轻量的 Cross 网络用极少的参数去捕捉特征之间的高阶交互。这篇内容会从它解决的问题出发把 Cross 层原理、整体架构、训练细节以及后来 DCN-V2 为什么要修正它逐个讲清楚。1. 特征交叉这座山LR、FM 到深度模型各自的死穴1.1 特征交叉为什么是 CTR 预估的胜负手CTR 预估本质上是在回答一个问题给定一个用户和一个候选 item用户点不点这个决策很少由单个特征决定。比如“女性用户”单独看不代表什么“口红商品”单独看也不代表什么但“女性用户 口红商品”这个组合点击概率就会显著高于随机。这种两个或多个特征组合起来才产生的信号就是特征交叉。在业务里特征交叉的表现形式非常多。用户活跃时段和商品品类交叉用户所处城市和商品 price 区间交叉用户最近点击的类目和当前 item 的类目交叉……这些信号如果只靠模型对单个特征建模完全学不到。早期工业界最朴素的做法是人工构造组合特征把“性别 商品类目”“年龄分桶 价格分桶”这类组合直接拼成新的稀疏特征喂给 LR。这个方法有效但问题也明显特征数量爆炸式增长100 个特征两两组合就有接近 5000 个新特征三阶组合更是天文数字。人工枚举不可能覆盖所有业务交叉而且维护成本极高。所以 CTR 模型领域的核心命题就变成了能不能让模型自己学会特征交叉而且最好是高阶交叉这个问题看起来简单实际上绕了很久才有比较靠谱的答案。1.2 三种经典方案的局限对照在 DCN 出现之前业界主流的自动化交叉方案大致可以分为三条路线各有各的天花板。第一条是 FM/FFM 路线。FM 用隐向量内积来建模二阶交叉参数规模从组合特征的 O(n^2) 降到了 O(nk)这是它最大的贡献。但 FM 显式建模的阶数停留在二阶想扩展到三阶、四阶计算复杂度会急剧上升实际工程里很少有人真去跑高阶 FM。FFM 引入了 field 概念表达能力强一些但参数量和训练开销也随之上涨。第二条是深度模型路线。DNN 理论上可以拟合任意函数喂进去 embedding 拼接向量中间叠几层全连接按说高阶交叉可以自己学出来。但实际训练中DNN 学高阶交叉的效率很低。每一层 d×d 的权重矩阵大部分参数并没有在学有效的交叉而是被冗余信息稀释了。特征维度一大网络容量容易被浪费在无关组合上还容易过拟合。第三条是传统手工特征工程 LR。可解释性强但前面说了人力成本太高且线上特征配置的迭代速度完全跟不上业务。为了更好地对比我把几条路线的特点整理成了下面的表格。方案自动交叉阶数参数开销特征工程成本主要瓶颈LR 手工特征依赖人工构造低高特征爆炸依赖经验FM / FFM二阶为主中低显式高阶交叉困难深度 DNN理论任意阶高低参数冗余训练效率低DeepCross Network显式高阶低低交叉精细度有限下文细说1.3 DCN 的破局思路用多项式近似的眼光看特征交叉DCN 这篇论文Deep Cross Network for Ad Click PredictionsGoogle 2017 年发表提出时思路和 DeepFM 那一路不太一样。它把特征交叉问题重新理解了一下所谓特征交叉就是在输入 x 上做多项式展开。比如二阶交叉对应 x_i * x_j三阶对应 x_i * x_j * x_k。如果有一种网络结构能直接构造出这些多项式项那不就不需要等 DNN 自己慢慢学了吗Cross Network 就是干这件事的。它的设计目标是每一层都在原有基础上增加若干“多项式级”的项层数越多构造出的交叉阶数越高。关键是它的参数极其省每一层只有一个权重向量和一个偏置向量而不是像 DNN 那样每层一个矩阵。理解了这一点你就抓住了 DCN 的灵魂。2. Cross 层逐层拆解每层 2d 个参数高阶交叉到底是怎么出来的2.1 公式与形状一层 Cross 到底做了什么先看 Cross 层的核心公式这里我会写得比较细因为很多人就是卡在这一步x_{l1} x0 * (x_l^T · w_l) b_l x_l其中 x_l 是第 l 层的隐状态w_l 和 b_l 是这一层要学习的参数它们都是 d 维向量。x0 是模型输入的原始 embedding 拼接向量在 Cross 网络里从头到尾保持不变。这个公式看起来简单但里面有几个关键点需要拆开看。首先看括号里 x_l^T · w_l这是 x_l 和 w_l 的内积结果是一个标量。你可以把它理解为“当前隐状态在 w_l 方向上的投影强度”。这个标量再乘以 x0相当于用这个投影强度去整体缩放最原始的输入向量。最后加上本层偏置 b_l 和残差 x_l。从张量形状上说x0 和 x_l 都是 (batch, d)内积得到 (batch, 1)再乘 x0 得到 (batch, d)加上 b_l 和 x_l 后维度不变。也就是说Cross 层输入输出维度一致可以无限堆叠。每层的可学习参数只有两个 d 维向量合计 2d 个参数。这就是 DeepCross Network 名字里“Deep”和“Cross”并列的底气所在——Cross 部分是真的轻量。2.2 为什么乘 x0 而不是学一个 d×d 矩阵这个设计是 Cross 层最反直觉的地方。按照一般 DNN 的思路两层之间要学交互至少得有一个 d×d 的权重矩阵吧但 DCN 偏偏只用向量。核心原因有两个。第一是参数效率。如果每一层都学一个 d×d 矩阵参数量就是 d^2 d特征维度一上来模型体积立刻失控。DCN 选择把每一层的输出都建立在 x0 的基础上相当于所有层共享同一个 base 向量每层只需要在这个 base 上做“缩放 平移 残差”。这大大压缩了参数量而且跨层之间天然建立了联系。第二是梯度路径。因为 x0 作为系数直接乘到了每一层输出上反向传播时梯度可以从深层直接传回输入路径短而且稳定。相比之下普通 DNN 的梯度经过多层矩阵相乘容易衰减或者爆炸。DCN 论文里也强调过这一点Cross 层的设计让模型训练更稳定调参压力小很多。2.3 从一阶到高阶展开公式看交叉信号到底藏在哪公式摆在那但“为什么这样叠几层就能做高阶交叉”这个问题还是要靠展开才能看明白。我们手推一下两层的情况。第一层x1 s0 * x0 b0 x0 (s0 1) * x0 b0其中 s0 x0^T · w0第二层x2 s1 * x0 b1 x1其中 s1 x1^T · w1把 x1 代进去s1 [(s0 1) * x0 b0]^T · w1 (s0 1) * (x0^T · w1) b0^T · w1这里出现了两个有意思的项。x0^T · w1 是 x0 各维度的加权和而 b0^T · w1 中包含了第一层偏置 b0 和 w1 的内积。如果 x0 是多个特征块拼接而成的比如用户特征块 u 和商品特征块 v那么 b0 在训练中会同时吸收 u 和 v 的信息b0^T · w1 就会产生跨特征块的交互信号。再往深层看s_l 的值会嵌套进下一层的 x_{l1}每一层都在前一层的基础上引入新的加权组合这就是“逐层提升多项式阶数”的直观含义。但是这里必须指出一个数学事实也是后来 DCN-V2 论文专门讨论的问题x_l^T · w_l 始终是一个标量也就是说每一层对整个 x0 的缩放是全局统一进行的。无论叠多少层如果没有偏置项Cross 网络的输出永远都在 x0 的方向上有了偏置项之后每层引入一个新的可学习方向但整体自由度依然远小于同规模的 MLP。这被称为 rank-1 限制。那 DCN 原版为什么还能work我的理解有三点第一x0 本身是 embedding 拼接embedding 是可学习的优化器会调整 x0 的表示让这个标量缩放更有区分度第二每层偏置 b_l 作为自由方向补充了一部分表达能力第三输出层还有全连接和 sigmoid会对 Cross 输出做进一步组合。所以它不是“没有交叉”而是交叉的精细度不够这个问题我们留到第 5 节细说。2.4 参数效率与推理成本的具体数字参数效率这件事数字最能说明问题。假设特征拼接后的维度 d 128Cross 网络叠 6 层总参数量是 6 × (128 128) 1536 个参数。而 Deep 分支里一个最简单的 128→128 全连接层参数量就是 128 × 128 128 16512差了十倍以上。三层 MLP 下去参数量轻松破五万。推理成本方面Cross 层在 serving 时只有一次内积、一次标量乘向量、一次偏置加法、一次残差加法复杂度是 O(d)和全连接层的 O(d^2) 完全不在一个量级。对广告、推荐这类延迟敏感的线上服务来说这个优势非常重要。我做线上评估的时候DCN 的 Cross 部分在 CPU 上跑单次请求增加的耗时基本可以忽略不计。3. DCN 整体数据流Embedding、Cross、Deep 三者如何配合3.1 输入侧稀疏类别特征和稠密数值特征怎么进模型DCN 的输入处理和大多数深度 CTR 模型类似但有一个细节值得单独强调。稀疏类别特征比如 user_id、item_id、商品类目这些先过 embedding 查表每个特征转成一个低维稠密向量。多值特征比如用户最近点击过的 50 个商品 id 列表可以先对每个 id 做 embedding再 sum pooling 或者 mean pooling 成一个定长向量。稠密数值特征比如价格、时长、点击间隔这些不能直接拼进向量必须先做归一化否则会给 Cross 网络埋下巨大的坑这个后面实操章节会详细说。所有 embedding 向量和归一化后的数值特征拼在一起得到最终的 x0。这个 x0 就是 Cross 网络和 Deep 网络共享的输入基向量。这里有个小建议不同特征的 embedding 维度不必都相同。高频特征可以给 16 维或 32 维低频特征给 4 维或 8 维最后拼接时长度不一致没有问题。这样能省不少参数对效果几乎没有负面影响。3.2 Cross 和 Deep 为什么要并行而不是串联DCN 的整体结构可以从数据流的角度这样描述x0 作为输入同时送入 Cross Network 和 Deep Network。Cross Network 叠 L 层输出 x_cross。Deep Network 叠若干层全连接一般用 ReLU输出 x_deep。把 x_cross 和 x_deep 拼接过一层全连接接 sigmoid输出点击概率。为什么要并行而不是串行我在实际项目中对比过两种结构。串行版本先 Cross 再 Deep在论文里也被提到过离线指标和并行版本差别不大但工程上有两个问题一是 Deep 分支吃到的输入已经被 Cross 层的数值尺度改变了需要额外适配二是调试时不方便单独观察 Cross 和 Deep 各自贡献的信号。并行结构把两条路径的作用分得很清楚调参时能分别控制学习率也方便做消融实验。所以我个人推荐并行结构。它让 Cross 分支专心做“显式交叉”让 Deep 分支处理“隐式非线性”两者互不干扰。最终拼接后过全连接层让模型自己决定该信任哪条路径的输出。3.3 输出层设计、损失函数与评估指标输出层的设计比较常规拼接之后的向量过一个输出维度为 1 的全连接层接 sigmoid 得到点击概率。损失函数用二元交叉熵logloss这也是 CTR 预估的标准做法。评估指标这里我多说一句。很多人只关注全局 AUC但在业务场景里GAUC 往往更值得看。GAUC 的做法是先按用户分组每个组内计算 AUC再按曝光量加权平均。这样做能避免“排序好是因为把某些用户的 item 整体排到了其他用户前面”这种误导性结果。我在 DCN 调参阶段主要盯 GAUC 和 logloss 两个指标其中 GAUC 和线上 A/B 效果的相关性明显更高。训练时一般用 Adam 优化器配合 L2 正则。具体参数怎么设下一章展开讲。4. 训练 DCN 最容易翻车的实操细节学习率、归一化、Embedding 维度4.1 学习率Cross 网络不是普通 DNN别用同一套参数DCN 的 Cross 层有一个特点每一层的输出都直接包含 x0 的标量倍数所以梯度中也会带着 x0 的倍数。如果输入向量的数值尺度偏大梯度会被放大训练前期 loss 直接飞掉是常见现象。我踩过的坑是这样的用默认的 Adam learning rate 0.001 去训 DCN前几百步 loss 先是正常下降然后突然变成 NaN。排查下来问题出在 Cross 层的梯度爆炸而不是 Deep 分支。解决办法也很直接对 Cross 层单独设置学习率降到 0.0001 到 0.0005Deep 分支保持 0.001 不动。分层学习率在 TensorFlow 和 PyTorch 里都很容易实现给 Cross 层的参数单独挂一个 optimizer 或者 param group 就行。另外要提醒一点如果训练过程中 loss 出现周期性的震荡不要急着调模型结构先确认一下学习率是不是太高。DCN 对学习率的敏感度比纯 DNN 高这是我反复验证过的经验。4.2 特征归一化是 Cross 网络的生死线这一条我觉得怎么强调都不过分。DNN 对特征尺度不敏感归一化最多是锦上添花但 Cross 网络里x0 的数值尺度会直接乘到每一层的输出上。举个例子假设一个数值特征“价格”的原始范围是 0 到 10000而其他 embedding 特征的数值范围在 -1 到 1 之间。两者拼接成 x0 后Cross 层计算 x_l^T · w_l 时价格维度的贡献会比 embedding 维度大好几个数量级导致这个标量缩放因子被价格特征单方面主导。这等于说模型在这个 Cross 层上学到的东西几乎只和价格特征相关其他特征之间的交叉被完全淹没了。解决办法是标准的所有稠密数值特征在进模型之前做标准化常见做法是 z-score 归一化或者缩放到 [0,1] 区间。我个人的习惯是先对数值特征做分位数变换把长尾分布压平再做 z-score这样对异常值更鲁棒。Embedding 的初始化也别太大TruncatedNormal(stddev0.01) 通常够用。4.3 Embedding 维度别贪大8 到 32 通常是甜点区做深度模型的人容易有一个惯性embedding 维度给大一点模型容量大效果应该更好。在 DCN 里这个惯性需要克制。原因在于x0 是 Cross 网络和 Deep 网络共享的。embedding 维度给大了Cross 层的内积计算量线性增长还可以接受但 Deep 分支的参数量是平方级增长的d128 的全连接层和 d256 的全连接层参数量差了四倍。训练时间、过拟合风险、线上推理耗时都会跟着上来。我实际项目里的经验值是常规类别特征 8 到 32 维特征特别稀疏或者频次很低的给 4 维高频核心特征给到 32 维很少超过 64。DCN 做的是 CTR 预估不是 NLP不需要那么高的表示维度。先用小维度跑通 baseline再根据验证集 GAUC 的增益决定要不要加维度这个流程比一开始就上大维度靠谱得多。4.4 正则、Early Stopping 与 GAUC 评估CTR 场景的特征普遍稀疏embedding 层很容易过拟合所以正则不能省。但 DCN 里的正则要讲究一个度。有些同学直接在 Cross 层和 Deep 层同时加 L2结果模型倾向于把 Cross 层的权重全部压到接近 0相当于把 Cross 网络整个废掉了GAUC 反而不如不加。我的做法是L2 主要加在 embedding 层上Cross 层和 Deep 层不加或者只加极小的权重然后靠验证集 GAUC 做 early stopping。这样既能控制主要过拟合来源又不会压制 Cross 层的交叉信号。评估频率上我习惯每个 epoch 结束算一次验证集 GAUCpatience 设 2 到 3 个 epoch。stoppage 之后用历史最优模型重新预测测试集这样能避免验证集上的过拟合。4.5 一个可复现的 Keras 最小实现理论讲再多不如给一个能跑起来的实现。下面是我在项目里用的简化版 CrossLayer基于 TensorFlow Keras。import tensorflow as tf class CrossLayer(tf.keras.layers.Layer): DCN 中的 Cross 层。 x0: 输入基向量全程不变 x: 当前层隐状态 def __init__(self, dim): super().__init__() self.w self.add_weight( namew, shape(dim,), initializertf.keras.initializers.TruncatedNormal(stddev0.01) ) self.b self.add_weight( nameb, shape(dim,), initializerzeros ) def call(self, x0, x): # x_l^T w_l 得到标量 s tf.reduce_sum(x * self.w, axis-1, keepdimsTrue) return s * x0 self.b x组装完整模型时逻辑也很直观。def build_dcn(input_dim, num_cross_layers6, deep_units[128, 64]): x0 tf.keras.Input(shape(input_dim,), namestacked_input) # Cross 分支 x_cross x0 for _ in range(num_cross_layers): x_cross CrossLayer(input_dim)(x0, x_cross) # Deep 分支 x_deep x0 for units in deep_units: x_deep tf.keras.layers.Dense(units, activationrelu)(x_deep) concat tf.keras.layers.Concatenate()([x_cross, x_deep]) output tf.keras.layers.Dense(1, activationsigmoid)(concat) model tf.keras.Model(inputsx0, outputsoutput) return model注意这个实现里输入是已经拼接好的 x0实际工程中 embedding 查表和特征归一化都在数据管道里完成不要在模型内部做否则线上 serving 和离线训练的数据一致性很难保证。4.6 线上推理时的 Cross 层计算优化Cross 层在 serving 阶段可以做一个非常简单的优化。公式里的 x_l^T · w_l 是标量所以整个 Cross 层的计算可以拆成四步一次内积、一次标量乘向量、一次向量加法、一次残差加法。这几步操作在 CPU 上单次耗时是微秒级的。我之前在自研的推荐引擎里做过性能测试一个 128 维输入、6 层 Cross 的分支整体耗时占比不到模型总耗时的 5%比同层数的全连接层节省了一个数量级的算力。这也让 DCN 在实时推荐这种对 p99 延迟有严格要求的场景里很有优势。5. 旧 DCN 的标量瓶颈与 DCN-V2 的矩阵化修正5.1 被论文一笔带过的 rank-1 限制Cross 层的实际自由度前面展开公式的时候提到过原版 DCN 的 Cross 层每一层都是用 x_l^T · w_l 这个标量去缩放 x0。DCN-V2 论文把这个限制讲得更明白原版 Cross 层的本质是一个 rank-1 投影所有特征维度共享同一个缩放因子。这意味着什么还是用例子说明。假设 x0 里同时有用户年龄、用户性别、商品价格三个维度。原版 Cross 层计算出的标量缩放因子是全局的年龄、性别、价格这三个维度拿到的是同一个缩放倍数。但真实的业务信号往往是维度特定的年龄和商品价格的交叉强度大概率不等于性别和商品价格的交叉强度。原版 DCN 没有能力区分这种差异它只能统一缩放。那为什么原版 DCN 的论文实验里效果依然不错我在实际使用中的理解是第一embedding 本身是可学习的优化器会把 x0 调整到适合这种统一缩放的状态第二每层偏置 b_l 是自由向量给模型提供了一些额外的表达方向第三输出层的全连接和 sigmoid 对 Cross 输出做了二次组合弥补了一部分非线性能力。但说到底Cross 网络的交叉精细度是有限的这就是原版 DCN 的上界。5.2 DCN-V2从标量到矩阵的低秩方案DCN-V2 的核心改动非常直接把每一层的标量缩放改成逐元素乘的矩阵变换。x_{l1} x0 ⊙ (W_l x_l) b_l x_l这里的 ⊙ 表示逐元素乘。W_l 是一个 d×d 的矩阵W_l x_l 得到一个 d 维向量这个向量再和 x0 逐元素相乘。这样一来不同维度可以有不同的交叉强度表达能力比原版强了很多。但代价也是明显的参数量从每层 2d 变成了每层 d^2 d。输入维度 d128 时每层参数量约 16k叠 6 层就是 100k这在有些场景下已经不可接受了。所以 DCN-V2 论文提出了低秩近似把 W_l 分解成 U_l V_l^T其中 U_l 和 V_l 都是 d×r 矩阵r 远小于 d。这样每层参数量从 d^2 d 降到 2dr d。r 是一个超参数需要根据输入维度平衡。我的实践经验是输入维度在 300 左右时r 取 16 到 64 比较合理如果离线 GAUC 提升不明显优先加大 r 而不是增加 Cross 层的层数。方案Cross 层参数/层交叉方式表达能力适用场景原版 DCN2d标量缩放rank-1有限但参数极省延迟敏感、基线模型DCN-V2矩阵d^2 d逐元素乘强但参数多维度较低、算力充足DCN-V2低秩2dr d低秩逐元素乘较强参数可控大部分实际业务场景5.3 什么场景值得选 DCN什么场景建议换别的路线结合我自己的落地经验DCN 的适用边界其实比较清晰。适合上 DCN 的场景有三个特征特征稀疏、交叉信号强、延迟敏感。典型的比如广告点击率预估、商品推荐排序、信息流冷启动。在这些场景里DCN 能用一个非常轻量的 Cross 分支拿到不错的高阶交叉收益训练成本低线上推理也没压力。把 DCN 当 baseline 尤其合适因为它收敛快、调参少验证完能很快给出一个可靠的下限。不太适合的场景也有。如果用户的兴趣主要由行为序列承载比如点击序列很长、会话内决策很明显那需要先用 DIN、DIEN 这类专门处理序列的模型把序列特征编码好再决定接不接 DCN。如果特征之间的关联很弱或者预测任务主要靠文本、图像模态的内容理解DCN 给不了太多增量不如把精力放在特征侧。我在实际项目里的体会是DCN 最大的价值不是刷分而是稳定。Cross 网络用极低的成本给模型注入高阶交叉信号让 Deep 分支专注于自己擅长的事情。如果你在做 CTR 建模先把 DCN 吃透后面看 DCN-V2、FinalMLP、DeepFM 这些变体都会轻松很多因为它们的底层逻辑绝大部分都能在 DCN 的 Cross 层设计里找到影子。