资讯详情

资讯详情

TensorFlow 2.0实战Wide Deep模型:原理、代码与调优全解析

TensorFlow 2.0入门到进阶系列写到现在前面已经跑通了DNN、CNN、RNN这些基础结构。这一篇来聊一个在推荐系统、广告CTR预估场景里非常经典而且至今还在大量使用的模型——Wide Deep。我最早接触这个模型还是在某次技术交流会上一个做应用商店推荐的哥们儿跟我抱怨用户行为序列、上下文特征都加进去了深度模型的AUC也提了但线上就是觉得推荐结果太飘老推一些用户完全没兴趣的新东西。后来他把模型换成Wide Deep才把这个问题压下去。这个故事特别能说明Wide Deep的价值它不是要取代深度模型而是给深度模型配一个记忆搭档。这篇文章我会帮你把Wide Deep的核心原理拆透把TensorFlow 2.0下的建模细节讲清楚再把我实操中踩过的坑和排查思路整理成一份可以直接对照的清单。不管你是刚开始接触推荐模型还是已经在用DNN做预估想进一步优化这篇都适用。1. Wide Deep模型到底为什么会出现1.1 从推荐系统的记忆与泛化说起推荐系统里有两个词出镜率极高记忆Memorization和泛化Generalization。很多人第一次听觉得是一对玄学概念其实用大白话讲特别简单。记忆就是模型能牢牢记住那些已经发生过、且被验证过有效的组合。比如某个用户连续三次点击了同一款游戏那下一次推荐里直接把这个游戏再推给他就是典型的记忆。这类行为是稀疏的、确定性的特征维度之间几乎没有交叉关系就是单纯A发生过所以A再出现时应该被放大。泛化则是模型能举一反三。用户没点过那款游戏但他点过同类型的塔防游戏而且那款游戏的题材跟他喜欢的科幻题材一致模型能推测出这个人可能也喜欢这款这就是泛化。这类逻辑依赖特征之间的交叉组合光靠单特征是不够的。传统做法里逻辑回归LR是记忆的扛把子。它简单、可解释性强、训练快喂进去的稀疏特征经过手工交叉以后它能把每一个组合的权重都老老实实学到。但它的问题也很明显泛化能力约等于零。你不可能把所有交叉组合都提前构造出来而且随着特征维度上升手工特征工程的工作量会把人压垮。深度模型DNN是泛化的好手。Embedding层把稀疏的高维特征压成稠密的低维向量MLP层自动学习特征之间的非线性交互。理论上只要Embedding表达得好模型就能泛化到没见过的新特征组合上。但DNN的问题在于它对稀疏的、确定的、长尾的特征组合不太敏感。比如某个用户历史上只点过一次某款小众游戏这个信号在Embedding空间里会被稀释掉模型反而学不干净。更麻烦的是深度模型在冷启动或者用户行为极少时容易推出一堆看起来合理但用户根本不认识的东西这就是前面说的推荐太飘。Wide Deep的出发点就是别让LR和DNN互相看不起干脆把两者焊在一起。Wide侧负责记忆Deep侧负责泛化两头各干各的最后拼接起来一起输出。1.2 模型的整体结构与数据流向Wide Deep这个名字从结构上就能看出来它由一条Wide分支和一条Deep分支组成最后汇合到同一个输出节点。Wide侧就是一个广义线性模型也就是LR。输入通常是稀疏的原始特征也可以加入手工构造的交叉特征。它做的事情就是对输入特征做加权求和然后送到输出层。这一侧的特点是简单直接权重更新快对高频、确定性的信号响应非常灵敏。Deep侧是一个标准的Embedding MLP结构。离散特征先过Embedding层变成稠密向量连续特征做归一化后直接拼接然后过几层全连接网络每一层带ReLU激活和BatchNorm之类的增强。两条分支的输出在最后一步拼接Concat再经过一个带Sigmoid激活的全连接层输出一个0到1之间的预测值。对于CTR预估来说这个值就是点击概率对于多分类来说可以换成Softmax输出。整个数据流向可以用一句话概括相同的输入样本一份走Wide通道做线性加权一份走Deep通道做非线性变换最终汇合输出。训练时两个分支的梯度各自回传互不干扰但又共享同一个损失函数和标签。这种设计最妙的地方在于Wide侧和Deep侧的优化目标一致但特征表达方式不同所以它们在训练过程中会自然形成分工。Wide侧抓住这个用户以前就喜欢这个Deep侧补充这个用户可能也喜欢这些。两者不是简单的平均关系而是让模型自己去学什么时候该信记忆、什么时候该靠泛化。1.3 为什么在TensorFlow 2.0里重写TensorFlow 1.x里实现Wide Deep其实挺痛苦的。你要手动维护两个计算图的初始化、训练、评估流程还要处理Estimator那套封装。记得早期还得用tf.contrib这个模块版本一升级代码就废。做特征列的时候稍不注意就掉进稀疏特征和Embedding的维度泥潭里。TensorFlow 2.0全面拥抱Keras之后这事简单了不止一个量级。函数式APIFunctional API天然支持多输入多输出Wide和Deep两条分支可以用两个独立的输入层定义最后合并成一个输出层compile和fit直接把整个流程包圆。tf.feature_column也稳定了许多数值列、类别列、Embedding列都可以直接接进Keras的Input层。所以本节的代码示例我全部基于TensorFlow 2.0 Keras函数式API来写。你本地装好2.0以上版本就能直接跑不需要任何旧版兼容代码。2. TensorFlow 2.0中Wide Deep建模核心细节2.1 特征类型分析与处理方案选型实操里决定模型效果上限的往往是特征处理而不是网络结构。Wide Deep之所以灵活就是因为它对不同类型的特征有天然的分工。我把推荐场景里常见的特征分成三大类每一类在Wide Deep里的归宿都不一样。第一类是离散ID类特征比如用户ID、物品ID、品类ID、城市ID。这类特征的特点是基数大、取值稀疏、彼此之间没有天然的大小关系。它们的归宿是Deep侧的Embedding层。每个ID映射成一个稠密向量向量维度一般取16到64之间。ID类特征原则上不需要进Wide侧因为Wide侧本质是做线性加权高基数的one-hot向量进来会让权重矩阵巨大且稀疏训练效率很低。第二类是连续数值类特征比如用户活跃天数、物品的累计点击次数、价格、时长。这类特征在Deep侧直接做归一化后拼进特征向量。归一化推荐用Z-Score或者Min-Max具体看分布形态。在Wide侧也可以放但建议做分桶Bucketize处理把连续值切成几个区间变成离散的桶ID再转成one-hot或embedding。这样Wide侧能捕捉到数值落在某个区间这种分段信号比直接用原始数值做线性加权更稳。第三类是交叉组合类特征这是Wide侧的主力。比如用户性别男和物品类别游戏交叉成一个新特征只有同时满足时才为1。这种特征在Deep侧也能学但需要经过Embedding和MLP的层层传递效率远不如在Wide侧直接塞给线性模型。手工构造少量高价值的交叉特征放进Wide侧是Wide Deep调优的关键操作。特征处理方案确定之后下一步就是在TensorFlow 2.0里把它们组织成代码。这里我强烈建议先用Pandas做探索和清洗再用tf.feature_column做特征描述最后组Input层。不要试图在tf.data流水线里做复杂的特征工程调试会非常痛苦。提示特征列feature_column是连接原始数据和模型之间的桥梁。它在TensorFlow 2.0里依然是核心抽象但API风格比1.x清爽很多。建议把每个特征的处理方式单独定义一个feature_column方便复用和排查。2.2 Keras函数式API实现Wide与Deep双分支TensorFlow 2.0里实现Wide Deep核心是Keras的函数式API。它允许你定义多个输入让它们各自经过不同的变换最后汇合到一个输出上。我先把整体骨架画出来Wide分支的输入层接收已经完成交叉处理的稀疏特征Deep分支的输入层接收原始特征先经过Embedding和数值归一化处理再过几层全连接两个分支的输出在最后做Concatenate接一个Dense(1)输出。这里有一个非常容易搞错的点Wide侧不是随便把特征拼一起就行它拿到的输入必须是人工构造好的交叉特征。如果你直接把原始特征一股脑塞给Wide侧那它就是一个普通的线性层交叉能力为零。我当时第一次实现的时候也踩过这个以为模型会自动学交叉结果Wide侧跟摆设一样Deep侧也没学到足够好的泛化整体效果跟纯DNN没啥区别。Deep侧的Embedding要注意维度设计。对于基数vocabulary size非常大的ID类特征Embedding维度可以按经验公式来定dim 6 * log2(vocab_size)或者直接用4次方根。比如500万基数的特征开4次方大概是47取个64左右比较合适。维度太小会欠拟合维度太大会让参数量爆炸训练和上线都难受。两条分支的汇合方式也有讲究。最常见的是Concatenate拼接然后过一个Dense层输出。也有人在拼接后加一个BatchNorm尤其是两边的数值尺度差异很大的时候。Wide侧输出接近线性加权的原始值Deep侧输出是ReLU之后的特征尺度可能完全不同加BatchNorm能帮梯度更稳。我实测下来加了之后收敛速度略有提升但不加也不至于崩可以作为一个可选配置。2.3 损失函数、优化器与评估指标的选择Wide Deep最常跑的任务是CTR预估和二分类所以损失函数用binary_crossentropy就好。TensorFlow 2.0的Keras里直接指定lossbinary_crossentropy即可不需要自己做one-hot编码只要标签是0和1的数值就行。优化器这里有个经验不要用SGD用Adam或者Adagrad。Wide侧的特征非常稀疏SGD在稀疏梯度上收敛慢且容易震荡。Adagrad对稀疏特征友好是Google原论文里用的优化器Adam则更通用收敛快就算特征分布没那么极端也能稳稳跑。我的建议是优先试Adam学习率从0.001开始如果Wide侧贡献的信号比较多再换Adagrad对比。评估指标方面除了accuracy这种全局指标强烈建议额外输出AUC。CTR预估里数据通常极不平衡——点击率可能只有百分之几accuracy会虚高AUC才能真实反映排序能力。TensorFlow 2.0里可以用tf.keras.metrics.AUC()在compile阶段以metrics列表传进去就行。如果想进一步看分层的表现还可以按特定用户群体或物品类别分组算AUC这对定位问题非常有帮助。注意accuracy只适合给你一个模型在正常工作的直觉判断不能用于业务决策。排序类任务推荐、广告永远以AUC、GAUC这些指标为准。3. 从数据到模型完整实操流程3.1 准备演示数据集与特征列配置为了让你能直接照着跑我构造一个简化的推荐场景数据集。假设我们要预测用户是否会点击某个物品样本包含用户IDhigh-cardinality离散特征物品IDhigh-cardinality离散特征用户活跃天数连续特征物品平均评分连续特征品类ID中等基数离散特征标签是1表示点击0表示未点击。我生成一份模拟数据共5万条样本。注意这只是一个演示用的玩具数据生产环境的特征量和样本量都会大得多但建模流程完全一致。代码里为了可复现我加了随机种子。先定义特征列。这是整个建模过程中最需要耐心的部分每一个特征怎么处理、进Wide还是Deep都要在这里明确。import tensorflow as tf # 假设数据集中这些列存在 user_id tf.feature_column.categorical_column_with_vocabulary_list( user_id, [fu{i} for i in range(1000)]) item_id tf.feature_column.categorical_column_with_vocabulary_list( item_id, [fi{i} for i in range(5000)]) category_id tf.feature_column.categorical_column_with_vocabulary_list( category_id, [fc{i} for i in range(100)]) # Deep侧ID类特征转成Embedding user_emb tf.feature_column.embedding_column(user_id, dimension16) item_emb tf.feature_column.embedding_column(item_id, dimension32) category_emb tf.feature_column.embedding_column(category_id, dimension8) # 连续特征Deep侧直接使用 active_days tf.feature_column.numeric_column(active_days) avg_rating tf.feature_column.numeric_column(avg_rating) # Wide侧手工交叉特征 # 这里做一个“品类ID 用户活跃度分段”的交叉 active_bucket tf.feature_column.bucketized_column( active_days, boundaries[7, 30, 90]) cross_feature tf.feature_column.crossed_column( [category_id, active_bucket], hash_bucket_size500) # 汇总 deep_columns [user_emb, item_emb, category_emb, active_days, avg_rating] wide_columns [cross_feature]代码里有个细节值得多说一句crossed_column会把两个离散特征交叉成一个新的稀疏ID。因为原始特征各有自己的词汇表交叉后的组合空间巨大所以用hash_bucket_size做哈希分桶。这个值不能太小否则碰撞严重也不能太大否则Wide侧参数太多训练慢。比如这里品类100个乘以活跃分桶4个实际有效组合只有400种我取500留一点余量。3.2 构建Wide Deep模型主体特征列定义好以后接下来的核心工作就是用Keras函数式API把两条分支搭起来。这里要注意TensorFlow 2.0的接口习惯keras.layers.Input创建输入层tf.feature_column通过keras.layers.DenseFeatures接入。from tensorflow import keras # Wide侧输入交叉特征 wide_inputs keras.layers.Input(shape(1,), namewide_input) # Deep侧输入原始特征 deep_inputs { user_id: keras.layers.Input(shape(1,), nameuser_id), item_id: keras.layers.Input(shape(1,), nameitem_id), category_id: keras.layers.Input(shape(1,), namecategory_id), active_days: keras.layers.Input(shape(1,), nameactive_days), avg_rating: keras.layers.Input(shape(1,), nameavg_rating), } # Wide分支直接把交叉特征输入接一个线性层 wide_feature_layer keras.layers.DenseFeatures(wide_columns) wide_out wide_feature_layer({wide_input: wide_inputs}) # Deep分支把特征列转换为稠密特征 deep_feature_layer keras.layers.DenseFeatures(deep_columns) deep_out deep_feature_layer({ user_id: deep_inputs[user_id], item_id: deep_inputs[item_id], category_id: deep_inputs[category_id], active_days: deep_inputs[active_days], avg_rating: deep_inputs[avg_rating], }) # Deep分支接MLP x keras.layers.Dense(128, activationrelu)(deep_out) x keras.layers.BatchNormalization()(x) x keras.layers.Dropout(0.3)(x) x keras.layers.Dense(64, activationrelu)(x) x keras.layers.BatchNormalization()(x) x keras.layers.Dropout(0.3)(x) # 合并两条分支 concat keras.layers.Concatenate()([wide_out, x]) output keras.layers.Dense(1, activationsigmoid)(concat) model keras.models.Model( inputs[wide_inputs] list(deep_inputs.values()), outputsoutput )这里有一个非常值得注意的细节Wide分支的DenseFeatures输入需要和前面定义的交叉特征结构一致。如果你在feature_column里对active_days做了bucketized_column那么输入的时候应该传原始数值列由DenseFeatures内部完成分桶转换。给Wide侧的输入是原始特征值但对应的feature_column已经是交叉处理后的形式。很多新手在这里搞混结果输入维度对不上直接报错。BatchNorm和Dropout是Deep侧的标准配置。Dropout建议放在激活之后这跟传统理解放在全连接层之前稍有差别但我在TensorFlow 2.0里实测放在激活之后对防过拟合更有效。你也可以按自己的习惯调整只要记住Deep侧过拟合速度比Wide侧快得多Dropout系数设在0.2到0.5之间比较合理。3.3 训练、评估与模型保存模型建好之后编译和训练就简单了。model.compile( optimizerkeras.optimizers.Adam(learning_rate0.001), lossbinary_crossentropy, metrics[accuracy, keras.metrics.AUC(nameauc)] ) history model.fit( train_dataset, validation_dataval_dataset, epochs20, batch_size1024 )数据集我建议用tf.data.Dataset.from_tensor_slices构造。你要确保每一步的输入字典和模型定义里Input层名称一一对应。这一步是TensorFlow 2.0里最容易翻车的地方Keras的输入字典key必须跟keras.layers.Input的name完全一致大小写都不能错。训练完成后保存模型用model.save(wide_deep_model)。这个格式是SavedModel里面包含了网络结构和权重上线时可以整个加载。如果你想在保存前看看模型的参数量和结构用model.summary()它会清清楚楚列出每一层以及参数量。我自己在跑这个模型的时候epochs设20batch_size设1024大概几轮之后训练AUC就能到0.85以上。如果你的数据更稀疏收敛会慢一些。关键看验证集AUC是否跟着涨如果训练AUC一路涨但验证AUC停滞甚至掉头那大概率是过拟合了优先加Dropout、降网络层数、加正则化。提示训练过程中不要频繁中断去改模型。先把一版完整流程跑完记录指标变化再基于实验结果做调整。我见过太多人训练到一半就去加特征、改结构最后根本分不清是哪个操作带来的收益。4. 从TensorFlow 1.x迁移与踩坑实录4.1 TF 1.x到TF 2.0的核心差异对照如果你的团队还在维护老代码或者你是在旧项目基础上做迭代一定会遇到从TF 1.x迁移到TF 2.0的问题。Wide Deep在TF 1.x里最经典的是tf.estimator和tf.contrib.learn那套实现到了TF 2.0这些接口要么废弃要么改头换面。我整理了一张对照表方便你快速定位问题功能点TensorFlow 1.xTensorFlow 2.0图构建tf.Graph tf.SessionKeras Model / tf.functionWide Deep封装tf.contrib.learn.WideDeepModel手写Keras函数式API特征列tf.feature_column基本一致tf.feature_columnAPI更稳定训练estimator.train(...)model.fit(...)评估estimator.evaluate(...)model.evaluate(...)模型导出estimator.export_saved_modelmodel.save(...)执行模式静态图Eager模式动态图核心差异一句话就能概括TF 1.x你得先建图再跑数据TF 2.0是边跑边建图。这个变化让调试友好了很多但也让某些老接口直接失效。比如tf.contrib整个模块在TF 2.0里被移除了你不能再指望contrib.learn提供现成的WideDeepModel必须自己用Keras搭。我迁移老项目的心得是不要试图一行行改老代码直接在TF 2.0里重新实现一份模型。老代码的逻辑可以参考但接口替换的成本跟重写差不多而且重写还能顺手优化结构和特征处理。4.2 高频报错与排查方案速查表我在反复实现和调试Wide Deep的过程中整理了下面这些高频问题。你可以直接对照排查。问题表现常见原因解决方案输入字典key不匹配报错特征列名和Input层name不一致统一命名规范调试时打印feature_column名逐一核对DenseFeatures输入维度和数据不一致数据里列名写错或类型不对打印一条样本数据核对列名、dtype必要时做cast训练时AUC不涨特征列配置错误比如Embedding维度太低或Wide侧没有有效交叉特征检查特征列清单确认Wide侧进入的是交叉特征而非原始特征验证AUC与训练AUC差距过大Deep侧过拟合增加Dropout率、减小网络层维度、加入L2正则化预测时输出全部为同一值标签泄漏或数据处理bug检查训练/验证数据切分是否有时间穿越标签是否被错误拼接到特征里模型体积太大Embedding维度太高或hash_bucket_size过大按经验公式缩小维度验证对AUC影响后确定训练速度慢特征列里做了大量实时预处理把能提前算的特征提前算好Dataset用batch预取prefetch这张表里我想特别强调一个最隐蔽的坑标签泄漏。推荐场景里如果你不小心把用户是否点击这个信息以任何形式放进了特征里比如该物品近30天点击率在某些实时更新的场景里可能就包含了目标信息模型会学到看到这个特征就输出1的捷径离线AUC会高得离谱线上完全失效。排查方法很简单对训练样本做一次特征重要性分析如果某个特征的重要性异常突出优先级高于所有其他特征就要怀疑它是不是泄漏了。4.3 训练过程中的典型诡异现象除了能查表的报错有些问题属于表面正常但结果诡异的类型我挑两个最典型的说说。第一个诡异现象训练loss下降正常但验证集AUC纹丝不动。这种一般不是模型代码的问题而是数据分布有问题。常见于训练集和验证集来自不同时间段、不同渠道导致两边的特征分布差异巨大。你可以用tf.data在构造验证集时做与训练集完全一致的随机打乱和时间切分确保两边同分布。再不行就把数据集重新切分让验证集来自跟训练集相近的时间窗口。第二个诡异现象Wide侧权重几乎不更新。表现为训练过程中Wide部分的梯度极小甚至为0。原因通常是交叉特征太稀疏大部分样本的交叉ID为0梯度传不过去。解决办法是检查crossed_column的hash_bucket_size是否合理以及交叉特征是否真的在数据中大量出现。如果大多数样本的交叉特征为空这条特征对模型贡献就很有限不如删掉换一个更有区分度的交叉组合。注意Wide侧的作用是强制记忆。如果你的业务场景里用户行为非常稀疏几乎无法形成有效的高频交叉信号Wide侧收益会很小。这时候要把重心放在Deep侧的特征表达上而不是硬凑交叉特征。4.4 特征选取的原则与顺序验证Wide Deep调优里特征选取的优先级跟普通DNN略有不同。我自己的经验是按下述顺序来第1步把ID类特征和连续特征加入Deep侧先把Deep侧跑通得到一个基础AUC。第2步在Wide侧加入1到2个最有业务直觉的交叉特征观察AUC是否提升。第3步确认Wide侧确实有效之后再逐步增加交叉特征但严格控制数量。Wide侧不是越多越好每增加一个交叉特征都会带来参数量和过拟合风险。第4步对Deep侧做结构调优比如调Embedding维度、网络层数、Dropout。第5步最后再做一次全量特征审查删除长期没有任何增益的特征。每一步都要通过AUC变化做判断不要凭感觉。我的一般判断标准是验证集AUC提升0.1%以上才认为有效低于0.05%可以直接放弃该项改动。CTR场景里0.1%的AUC提升已经足够影响业务收入了所以这个标准并不苛刻。5. Wide侧与Deep侧的权重平衡与业务理解5.1 模型可解释性的价值Wide侧的权重解读很多人学Wide Deep只关注AUC忽略了它还有一个巨大优势Wide侧权重可以被解读。这一点在实际业务里太重要了。比如你构造了一个交叉特征用户性别男物品类别游戏训练完成后这个特征在Wide侧的权重如果是正的说明男性用户对游戏类物品确实有天然偏好如果权重接近0说明这个交叉没有增量信息如果是负的说明存在负向关联。你把这些权重列出来直接就能跟运营同学聊我们平台上这个交叉特征信号很强建议把它做成运营策略。这在以深度学习为主的黑盒模型里很难做到。Deep侧也能做一定的解释比如看某个特征的Embedding向量分布或者用SHAP分析但成本比Wide侧高得多。所以我的建议是每做一版Wide Deep模型都顺手把Wide侧权重导出成一张表格留档这对后续特征迭代、异常排查都特别有帮助。这里有一个小技巧TF 2.0里获取Wide侧权重可以在model.layers里找到对应DenseFeatures层取它的kernel变量。打印出来跟特征列名字对齐就能看到每个交叉特征的权重。5.2 两个分支信号冲突时的取舍策略Wide侧和Deep侧虽然分工但两边的预测可能出现打架Wide侧认为应该推荐ADeep侧认为应该推荐B。最终输出被Sigmoid压到一起后结果到底偏向谁取决于权重初始化、训练数据分布和模型收敛状态。遇到这种情况我的处理思路不是去调网络的拼接权重而是回到数据层面判断这个冲突是记忆信号更可靠还是泛化信号更可靠举个例子某个用户过去10次点击全部集中在某几个固定的物品类别上Wide侧的交叉特征会给出强信号同时用户资料显示他最近搜索过一些新领域的关键词Deep侧通过Embedding和MLP能捕捉到这个泛化信号。两者冲突时我们应该相信哪个业务上通常更倾向于相信用户的近端行为记忆因为搜索行为可能存在偶然性。所以我会在Wide侧增加这类高频交叉特征的权重因子或者通过调整特征构造方式比如把搜索行为单独做成Wide侧交叉特征来强化它所代表的确定性信号。如果两边信号冲突非常频繁且难以判断哪个更合理我会做一个消融实验训练三个模型一个只有Wide一个只有Deep一个Wide Deep。分别看它们在具体样本上的表现差异这样能清楚知道是哪个分支在拖后腿还是拼接方式有问题。提示不要试图在模型结构里用魔法权重解决信号冲突。结构层面的调整比如把某些特征只放进Wide侧或只放进Deep侧比调超参数有效得多。6. 扩展应用从推荐系统到通用预估场景6.1 结合某新能源车企用户分群的实际案例Wide Deep的应用场景远不止应用商店和广告CTR。我最近在做一个汽车行业的用户意向分群项目就用到了它效果比之前的纯DNN和GBDT都稳定。项目背景是某新能源车企想根据用户的行为数据预测用户对某一细分车型的意向等级。数据里有用户注册时长、试驾次数、浏览车型记录、所在城市级别等特征。这个任务跟CTR预估很相似但标签不是点击而是用户填写的意向等级高/中/低。在这个场景里Wide侧我放了几个经过业务验证的交叉特征比如城市级别一线 关注车型SUV、用户注册天数180 试驾次数1。这些特征来源于销售团队的业务经验对实际转化有很强的先验关联。Deep侧则放了用户行为序列的统计量、车型偏好Embedding等特征用来捕捉更复杂的偏好模式。结果很有意思纯Deep模型在整体AUC上稍好一点但在最近30天有试驾行为这个高意向人群上Wide Deep明显更好。原因就是Wide侧把这些业务先验强特征直接焊进了模型保证了高确定性信号的优先响应。事后分析如果只用Deep模型这些强业务信号会在Embedding里经过多层非线性变换后被扭曲反而学不到位。这个案例给我们的启发是当业务方能够提供经过市场验证的强先验规则时把它们做成Wide侧交叉特征几乎总是能带来稳定提升。6.2 冷启动场景下的Wide Deep改进Wide Deep的经典短板是冷启动。新用户没有足够行为历史Wide侧的记忆信号几乎为空Deep侧虽然能靠用户画像泛化但效果也有限。针对这个问题我见过几种有效的改造思路。第一种是把Wide侧的特征从用户-物品交叉扩展为用户侧交叉和物品侧交叉。比如新用户7天 注册渠道某渠道、物品新上线3天 品类某品类这样即使没有用户-物品交互记录Wide侧也能源源不断提供记忆信号。第二种是给Deep侧引入更多与行为无关的静态特征比如年龄区间、城市级别、设备型号、注册来源。这些特征容易过拟合但由于冷启动阶段数据稀疏Deep侧相比Wide侧更容易泛化加这些特征能显著提升新样本的表现。第三种是结合强化学习或探索策略的思路在训练样本里刻意加重冷启动用户样本的权重让模型更多地学习这类样本的模式。但这个做法要谨慎权重设置不合理会导致模型在其他群体上衰退。注意冷启动阶段不是说Wide侧不重要了。恰恰相反由于数据极少模型能依赖的确定性知识就更珍贵。关键是交叉特征的设计要从用户×物品转向用户自身×环境场景让模型在没有行为时也能借用群体经验。7. 几轮迭代之后的个人心得最后聊几点我在这个模型上反复实践后形成的个人体会都是踩坑踩出来的可能比前面所有代码都值钱。第一Wide侧不要贪多。我见过一上来就堆十几个交叉特征的结果训练慢、过拟合、权重解释性也变差。交叉特征贵精不贵多三五个已经很多了关键是每一个都有明确的业务假设支撑。一个冷冰冰的统计显著特征价值远不如一个有业务逻辑的交叉特征。第二Deep侧再牛也别扔Wide侧。很多人到后面沉迷调Deep侧的网络结构觉得AUC还有提升空间就把Wide侧晾在一边。实际业务里用户行为中存在大量确定性重复信号这些信号由Wide侧直接捕捉效率远高于Deep侧隐式学习。如果你发现Wide侧对最终结果几乎没贡献问题往往不在模型结构而在交叉特征设计上。第三训练过程要盯GAUC这类分组指标。整体AUC提升不代表每个细分人群都变好了。我遇到过整体AUC上涨、但某个人群比如新用户AUC反而下降的情况。如果业务上很看重这部分人群就需要对训练样本做加权或者单独评估这个人群的表现。第四上线推理时要特别小心。模型训练时输入的是Pandas DataFrame或者tf.data上线时通常走的是特征平台格式可能完全不一样。我踩过一次大坑训练时特征列类型是int64上线时某个ID字段被读成了string结果线上预测结果乱飞。建议在模型上线前构造一批和训练数据分布完全一致的测试样本用SavedModel加载后逐条比对输出。第五Wide Deep的变体很多但你先别急着上变体。DeepFM、xDeepFM这些模型我会在后面的章节里单独展开但它们的底层逻辑里都有Wide Deep的影子。把Wide Deep吃透了后面学任何变体都是顺水推舟的事。先把这个模型的训练、调优、排查全流程跑通再考虑复杂度升级这是我在推荐模型路上最稳的一条路线。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →