【搜索推荐学习】生成式召回
- 生成式召回
- [1 双塔模型](#1 双塔模型)
- [2 VQ-VAE](#2 VQ-VAE)
-
- [梯度直通估计器STE( Straight-Through Estimator)](#梯度直通估计器STE( Straight-Through Estimator))
- 损失函数
- [VQ‑VAE + PixcelCNN两阶段训练](#VQ‑VAE + PixcelCNN两阶段训练)
-
- VQ‑VAE训练(重建任务)
- [PixelCNN训练:学习离散code的先验分布 p ( I ) p(I) p(I)](#PixelCNN训练:学习离散code的先验分布 p ( I ) p(I) p(I))
- 推理生成图像(采样)
- [3 RQ-VAE](#3 RQ-VAE)
-
- [RQ(Residual Quantization)残差量化核心流程](#RQ(Residual Quantization)残差量化核心流程)
- [4 RQ-Kmeans](#4 RQ-Kmeans)
-
- [RQ‑VAE 的问题](#RQ‑VAE 的问题)
- RQ‑Kmeans算法
- [5 生成式召回的两种推理范式](#5 生成式召回的两种推理范式)
- 参考资料
生成式召回
生成式召回使用LLM作为backbone来生成召回列表,这里面涉及到:
- 召回模型是什么?
- LLM生成的是next token,生成式召回,是让LLM生成什么内容?对于分类任务,有可能是让LLM直接生成类别ID,对于生成式召回呢?
- LLM生成很慢,召回对延时要求很高,如何让LLM能够完成召回任务的?
1 双塔模型
推荐系统pipeline通常包括:召回------粗排------精排------重排这几个模块,召回层负责快速的筛选出TopK个相关的物品,对召回率和耗时要求更高,对精确度要求不高。
常见的召回模型方法包括协同过滤(ItemCF、UserCF)、I2I、双塔模型、序列召回、流式召回等。以双塔模型为例,DSSM(Deep Structured Semantic Models)是微软2013年提出的一种召回方法,模型分为Query塔(用户侧塔) 和Document塔(物品侧塔) ,两个塔网络参数相互独立:

Query塔(用户塔)
-
输入:用户侧特征(搜索query文本 / 用户行为特征 / 用户属性)
-
网络层:多层全连接神经网络,逐层做非线性变换
-
输出:固定维度的低维稠密向量 u ∈ R d \boldsymbol{u} \in \mathbb{R}^d u∈Rd(用户Embedding)
Document塔(物品塔)
-
输入:物品侧特征(doc文本 / item特征 / 物料属性)
-
网络层:多层全连接神经网络,和Query塔网络结构对称,参数不共享
-
输出:固定维度的低维稠密向量 v ∈ R d \boldsymbol{v} \in \mathbb{R}^d v∈Rd(物品Embedding)
使用余弦相似度衡量query与document匹配程度:
cos ( u , v ) = u ⋅ v ∣ ∣ u ∣ ∣ ∣ ∣ v ∣ ∣ \cos(\boldsymbol{u},\boldsymbol{v})=\frac{\boldsymbol{u} \cdot \boldsymbol{v}}{||\boldsymbol{u}|| \ ||\boldsymbol{v}||} cos(u,v)=∣∣u∣∣ ∣∣v∣∣u⋅v
将余弦相似度经过缩放,得到匹配得分:
R ( Q , D ) = cosine ( u q , v d ) R(Q,D)=\text{cosine}(\boldsymbol{u}_q,\boldsymbol{v}_d) R(Q,D)=cosine(uq,vd)
训练模型时采用分类交叉熵损失 ,基于负采样构造样本:
对于一个query,有1个正样本 D + D^+ D+(例如用户浏览过或者点击过的作为正样本), N N N个负样本 { D 1 − , D 2 − , . . . , D N − } \{D_1^-,D_2^-, ... ,D_N^-\} {D1−,D2−,...,DN−}。
- 对每一组query下的全部正负样本,做softmax归一化:
P ( D + ∣ Q ) = e γ ⋅ R ( Q , D + ) e γ ⋅ R ( Q , D + ) + ∑ i = 1 N e γ ⋅ R ( Q , D i − ) P(D^+|Q) = \frac{e^{\gamma \cdot R(Q,D^+)}}{e^{\gamma \cdot R(Q,D^+)}+\sum_{i=1}^{N}e^{\gamma \cdot R(Q,D_i^-)}} P(D+∣Q)=eγ⋅R(Q,D+)+∑i=1Neγ⋅R(Q,Di−)eγ⋅R(Q,D+)
- γ \gamma γ:平滑缩放因子,论文中为固定超参数。
- 损失为负对数似然损失:
L = − log P ( D + ∣ Q ) L = -\log P(D^+|Q) L=−logP(D+∣Q)
双塔分别编码,线上推理时,物品塔可以提前离线计算好所有item向量存入向量库;线上只需要实时计算用户侧向量,做向量库相似度检索,实现大规模召回,一次召回计算,选出TopK。
2 VQ-VAE
LLM生成的是next token,生成式召回,是让LLM生成什么内容?对于分类任务,有可能是让LLM直接生成类别ID,对于生成式召回呢?生成式召回对于LLM的生成内容进行了结构化的设计,把物品映射为编码,让LLM输出编码ID,使用了到了 码本("codebook") 的概念,这一方法最早在生成模型VQ-VAE中进行使用。对VQ-VAE的理解可以参考大佬的这篇解读。
最早时,自动编码器AE可以用来压缩图像,对于输入图像 x x x,编码器 E n c Enc Enc将输入图像 x x x压缩为 z = e ( x ) z=e(x) z=e(x),压缩向量 z z z可以被解码器 D e c Dec Dec还原为 x ^ \hat{x} x^。但是自动编码器AE只能用在压缩上,如果随机从向量空间里面采样一个向量,而不是编码器编码出来的,解码器解码得到的图片没有意义。VAE基于AE进行改进,要求向量空间是规整的正态分布,训练完了之后,就可以直接从正态分布里面随机采样再用解码器解码,得到多样性的图像。
AE和VAE都是把图片编码为连续的向量,VQ-VAE则是使用了码本,把图片编码为离散的向量,像token一样。为了让神经网络能输入离散的token,再把离散的token过一个"词嵌入embedding层",最后再输入到解码器进行解码。包含四大核心组件:Encoder编码器、Codebook码本、查码本模块、Decoder解码器。

-
Encoder编码器 : 输入原始数据 x x x,经过编码器卷积/全连接网络输出大小为 H × W × d H \times W \times d H×W×d 的向量 z e z_e ze, d d d是隐空间大小。
-
Codebook 码本 : 可学习的嵌入表 e \boldsymbol e e, K K K为码本大小,即离散token总数量,每一行 e k \boldsymbol e_k ek 是一个大小为 d d d的码本向量。
-
Vector Quantisation向量量化模块
对编码器输出的每一维向量 z e z_e ze,在码本中找距离最近的码本向量:
k = arg min j ∈ 1 , K ∥ z e − e j ∥ 2 k=\arg\min_{j\in1,K}\|z_e-\boldsymbol e_j\|_2 k=argj∈1,Kmin∥ze−ej∥2
再取对应的码本向量作为量化后向量 z q = e k z_q = \boldsymbol e_k zq=ek。
-
Decoder解码器 :接收量化离散向量 z q z_q zq,重建原始输入 x ^ \hat x x^
VQ-VAE查码本的过程,是把原始的连续的向量 z e z_e ze,变成了查到的argmin的下标,这些下标就像是离散的NLP的token。然后使用下标从码本取出连续向量 e k \boldsymbol e_k ek的过程,类似NLP中embedding层。
梯度直通估计器STE( Straight-Through Estimator)
在把连续向量变成离散向量的过程中使用了argmin查表的方式:
z q = e k , k = arg min j ∥ z e − e j ∥ 2 z_q = e_k,\quad k=\arg\min_j \|z_e-e_j\|_2 zq=ek,k=argjmin∥ze−ej∥2
arg min \arg\min argmin 是离散取索引操作,反向传播时数学上导数为0 / 不存在 。如果直接反向传播,编码器 z e z_e ze 拿不到梯度,网络完全无法更新。
STE技巧预期让
- 前向传播 :真实执行量化逻辑,输出 z q z_q zq
- 反向传播 :忽略 arg min \arg\min argmin 的梯度,直接把 ∂ L ∂ z q \frac{\partial \mathcal L}{\partial z_q} ∂zq∂L 拷贝赋值给 ∂ L ∂ z e \frac{\partial \mathcal L}{\partial z_e} ∂ze∂L
具体实现方式为改写 z q z_q zq,加上停止梯度项 s g ( ⋅ ) sg(\cdot) sg(⋅)
z q = z e + sg e k − z e z_q = z_e + \text{sg}\big\\boldsymbol e_k - z_e\\big zq=ze+sgek−ze
sg ( x ) \text{sg}(x) sg(x)表示停止梯度算子,前向传播 sg ( x ) = x \text{sg}(x)=x sg(x)=x,反向传播梯度为0,在pytorch中实现方式为 z q = z e + ( e k − z e ) . d e t a c h ( ) z_q = z_e + (e_k - z_e).detach() zq=ze+(ek−ze).detach()
这样在VQ-VAE中:
z q forward = e k , ∂ L ∂ z e ← ∂ L ∂ z q z_q^{\text{forward}} = \boldsymbol e_k,\quad \frac{\partial \mathcal L}{\partial z_e} \leftarrow \frac{\partial \mathcal L}{\partial z_q} zqforward=ek,∂ze∂L←∂zq∂L
损失函数
总损失由三部分组成:重建损失 + 码本损失 + Commitment损失:
L = − log p ( x ∣ z q ) ⏟ L recon + ∥ sg z e − z q ∥ 2 2 ⏟ L codebook + β ⋅ ∥ z e − sg z q ∥ 2 2 ⏟ L commit \mathcal L=\underbrace{-\log p(x|z_q)}{\mathcal L{\text{recon}}} +\underbrace{\|\text{sg}z_e-z_q\|2^2}{\mathcal L_{\text{codebook}}} +\beta\cdot\underbrace{\|z_e-\text{sg}z_q\|2^2}{\mathcal L_{\text{commit}}} L=Lrecon −logp(x∣zq)+Lcodebook ∥sgze−zq∥22+β⋅Lcommit ∥ze−sgzq∥22
-
L recon \mathcal L_{\text{recon}} Lrecon 重建损失
编码器+解码器参与优化,目标是让解码器从量化向量中尽可能还原原始输入。图像任务常用MSE替代对数似然。 L recon = ∥ x − x ^ ∥ 2 2 \mathcal L_{\text{recon}} = \|x-\hat x\|_2^2 Lrecon=∥x−x^∥22
-
L codebook \mathcal L_{\text{codebook}} Lcodebook 码本损失
更新码本向量, sg z e \text{sg}z_e sgze阻断梯度流向编码器,只更新码本向量,让码本向量向编码器输出靠近。
-
L commit \mathcal L_{\text{commit}} Lcommit 承诺损失
s g ( ⋅ ) sg(\cdot) sg(⋅)阻断梯度流向码本,只更新编码器,强迫编码器输出向码本向量对齐,防止编码器输出游离在码本向量之外,保证编码器"承诺"使用码本里的向量。
VQ‑VAE + PixcelCNN两阶段训练
- 整体思想是,让VQ-VAE负责学会压缩和重建,让PixcelCNN负责学会直接生成小图,然后用VQ-VAE的解码器把小图放大。
VQ‑VAE训练(重建任务)
输入图像 x x x
- Encoder: x → z e x \rightarrow z_e x→ze(连续隐特征)
- VQ向量量化: z e → z q z_e \rightarrow z_q ze→zq(从码本查出来的离散向量)
- Decoder: z q → x ^ z_q \rightarrow \hat x zq→x^,重建图像
本阶段优化:重建损失 + codebook损失 + commitment损失。这个阶段做完之后:Encoder、Codebook、Decoder全部固定冻结,不再更新。
此时的码本每一个向量对应一个整数索引 k ∈ { 1 , 2 , . . . , K } k \in \{1,2,...,K\} k∈{1,2,...,K}。
一张图像经过 Encoder+VQ量化之后,输出的不是向量,而是一张索引图 I I I 。
I h , w = k , z q ( h , w ) = e I h , w I_{h,w}=k,\quad z_q^{(h,w)}=\boldsymbol e_{I_{h,w}} Ih,w=k,zq(h,w)=eIh,w
I I I 是二维整数矩阵,每个位置的值是码本索引,是离散token。PixelCNN学习的对象,就是这张小的索引图 I I I。
PixelCNN训练:学习离散code的先验分布 p ( I ) p(I) p(I)
原始VQ‑VAE,只有编码器可以把图像转code索引,没有办法凭空生成一组code索引。
Decoder只能接收 z q z_q zq(码本向量)做图像重建,但Decoder不能自己创造code索引 。
PixelCNN是自回归模型,目标建模:
p ( I ) = ∏ h , w p ( I h , w ∣ I < h , w ) p(I)=\prod_{h,w}p(I_{h,w}\mid I_{<h,w}) p(I)=h,w∏p(Ih,w∣I<h,w)
- 输入:从训练集图像经过冻结好的VQ‑VAE(Encoder+VQ)得到的索引矩阵 I I I
- 输出:每个位置上,码本索引的分类概率分布(共K个类别,K是码本大小)
- 训练目标:交叉熵,预测每个位置的code索引,自回归从左上到右下逐像素预测token。
推理生成图像(采样)
- 通过PixelCNN自回归采样,逐位置生成一张全新的索引矩阵 I sample I_{\text{sample}} Isample。
- 拿索引 I sample I_{\text{sample}} Isample去码本查表,得到量化向量 z q , sample z_{q,\text{sample}} zq,sample。
- 将 z q , sample z_{q,\text{sample}} zq,sample送入冻结的Decoder,输出生成图片。
3 RQ-VAE
标准VQ‑VAE,每个空间位置只输出1个code索引。
- 如果想要降低特征图空间分辨率(例如从16×16降到8×8,缩短自回归序列长度,降低Transformer计算量),为保证重建质量,码本大小 K K K 需要指数级膨胀
- 但是使用超大码本带来参数量暴涨,极易发生码本坍塌(codebook collapse),只有极少数码字被频繁选中并更新,其余绝大部分码字从未被使用,训练不稳定,直接用这个查的argmin的ID来表示物品不够合理
RQ‑VAE思路:不扩大码本尺寸K,改用多轮递归残差量化,1个空间位置输出D个堆叠code 。

RQ(Residual Quantization)残差量化核心流程
输入单条特征向量 z z z,量化深度 D D D,码本 C \mathcal C C。
- 初始化残差 r 0 = z r_0 = z r0=z
- 循环 d = 1 , 2 , . . . , D d=1,2,...,D d=1,2,...,D:
{ k d = arg min k ∥ r d − 1 − e k ∥ 2 2 r d = r d − 1 − e k d \begin{cases} k_d=\arg\min_k \|r_{d-1}-e_k\|2^2 \\ r_d=r{d-1}-e_{k_d} \end{cases} {kd=argmink∥rd−1−ek∥22rd=rd−1−ekd
- k d k_d kd:第 d d d层得到的code索引;
- r d r_d rd:减去当前码本向量之后剩下的残差,送入下一轮量化;
- 累计量化向量: z ^ ( d ) = ∑ i = 1 d e k i \hat z^{(d)}=\sum_{i=1}^{d}e_{k_i} z^(d)=∑i=1deki;最终 z ^ = z ^ ( D ) \hat z=\hat z^{(D)} z^=z^(D)。
码本ID(索引)天然带前缀层次化
输出的这一组有序整数索引 ( k 1 , k 2 , . . . , k D ) \boldsymbol{(k_1,k_2,...,k_D)} (k1,k2,...,kD),也就是推荐里常说的 Semantic ID
- k 1 k_1 k1:第1轮残差量化,对原始向量 r 0 = z r_0=\boldsymbol z r0=z 做量化;对应粗粒度主体语义。
- k 2 k_2 k2:对残差 r 1 = z − e k 1 r_1 = z-e_{k_1} r1=z−ek1 量化;对粗结果做第一层修正。
- k 3 , ... , k D k_3,\dots,k_D k3,...,kD:逐层对剩下越来越小的残差量化,捕捉细粒度细节/修正项。
语义相近的向量,前面的ID更容易相同(共享前缀),后面ID才开始分叉。
向量A: 7, 1, 4, 12
向量B: 7, 1, 9, 33
前缀7,1一致,代表二者高层语义接近;后面的id区分细粒度差异。
4 RQ-Kmeans
RQ‑VAE 的问题
RQ‑VAE 的语义 ID 是模型内生出来的 ,ID 和码本、编码器权重强绑定。只要把 RQ‑VAE 重新训练 /finetune 一遍,编码器、码本向量会变。同样一个物品,输入新模型跑出来的那串 code ID 序列 ( k 1 , k 2 , ... , k D ) (k_1,k_2,...,k_D) (k1,k2,...,kD)就会变。
线上检索是拿这套语义 ID 建索引库的,模型一更新,全库所有物品的 ID 全部失效,必须把全量物料重新过一遍模型,重新生成 ID,再重建向量索引。物料库如果是百万、亿级别,全量重推、重建索引成本极高,耗时久,线上服务压力大,还会带来版本对齐问题:一部分老 ID,一部分新 ID,检索会乱。
如果能把 "向量生成" 和 "量化出 ID" 拆开:(1)向量用通用预训练模型(CLIP/BERT),这个大模型尽量不动,产出的物品向量就稳定;(2)随后基于离线预训练并冻结的聚类中心或码本对向量执行分配量化得到语义 ID,新样本仅做最近邻指派,无需重新拟合中心,保障存量物品 ID 稳定性。
RQ‑Kmeans解决了这个问题,整体的流程为:
- 输入预训练得到的物品向量,逐层对残差执行K‑means聚类,每一层产出一套独立码本
- 每层分配聚类ID后减去匹配的聚类中心得到新残差,残差送入下一层继续量化。最终每个物品输出有序ID序列 { s i 1 , s i 2 , . . . , s i L t } \{s_i^1,s_i^2,...,s_i^{L_t}\} {si1,si2,...,siLt},靠前ID代表粗粒度语义,靠后ID做细粒度残差修正。
RQ‑Kmeans算法
-
第1层初始化残差
l = 1 l=1 l=1,初始残差集合等于原始物品向量集合:
R ( 1 ) = { M ~ i ∈ R N M ~ × d t } \mathcal R^{(1)}=\{\tilde{\mathbf M}i\in\mathbb R^{N{\tilde M}\times d_t}\} R(1)={M~i∈RNM~×dt}
M ~ i \tilde{\mathbf M}_i M~i:预训练得到的固定物品向量。
-
第 l l l层:残差集合K‑means生成本层码本
C ( l ) = K‑means ( R ( l ) , N t ) \mathcal C^{(l)}=\text{K‑means}\big(\mathcal R^{(l)},N_t\big) C(l)=K‑means(R(l),Nt)
- C ( l ) \mathcal C^{(l)} C(l):第 l l l层独立码本,包含 N t N_t Nt 个聚类中心 c k ( l ) \boldsymbol c_k^{(l)} ck(l)
- N t N_t Nt:单层码本大小
-
最近邻分配,得到本层语义ID
s i l = arg min k ∥ R i ( l ) − c k ( l ) ∥ 2 s_i^l=\arg\min_k \big\|\mathcal R_i^{(l)}-\boldsymbol c_k^{(l)}\big\|_2 sil=argkmin Ri(l)−ck(l) 2
s i l s_i^l sil:物品 i i i在第 l l l层的码本ID,使用欧氏距离选择最近聚类中心。
-
更新残差,传入下一层
R i ( l + 1 ) = R i ( l ) − c s i l ( l ) \mathcal R_i^{(l+1)}=\mathcal R_i^{(l)}-\boldsymbol c_{s_i^l}^{(l)} Ri(l+1)=Ri(l)−csil(l)
减去当前匹配的聚类中心,剩余残差交给下一层继续量化。
迭代 L t L_t Lt 层,最终输出多级语义SID序列:
{ s i 1 , s i 2 , ... , s i L t } \{s_i^1,s_i^2,\dots,s_i^{L_t}\} {si1,si2,...,siLt}
5 生成式召回的两种推理范式
范式一:SID倒排查表范式
离线构建
对全量物品离线生成唯一层级SID,构建 SID → 候选物品列表 的倒排索引库(会存在少量SID碰撞,多物品共享同一SID)。
线上推理
- 自回归模型通过 Beam Search 从用户行为序列预测合法SID序列;
- 直接检索索引,命中库中预先绑定该SID的真实物品;
- 过滤无效SID、候选聚合后送入排序层。
生成式召回只有 3 个关键参数 :
(1)序列长度 Length = RQ 层数 L t L_t Lt,SID 长度由 RQ离线量化层数决定
(2)束宽度 Beam Size:每一步生成,保留多少条最优前缀序列。Beam 越大,保留的路径越多,最终产出候选 SID 越多,召回覆盖越高。同时Beam 越大,算力线性上涨,QPS 下降
(3)温度系数: T → 0 趋近贪心,只选最高概率 SID,结果精准、多样性差;T → 1:分布更平滑,允许次优 SID 出现,召回多样性强,非法 SID概率上升
范式二:SID向量重建+ANN检索范式
该范式不依赖SID倒排表 ,利用RQ-Kmeans可累加重建向量的特性,兼容任意虚拟SID。
线上推理
- 自回归模型自由生成SID序列(允许库中不存在的虚拟SID);
- 根据每层SID查表取出聚类中心,累加重建出虚拟查询向量:
z ^ q u e r y = ∑ ℓ = 1 L t c s ( ℓ ) ( ℓ ) \hat{\mathbf z}{query} = \sum{\ell=1}^{L_t} \boldsymbol c_{s^{(\ell)}}^{(\ell)} z^query=ℓ=1∑Ltcs(ℓ)(ℓ) - 将该虚拟语义向量送入Faiss ANN索引,检索向量空间最接近的真实物品。
参考资料
- DataWhale小麦书:https://datawhalechina.github.io/fun-rec/chapter_5_gr_basic/4.codebook.html
- 详解VQ-VAE:https://zhuanlan.zhihu.com/p/633744455
- 生成式召回大模型的工业级落地实践:https://hub.baai.ac.cn/view/51060
- 快手OneRec:《OneRec Technical Report》