恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

TensorFlow子类API实战:从零实现widedeep推荐模型

  • 首页
  • 资讯中心
  • /
  • TensorFlow子类API实战:从零实现widedeep推荐模型

相关资讯

monodepth2训练机制深度解析:多尺度损失与光度一致性设计 2026/10/16 11:53:04
告别开会 2 小时整理 3 小时:2026 实测 5 款 AI 会议纪要效率神器与 TaoToken 统一 Key 接入 2026/10/16 11:53:04
图卷积神经网络在药物-靶点相互作用预测中的工程实践 2026/10/16 11:53:04

最新资讯

Deepseek润色指令:从模板库到质控清单的进阶指南
深度卷积神经网络图像去噪:残差学习与PyTorch工程实践
HMM、CTC、RNN-T:语音识别对齐机制的演进之路
图神经网络GNN入门:从非欧式数据到聚合更新机制
字符级RNN文本生成实战:从数据预处理到采样调优的完整指南
从零搭建AI系统权限控制:认证、授权与审计实战教程

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

TensorFlow子类API实战:从零实现widedeep推荐模型

发布时间:2026/10/16 11:53:04
TensorFlow子类API实战:从零实现widedeep推荐模型 1. 为什么还要折腾子类APITensorFlow 2.0 把tf.keras扶正之后搭模型的路子一下子多了起来Sequential、Functional、Subclassing 三种写法并存。很多刚上手的朋友会问既然 Sequential 三行就能堆一个网络Functional 画个图就能搞定多输入多输出为什么还要费劲去写子类这个问题我在第一次接触 widedeep 的时候也纠结过。先说结论widedeep 这种两条腿走路的结构用子类 API 写是最顺手的。原因不复杂——wide 部分和 deep 部分吃的是不同的特征走的是不同的前向路径最后才把 logits 加起来。Functional API 当然也能写但一旦你想在 forward 里塞点自定义逻辑比如动态加权、条件分支、特征交叉的中间处理Functional 就会开始别扭而子类 API 天然就是我自己定义call里发生什么自由度最高。这篇文章我打算把子类 API 实现 widedeep 这件事从头到尾捋一遍。适合谁看已经会写 Sequential、知道Dense、Embedding大概怎么回事但一碰到多输入 自定义前向就卡壳的朋友。看完你应该能自己动手搭一个能跑的 widedeep并且知道每一行为什么这么写。我会把踩过的坑、参数怎么定、调试怎么查都摊开讲尽量做到你照着抄就能跑通。widedeep 这个模型本身是推荐系统里的经典结构2016 年那篇论文之后被广泛用在 CTR 预估场景。它的核心思想用一句话概括wide 部分负责记忆memorization记住那些高频共现的特征组合deep 部分负责泛化generalization通过 embedding 把稀疏特征映射到低维稠密空间挖掘没见过的组合。两者互补一个管见过的一个管没见过的。2. widedeep 的结构拆解与选型考量2.1 wide 侧和 deep 侧到底各管什么先把结构讲清楚不然后面写代码就是照猫画虎。wide 侧本质上是一个广义线性模型输入是原始特征和交叉特征输出是w·x b。它的强项是记忆能力比如用户装了某类应用 AND 当前是晚上八点这个组合在训练集里出现过很多次且转化率高wide 侧就能通过这个交叉特征直接把这个模式记下来。缺点是它没法泛化训练集里没出现过的组合权重就是零预测不出来。deep 侧是一个前馈网络稀疏的类别特征先过Embedding层变成低维稠密向量然后拼接所有特征过几层全连接。它的强项是泛化即使某个特征组合没在训练集里出现过embedding 之间的相似性也能让模型给出一个合理的估计。缺点是容易过拟合而且对高频的、明确的特征组合记忆不如 wide 侧精准。两者最后把输出 logits 相加过一个 sigmoid 得到最终概率。注意是logits 相加不是概率相加这个细节很多人第一次写会搞错。2.2 为什么选子类 API 而不是 Functional我拿 Functional API 试过同一个结构能写但有两个地方很难受。第一个是特征处理逻辑的嵌入。wide 侧经常需要做特征交叉比如把城市和品类两个类别特征拼成一个新特征。在 Functional 里你得在输入层外面先做好交叉再喂进去模型定义和数据处理被割裂成两块。子类 API 里你可以在call里直接对输入做处理模型是自包含的。第二个是动态结构。有些场景下 wide 侧的权重需要根据 batch 内的统计量动态调整或者 deep 侧的层数想根据配置灵活变化。Functional 是静态图定义完就固定了子类 API 的call是每次前向都执行的 Python 代码想加什么逻辑加什么逻辑。当然子类 API 也有代价它不能直接用model.summary()看到完整结构因为 Keras 不知道你的call里到底连了什么。想看结构得自己打印或者用tf.keras.utils.plot_model配合expand_nested但效果也一般。这是自由度的代价心里有数就行。2.3 输入数据的组织方式widedeep 是多输入模型wide 侧和 deep 侧吃的特征不一样。我一般这样组织wide 输入稀疏的、需要交叉的类别特征通常用one-hot或者multi-hot表示维度可能很高但很稀疏。deep 输入需要 embedding 的类别特征 连续的数值特征。在子类 API 里call的签名可以写成call(self, inputs)inputs是一个字典或者列表。我个人偏好字典因为可读性好inputs[wide]、inputs[deep_cat]、inputs[deep_num]一目了然调试的时候不容易搞混顺序。列表虽然省事但一旦特征多了第几个对应哪个全靠记容易出错。3. 子类 API 实现的核心细节3.1 类的骨架怎么搭先看整体骨架我习惯把 wide 和 deep 拆成两个子模块主模型负责调度。import tensorflow as tf from tensorflow.keras import layers class WidePart(layers.Layer): def __init__(self, wide_dim, **kwargs): super().__init__(**kwargs) self.wide_dim wide_dim self.linear layers.Dense(1, use_biasTrue, namewide_linear) def call(self, wide_inputs): # wide_inputs: [batch, wide_dim] return self.linear(wide_inputs) class DeepPart(layers.Layer): def __init__(self, vocab_sizes, embed_dim, num_dense, hidden_units, **kwargs): super().__init__(**kwargs) self.embeddings [] for i, vs in enumerate(vocab_sizes): self.embeddings.append( layers.Embedding(input_dimvs, output_dimembed_dim, namefemb_{i}) ) self.dense_layers [layers.Dense(u, activationrelu) for u in hidden_units] self.out layers.Dense(1, namedeep_out) def call(self, cat_inputs, num_inputs): # cat_inputs: list of [batch, 1] tensors emb_list [] for i, x in enumerate(cat_inputs): emb_list.append(self.embeddings[i](x[:, 0])) x tf.concat(emb_list [num_inputs], axis-1) for layer in self.dense_layers: x layer(x) return self.out(x)这里有个细节值得说Embedding层的输入维度是词表大小输出维度是 embedding 维度。词表大小怎么定如果你的类别特征已经做了 label encoding最大值是 999那input_dim至少是 1000。我一般会留一点余量写成max_id 2防止边界情况。embedding 维度有个经验公式min(50, (vocab_size 1) // 2)但实际用的时候 8 到 32 之间比较常见太大容易过拟合太小表达不够。3.2 主模型的 call 里发生了什么主模型把两边拼起来class WideAndDeep(tf.keras.Model): def __init__(self, wide_dim, vocab_sizes, embed_dim, num_dense, hidden_units, **kwargs): super().__init__(**kwargs) self.wide WidePart(wide_dim) self.deep DeepPart(vocab_sizes, embed_dim, num_dense, hidden_units) def call(self, inputs): wide_logit self.wide(inputs[wide]) deep_logit self.deep(inputs[deep_cat], inputs[deep_num]) logits wide_logit deep_logit return tf.nn.sigmoid(logits)注意最后返回的是 sigmoid 之后的概率。如果你用tf.keras.losses.BinaryCrossentropy(from_logitsFalse)那就返回概率如果from_logitsTrue那call里就别加 sigmoid直接返回 logits让损失函数内部处理。这两种写法数值稳定性不一样from_logitsTrue更稳因为 sigmoid 和交叉熵合并计算能避免溢出。我一般推荐后者。3.3 参数初始化和正则化wide 侧的线性层权重初始化用zeros其实也行但更常见的是glorot_uniform。deep 侧的 embedding 初始化默认是uniform范围是[-0.05, 0.05]这个范围对大多数场景够用。如果你发现训练初期 loss 震荡厉害可以把 embedding 的初始化范围调小一点。正则化方面wide 侧一般不加 dropout因为它就是个线性层加了反而影响记忆能力。deep 侧可以在每个全连接后面加Dropout(0.2~0.5)具体多少看数据量。数据量小就加大一点数据量大就小一点。L2 正则我一般加在 embedding 上embeddings_regularizertf.keras.regularizers.l2(1e-5)防止 embedding 向量长得太大。4. 完整实操流程与关键环节4.1 数据准备与特征工程widedeep 的效果七分靠特征三分靠模型。我拿一个模拟的推荐场景举例假设有用户 ID、物品 ID、用户所在城市、物品品类四个类别特征外加一个连续特征用户历史点击率。wide 侧我准备这样构造输入用户 ID 和物品 ID 做交叉城市和品类做交叉再加上原始的四个特征全部 one-hot 后拼成一个大向量。假设用户 1000 个、物品 500 个、城市 50 个、品类 20 个交叉后维度是1000*500 50*20 1000 500 50 20大概 50 万维。这个维度很高但因为是 one-hot实际非零的没几个用稀疏张量存就行。deep 侧就简单了四个类别特征各自 label encoding 后过 embedding连续特征直接拼上去。import numpy as np def make_dummy_data(n_samples10000): np.random.seed(42) user_id np.random.randint(0, 1000, n_samples) item_id np.random.randint(0, 500, n_samples) city np.random.randint(0, 50, n_samples) category np.random.randint(0, 20, n_samples) hist_ctr np.random.rand(n_samples).astype(np.float32) label (np.random.rand(n_samples) 0.7).astype(np.float32) # wide 侧简化为拼接的 id 组合实际项目里用 tf.feature_column 或自己 one-hot wide np.stack([user_id, item_id, city, category], axis1).astype(np.float32) deep_cat [user_id.reshape(-1,1), item_id.reshape(-1,1), city.reshape(-1,1), category.reshape(-1,1)] deep_num hist_ctr.reshape(-1,1) return {wide: wide, deep_cat: deep_cat, deep_num: deep_num}, label这里为了 demo 简单wide 侧直接用了原始 id 拼接实际项目里应该做 one-hot 或者用tf.feature_column的crossed_column。注意 wide 侧的输入维度要和WidePart里的wide_dim对上我这里wide_dim4但真实场景可能是几十万所以 wide 侧通常用tf.sparse或者Embedding加sum的方式来实现避免稠密大矩阵。4.2 模型编译与训练编译的时候有几个参数要定model WideAndDeep( wide_dim4, vocab_sizes[1000, 500, 50, 20], embed_dim16, num_dense1, hidden_units[128, 64] ) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losstf.keras.losses.BinaryCrossentropy(from_logitsFalse), metrics[tf.keras.metrics.AUC(nameauc)] )优化器选 Adam 是常规操作学习率 1e-3 起步。如果 wide 侧维度特别高稀疏梯度多可以考虑Adagrad或者FTRL这两个对稀疏特征更友好。损失函数用二分类交叉熵评估指标用 AUC因为 CTR 场景下正负样本极不均衡准确率没意义。训练的时候batch_size一般 256 到 1024 之间。太小梯度噪声大太大收敛慢。epochs看早停我一般设 20 然后加EarlyStopping(patience3)。callbacks [ tf.keras.callbacks.EarlyStopping(monitorval_auc, patience3, modemax, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(monitorval_loss, factor0.5, patience2) ] history model.fit( train_inputs, train_labels, validation_data(val_inputs, val_labels), batch_size512, epochs20, callbackscallbacks )4.3 训练过程中的监控要点训练时重点看三个东西训练 loss 和验证 loss 的差距、AUC 的走势、wide 侧和 deep 侧各自的贡献。第一个看是否过拟合。如果训练 loss 一直降但验证 loss 开始升说明 deep 侧过拟合了加 dropout 或者减层数。第二个看 AUC 是否还在涨涨不动了就早停。第三个比较有意思你可以在call里把 wide_logit 和 deep_logit 分别返回训练时观察两者的量级。如果 wide 侧的输出一直是零附近说明 wide 侧没学到东西可能是特征交叉没做好或者学习率对 wide 侧不合适。我踩过一个坑wide 侧和 deep 侧共用一个优化器但两边梯度量级差很多导致 wide 侧几乎不更新。后来给 wide 侧单独设了更大的学习率或者用tf.keras.optimizers.Adam配合梯度裁剪才正常。如果你的 wide 侧不 work先查梯度。5. 常见问题与排查技巧实录5.1 维度对不上怎么办这是新手最容易遇到的报错。Embedding层的input_dim如果小于实际最大 id会报InvalidArgumentError: indices[0] 1000 is not in [0, 1000)。解决办法是训练前先扫一遍数据拿到每个类别特征的最大值input_dim设成max_id 1。wide 侧的维度对不上通常是Dense层的units和输入维度不匹配。子类 API 里Dense会自动推断输入维度但如果你的 wide 输入是稀疏张量得先tf.sparse.to_dense或者用tf.sparse.sparse_dense_matmul。我一般建议 wide 侧也用Embedding加sum的方式把稀疏 one-hot 转成稠密向量这样和 deep 侧的处理逻辑统一调试也方便。5.2 模型不收敛的几种可能现象可能原因排查方法loss 一直是 0.69 左右输出层没接对或者标签没对齐打印model(inputs)看输出范围loss 震荡剧烈学习率太大或 batch_size 太小降学习率到 1e-4增大 batchAUC 一直 0.5特征没信息量或 embedding 没更新检查特征和标签的相关性验证 loss 上升过拟合加 dropout、L2、减层数wide 侧梯度为零稀疏特征没被激活检查 one-hot 是否全零5.3 子类 API 调试的独门技巧子类 API 最大的痛点是看不到中间张量。我的做法是在call里临时加tf.print或者把中间结果存到self.debug_dict里训练完再取出来看。def call(self, inputs, trainingNone): wide_logit self.wide(inputs[wide]) deep_logit self.deep(inputs[deep_cat], inputs[deep_num]) if training: tf.print(wide_logit mean:, tf.reduce_mean(wide_logit)) tf.print(deep_logit mean:, tf.reduce_mean(deep_logit)) return tf.nn.sigmoid(wide_logit deep_logit)tf.print只在trainingTrue时打印避免推理时刷屏。另外model.summary()在子类 API 下只显示顶层 layer想看细节可以用model.layers遍历或者给每个子 layer 起个有意义的名字summary里至少能看到名字。还有一个技巧先用小数据跑通再上全量。我一般会造 100 条样本跑 2 个 epoch确认没有维度错误、没有 NaN再换全量数据。这样调试成本低很多。5.4 性能优化的一些经验wide 侧如果维度真的到了百万级稠密矩阵存不下必须用稀疏表示。tf.sparse.SparseTensor配合tf.sparse.sparse_dense_matmul可以做稀疏矩阵乘法。但注意稀疏操作在 GPU 上支持有限如果 wide 侧特别稀疏放 CPU 上算可能更快。deep 侧的 embedding 如果词表特别大比如千万级可以用tf.keras.layers.Embedding配合input_dim大值但显存会吃紧。这时候可以考虑 hash embedding把大词表哈希到固定大小的桶里代价是会有冲突但实际效果通常可以接受。混合精度训练在 TF 2.0 里用tf.keras.mixed_precision.set_global_policy(mixed_float16)就能开能省显存、加速训练但要注意 loss scaling不然梯度会下溢。我一般只在模型比较大、显存吃紧的时候开。6. 从 widedeep 延伸出去的几个方向widedeep 是个很好的起点理解了它很多后续模型都能顺藤摸瓜。比如DeepFM把 wide 侧的线性部分换成了 FM自动做二阶特征交叉省去了人工交叉的麻烦。DCN用 cross network 显式做高阶交叉。AutoInt用 attention 做特征交互。这些模型的结构都比 widedeep 复杂但核心思想一脉相承记忆和泛化要兼顾稀疏特征要 embedding交叉特征要显式建模。如果你想在 widedeep 上继续折腾我建议从这几个方向入手一是把 wide 侧的人工交叉换成自动交叉试试 FM 或者 cross layer二是给 deep 侧加 attention让模型自己学哪些特征重要三是把 embedding 换成预训练的用物品的 side information 初始化冷启动场景下效果提升明显。最后分享一个我在实际项目里的体会widedeep 的调参优先级是 特征 结构 超参。特征工程做得好一个简单的 widedeep 就能打败精心调参的复杂模型。所以别一上来就纠结 hidden_units 设多少、embed_dim 设多少先把特征交叉做扎实把类别特征的覆盖率提上去效果自然就来了。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号