Wide & Deep
动机
在 CTR 预估任务中利用手工构造的交叉组合特征来使线性模型具有 “记忆性”,使模型记住共现频率较高的特征组合,往往也能达到一个不错的 baseline,且可解释性强。但这种方式有着较为明显的缺点:
- 特征工程需要耗费太多精力。
- 模型是强行记住这些组合特征的,对于未曾出现过的特征组合,权重系数为 0,无法进行泛化。
为了加强模型的泛化能力,研究者引入了 DNN 结构,将高维稀疏特征编码为低维稠密的 Embedding vector,这种基于 Embedding 的方式能够有效提高模型的泛化能力。但是,基于 Embedding 的方式可能因为数据长尾分布,导致长尾的一些特征值无法被充分学习,其对应的 Embedding vector 是不准确的,这便会造成模型泛化过度。
Wide&Deep 模型就是围绕记忆性和泛化性进行讨论的,模型能够从历史数据中学习到高频共现的特征组合的能力,称为是模型的 Memorization。能够利用特征之间的传递性去探索历史数据中从未出现过的特征组合,称为是模型的 Generalization。Wide&Deep 兼顾 Memorization 与 Generalization 并在 Google Play store 的场景中成功落地。
模型结构及原理

其实 wide&deep 模型本身的结构是非常简单的,对于有点机器学习基础和深度学习基础的人来说都非常的容易看懂,但是如何根据自己的场景去选择那些特征放在 Wide 部分,哪些特征放在 Deep 部分就需要理解这篇论文提出者当时对于设计该模型不同结构时的意图了,所以这也是用好这个模型的一个前提。
如何理解 Wide 部分有利于增强模型的 “记忆能力”,Deep 部分有利于增强模型的 “泛化能力”?
wide 部分是一个广义的线性模型,输入的特征主要有两部分组成,一部分是原始的部分特征,另一部分是原始特征的交叉特征 (cross-product transformation),对于交互特征可以定义为:
是一个布尔变量,当第 i 个特征属于第 k 个特征组合时, 的值为 1,否则为 0, 是第 i 个特征的值,大体意思就是两个特征都同时为 1 这个新的特征才能为 1,否则就是 0,说白了就是一个特征组合。用原论文的例子举例:AND (user_installed_app=QQ, impression_app=WeChat),当特征 user_installed_app=QQ, 和特征 impression_app=WeChat 取值都为 1 的时候,组合特征 AND (user_installed_app=QQ, impression_app=WeChat) 的取值才为 1,否则为 0。
对于 wide 部分训练时候使用的优化器是带
正则的 FTRL 算法 (Follow-the-regularized-leader),而 L1 FTLR 是非常注重模型稀疏性质的,也就是说 W&D 模型采用 L1 FTRL 是想让 Wide 部分变得更加的稀疏,即 Wide 部分的大部分参数都为 0,这就大大压缩了模型权重及特征向量的维度。**Wide 部分模型训练完之后留下来的特征都是非常重要的,那么模型的 “记忆能力” 就可以理解为发现 "直接的",“暴力的”,“显然的” 关联规则的能力。** 例如 Google W&D 期望 wide 部分发现这样的规则:用户安装了应用 A,此时曝光应用 B,用户安装应用 B 的概率大。Deep 部分是一个 DNN 模型,输入的特征主要分为两大类,一类是数值特征 (可直接输入 DNN),一类是类别特征 (需要经过 Embedding 之后才能输入到 DNN 中),Deep 部分的数学形式如下:
** 我们知道 DNN 模型随着层数的增加,中间的特征就越抽象,也就提高了模型的泛化能力。** 对于 Deep 部分的 DNN 模型作者使用了深度学习常用的优化器 AdaGrad,这也是为了使得模型可以得到更精确的解。
Wide 部分与 Deep 部分的结合
W&D 模型是将两部分输出的结果结合起来联合训练,将 deep 和 wide 部分的输出重新使用一个逻辑回归模型做最终的预测,输出概率值。联合训练的数学形式如下:需要注意的是,因为 Wide 侧的数据是高维稀疏的,所以作者使用了 FTRL 算法优化,而 Deep 侧使用的是 Adagrad。
代码实现
Wide 侧记住的是历史数据中那些常见、高频的模式,是推荐系统中的 “红海”。实际上,Wide 侧没有发现新的模式,只是学习到这些模式之间的权重,做一些模式的筛选。正因为 Wide 侧不能发现新模式,因此我们需要根据人工经验、业务背景,将我们认为有价值的、显而易见的特征及特征组合,喂入 Wide 侧
Deep 侧就是 DNN,通过 embedding 的方式将 categorical/id 特征映射成稠密向量,让 DNN 学习到这些特征之间的深层交叉,以增强扩展能力。
模型的实现与模型结构类似由 deep 和 wide 两部分组成,这两部分结构所需要的特征在上面已经说过了,针对当前数据集实现,我们在 wide 部分加入了所有可能的一阶特征,包括数值特征和类别特征的 onehot 都加进去了,其实也可以加入一些与 wide&deep 原论文中类似交叉特征。只要能够发现高频、常见模式的特征都可以放在 wide 侧,对于 Deep 部分,在本数据中放入了数值特征和类别特征的 embedding 特征,实际应用也需要根据需求进行选择。
# Wide&Deep 模型的wide部分及Deep部分的特征选择,应该根据实际的业务场景去确定哪些特征应该放在Wide部分,哪些特征应该放在Deep部分
def WideNDeep(linear_feature_columns, dnn_feature_columns):
# 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)
# 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding
linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns))
# 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
# 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
# Wide&Deep模型论文中Wide部分使用的特征比较简单,并且得到的特征非常的稀疏,所以使用了FTRL优化Wide部分(这里没有实现FTRL)
# 但是是根据他们业务进行选择的,我们这里将所有可能用到的特征都输入到Wide部分,具体的细节可以根据需求进行修改
linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)
# 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)
dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns))
# 在Wide&Deep模型中,deep部分的输入是将dense特征和embedding特征拼在一起输入到dnn中
dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, embedding_layers)
# 将linear,dnn的logits相加作为最终的logits
output_logits = Add()([linear_logits, dnn_logits])
# 这里的激活函数使用sigmoid
output_layer = Activation("sigmoid")(output_logits)
model = Model(input_layers, output_layer)
return model
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
关于每一块的细节,这里就不解释了,在我们给出的 GitHub 代码中,我们已经加了非常详细的注释,大家看那个应该很容易看明白, 为了方便大家的阅读,我们这里还给大家画了一个整体的模型架构图,帮助大家更好的了解每一块以及前向传播。(画的图不是很规范,先将就看一下,后面我们会统一在优化一下这个手工图)。

下面是一个通过 keras 画的模型结构图,为了更好的显示,数值特征和类别特征都只是选择了一小部分,画图的代码也在 github 中。

思考
- 在你的应用场景中,哪些特征适合放在 Wide 侧,哪些特征适合放在 Deep 侧,为什么呢?
- 为什么 Wide 部分要用 L1 FTRL 训练?
- 为什么 Deep 部分不特别考虑稀疏性的问题?
思考题可以参考见微知著,你真的搞懂 Google 的 Wide&Deep 模型了吗?
参考资料