2.2.2. U2I召回

在完成了i2i召回的探讨后,我们转向另一条同样重要的技术路径:u2i(用户到物品)召回。如果说i2i召回解决的是“买了这个商品的人还会买什么”的问题,那么u2i召回直面的则是推荐系统的核心命题——“这个用户会喜欢什么商品”。

u2i召回的核心挑战在于如何在庞大的物品库中,快速找到与用户兴趣高度匹配的候选集。传统的协同过滤方法虽然有效,但在面对数亿用户和数千万商品时,计算复杂度成为不可逾越的障碍。u2i召回的演进历程,本质上是一个将复杂的“匹配”问题逐步简化为高效“搜索”问题的过程。

这一转变的关键突破来自于一个统一的架构思想:双塔模型(Two-Tower Model)。无论是经典的因子分解机FM、深度结构化语义模型DSSM,还是YouTube的深度神经网络YouTubeDNN,它们在表面上看起来差异巨大,但在本质上都遵循着同一个设计哲学——将用户和物品分别编码为向量,然后通过向量间的相似度计算来衡量匹配程度。

双塔模型的核心思想是将推荐问题分解为两个相对独立的子问题。用户塔(User Tower)专注于理解用户——处理用户的历史行为、人口统计学特征、上下文信息等,最终输出一个代表用户兴趣的向量\(u\)物品塔(Item Tower)则专精于刻画物品——整合物品的ID、类别、属性、内容特征等,输出一个表征物品特性的向量\(v\)

这种“分而治之”的设计带来了巨大的工程优势。在训练完成后,所有物品的向量都可以离线预计算并存储在高效的向量检索系统中(如Faiss、Annoy等)。当用户发起推荐请求时,系统只需实时计算用户向量,然后通过近似最近邻(ANN)搜索快速找到最相似的物品向量。这种架构的优雅之处在于,它将原本需要\(O(U \times I)\)的用户-物品匹配复杂度,降低到了\(O(U + I)\)的向量计算复杂度。

用户与物品的匹配度通过两个向量的点积余弦相似度来衡量:

(2.2.10)\[score(u, v) = u \cdot v = \sum_{i=1}^{d} u_i v_i\]

其中\(d\)是向量维度, \(u_i\)\(v_i\)是向量\(u\)\(v\)的第\(i\)个分量。这个简单的数学操作背后,蕴含着“语义相似性”的深刻含义——向量空间中的距离反映了用户兴趣与物品特性的匹配程度。

接下来,我们将沿着双塔模型的演进轨迹,从经典的数学基础到现代的深度学习实现,逐一探讨这些里程碑式的工作。

2.2.2.1. FM(因子分解机):双塔模型的雏形

虽然因子分解机(Factorization Machine, FM) (Rendle, 2010) 诞生于深度学习兴起之前,但它在思想上可以说是双塔模型的雏形。FM的核心贡献在于,它首次将用户-物品的复杂交互,优雅地分解为两个低维向量的内积操作。

2.2.2.1.1. 从交互矩阵到向量内积

FM模型的完整数学表达式为:

(2.2.11)\[\hat{y}(\mathbf{x}):=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle\mathbf{v}_{i}, \mathbf{v}_{j}\right\rangle x_{i} x_{j}\]

这个公式看起来复杂,但其核心思想简单而深刻:每个特征\(i\)都对应一个\(k\)维的隐向量\(\mathbf{v}_i\),特征间的交互通过这些隐向量的内积\(\langle\mathbf{v}_{i}, \mathbf{v}_{j}\rangle = \sum_{f=1}^{k} v_{i,f} \cdot v_{j,f}\)来建模。

FM的真正巧妙之处在于一个数学变换技巧。原本\(O(n^2)\)复杂度的二阶交互项,可以通过代数运算重写为:

(2.2.12)\[\sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle\mathbf{v}_{i}, \mathbf{v}_{j}\right\rangle x_{i} x_{j} = \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{i=1}^{n} v_{i, f} x_{i}\right)^{2}-\sum_{i=1}^{n} v_{i, f}^{2} x_{i}^{2}\right)\]

这一变换将计算复杂度从\(O(kn^2)\)降低到\(O(kn)\),使得FM能够处理大规模稀疏数据。

2.2.2.1.2. 分解为双塔结构

当我们将FM应用于召回任务时,关键洞察是将特征分为用户侧特征集\(U\)和物品侧特征集\(I\)。此时需要考虑一个重要的观察:对于同一用户,即便其与不同物品进行交互,用户特征内部之间的一阶、二阶交互项得分都是相同的。这意味着在比较用户与不同物品之间的匹配分时,只需要比较:(1)物品内部之间的特征交互得分;(2)用户和物品之间的特征交互得分。

基于这一观察,我们可以将FM的二阶交互项按照用户和物品特征进行拆分:

(2.2.13)\[\begin{split}\begin{aligned} & \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{i=1}^{n} v_{i, f} x_{i}\right)^{2}-\sum_{i=1}^{n} v_{i, f}^{2} x_{i}^{2}\right) \\ =& \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{u \in U} v_{u, f} x_{u} + \sum_{t \in I} v_{t, f} x_{t}\right)^{2}-\sum_{u \in U} v_{u, f}^{2} x_{u}^{2} - \sum_{t\in I} v_{t, f}^{2} x_{t}^{2}\right) \\ =& \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{u \in U} v_{u, f} x_{u}\right)^{2} + \left(\sum_{t \in I} v_{t, f} x_{t}\right)^{2} + 2{\sum_{u \in U} v_{u, f} x_{u}}{\sum_{t \in I} v_{t, f} x_{t}} - \sum_{u \in U} v_{u, f}^{2} x_{u}^{2} - \sum_{t \in I} v_{t, f}^{2} x_{t}^{2}\right) \end{aligned}\end{split}\]

为了实现向量化召回,我们可以丢弃用户特征内部的交互项(因为对排序无影响),保留物品内部交互和用户-物品交互:

(2.2.14)\[\text{score}_{FM} = \sum_{t \in I} w_{t} x_{t} + \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{t \in I} v_{t, f} x_{t}\right)^{2} - \sum_{t \in I} v_{t, f}^{2} x_{t}^{2}\right) + \sum_{f=1}^{k}\left( {\sum_{u \in U} v_{u, f} x_{u}}{\sum_{t \in I} v_{t, f} x_{t}} \right)\]

这个表达式的关键洞察在于,最后一项实际上是两个向量的内积:\(\sum_{u \in U} v_{u} x_{u}\)\(\sum_{t \in I} v_{t} x_{t}\)。基于这一观察,我们可以将整个匹配分数重新组织为两个向量的内积形式:

(2.2.15)\[\text{score}_{FM} = V_{item} \cdot V_{user}^T\]

其中:

  • 用户向量:\(V_{user} = [1; \sum_{u \in U} v_{u} x_{u}]\)

  • 物品向量:\(V_{item} = [\sum_{t \in I} w_{t} x_{t} + \frac{1}{2} \sum_{f=1}^{k}((\sum_{t \in I} v_{t, f} x_{t})^{2} - \sum_{t \in I} v_{t, f}^{2} x_{t}^{2}); \sum_{t \in I} v_{t} x_{t}]\)

用户向量由两部分拼接得到:第一项为常数1(对应物品向量中的复杂交互项),第二项是用户特征向量的sum pooling。物品向量同样由两部分组成:第一项包含物品的一阶和二阶内部交互,第二项是物品特征向量的sum pooling(对应用户-物品交互)。

这个分解证明了复杂的特征交互可以被“编码”到两个独立的向量中,然后通过简单的内积操作来恢复原始的匹配分数。作为较早期的模型,FM展示了双塔结构在推荐系统中的可行性。

2.2.2.1.3. 代码实践

首先,我们导入funrec库中的核心组件:

import sys
import funrec
import tensorflow as tf
from funrec.utils import build_metrics_table

构建、训练和评估FM召回模型:

config = funrec.load_config('fm_recall')

# 加载数据
train_data, test_data = funrec.load_data(config.data)

# 准备特征
feature_columns, processed_data = funrec.prepare_features(config.features, train_data, test_data)

# 训练模型
models = funrec.train_model(config.training, feature_columns, processed_data)

# 评估模型
metrics = funrec.evaluate_model(models, processed_data, config.evaluation, feature_columns)

print(build_metrics_table(metrics))
+---------------+--------------+-----------+----------+----------------+---------------+
|   hit_rate@10 |   hit_rate@5 |   ndcg@10 |   ndcg@5 |   precision@10 |   precision@5 |
+===============+==============+===========+==========+================+===============+
|         0.052 |       0.0267 |     0.023 |   0.0148 |         0.0052 |        0.0053 |
+---------------+--------------+-----------+----------+----------------+---------------+

2.2.2.2. DSSM:深度结构化语义模型的双塔实现

虽然FM在数学上优雅地实现了向量分解,但它本质上仍是线性模型,对于复杂的非线性用户-物品关系表达能力有限。深度结构化语义模型(Deep Structured Semantic Model, DSSM) (Huang et al., 2013) 的出现,将双塔模型的表达能力推向了新的高度——通过深度神经网络替代线性变换,实现了更强的特征学习和表示能力。 DSSM最初在自然语言处理领域用于文本语义匹配,后来被成功迁移到推荐系统中。其核心思想是通过深度神经网络将用户和物品映射到共同的语义空间中,通过向量间的相似度计算来衡量匹配程度。

../../_images/dssm_architecture.svg

图2.2.5 DSSM双塔架构

2.2.2.2.1. 推荐中的双塔架构

在推荐系统中,DSSM的架构包括两个核心部分:用户塔和物品塔,每个塔都是独立的DNN结构。用户特征(如历史行为、人口统计学信息等)经过用户塔处理后输出用户embedding,物品特征(如ID、类别、属性等)经过物品塔处理后输出物品embedding。两个embedding的维度必须保持一致,以便进行后续的相似度计算。

相比FM的线性组合,DSSM的深度结构能够使用户侧和物品侧的特征各自在塔内进行复杂的非线性变换,但两塔之间的交互仅在最终的内积计算时发生。这种设计带来了显著的工程优势——物品向量可以离线预计算,用户向量可以实时计算,然后通过高效的ANN检索完成召回。

2.2.2.2.2. 多分类训练范式

DSSM将召回任务视为一个极端多分类问题,将物料库中的所有物品看作不同的类别。模型的目标是最大化用户对正样本物品的预测概率:

(2.2.16)\[P(y|x,\theta) = \frac{e^{s(x,y)}}{\sum_{j\in M}e^{s(x,y_j)}}\]

这里\(s(x,y)\)表示用户\(x\)和物品\(y\)的相似度分数,\(P(y|x,\theta)\)表示匹配概率,\(M\)表示整个物料库。由于物料库规模庞大,直接计算这个softmax在计算上不可行,因此实际训练时采用负采样技术,为每个正样本采样一定数量的负样本来近似计算。

2.2.2.2.3. 双塔模型的细节

除了相对简单的模型结构外,双塔模型在实际应用中的一些关键细节同样值得深入探讨。这些细节往往决定了模型的最终效果,(Yi et al., 2019) 等研究对此进行了分析。

向量归一化:对用户塔和物品塔输出的embedding进行L2归一化:

(2.2.17)\[u \leftarrow \frac{u}{||u||_2}, \quad v \leftarrow \frac{v}{||v||_2}\]

归一化的核心作用是解决向量点积的非度量性问题。原始的向量点积不满足三角不等式,可能导致“距离”计算的不一致性。例如,对于三个点\(A=(10,0)\)\(B=(0,10)\)\(C=(11,0)\),使用点积计算会得到\(\text{dist}(A,B) < \text{dist}(A,C)\),但这与直观的几何距离不符。

通过归一化,向量点积被转化为欧式距离的度量形式。对于归一化向量\(u\)\(v\),它们的欧式距离为:

(2.2.18)\[||u - v|| = \sqrt{2-2\langle u,v \rangle}\]

这种转换的关键意义在于训练与检索的一致性:模型训练时使用的相似度计算(归一化后的点积)与线上ANN检索系统使用的距离度量(欧式距离)本质上是等价的。这确保了离线训练学到的向量关系能够在线上检索中得到正确体现,避免了训练-服务不一致的问题。

温度系数调节:在归一化后的向量计算内积后,除以温度系数\(\tau\)

(2.2.19)\[s(u,v) = \frac{\langle u,v \rangle}{\tau}\]

温度系数的引入可以调节softmax分布的“锐利程度”,较小的\(\tau\)使得模型预测更加“确定”,有助于提升训练效果。

2.2.2.2.4. 双塔模型的效率与精度权衡

DSSM在推荐系统中广受欢迎的根本原因在于它在效率和效果之间找到了合适的平衡点。双塔结构使得模型在海量候选数据的召回场景下速度极快,虽然效果可能不是最优的,但通常已经足够满足召回阶段的需求。

然而,这种设计也带来了固有的局限性。由于用户塔和物品塔相互独立,两侧特征间的交互信息无法得到充分利用。相比之下,精排模型中用户和物品的特征可以在网络的第一层就开始进行细粒度交互,而双塔模型只能在最后的内积计算时才发生交互。这意味着许多有价值的交互信息在经过各自的DNN结构时可能已经被其他特征所混淆,这是双塔架构天然存在的精度损失。

正是这种权衡关系,使得DSSM成为了召回阶段的经典选择——它用可控的精度损失换取了显著的效率提升,为后续更复杂的排序模型留出了计算资源和优化空间。

2.2.2.2.5. 代码实践

现在我们构建、训练和评估DSSM模型:

config = funrec.load_config('dssm')

# 加载数据
train_data, test_data = funrec.load_data(config.data)

# 准备特征
feature_columns, processed_data = funrec.prepare_features(config.features, train_data, test_data)

# 训练模型
models = funrec.train_model(config.training, feature_columns, processed_data)

# 评估模型
metrics = funrec.evaluate_model(models, processed_data, config.evaluation, feature_columns)

print(build_metrics_table(metrics))
+---------------+--------------+-----------+----------+----------------+---------------+
|   hit_rate@10 |   hit_rate@5 |   ndcg@10 |   ndcg@5 |   precision@10 |   precision@5 |
+===============+==============+===========+==========+================+===============+
|        0.0397 |       0.0368 |    0.0171 |   0.0161 |          0.004 |        0.0074 |
+---------------+--------------+-----------+----------+----------------+---------------+

2.2.2.3. YouTubeDNN:从匹配到预测用户下一行为

YouTube深度神经网络推荐系统 (Covington et al., 2016) 代表了双塔模型演进的一个重要里程碑。YouTubeDNN在架构上延续了双塔设计,但引入了一个关键的思想转变:将召回任务重新定义为“预测用户下一个会观看的视频”。

../../_images/youtubednn_candidate.png

图2.2.6 YouTubeDNN候选生成模型架构

YouTubeDNN采用了“非对称”的双塔架构:用户塔集成了观看历史、搜索历史、人口统计学特征等多模态信息,用户观看的视频ID通过嵌入层映射后进行平均池化聚合,模型还引入了“Example Age”特征来建模内容新鲜度的影响;物品塔则相对简化,本质上是一个巨大的嵌入矩阵,每个视频对应一个可学习的向量,避免了复杂的物品特征工程。

这种“预测下一个观看视频”的任务设定,本质上类似于NLP中的next token预测,可以自然地建模为一个极端多分类问题:

(2.2.20)\[P(w_t=i|U,C) = \frac{e^{v_i \cdot u}}{\sum_{j \in V} e^{v_j \cdot u}}\]

这里\(w_t\)表示用户在时间\(t\)观看的视频,\(U\)是用户特征,\(C\)是上下文信息,\(V\)是整个视频库。由于视频库规模庞大,直接计算全量softmax不可行,因此采用sampled softmax进行高效训练。

2.2.2.3.1. 关键的工程技巧

YouTubeDNN的成功不仅来自于模型设计,更来自于一系列精心设计的工程技巧:

非对称的时序分割:传统协同过滤通常随机保留验证项目,但这种做法存在未来信息泄露问题。视频消费具有明显的不对称模式——剧集通常按顺序观看,用户往往从热门内容开始逐步深入小众领域。因此,YouTubeDNN采用时序分割策略:对于作为预测目标的用户观看记录,只使用该目标之前的历史行为作为输入特征。这种“回滚”机制更符合真实的推荐场景。

../../_images/youtubednn_temporal_split.png

图2.2.7 非对称共同观看模式

负采样策略:为了高效处理数百万类别的softmax,模型采用重要性采样技术,每次只对数千个负样本进行计算,将训练速度提升了100多倍。

用户样本均衡:为每个用户生成固定数量的训练样本,避免高活跃用户主导模型学习。这个看似简单的技巧,对提升长尾用户的推荐效果至关重要。

YouTubeDNN的成功在于建立了一套可扩展、可工程化的推荐系统范式——训练时使用复杂的多分类目标和丰富的用户特征,服务时通过预计算物品向量和实时计算用户向量,配合高效的ANN检索完成召回。这种设计实现了训练复杂度和服务效率的有效平衡,至今仍被广泛借鉴。

2.2.2.4. 代码实践

构建、训练和评估YouTubeDNN模型:

config = funrec.load_config('youtubednn')

# 加载数据
train_data, test_data = funrec.load_data(config.data)

# 准备特征
feature_columns, processed_data = funrec.prepare_features(config.features, train_data, test_data)

# 训练模型
models = funrec.train_model(config.training, feature_columns, processed_data)

# 评估模型
metrics = funrec.evaluate_model(models, processed_data, config.evaluation, feature_columns)

print(build_metrics_table(metrics))
+---------------+--------------+-----------+----------+----------------+---------------+
|   hit_rate@10 |   hit_rate@5 |   ndcg@10 |   ndcg@5 |   precision@10 |   precision@5 |
+===============+==============+===========+==========+================+===============+
|        0.0086 |       0.0033 |    0.0038 |   0.0021 |         0.0009 |        0.0007 |
+---------------+--------------+-----------+----------+----------------+---------------+