.. _generateive_recall: 生成式召回方法 ============== 上一小节,我们探讨了如MIND和SDM等序列召回方法,它们的核心目标是分析用户的历史行为,并将其“总结”成一个或多个能够代表用户兴趣的向量。这种方法好比是为用户拍摄一张静态的“兴趣快照”,用以在海量物品中进行检索。 本章将介绍一种截然不同的思路:\ **生成式召回**\ 。它不再试图去“总结”用户,而是直接“预测”用户的下一个行为。其核心是直接对序列中物品与物品之间的动态依赖关系进行建模,关注“用户接下来会做什么”而非“用户是怎样的人”。 依循这条思路,我们将探讨三种代表性的生成式召回模型,它们清晰地展示了该领域的一条核心演进脉络。首先是\ **SASRec**\ ,它开创性地将自然语言处理领域的Transformer模型直接应用于推荐序列预测,奠定了“预测下一个物品ID”这一基础范式。 在SASRec验证了该范式的巨大潜力之后,后续的探索主要沿着两个方向继续深化: 1. **深化对“输入”的理解**\ :既然模型可以处理序列,我们能否给它提供一个信息更丰富的序列?\ **HSTU**\ 模型便是这一方向的杰出代表。它不再满足于简单的物品ID,而是将用户的属性、行为类型、时间等所有异构信息都融合成一个复杂的“**事件流**”作为输入,增强了模型对行为上下文的理解能力。 2. **重塑对“输出”的定义**\ :我们预测的目标——那个孤立的物品ID——本身能否更具信息量?\ **TIGER** 模型开创性地回答了这个问题。它认为预测一个无意义的原子ID效率低下,转而通过生成一个由多个“码字”构成的、结构化的“**语义ID**”来代表物品。这彻底改变了推荐的终极目标,从“匹配ID”转向了“生成描述”。 本节中,我们将沿着 **SASRec (奠基) -> HSTU (深化输入) -> TIGER (重塑输出)** 这条清晰的脉络,深入理解生成式召回技术的演进与核心思想。 SASRec:基于自注意力的序列推荐 ------------------------------ 在SASRec出现之前,主流的序列模型,如基于马尔可夫链或RNN的方法,存在各自的局限。马尔可夫链 :cite:`rendle2010factorizing` 通常只考虑最近的少数几个行为,视野有限;而RNN :cite:`dobrovolny2021session` 虽然理论上能捕捉长期依赖,但其串行计算的特性导致训练效率低下。SASRec :cite:`kang2018self` 的出发点便是:能否找到一种方法,既能像RNN一样看到完整的历史序列,又能高效地捕捉其中最重要的依赖关系?答案是借用在自然语言处理(NLP)领域大获成功的Transformer模型 :cite:`vaswani2017attention` 。 SASRec的核心思想是将用户的行为序列视为一个句子来处理。对于序列中的每个商品,模型会自动学习它与序列中其他所有商品的相关性,然后基于这些相关性来预测下一个用户可能交互的商品。这样既保留了对长期行为的建模能力,又能根据数据特性灵活地调整对不同历史行为的关注度。 .. _generateive-recall: .. figure:: ../../img/generateive_recall.png 生成式召回模型的基本架构 类似于Transformer,SASRec的基本模块如 :numref:`generateive-recall` 左 所示,主要包含自注意力层和前馈网络层两个组件。 **自注意力层** 对于序列中的每个商品,我们通过嵌入矩阵 :math:`\bf{M}\in\mathbb{R}^{|\mathcal{I}|\times d}` 将其映射为d维向量,其中 :math:`|\mathcal{I}|` 是商品总数。输入序列的嵌入矩阵记为 :math:`\bf{E}\in\mathbb{R}^{n\times d}`\ ,其中 :math:`\bf{E}_i=\bf{M}_{s_i}`\ 。 由于自注意力机制本身无法感知位置信息,我们引入可学习的位置嵌入 :math:`\bf{P}\in\mathbb{R}^{n\times d}`\ 。最终的输入表示为: .. math:: \widehat{\bf{E}}=\left[ \begin{array}{c} \bf{M}_{s_1}+\bf{P}_{1} \\ \bf{M}_{s_2}+\bf{P}_{2} \\ \dots \\ \bf{M}_{s_n}+\bf{P}_{n} \end{array} \right] 在得到商品的embedding后,基于缩放点积注意力机制: .. math:: \text{Attention}(\bf{Q},\bf{K},\bf{V})=\text{softmax}\left(\frac{\bf{Q}\bf{K}^T}{\sqrt{d}}\right)\bf{V} 这里 :math:`\bf{Q}`\ 、\ :math:`\bf{K}`\ 、\ :math:`\bf{V}` 分别表示查询、键、值矩阵。在自注意力中,这三个矩阵都由输入嵌入通过线性变换得到: .. math:: \bf{S}=\text{SA}(\widehat{\bf{E}})=\text{Attention}(\widehat{\bf{E}}\bf{W}^Q,\widehat{\bf{E}}\bf{W}^K,\widehat{\bf{E}}\bf{W}^V) 其中 :math:`\bf{W}^Q`\ 、\ :math:`\bf{W}^K`\ 、\ :math:`\bf{W}^V` 都是 :math:`d\times d` 的可学习权重矩阵,\ :math:`\sqrt{d}` 是缩放因子用于稳定训练。 值得注意的是,这里的自注意力机制需要采用因果掩码,确保在预测第 :math:`t` 个位置的下一个物品时,模型只能利用 :math:`1` 到 :math:`t-1` 的历史信息,而不能‘偷看’未来的行为。 **前馈网络层** 得到自注意力层的输出后,前馈网络为模型引入了非线性变换能力: .. math:: \bf{F}_i = \text{FFN}(\bf{S}_i)=\text{ReLU}(\bf{S}_i\bf{W}^{(1)}+\bf{b}^{(1)})\bf{W}^{(2)}+\bf{b}^{(2)} 其中 :math:`\bf{W}^{(1)}`\ 、\ :math:`\bf{W}^{(2)}` 是权重矩阵,\ :math:`\bf{b}^{(1)}`\ 、\ :math:`\bf{b}^{(2)}` 是偏置向量,\ :math:`\bf{F}_i` 是第\ :math:`i`\ 层前馈网络的输出。 **预测与训练** 经过多层Transformer模块的加工后,模型会基于最后一个物品(第\ :math:`t`\ 个)的输出表示\ :math:`\bf{F}_t`\ ,来预测用户最可能交互的下一个物品\ :math:`i`\ 。这个预测过程,本质上就是在整个物品库中,寻找与该输出表示向量最相似的物品向量。 商品\ :math:`i`\ 的相关性分数为: .. math:: r_{i,t}=\bf{F}_t\bf{M}_i^T 这里复用了商品嵌入矩阵 :math:`\bf{M}`\ ,既减少了参数量又提高了性能。 类似于Decoder Only的Transformer模型,SASRec的训练目标也是预测用户下一个交互的物品。对于时间步t,期望输出 :math:`o_t` 定义为: .. math:: o_t=\begin{cases} \texttt{} & \text{如果 } s_t \text{ 是填充项}\\ s_{t+1} & 1\leq t}`\ 表示填充项,\ :math:`\mathcal{S}^{u}`\ 表示用户\ :math:`u`\ 的交互序列。模型训练时,输入为\ :math:`s`\ ,期望输出为\ :math:`o`\ ,采用二元交叉熵作为损失函数: .. math:: -\sum_{\mathcal{S}^{u}\in\mathcal{S}}\sum_{t\in[1,2,\dots,n]}\left[\log(\sigma(r_{o_t,t}))+\sum_{j\not\in \mathcal{S}^{u}}\log(1-\sigma(r_{j,t}))\right] 其中 :math:`\sigma` 是sigmoid函数,\ :math:`r_{o_t,t}` 是正样本分数,\ :math:`r_{j,t}` 是负样本分数。 代码实践 ~~~~~~~~ .. raw:: latex \diilbookstyleinputcell .. code:: python import sys import funrec from funrec.utils import build_metrics_table config = funrec.load_config('sasrec') # 加载数据 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 | +===============+==============+===========+==========+ | 0.0192 | 0.0076 | 0.0081 | 0.0044 | +---------------+--------------+-----------+----------+ HSTU:为复杂的“事件流”输入定制生成式架构 ---------------------------------------- SASRec成功地验证了将推荐问题视为序列预测的可行性,但它将视线聚焦于一个相对简化的世界:一个仅由物品ID构成的序列。一个自然而然的问题是:用户的行为序列远比这丰富,能否将用户的属性、行为类型(点击、加购、购买)、上下文时间等所有信息都融入这个序列,让模型拥有一个更全面的“上帝视角”? HSTU :cite:`zhai2024actions` 模型正是对这个问题的一次深入探索。它代表了生成式范式的一个重要演进方向:\ **不再满足于简单的物品ID序列,而是将所有异构特征统一编码为单一的、更复杂的“事件流”(Event Stream)作为模型输入。** HSTU的目标,就是学习这个复杂的“句子”,并预测下一个可能的事件。 这种统一化的架构设计虽然优雅,但在实现过程中面临着诸多技术挑战。HSTU的设计巧妙地解决了特征处理、模型架构和信号传递等关键问题,旨在用单一的模块替换传统推荐模型中特征提取、交互和预测等多个异构组件。 **特征处理** HSTU的特征处理分为两个策略。对于\ **类别特征**\ ,它将所有信息按时间戳“拉平”成一个统一的序列。想象一个用户的行为流:\ ``[用户年龄=30, 登录, 浏览商品A(类别:手机), 浏览商品B(类别:手机壳), 将B加入购物车, 退出]``\ 。SASRec可能只看到 ``[A, B]``\ 。而HSTU则试图理解整个事件流:\ ``[(特征:年龄,值:30), (行为:登录), (行为:浏览,物品:A), (行为:浏览,物品:B), (行为:加购,物品:B), (行为:退出)]``\ 。 对于变化频繁的\ **数值特征**\ (如加权计数器、比率等),HSTU则采用隐式建模策略,让模型通过观察用户在序列中的实际行为模式来自动推断这些数值信息。 因此,HSTU的输入输出可以表示为: - **输入**\ (:math:`x_i`):\ :math:`(\Phi_0,a_0), (\Phi_1,a_1), \ldots, (\Phi_{n_c-1},a_{n_c-1})` - **输出**\ (:math:`y_i`):\ :math:`\Phi_1',\Phi_2',\ldots,\Phi_{n_c-1}',\varnothing`\ , 其中 :math:`\Phi_i` 表示用户在时刻 :math:`i` 交互的内容,\ :math:`a_i` 表示用户在时刻 :math:`i` 的行为,\ :math:`\Phi_i'` 表示用户在时刻 :math:`i` 交互的内容,\ :math:`\Phi_i' = \Phi_i` (如果 :math:`a_i` 为正向行为),否则为 :math:`\varnothing`\ 。 在推荐系统的召回阶段,模型学习一个条件分布\ :math:`p(\Phi_{i+1}|u_i)`\ ,其中 :math:`u_i` 是用户在当前时刻的表示向量,\ :math:`\Phi_{i+1}` 是候选物品。召回的目标是从物品库 :math:`\mathbb{X}_c` 中选择能够最大化用户满意度的物品,即 :math:`\text{argmax}_{\Phi \in \mathbb{X}_c} p(\Phi|u_i)`\ 。HSTU召回与标准的序列生成任务有两个不同的地方:首先用户可能对推荐的物品产生负面反应,因此监督信号不总是下一个物品;其次当序列中包含非交互相关的特征(如人口统计学信息)时,对应的输出标签是未定义的。通过这种方式,HSTU将复杂的推荐问题转化为序列到序列的学习问题,使得模型能够基于用户的历史行为序列预测其未来可能感兴趣的内容。 **架构统一:HSTU单元** 为了用一个模块就能替换DLRM中的异构组件,HSTU引入了层次序列转换单元(Hierarchical Sequential Transduction Unit,HSTU) 如 :numref:`generateive-recall` 右所示。HSTU由堆叠的相同层组成,每层包含三个关键子模块: - **点向投影(Pointwise Projection)**\ :使用单一线性变换后分割的方式同时产生注意力计算所需的查询、键、值以及门控权重: :math:`U(X), V(X), Q(X), K(X) = \text{Split}(\phi_1(f_1(X)))` - **点向聚合(Pointwise Aggregation)**\ :采用点向聚合而非传统softmax来保持推荐系统中用户偏好强度信息: :math:`A(X)V(X) = \phi_2\left(Q(X)K(X)^T + \text{rab}^{p,t}\right)V(X)` - **点向转换(Pointwise Transformation)**\ :使用门控机制实现类似MoE的特征选择能力: :math:`Y(X) = f_2\left(\text{Norm}\left(A(X)V(X)\right) \odot U(X)\right)` 其中:\ :math:`f_i(X) = W_i X + b_i`\ 表示线性变换,\ :math:`\phi_1`\ 和\ :math:`\phi_2`\ 使用SiLU激活函数,\ :math:`\text{Norm}`\ 表示Layer Normalization(层归一化),\ :math:`\text{rab}^{p,t}`\ 表示包含位置和时间信息的相对注意力偏置,\ :math:`\odot`\ 表示元素级乘法。 这三个子模块巧妙地统一处理了传统DLRM的三个主要阶段:特征提取通过注意力机制实现目标感知的特征池化;特征交互通过门控机制实现从原始特征到交叉特征的动态结合,注意力部分(:math:`A(X)V(X)`)捕捉高阶关系,门控部分(:math:`U(X)`)保留低阶信息;表示转换中门控信号\ :math:`U(X)`\ 充当为每个特征维度定制的“路由器”,实现类似MoE的条件计算效果。 **替换softmax归一化机制** HSTU Pointwise Aggregation 步骤放弃了传统的softmax归一化,而使用了不一样的聚合方式。传统Transformer使用softmax注意力虽然在语言任务中表现优异,但在推荐场景下存在信息丢失的问题。在推荐场景中,用户兴趣的“强度”是一个至关重要的信号。传统的softmax注意力机制会将所有历史行为的注意力权重归一化,使其总和为1。这本质上是在计算一种\ **相对重要性**\ ,而非\ **绝对相关性**\ ,从而扭曲了真实的偏好强度。例如,假设目标是推荐一部科幻电影。用户A的历史记录中有10部科幻片和1部喜剧片,而用户B只有1部科幻片和1部喜剧片。对于用户A,其对科幻的强烈偏好是一个非常强的信号。然而,softmax归一化可能会迫使这10部科幻片瓜分注意力权重,从而削弱“大量观看科幻”这一整体信号的强度,使其与用户B的信号差异不再显著。相比之下,HSTU的点向聚合(通过SiLU激活)独立计算每个历史项目的相关性得分,不进行跨项归一化。这使得模型能够保留偏好的原始强度——用户A历史中的科幻片可以共同产生一个非常高的激活总分,这对于准确预估点击率、观看时长等具体数值至关重要,而不仅仅是进行排序。 **训练与召回** HSTU的完整工作流程包含训练和召回两个阶段。在训练阶段,模型基于前面描述的输入输出格式进行标准的Transformer训练——通过预测用户行为序列中的下一个正向交互物品来学习用户偏好模式和物品间的依赖关系。在召回阶段,经过多层HSTU编码器处理后,模型将用户的完整行为事件序列转换为能够概括用户历史、捕捉当前兴趣的实时动态表示\ :math:`u_i`\ 。拥有了用户表示向量\ :math:`u_i`\ 后,召回过程就转变为一个标准的向量检索问题——将\ :math:`u_i`\ 与物品语料库中所有物品的嵌入向量进行高效的相似度计算,通过近似最近邻(ANN)搜索从海量物品池中快速检索出与用户当前兴趣最匹配的Top-K个物品,形成召回列表。 总而言之,如果说SASRec是将推荐序列视为一句由‘单词’(物品ID)组成的句子,那么HSTU则是将推荐场景中的所有信息(用户属性、行为、物品、上下文)都看作一种更复杂的‘语言’,并为此设计了一套全新的‘语法规则’(HSTU单元)来理解和生成这种语言。它的核心创新在于\ **丰富了模型的输入**\ ,从而实现了对用户行为更深层次的理解。 代码实践 ~~~~~~~~ .. raw:: latex \diilbookstyleinputcell .. code:: python config = funrec.load_config('hstu') # 加载数据 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 | +===============+==============+===========+==========+ | 0.0197 | 0.0086 | 0.0083 | 0.0047 | +---------------+--------------+-----------+----------+ TIGER:通过生成语义ID重塑推荐“输出” ----------------------------------- HSTU通过极大地丰富输入信息,提升了模型对用户行为上下文的理解能力。然而,生成式召回的演进还有另一条同样重要的路径。这条路径将目光从输入侧转向了输出侧,它反思了一个根本问题:即使我们拥有了完美的输入,但我们预测的目标——一个孤立的、无语义的原子ID —— 本身是否就是最佳选择? 直接预测原子ID存在两个固有局限: 1. **语义鸿沟**\ :模型难以理解物品间的内在联系。它不知道“耐克篮球鞋A”和“耐克篮球鞋B”在本质上高度相似,这种相似性只能通过海量共现数据间接学习,效率低下。 2. **泛化难题**\ :对于从未在训练集中出现过的新物品(冷启动问题),模型完全无法进行推荐,因为它的词汇表中根本没有这个ID。 TIGER (Transformer Index for GEnerative Recommenders) :cite:`rajput2023recommender` 模型便是在这一思考下诞生的革命性工作。它提出,\ **推荐任务的核心不应是预测一个无意义的ID,而是生成一个能够描述物品内在核心语义的、结构化的‘语义ID’**\ 。 这种范式转换带来了多项优势:允许在语义相似的物品间共享知识以解决冷启动问题;通过代码字元组高效表示大型物料库;并最终简化了推荐系统架构。 TIGER框架由两个主要阶段组成:首先利用物品内容特征生成语义ID,然后在这些语义ID序列上训练一个生成式推荐模型。 **语义ID的生成 (RQ-VAE)** 语义ID的核心是为每个物品创建一个语义上有意义的代码字元组。内容特征相似的物品,其语义ID也应当相似(部分重叠),从而让模型能学习物品间的深层语义关系。 TIGER采用残差量化变分自编码器 (RQ-VAE) :cite:`zeghidour2021soundstream` 来实现这一目标。RQ-VAE是一种多层向量量化器,它通过对残差进行迭代量化来生成代码字元组。 .. figure:: ../../img/tiger_rqvae.png RQ-VAE量化方法 具体过程如下: 1. 一个预训练的内容编码器(如Sentence-T5)先将物品内容特征(如标题、描述)处理成语义嵌入向量 :math:`\mathbf{x}`\ 。 2. RQ-VAE的编码器 :math:`\mathcal{E}` 将输入向量 :math:`\mathbf{x}` 进一步编码为潜在表示 :math:`\mathbf{z} := \mathcal{E}(\mathbf{x})`\ ,并将初始残差定义为 :math:`\mathbf{r}_0 := \mathbf{z}`\ 。 3. 模型包含 :math:`m` 个层级,每个层级 :math:`d \in \{0, 1, ..., m-1\}` 都有一个独立的码本 :math:`\mathcal{C}_d`\ 。在第 :math:`d` 层,通过在码本中寻找与当前残差 :math:`\mathbf{r}_d` 最接近的码字来完成量化: .. math:: c_d = \arg\min_{k} \|\mathbf{r}_d - \mathbf{e}_k\|^2 \quad \text{其中 } \mathbf{e}_k \in \mathcal{C}_d 4. 计算新的残差并送入下一层: .. math:: \mathbf{r}_{d+1} := \mathbf{r}_d - \mathbf{e}_{c_d} 5. 重复此过程 :math:`m` 次,最终得到一个由 :math:`m` 个码字组成的语义ID元组 :math:`(c_0, c_1, ..., c_{m-1})`\ 。 RQ-VAE通过一个联合损失函数进行端到端训练,该函数包含重建损失(确保解码器能从量化表示中恢复原始信息)和量化损失(用于更新码本),从而联合优化编码器、解码器和所有码本。 为确保唯一性,如果多个物品在量化后产生了完全相同的语义ID,TIGER会在ID末尾追加一个额外的索引位来区分它们,例如 ``(12, 24, 52, 0)`` 和 ``(12, 24, 52, 1)``\ 。 **基于语义ID的生成式检索** 拥有了每个物品的语义ID后,推荐任务就转变为一个标准的序列到序列生成问题。 .. figure:: ../../img/tiger_transformer.png 基于语义ID的生成式检索 1. 将用户的历史交互序列 :math:`(\text{item}_1, \text{item}_2, ..., \text{item}_n)` 转换为其对应的语义ID序列: .. math:: ((c_{1,0}, ..., c_{1,m-1}), (c_{2,0}, ..., c_{2,m-1}), ..., (c_{n,0}, ..., c_{n,m-1})) 其中 :math:`(c_{i,0}, ..., c_{i,m-1})` 是 :math:`\text{item}_i` 的语义ID 2. 将每个物品的语义ID元组展平,形成一个长序列作为Transformer模型的输入: .. math:: (c_{1,0}, ..., c_{1,m-1}, c_{2,0}, ..., c_{2,m-1}, ..., c_{n,0}, ..., c_{n,m-1}) 3. 训练一个标准的Encoder-Decoder Transformer模型。Encoder负责理解用户历史序列的上下文,Decoder则自回归地、逐个码字地生成下一个最可能被交互物品的语义ID :math:`(c_{n+1,0}, ..., c_{n+1,m-1})`\ 。 4. 在推理阶段,一旦生成了完整的预测语义ID,就可以通过查找表将其映射回具体的物品,完成推荐。