参考

精排(一)记忆与泛化

在构建推荐模型时,我们常常追求两个看似矛盾的目标:记忆(Memorization)泛化(Generalization)

  • 记忆能力,指的是模型能够学习并记住那些在历史数据中频繁共同出现的特征组合。例如,模型记住“买了A的用户,通常也会买B”。这种能力可以精准地捕捉显性、高频的关联,为用户提供与他们历史行为高度相关的推荐。

  • 泛化能力,指的是模型能够发掘特征之间更深层次的关联,探索那些在数据中从未或很少出现过的全新特征组合。例如,模型通过学习发现“物品A和物品C都属于某个抽象类别,而用户喜欢该类别的物品”,从而向喜欢A的用户推荐了他们从未见过的C。这种能力有助于提升推荐的多样性和新颖性。

如何在一个模型中平衡并兼具这两种能力,是推荐系统领域的一个核心挑战。于2016年提出的Wide & Deep模型 :cite:cheng2016wide ,为此提供了一个影响深远的经典架构。它并非简单的模型集成,而是通过一种巧妙的 联合训练(Joint Training) 机制,将两种能力无缝融合。

这个架构的核心思想是将模型结构拆分为两个部分,分别承担不同的职责,如下图所示:

Wide & Deep 模型结构图

记忆的捷径:Wide部分

Wide部分本质上是一个广义线性模型,比如逻辑回归。它的优势在于结构简单、可解释性强,并且能高效地“记忆”那些显而易见的关联规则。其数学表达形式如下:

y=wTx+by=\mathbf{w}^T \mathbf{x}+b

其中,y是预测值,w\mathbf{w} 是模型权重,x\mathbf{x}是特征向量,b是偏置项。

Wide部分的关键在于其输入的特征向量x\mathbf{x}。它不仅包含原始特征,更重要的是包含了大量人工设计的交叉特征(Cross-product Features)。交叉特征可以将多个独立的特征组合成一个新的特征,用于捕捉特定的共现模式。例如,在应用商店的推荐场景中,我们可以创建一个交叉特征AND(installed_app=photo_editor, impression_app=filter_pack),它代表用户已经安装了“照片编辑器”应用,并且现在看到了“滤镜包”应用的推荐。

通过这种方式,Wide部分能够直接、快速地学习到“照片编辑器用户对滤镜包应用有更高的安装意愿”这类强关联规则,这正是“记忆能力”的直接体现。

泛化的深度:Deep部分

Deep部分是一个标准的前馈神经网络(DNN),它负责模型的“泛化能力”。与Wide部分依赖人工特征工程不同,Deep部分可以自动学习特征之间的高阶、非线性关系。

它的工作流程如下:首先,对于那些高维稀疏的类别特征(如用户ID、物品ID),通过一个**嵌入层(Embedding Layer)**将它们映射为低维、稠密的向量。这些嵌入向量能够捕捉到特征的潜在语义信息,是实现泛化的基础。例如,《流浪地球》和《三体》的电影ID在嵌入空间中的距离,可能会比《流浪地球》和《熊出没》更近。

随后,这些嵌入向量与其他数值特征拼接在一起,被送入多层神经网络中进行前向传播:

a(l+1)=f(W(l)a(l)+b(l))a^{(l+1)}=f(W^{(l)}a^{(l)}+b^{(l)})

其中,a(l)a^{(l)}是第ll层的激活值,W(l)W^{(l)}b(l)b^{(l)}是该层的权重和偏置,ff是激活函数(如ReLU)。通过逐层抽象,DNN能够发掘出数据中隐藏的复杂模式,从而对未曾见过的特征组合也能做出合理的预测。

融合与演进

Wide & Deep模型通过联合训练,将两部分的输出结合起来进行最终的预测。其预测概率由下式给出:

P(Y=1x)=σ(wwideT[x,ϕ(x)]+wdeepTa(lf)+b)P(Y=1|\mathbf{x})=\sigma(\mathbf{w}_{wide}^T[\mathbf{x},\phi(\mathbf{x})]+\mathbf{w}_{deep}^T a^{(lf)}+b)

在这里,σ是Sigmoid函数,[x,ϕ(x)][\mathbf{x}, \phi(\mathbf{x})]代表Wide部分的输入(包含原始特征和交叉特征),a(lf)a^{(lf)}是Deep部分最后一层的输出向量,wwide\mathbf{w}_{wide}wdeep\mathbf{w}_{deep}bb是最终预测层的权重和偏置。模型的梯度在反向传播时会同时更新Wide和Deep两部分的所有参数。

一个值得注意的工程细节是,由于两部分处理的特征类型不同,它们通常会采用不同的优化器。

  • Wide部分的输入特征非常稀疏,常使用带L1正则化的FTRL :cite:ferreira2025follow 等优化器。L1正则化可以产生稀疏的权重,相当于自动进行特征选择,让模型只“记住”最重要的规则。

  • Deep部分的参数是稠密的,更适合使用像AdaGrad :cite:duchi2011adaptive 或Adam :cite:kingma2014adam 这样的优化器。

总而言之,Wide & Deep不仅是一个具体的模型,更是一种重要的设计哲学,它为如何在推荐系统中平衡准确性与多样性、精确记忆与有效泛化,提供了清晰而强大的解决思路。

代码

wide_deep.py 文件:

1
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import tensorflow as tf

from .utils import (
build_input_layer,
build_group_feature_embedding_table_dict,
concat_group_embedding,
add_tensor_func,
get_linear_logits,
get_cross_logits,
)
from .layers import PredictLayer, DNNs


def build_wide_deep_model(feature_columns, model_config):
"""
构建Wide&Deep模型

参数:
feature_columns: 特征列配置
model_config: 模型配置字典,包含:
- dnn_units: DNN层单元数 (默认: [64, 32])
- dnn_dropout_rate: 丢弃概率 (默认: 0.1)
"""
# 从配置中提取参数并设置默认值
dnn_units = model_config.get("dnn_units", [64, 32])
dnn_dropout_rate = model_config.get("dnn_dropout_rate", 0.1)
# 构建输入层
input_layer_dict = build_input_layer(feature_columns)

# 构建特征embedding表
group_embedding_feature_dict = build_group_feature_embedding_table_dict(
feature_columns, input_layer_dict, prefix="embedding/"
)

# 拼接组特征
group_feature_dict = {}
for group_name, _ in group_embedding_feature_dict.items():
group_feature_dict[group_name] = concat_group_embedding(
group_embedding_feature_dict, group_name, axis=1, flatten=True
) # B x (N * D)

# 深度部分输出
deep_logits = []
for group_name, group_feature in group_feature_dict.items():
deep_out = DNNs(
units=dnn_units, activation="relu", dropout_rate=dnn_dropout_rate
)(group_feature)
deep_logit = tf.keras.layers.Dense(1, activation=None)(
deep_out
) # 保持为 (B, 1)
deep_logits.append(deep_logit)

# 宽度部分输出
linear_logit = get_linear_logits(input_layer_dict, feature_columns)
cross_logit = get_cross_logits(input_layer_dict, feature_columns)

# 构建模型
wide_deep_logits = add_tensor_func(deep_logits + [linear_logit, cross_logit])
# 展平以确保输出为 (batch_size,) 用于二分类
wide_deep_logits = tf.keras.layers.Flatten()(wide_deep_logits)
output = tf.keras.layers.Dense(1, activation="sigmoid", name="wide_deep_output")(
wide_deep_logits
)
output = tf.keras.layers.Flatten()(output) # 确保最终输出为 (batch_size,)
model = tf.keras.Model(inputs=list(input_layer_dict.values()), outputs=output)

# 排序模型返回 (model, None, None),因为没有单独的用户/物品模型
return model, None, None

性能效果

1
2
3
4
5
+-----------+--------+--------+------------+
| model | auc | gauc | val_user |
+===========+========+========+============+
| wide_deep | 0.5928 | 0.5749 | 928 |
+-----------+--------+--------+------------+