.. _i2i: I2I召回 ======= 在推荐系统中,i2i(Item-to-Item)召回是一个核心任务:给定一个物品,如何快速找出与之相似的其他物品?这个看似简单的问题,实际上蕴含着深刻的洞察——“相似性”并非仅仅由物品的内在属性决定,而是与用户的行为所共同定义的。如果两个商品经常被同一批用户购买,两部电影被同一群观众喜欢,那么它们之间就可能存在某种关联。 这种思想的灵感来源于自然语言处理领域的一个重要发现。在语言学中,有一个著名的分布假说 :cite:p:`philological1957studies` :“You shall know a word by the company it keeps”(观其伴,知其义)。一个词的含义可以通过它经常与哪些词一起出现来推断。Word2Vec正是基于这一思想,通过分析大量文本中词语的共现关系,学习出了能够捕捉语义相似性的词向量。本节将首先介绍Word2Vec的核心思想,为后续的i2i召回模型奠定理论基础。 接下来,我们将看到所有i2i召回方法的本质都是在回答同一个问题:如何更好地定义和利用“序列”来学习物品之间的相似性。从最直接的用户行为序列,到融合属性信息的增强序列,再到面向业务目标的会话序列,每一种方法都是对“序列”概念的不同诠释和深化。 Word2Vec:序列建模的理论基础 ---------------------------- Word2Vec :cite:`mikolov2013distributed` 的成功建立在一个简单而深刻的假设之上:在相似语境中出现的词语往往具有相似的含义。通过分析海量文本中词语的共现模式,我们可以为每个词学习一个稠密的向量表示,使得语义相近的词在向量空间中距离更近。 Word2Vec主要包含两种模型架构:\ **Skip-Gram**\ 和\ **CBOW**\ (Continuous Bag of Words)。Skip-Gram模型通过给定的中心词来预测其周围的上下文词,而CBOW模型则相反,通过上下文词来预测中心词。在推荐系统中,Skip-Gram模型由于其更好的性能表现而被更广泛地采用。 Skip-Gram模型详解 ~~~~~~~~~~~~~~~~~ .. figure:: ../../img/w2v_skip_gram.svg Word2Vec Skip-Gram模型示意图 在Skip-Gram模型中,给定文本序列中位置\ :math:`t`\ 的中心词\ :math:`w_t`\ ,模型的目标是最大化其上下文窗口内所有词语的出现概率。具体而言,对于窗口大小为\ :math:`m`\ 的情况,模型要预测\ :math:`w_{t-m}, w_{t-m+1}, \ldots, w_{t-1}, w_{t+1}, \ldots, w_{t+m}`\ 这些上下文词的概率。 中心词\ :math:`w_t`\ 预测上下文词\ :math:`w_{t+j}`\ 的条件概率定义为: .. math:: P(w_{t+j} | w_t) = \frac{e^{v_{w_{t+j}}^T v_{w_t}}}{\sum_{k=1}^{|V|} e^{v_{w_k}^T v_{w_t}}} 其中\ :math:`v_{w_i}`\ 表示词\ :math:`w_i`\ 的向量表示,\ :math:`V`\ 是词汇表。这个softmax公式确保了所有词的概率之和为1,而分子中的内积\ :math:`v_{w_{t+j}}^T v_{w_t}`\ 衡量了中心词与上下文词的相似度。 负采样优化 ~~~~~~~~~~ 直接计算上述softmax的分母需要遍历整个词汇表,在实际应用中计算代价过高。为了解决这个问题,Word2Vec采用了负采样(Negative Sampling)技术。这种方法将原本的多分类问题转化为多个二分类问题: .. math:: \log \sigma(v_{w_{t+j}}^T v_{w_t}) + \sum_{i=1}^{k} \mathbb{E}_{w_i \sim P_n(w)} \log \sigma(-v_{w_i}^T v_{w_t}) 其中\ :math:`\sigma(x) = \frac{1}{1 + e^{-x}}`\ 是sigmoid函数,\ :math:`k`\ 是负样本数量,\ :math:`P_n(w)`\ 是负采样分布。负采样的直观解释是:对于真实的词对,我们希望增加它们的相似度;对于随机采样的负样本词对,我们希望降低它们的相似度。 这种优化策略不仅大幅提升了训练效率,还为后续推荐系统中的模型训练提供了重要的技术范式。当我们将这一思想迁移到推荐领域时,“词语”变成了“物品”,“句子”变成了“用户行为序列”,但核心的序列建模思想保持不变。 Item2Vec:最直接的迁移 ---------------------- Word2Vec在自然语言处理领域的成功,自然引发了一个问题:能否将这种基于序列的学习方法直接应用到推荐系统中?Item2Vec给出了肯定的答案。 从词语到物品的映射 ~~~~~~~~~~~~~~~~~~ Item2Vec :cite:`barkan2016item2vec` 的核心洞察在于发现了用户行为数据与文本数据的结构相似性。在文本中,一个句子由多个词语组成,词语之间的共现关系反映了语义相似性。类似地,在推荐系统中,每个用户的交互历史可以看作一个“句子”,其中包含的物品就是“词语”。如果两个物品经常被同一个用户交互,那么它们之间就存在相似性。 这种映射关系可以表示为: - **词语** → **物品** - **句子** → **用户交互序列** - **词语共现** → **物品共同被用户交互** 模型实现 ~~~~~~~~ Item2Vec直接采用Word2Vec的Skip-Gram架构,但在序列构建上有所简化。给定数据集\ :math:`\mathcal{S} = \{s_1, s_2, \ldots, s_n\}`\ ,其中每个\ :math:`s_i`\ 包含用户\ :math:`i`\ 交互过的所有物品,Item2Vec将每个用户的交互历史视为一个集合而非序列,忽略了交互的时间顺序。 优化目标函数与Word2Vec保持一致: .. math:: \mathcal{L} = \sum_{s \in \mathcal{S}} \sum_{l_{i} \in s} \sum_{-m \leq j \leq m, j \neq 0} \log P(l_{i+j} | l_{i}) 其中\ :math:`l_i`\ 表示物品,\ :math:`m`\ 是上下文窗口大小,\ :math:`P(l_{i+j} | l_{i})`\ 采用与Word2Vec相同的softmax形式计算。 代码实践 ~~~~~~~~ .. raw:: latex \diilbookstyleinputcell .. code:: python import os import sys import funrec from funrec.utils import build_metrics_table # 加载配置 config = funrec.load_config('item2vec') # 加载数据 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)) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output +---------------+--------------+-----------+----------+----------------+---------------+ | hit_rate@10 | hit_rate@5 | ndcg@10 | ndcg@5 | precision@10 | precision@5 | +===============+==============+===========+==========+================+===============+ | 0.0098 | 0.0033 | 0.0035 | 0.0015 | 0.001 | 0.0007 | +---------------+--------------+-----------+----------+----------------+---------------+ EGES:用属性信息增强序列 ------------------------ Item2Vec虽然验证了序列建模在推荐系统中的可行性,但其简单的设计也带来了明显的局限性。首先,将用户交互历史简单视为无序集合,忽略了时序信息可能丢失重要的用户行为模式。其次,对于新上架的物品由于缺乏用户交互历史,Item2Vec无法生成有意义的向量表示。 EGES(Enhanced Graph Embedding with Side Information):cite:wang2018billion 正是为了解决这些核心挑战而提出的。该方法通过两个关键创新来改进传统的序列建模:一是基于会话构建更精细的商品关系图来更好地反映用户行为模式,二是融合商品的辅助信息来解决冷启动问题。 构建商品关系图 ~~~~~~~~~~~~~~ EGES的第一个创新是将物品序列的概念从简单的用户交互扩展为更精细的会话级序列。考虑到用户行为的复杂性和计算效率,研究者设置了一小时的时间窗口,只选择窗口内的用户行为构建商品关系图。 .. figure:: ../../img/eges_item_graph.png 商品图构建过程 如图所示,如果两个商品在用户行为序列中连续出现,则在它们之间建立一条有向边,边的权重等于商品转移在所有用户行为历史中的频率。这种基于会话的图构建方法比简单的用户交互序列更能捕捉用户的短期兴趣模式。 在构建好的商品图上,EGES采用带权随机游走策略生成训练序列。从一个节点出发,转移概率由边权重决定: .. math:: P(v_j|v_i) = \begin{cases} \frac{M_{ij}}{\sum_{j=1}^{|N_+(v_i)|}M_{ij}} & \text{if } v_j \in N_+(v_i) \\ 0 & \text{if } e_{ij} \notin E \end{cases} 其中\ :math:`M_{ij}`\ 表示节点\ :math:`v_i`\ 到节点\ :math:`v_j`\ 的边权重,\ :math:`N_+(v_i)`\ 表示节点\ :math:`v_i`\ 的邻居集合。通过这种随机游走过程,可以生成大量的商品序列用于后续的embedding学习。 融合辅助信息解决冷启动 ~~~~~~~~~~~~~~~~~~~~~~ 基于上述商品图,我们可以采用类似Word2Vec的方法学习商品的向量表示。然而,对于新上架或交互稀少的商品,仍然难以学习到准确的embedding。为了解决这个问题,\ **GES(Graph Embedding with Side Information)** 方法引入了商品的辅助信息,如类别、品牌、价格区间等。 GES的核心思想是将商品本身的embedding与其各种属性的embedding进行平均聚合: .. math:: H_v=\frac{1}{n+1} \sum_{s=0}^n{W_v^s} 其中\ :math:`W_v^s`\ 表示商品\ :math:`v`\ 的第\ :math:`s`\ 种属性的向量表示,\ :math:`W_v^0`\ 表示商品ID的向量表示。这种方法虽然有效缓解了冷启动问题,但存在一个明显的局限:它假设所有类型的辅助信息对商品表示的贡献是相等的,这显然不符合实际情况。 **EGES的核心创新**\ 在于认识到不同类型的辅助信息应该有不同的重要性。对于手机,品牌可能比价格更重要;对于日用品,价格可能比品牌更关键。 .. figure:: ../../img/eges_model.png EGES模型架构 对于具有\ :math:`n`\ 种辅助信息的商品\ :math:`v`\ ,EGES为其维护\ :math:`n+1`\ 个向量表示:一个商品ID的向量表示,以及\ :math:`n`\ 个属性的向量表示。商品的最终向量表示通过加权聚合得到: .. math:: H_v = \frac{\sum_{j=0}^n e^{a_v^j} W_v^j}{\sum_{j=0}^n e^{a_v^j}} 其中\ :math:`a_v^j`\ 是可学习的权重参数。这种设计的精妙之处在于,不同类型的辅助信息对不同商品的重要性是不同的——对于手机,品牌可能比价格更重要;对于日用品,价格可能比品牌更关键。 训练优化 ~~~~~~~~ EGES采用与Word2Vec类似的负采样策略,但损失函数经过了优化: .. math:: L(v,u,y) = -[y\log(\sigma(H_v^TZ_u)) + (1-y)\log(1-\sigma(H_v^TZ_u))] 其中\ :math:`y`\ 是标签(1表示正样本,0表示负样本),\ :math:`H_v`\ 是商品\ :math:`v`\ 的向量表示,\ :math:`Z_u`\ 是上下文节点\ :math:`u`\ 的向量表示。 通过这种方式,即使是刚上架、没有任何用户交互的新商品,也能通过其属性信息获得有意义的向量表示,从而被纳入推荐候选集。 EGES在淘宝的实际部署效果显著:在包含十亿级训练样本的大规模数据集上,相比传统方法在推荐准确率上有了显著的提升,同时有效解决了新商品的冷启动问题。 代码实践 -------- .. raw:: latex \diilbookstyleinputcell .. code:: python import os import sys import funrec from funrec.utils import build_metrics_table # 加载配置 config = funrec.load_config('eges') # 加载数据 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)) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output +---------------+--------------+-----------+----------+----------------+---------------+ | hit_rate@10 | hit_rate@5 | ndcg@10 | ndcg@5 | precision@10 | precision@5 | +===============+==============+===========+==========+================+===============+ | 0.0089 | 0.0051 | 0.004 | 0.0028 | 0.0009 | 0.001 | +---------------+--------------+-----------+----------+----------------+---------------+ Airbnb:将业务目标融入序列 -------------------------- Airbnb作为全球最大的短租平台,面临着与传统电商不同的挑战。房源不是标准化商品,用户的预订行为远比点击浏览稀疏,而且地理位置成为了一个关键因素。更重要的是,Airbnb需要的不仅仅是相似性,而是能够真正促进最终预订转化的推荐。 面向业务的序列构建 ~~~~~~~~~~~~~~~~~~ Airbnb重新定义了“序列”的概念 :cite:`grbovic2018real` 。它不是简单地将用户交互过的所有房源放在一起,而是基于用户的点击会话(Click Sessions)构建序列。当用户连续点击间隔超过30分钟时,就会开始一个新的会话。这种基于会话的序列更能反映用户在特定时间段内的真实搜索意图。 更关键的是,Airbnb引入了一个重要的业务洞察:不是所有的用户行为都同等重要。用户的最终预订行为比简单的点击浏览具有更强的信号强度。 全局上下文机制 ~~~~~~~~~~~~~~ 为了强化模型对最终转化行为的学习,Airbnb设计了全局上下文机制。在传统的Skip-Gram模型中,只有在滑动窗口内的物品才被视为上下文,但这种局部窗口无法充分利用最终预订这一强烈的正向信号。因此,Airbnb让用户最终预订的房源(booked listing)与序列中的每一个浏览房源都形成正样本对进行训练,无论它们在序列中的距离有多远。 .. figure:: ../../img/airbnb_global_context.png Airbnb预订房源全局上下文 修改后的优化目标增加了全局上下文项: .. math:: \text{argmax}_{\theta} \sum_{(l, l^+) \in \mathcal{D_p}} \log \frac{1}{1 + e^{-v_{l^+}^T v_l}} + \sum_{(l, l^-) \in \mathcal{D_n}} \log \frac{1}{1 + e^{v_{l^-}^T v_l}} + \log \frac{1}{1 + e^{-v_{l_b}^T v_l}} 其中\ :math:`l_b`\ 表示用户最终预订的房源。这种设计确保了模型学习到的相似性不仅基于用户的浏览行为,更偏向于那些能够促进最终转化的房源。从训练动态的角度看,预订房源的全局上下文项为序列中的每个房源都提供了额外的梯度信号,使得模型能够更强烈地学习到“什么样的房源组合最终会导致预订”这一关键模式。 市场感知的负采样 ~~~~~~~~~~~~~~~~ Airbnb的另一个创新是改进了负采样策略。传统方法从整个物品库中随机选择负样本,但Airbnb观察到用户通常只会在同一个市场(城市或地区)内进行预订。如果负样本来自不同的地理位置,模型就容易学到地理位置这种“简单特征”,而忽略了房源本身的特点。 因此,Airbnb增加了“同市场负采样”策略,一部分负样本从与正样本相同的地理市场中选择: .. math:: \sum_{(l, l_m^-) \in \mathcal{D_m}} \log \frac{1}{1 + e^{v_{l_m^-}^T v_l}} 其中\ :math:`l_m^-`\ 表示来自相同市场的负样本。这迫使模型学习同一地区内房源的细微差别,提升了推荐的精细度。 冷启动解决方案 ~~~~~~~~~~~~~~ 对于每天新增的房源,Airbnb采用了基于属性的初始化策略。系统根据新房源的属性(地理位置、价格区间、房源类型等)找到相似的已有房源,使用它们embedding的均值来初始化新房源的向量表示。这种方法有效解决了98%以上新房源的冷启动问题。