ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Deep Crossing模型精讲:Embedding+残差网络如何实现自动特征组合与CTR预估

Deep Crossing模型精讲:Embedding+残差网络如何实现自动特征组合与CTR预估 做了这么多年搜索广告和推荐系统的点击率预估我最头大的事情之一就是特征工程。早期模型大量依赖人工拼特征组合比如性别男 品类游戏、城市一线 时段晚间这种二阶甚至三阶交叉每次上新策略都要靠业务分析和线上AB实验反复验证费时费力还不一定能找到有效组合。后来读到微软提出的Deep Crossing模型才发现自动特征组合这条路可以直接交给网络自己学把离散稀疏特征映射成稠密向量后通过网络深层的非线性变换来自动发现高阶交叉效果还往往优于手工特征工程。这篇文章就把我对Deep Crossing的理解、复现过程以及落地时踩过的一些坑整理出来希望能给正在做CTR预估或者推荐召回/排序的读者一些参考。1. 特征工程的旧时代为什么手工拼特征注定走不通在讨论Deep Crossing之前得先搞清楚一个问题为什么我们这么需要自动特征组合不是大家不想做特征工程而是传统方法在特征组合这件事上的天花板太低低到你只能靠人工去碰运气。1.1 LR与FM的边界二阶交互已经是天花板先看最经典的逻辑回归。LR对每个特征学一个独立权重预测时把特征加权求和再套sigmoid本质上是一个线性模型。线性模型的好处是简单、可控、容易解释但坏处也很明显它默认特征之间是相互独立的。如果用户刚看完汽车评测和广告位在首页首屏这两个信号单独看都不强但组合起来对点击有强拉动LR永远学不到这个规律除非你在训练样本里额外构造一个汽车评测首页首屏的交叉特征。所以传统做法是人工枚举交叉特征。二阶交叉还能枚举到了三阶、四阶就指数爆炸。而且很多交叉特征在样本里非常稀疏比如男性25岁科技频道晚间这类深度组合可能全量样本里只有几百个正例模型学起来噪声极大。FM通过隐向量内积实现了自动二阶交叉这是一个重要突破。每个特征不再是一个标量权重而是一个k维向量两个特征的交叉权重由两个向量内积得到。FM能自动学到哪个组合重要不需要人工枚举。这里最让我欣赏的是它的工程技巧FM把二阶交叉参数的从nn压缩到nk泛化能力也更强。但FM的边界非常明显它只能建模二阶交互。你想让它自动发现性别品类时段三阶组合它无能为力。虽然理论上可以把二阶项堆成高阶也就是使用更高阶的交互项但参数复杂度、训练难度都会失控实践中极少有团队真的去跑三阶FM。1.2 GBDTLR的临时方案与它的先天问题工业界很长一段时间的主流方案是GBDTLR这个组合解决的问题也是自动特征组合。原理很巧妙GBDT每棵树的叶子节点代表一个数据分区路径训练完成后把样本落入的叶子节点位置做成one-hot向量再丢给LR学习权重。相当于GBDT把原始特征空间切成了很多高阶非线性区域LR负责在这些区域上学习打分。这个方案在当时算是用一个模型自动发现高阶组合的可行路径我也在不少项目里用过效果确实比纯LR好。但它有几个先天问题让我始终觉得别扭第一GBDT对超高维稀疏类别特征很不友好。ID类特征比如用户ID、广告ID、商品ID做one-hot或hash后会形成极稀疏的高维输入GBDT的树分裂在这个空间里效果很差。所以实际业务里通常只把数值型特征、枚举型特征放进GBDT而那些最重要的ID类特征还是留给LR单独处理。第二GBDTLR是两阶段串行训练目标不一致。GBDT训练时以预测类别为准它内部的叶子划分不一定是为了LR分类最优而生成再加上GBDT和LR用了不同目标函数整个系统没有端到端联合优化。第三特征组合不可控。GBDT生成的组合数量由叶子节点决定叶子多了容易过拟合叶子少了组合能力不够调起来非常麻烦。而且模型已经生成你没法判断每个组合的业务含义。所以我把这套方案归为过渡产物。Deep Crossing打动我的一点就是它彻底放下了先把特征组合好再学权重的思路把特征Embedding、特征交叉、目标优化全部放进同一个神经网络里端到端地完成自动特征组合。2. Deep Crossing的四层结构把特征组合从人肉活变成网络行为Deep Crossing的网络结构非常简洁一共就四层Embedding层、Stacking层、多个Residual Unit组成的残差层最后是Scoring层。整个前向过程可以理解为先把所有高维稀疏特征压成低维稠密向量拼接成一个长向量然后通过多层残差网络逐层提炼特征交互最后一层输出点击率预估值。2.1 Embedding层从稀疏one-hot到稠密向量这里藏着真正的玄机Embedding层最容易被忽略因为很多人觉得它只是降维或者查表。但Deep Crossing里Embedding承担的是语义向量化的任务。原始输入可能是用户ID、广告ID、创意ID、行业ID、设备类型等类别特征。在做独热之后每个特征的维度可以是几百万甚至上亿直接输入深度网络是灾难。Deep Crossing为每个特征类别单独维护一个Embedding矩阵通过查表把one-hot索引转换成一个固定维度的稠密向量。这里有一个值得展开的细节为什么要每个类别单独一个Embedding矩阵而不是把所有类别特征拼成一个共享词典比如用户ID123和广告ID456如果放在同一个Embedding空间学出来的向量会混合两种不同语义网络无法区分。用户ID和广告ID本身就是完全不同空间的对象强制它们共享向量空间会破坏各自的结构。所以每个类别一个独立的Embedding矩阵让每个ID在自己的空间里单独表达。Embedding维度怎么选我的经验是16~32维起步。维度太小表达力不够学不出复杂语义维度太大带来过拟合因为CTR场景的ID类特征请求频次是幂律分布绝大多数长尾ID在训练中出现的次数很少给它们太多维度只会让模型去硬记训练样本。另外建议Embedding维度选4的倍数方便硬件对齐推理时也能快一些。训练完成后Embedding向量本身就是很有价值的副产物。语义相近的广告ID会在向量空间里靠得更近可以直接拿来做召回的向量化索引。这一点是我在业务落地时才意识到的Deep Crossing除了解决排序问题还顺手给了你一套可复用的稠密特征表达。2.2 Stacking与Residual Unit为什么非要用残差Stacking层没什么好神秘的就是把所有经过Embedding的特征向量横向拼接成一个长向量。这里要提醒的是拼接顺序必须固定否则线上推理和训练时向量位置的错位会让Embedding查表完全失效。我自己在早期原型里就吃过这个亏把拼接顺序写死在配置里才避免线上出大问题。真正值得深挖的是Residual Unit。Deep Crossing的核心创新其实不在Embedding而在它把残差网络引入了CTR预估这类稀疏场景。Residual Unit的基础结构是两层全连接ReLU形式上可以写成h ReLU(W2 * ReLU(W1 * x b1) b2) y h x这里的y h x就是残差连接的关键。如果没有这条恒等映射路径多层网络在训练时梯度要逐层回传层数一多很容易出现梯度消失CTR数据又是海量稀疏样本收敛极慢。加了残差连接后梯度可以通过恒等路径直接回传到浅层整个深层网络被拆解成一系列增量学习模块。每个残差单元不需要从零学出一个完整映射只需要在输入基础上学一个小的修正量。这好比做菜时不是每次从洗菜切菜开始而是在上一道工序基础上加一点调味最终成品的复杂度由多个模块共同叠加出来。在CTR场景里这种增量映射尤其适用。底层Embedding已经包含了单特征语义残差单元第一层负责在稠密向量上做非线性变换第二层把变换结果再加回原始向量。堆叠多个残差单元后网络就能逐步组合出二阶、三阶甚至更高阶的特征交互。读者可以把每个残差单元想象成一个特征组合机两个向量在这个单元里被挤压、变换、融合再把融合结果传给下一个单元。实验里我用到的通用配置是堆叠2~3个Residual Unit每个单元隐层宽度256~512。堆太多并不总是好事这个后面聊过拟合时细说。2.3 Scoring层与损失设计点击率预估的最后一步Scoring层结构上就是一层线性变换把残差网络输出的深层特征向量映射成一个logit再经过sigmoid得到点击概率。训练目标用二元交叉熵损失这也是CTR预估的标配。端到端的意义在这里真正体现出来。损失从最终输出回传时每一层的参数包括Embedding矩阵、残差单元权重都会向着降低预测误差的方向一起更新。手工特征工程时代你只能在模型外围绕弯去构造特征Deep Crossing把特征表达和特征组合统一进了同一个目标函数里这是它和所有两阶段方案的本质区别。还有一个小细节Score层之前是否需要额外的全连接层做过渡论文里没有特别强调我的实践是在残差单元之后通常不再加额外全连接直接线性映射到1维即可减少参数也让残差网络输出的高层语义直接参与决策。模型是否要增加这一层可以在验证集上看AUC变化来决定。3. 动手复现一个可落地的Deep Crossing实现理论说再多不如直接跑一个能用的实现。下面我用PyTorch给出一份可以在公开数据集比如Criteo或者MovieLens转化率数据上直接跑的代码框架。重点不是把代码贴满而是把每个环节的关键决策讲清楚。3.1 数据准备稀疏特征与连续特征怎么喂进模型实际数据集里特征一般分两类类别特征和连续特征。以广告点击日志为例类别特征包括用户ID、广告ID、素材ID、行业ID、设备型号等连续特征包括出价、曝光时长、历史点击率等。我的做法是类别特征先做ID映射每个特征维护一个字典把原始字符串映射成连续整数ID未出现的值统一映射到0。数值特征做归一化连续特征用min-max或者log变换压缩到[0,1]区间避免量纲差异影响网络训练。准备样本时类别特征保存的是ID整数而不是one-hot向量真正转one-hot的工作交给Embedding层完成。这里有个很关键的工程习惯在训练开始前把每个类别特征的词典大小统计出来因为nn.Embedding初始化需要知道词表大小。如果是全部特征共享一个词表那就是共享Embedding模式Deep Crossing建议每个类别特征独立词表所以你需要为每个类别特征单独建一个nn.Embedding。3.2 核心代码Residual Unit与整体结构下面是一个简洁的PyTorch实现把Deep Crossing缩成几个可以直接改的类。import torch import torch.nn as nn import torch.nn.functional as F class ResidualUnit(nn.Module): def __init__(self, hidden_units, dropout0.0): super().__init__() self.fc1 nn.Linear(hidden_units, hidden_units) self.fc2 nn.Linear(hidden_units, hidden_units) self.dropout nn.Dropout(dropout) def forward(self, x): h F.relu(self.fc1(x)) h self.fc2(h) h self.dropout(h) return h x class DeepCrossing(nn.Module): def __init__(self, feature_dims, embed_dim32, hidden_units256, num_residual_units3, dropout0.0): super().__init__() # feature_dims: dict例如 {user_id: 100000, ad_id: 50000} self.embeddings nn.ModuleDict({ name: nn.Embedding(num_embeddingsdim, embedding_dimembed_dim) for name, dim in feature_dims.items() }) total_dim len(feature_dims) * embed_dim self.residual_blocks nn.Sequential(*[ ResidualUnit(hidden_units, dropout) for _ in range(num_residual_units) ]) self.projection nn.Linear(total_dim, hidden_units) self.scoring nn.Linear(hidden_units, 1) def forward(self, inputs): # inputs: dict of LongTensor每个都对应一个类别特征ID embedding_list [self.embeddings[name](inputs[name]) for name in inputs] stacked torch.cat(embedding_list, dim1) x F.relu(self.projection(stacked)) x self.residual_blocks(x) logit self.scoring(x).squeeze(1) return logit有几个地方要特别说明。第一个是projection层。残差单元要求输入输出维度相等而Embedding拼接后的维度比如4个特征每个32维拼接后128维通常不等于后面残差单元的宽度256。所以需要先用一个投影层把拼接向量映射到统一宽度再接残差块。第二个是残差块内部不降维。我倾向于所有残差单元的隐藏宽度都保持一致否则残差连接两侧维度对不上实现起来会很别扭。第三个是Embedding的初始化。PyTorch默认Embedding初始化是标准正态分布实际训练中我建议把Embedding参数换成更小的均匀分布初始化比如uniform_(-0.05, 0.05)。CTR特征ID大量是低频的初始化分布太宽会导致低频ID的向量在训练初期乱撞后续很难收敛。训练循环的代码反而没什么特别用二元交叉熵加Adam优化器就可以跑起来model DeepCrossing(feature_dims{user_id: 200000, ad_id: 80000}, ...) criterion nn.BCEWithLogitsLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3)3.3 训练细节优化器、学习率、批量大小Deep Crossing论文里使用的优化器是Adagrad初始学习率设在0.01量级。我在自己的数据集上对比过Adam和Adagrad发现Adam通常收敛更快尤其是在Embedding参数多、学习率敏感的情况下Adam的逐参数自适应特性让调参过程更省心。想完全复现论文的读者可以用Adagrad工程落地用Adam问题不大。batch size的选择对CTR模型影响很大。我的建议是从1024起步数据量特别大时可以用4096。batch太小梯度噪声大Embedding更新不稳定batch太大训练速度慢且模型容易收敛到尖锐极小值泛化能力差一点。另外注意训练时的负采样比例正负样本比例严重失衡会让模型输出的概率整体偏低统计AUC时还好看但线上校准就很痛苦。建议训练集内保证正样本比例不低于5%到10%。训练到最后一个epoch时不妨把Embedding学习率调低一个量级。这一步对Embedding向量的质量有明显帮助可以让低频ID在后期微调得更稳定我当时试了之后AUC有小幅提升你也可以试试。4. 实际业务落地时踩过的坑纸上谈兵阶段一切都很美好真正放到业务里跑各种小问题就冒出来了。我把自己踩过或者帮朋友排查过的坑挑几个最典型的写在这里。4.1 低频特征不收敛Embedding的冷启动问题CTR场景最大的特点之一就是幂律分布。大部分用户ID、广告ID出现的次数极少。一个Embedding向量如果只被更新过几十次基本无法学到有效语义它相当于一个随机向量不仅对预测没有帮助还会给后续网络带来噪声。我当时在一个广告推荐项目里发现线上新广告ID带来的曝光很少离线训练时所有低频广告ID的Embedding几乎都停留在初始值附近。这直接导致模型对新广告毫无区分能力冷启动阶段AUC下跌明显。几个实测有效的做法对出现次数小于阈值的ID做统一替换并入一个UNK桶让模型至少学到一个未知ID的通用表示。使用特征hash把原始ID映射到固定大小的hash空间用hash碰撞来共享参数适合需要及时上线新ID的广告场景。对低频ID的Embedding做正则约束例如限制L2范数避免它们偏离初始值太远。在训练时对ID做概率性dropout让模型不依赖单个ID对冷启动也有微弱帮助。4.2 深层残差网络也会过拟合第一次把残差单元从2层加到5层时离线AUC涨了一点我当时还挺开心结果上了AB实验线上实际CTR反而没涨。复盘时才意识到离线AUC的上涨部分来自模型对训练集里高维稀疏ID组合的死记硬背而这种记忆并不能泛化到未见过的新组合上。控制过拟合的思路按我的经验排序第一优先减少残差单元层数或隐藏宽度。Deep Crossing本身就不是越深越好2~3个残差单元已经能覆盖足够高的特征组合阶数再增加层数只是让模型容量增大。第二在Embedding输出上添加Dropout。有论文和工程实践都表明在Embedding层做dropout能显著提升稀疏模型的泛化能力。我在残差单元内部加了0.1~0.3的dropout效果比在最后加Dropout更直接。第三对Embedding参数做L2约束。这个操作相当于限制了ID向量的范数尤其是低频ID能有效减少记住噪声。4.3 线上pipeline与训练pipeline的一致性问题这个坑几乎每个团队都会遇到。训练时用的特征ID字典是按历史日志构建的线上推理时会出现训练期间从未见过的ID。如果线上直接把未知ID映射成0通常对应UNK理论上没问题但危险的是很多人没有在训练数据里显式构造未知ID的样本导致模型根本没见过这个桶线上遇到时只能输出随机初始化Embedding对应的结果。我的建议是训练时留出一部分最低频的ID把它们强制替换为UNK桶编号让模型在训练阶段就学会未知ID该给出什么样的保守预测。同时线上与训练共用同一份Embedding表不要各自维护一份词典否则一定会出现字典对不上的事故。用版本号管理Embedding表和特征映射配置线上加载时强制校验版本号是我目前用过最稳妥的方式。5. 复盘Deep Crossing的历史坐标和后来者很多东西的价值要放到历史坐标里看才清楚。Deep Crossing提出的2016年深度神经网络在推荐系统领域刚开始大规模出现之后的FNN、PNN、WideDeep、DeepFM等模型一个接一个涌出来。回头看Deep Crossing最大的贡献未必是它自身的效果而是它证明了Embedding多层非线性残差结构这个组合可以在大规模稀疏场景下稳定落地。5.1 和同期模型的定位差异为了讲清楚这些模型的区别我列一张自己整理的对比表模型特征组合方式显式低阶项关键结构Deep Crossing深层残差网络自动组合无Embedding 残差单元FNN先FM得到Embedding再上层DNN无依赖预训练EmbeddingFM初始化EmbeddingPNN在Embedding后加Product层做向量内积/外积有一定二阶显式交叉Embedding Product LayerWideDeepWide线性部分Deep隐式交叉有Wide线性组合Wide Deep双分支DeepFMFM捕获低阶DNN捕获高阶有FM代替WideFM Deep端到端联合训练这张表的读法是WideDeep和DeepFM都保留了显式的低阶特征项用来弥补深度网络对强记忆信号的弱化PNN则努力在Embedding向量之间直接引入乘积操作一步到位建模二阶向量交互DeepCrossing最纯粹它相信只要网络足够深高阶组合都能自动学出来。5.2 它的局限纯粹是把双刃剑。Deep Crossing的局限也很典型。第一没有显式的低阶交叉。残差网络虽然能学高阶组合但低阶的强信号比如某广告本身很优质这种主效应被淹没在深层非线性变换里反而没有线性模型那么敏锐。实践中观察到如果数据集里低阶信号主导Deep Crossing在AUC上可能只比LR高一点而DeepFM因为带着FM的低阶项往往表现更稳健。第二Embedding维度成为表达瓶颈。整个模型的信息容量很大程度由各Embedding向量维度决定而残差单元是在拼接后的长向量上做变换计算量和参数量都随着特征数量线性膨胀。特征数量一多训练和推理成本明显上升。第三没有注意力机制。今天大家习以为常的根据用户历史行为动态加权不同特征的机制在Deep Crossing里是缺席的。它把每个特征视为同等参与组合不会刻意强化当前样本更需要哪些特征。5.3 对今天模型的遗产尽管Deep Crossing现在看起来结构简单但它的范式影响力无处不在。把稀疏ID转换为Embedding再通过多层非线性网络做特征交互这个过程几乎成为了推荐/广告深度学习模型的标准底座。今天的主流模型要么在这个底座上加了注意力比如注意力跨网络模型要么加了序列建模把行为序列通过GRU/Transformer编码后拼进来要么加了低阶结构修正比如DeepFM的FM分支、WideDeep的Wide分支。我自己在后续好几个项目里做模型迭代时也是以Deep Crossing的Embedding残差网络为起点逐步加入行为序列、注意力、多任务塔。这个演进路径说明了它作为地基的可靠性你不需要推翻它只需要在它之上按场景叠加新机制。从实践角度看如果你刚开始尝试自动特征组合我建议直接复现一个Deep Crossing作为baseline跑通后再对比DeepFM等进阶模型。这样你能清楚地看到低阶项、交互项、注意力机制分别带来了多少增量收益而不是一开始就上一个黑盒大模型。最后说一点个人体会。如果你刚接触CTR模型不要急着引入各种花哨的新结构。先把Deep Crossing这类基础模型吃透理解Embedding、Stacking、残差单元各自到底在解决什么问题再去看后面的模型就有一种万变不离其宗的感觉。我至今仍会在我负责的系统中保留一个Deep Crossing版本作为长期对照模型用来衡量新增模块的真实效果。它的价值不会随时间褪色反而会成为你理解整个深度推荐模型演化史的坐标系。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表