Where Beam Search Loses Top-Ranked Items:First-Step Coverage in Generative Recommendation

Heng Yao, Hanwei Zhu · arXiv 2611.01072 · 发表 2026-10-31
14 分钟阅读时长 5421字 4.25综合分 第 5 名11-04 Generative 日报
目录 · 9 节1它到底在解决什么问题?2怎么知道丢在了哪一步?3宽度怎么重新分配?4实验:挪宽度真的有用吗?5输出完全一样,能省多少时间?6输出变多以后呢?7我认为它的局限8一个推荐团队可以怎么试9结论
相关 4新颖 4实用 5证据 4时效 4

做生成式推荐、正在为束搜索的延迟和召回发愁的团队值得读。生成式推荐用束搜索逐位生成 SID,想少漏好结果,通常的做法是把每一步的宽度一起调大。这篇论文在 TIGER 的六组完整测试设置里逐个排查:按模型自己的打分本该进前 10、却被剪掉的结果,全都是在第一步就丢掉的。所以更划算的做法是把宽度集中给第一步,后面几步收窄。模型、SID 和打分都不动,改的只是每一步留多少个前缀。

这样分配后,只用约一半的前缀预算(所有步骤保留的前缀总数),就复现了宽度翻倍的束搜索给出的前 10 名,连顺序都一样:三个 RQ-VAE 数据集的 77,373 个测试用户全部一致,预算从 301 降到 151。同样的预算下,把宽度挪到第一步,模型自己的前 10 名能多找回 0.96 到 10.90 个百分点。解码耗时只降了 6.35% 到 6.39%,因为大部分时间花在每一轮的前向调用和缓存管理上,这部分开销没有变。束宽应该花在最容易丢结果的那一步。要验证这一点,只需离线抽一批用户算一次精确打分,不用动线上服务。

它到底在解决什么问题?

先看前缀是什么。每个商品编成一串 4 个 token 的 SID,例如 <a><b><c><d>。模型看着用户历史,一个 token 一个 token 地往下生成。生成到一半的那段 SID 叫前缀,比如只生成了第一个 token 的 <a>。一个前缀下面挂着一组商品,越靠前的前缀挂的商品越多。前缀一旦在某一步被剪掉,它下面挂的所有商品也就跟着出局了。

麻烦在于,前缀的打分只算到当前这一步。一个前缀第一个 token 的概率不高,但它下面可能有一个后面几个 token 概率都很高的商品,整条 SID 加起来的总分反而最高。束搜索在第一步看不到后面的分数,就可能把它剪掉。下面是一个示意:

前缀(示意)第一个 token 的对数概率下面最好那个商品后三个 token 的对数概率整条 SID 总分宽度 1 时第一步
A-1.0-3.0-4.0保留
B-2.0-0.5-2.5剪掉

按整条 SID 的总分,B 下面的商品才是第一名;但宽度只有 1 时,第一步只看 -1.0 和 -2.0,B 就被剪掉了。后面几步再宽也救不回来,因为 B 这一支已经不在束里了。

SID 路径和宽度重新分配:上半部分是一个商品对应的完整 SID 路径,它的第一个 token 前缀在第一步只排第 75 名;下半部分对比两种宽度安排,均匀安排每步留 50 个,重新分配后第一步留 100 个、后面依次 30、20、10,两者前缀预算都是 151
SID 路径和宽度重新分配:上半部分是一个商品对应的完整 SID 路径,它的第一个 token 前缀在第一步只排第 75 名;下半部分对比两种宽度安排,均匀安排每步留 50 个,重新分配后第一步留 100 个、后面依次 30、20、10,两者前缀预算都是 151

上图就是论文的例子:一个商品的第一个前缀在第一步排第 75 名。每步留 50 个时它被剪掉;第一步留 100 个时它能留下来,而总预算一样。

怎么知道丢在了哪一步?

要找到丢失的位置,先得知道“本该找到什么”。作者做了一个只在离线评测时用的参照。他们让模型给目录里每个商品的完整 SID 都打一遍分,分数是 4 个 token 的对数概率之和。总分最高的 10 个,就是模型自己心目中真正的前 10 名,论文叫精确前 10(exact top-10)。线上推理时没有这个参照,它只用来诊断束搜索漏了什么。

有了参照,就能分出两种错误。用户真实下一次点击的商品没被推荐出来,可能是模型本身打分不准,也可能是束搜索剪掉了。拿精确前 10 来比,就只看搜索造成的损失。作者用一个指标衡量它,叫搜索召回(Search Recall@10):束搜索给出的前 10 里,有几成落在精确前 10 中。

然后对每个被漏掉的精确前 10 商品,查它的 SID 前缀是在第几步第一次掉出束的,论文叫首次剪除深度。下图是 Beauty 数据集、RQ-KM 编码(编码方式见实验一节)、每步留 50 个时的一次结果:

被剪掉的位置分布:上面一条是用户真实点击的商品,约一半在第 1 步就掉出束、37% 在第 2 步;下面一条是模型自己的精确前 10,89% 一直留到最后,掉出的 11% 全在第 1 步
被剪掉的位置分布:上面一条是用户真实点击的商品,约一半在第 1 步就掉出束、37% 在第 2 步;下面一条是模型自己的精确前 10,89% 一直留到最后,掉出的 11% 全在第 1 步

两条对比很能说明问题。用户真实点击的商品,丢失分散在第 1 步和第 2 步,因为里面混着模型本身打分不准的那部分。模型自己的精确前 10,89.34% 留到了最后,被剪掉的全部在第 1 步。作者在三个数据集、两种 SID 构造方式的六组设置里都查了一遍,结论都一样。被剪掉的精确前 10 全部丢在第一步,原因是第一步的前缀下面挂的商品最多,这时还看不到后面 token 的分数。

宽度怎么重新分配?

既然问题在第一步,就把宽度往第一步挪。为了公平比较,作者先定义了前缀预算:除最后一步外,每一步保留的前缀个数加起来,再加上起点的 1 个。最后一步留下的是完整的 SID,不再往下扩展,所以不算。

用论文的配置手算一下。每步留 50 个(记作 Beam-50),预算是 1 + 50 + 50 + 50 = 151。每步留 100 个(Beam-100),预算是 1 + 100 + 100 + 100 = 301,所以宽度翻倍,预算也翻倍。

作者提出的 Front-151 是第一步留 100、第二步 30、第三步 20、最后留 10,预算是 1 + 100 + 30 + 20 = 151。这意味着 Front-151 和 Beam-50 花的预算一样多,只是花在了不同的步骤上。

每一步怎么挑前缀,和普通束搜索一样:把上一步留下的所有前缀各往后接一个 token,只接能对应到真实商品的那些;所有接出来的新前缀放在一起,按截至这一步的累计对数概率排序,取前 b 个,b 就是这一步的宽度。Front-151 改的只是每一步的 b。 Hoot 说

Hoot 说宽度是所有分支共用的名额:走两步看看(示意)收起 ✕
各分支接出的新前缀合在一起排名,宽度是共用名额;落选的前缀连同下面所有商品一起出局。
起点接出 A、B、C 三个前缀,分数 -1.0、-2.0、-2.6,宽 2 留下 A 和 B,C 被剪掉起点用户历史A-1.0,留B-2.0,留C-2.6,剪掉
起点接出 A、B、C 三个前缀,分数 -1.0、-2.0、-2.6,宽 2 留下 A 和 B,C 被剪掉起点用户历史A-1.0,留B-2.0,留C-2.6,剪掉
1/2 · 第一步:宽 2(示意)三个第一个 token 按对数概率排,留前 2 个,C 和它下面的商品就此出局。
A 接出 A1 累计 -2.5、A2 累计 -3.0,B 接出 B1 累计 -2.1、B2 累计 -3.2;合在一起排序,宽 1 只留 B1A累计 -1.0B累计 -2.0A1接 -1.5,累计 -2.5A2接 -2.0,累计 -3.0B1接 -0.1,累计 -2.1B2接 -1.2,累计 -3.2合在一起排名B1 第一,宽 1 只留 B1
A 接出 A1 累计 -2.5、A2 累计 -3.0,B 接出 B1 累计 -2.1、B2 累计 -3.2;合在一起排序,宽 1 只留 B1A累计 -1.0B累计 -2.0A1接 -1.5,累计 -2.5A2接 -2.0,累计 -3.0B1接 -0.1,累计 -2.1B2接 -1.2,累计 -3.2合在一起排名B1 第一,宽 1 只留 B1
2/2 · 第二步:宽 1,四个新前缀一起排(示意)A、B 各接两个能对应真实商品的 token,分数累加后放在一起排。名额只有 1 个,B1 拿走;A 下面两条一起出局,不是每个分支各留一个。
1/2

示意数字。每一步把留下的前缀各接一个 token,所有新前缀按累计对数概率放在一起排,取前几名。 出处: 论文 §2.1 式 (2):每一步把留下的前缀各接一个合法 token,按截断到这一步的累计分数取前 b 个;数字为示意

这里的预算是一个计数,不是耗时。它是要扩展的前缀个数的上限:某个分支下面可选的 token 不够多时,实际扩展的会更少。实际耗时后面单独测。整个改动只在解码时生效:模型参数、SID 和每个 token 的打分都不变。

实验:挪宽度真的有用吗?

实验用开源工具包里的 TIGER 实现,数据是 Amazon 的 Beauty、Sports、Toys 三个品类,每个品类各用两种方式构造 SID:残差 K-means(论文记作 RQ-KM)和 RQ-VAE,一共六组。搜索召回和输出是否一致,都在完整测试集上算;耗时只抽了每个模型 512 个用户来测。

第一个问题:同样的预算下,Front-151 能比 Beam-50 多找回多少精确前 10?

数据集 / SID 构造测试用户Beam-50(预算 151)Front-151(预算 151)提升Beam-100(预算 301)
Beauty / RQ-KM22,3630.89340.9813+0.08790.9814
Sports / RQ-KM35,5980.89470.9882+0.09350.9898
Toys / RQ-KM19,4120.98560.9999+0.01430.9999
Beauty / RQ-VAE22,3630.94660.9950+0.04830.9950
Sports / RQ-VAE35,5980.88390.9929+0.10900.9929
Toys / RQ-VAE19,4120.99030.9999+0.00960.9999

六组全部提升,而且 Front-151 基本追平了预算翻倍的 Beam-100。Toys 提升小,是因为 Beam-50 在 Toys 上已经找回了 98% 以上,没什么可补的。

第二个问题:提升真的来自“宽度给了第一步”,而不是别的什么?作者在 Sports / RQ-VAE 上做了对照,预算都是 151,只换宽的那一步的位置。Front-151 是 100、30、20、10,找回 99.29%。把 100 挪到第二步(30、100、20、10)或第三步(30、20、100、10),都只找回 74.92%,差了 24.37 个百分点。这说明光是宽度放在哪一步,就足以造成 24 个百分点的差距:后面的步骤只能在已经留下的分支里挑,挑不回第一步丢掉的分支。

第三个问题:找回的是不是只是第 6 到第 10 名这些边缘结果?作者把同样的比较换成前 5 名,六组仍然全部提升,Sports / RQ-VAE 在前 5 名上提升 6.48 个百分点。也就是说,最靠前的几个推荐也会被第一步剪掉。

预算和搜索召回的关系:横轴是前缀预算,纵轴是搜索召回,把宽度集中在前面的安排在更少的预算下就达到了均匀加宽的水平
预算和搜索召回的关系:横轴是前缀预算,纵轴是搜索召回,把宽度集中在前面的安排在更少的预算下就达到了均匀加宽的水平

再看不同预算下的表现,如上图。作者又试了一个稍宽的安排 Front-211,第一步留 140 个,后面依次是 40、30、10。它在六组设置里的搜索召回都超过了 Beam-100,预算却少了约 30%。这说明把宽度集中在前面,用更少的预算就能达到均匀加宽的水平。

输出完全一样,能省多少时间?

最有工程价值的结果是这一条:在三个 RQ-VAE 数据集的全部 77,373 个测试用户上,Front-151 和 Beam-100 给出的前 10 名完全相同,顺序也一样。对下游来说,换成 Front-151 拿到的输入和原来没有任何区别,不用重新评估排序效果。

耗时上,作者拿 Front-151 和“每步留 100、最后一步也只留 10”的安排比解码器时间。最后一列来自另一次单独的性能剖析,看的是这个对照安排里前向调用和缓存管理占了多少时间:

数据集 / SID 构造每步 100、最后 10 的耗时(ms)Front-151 耗时(ms)节省(%)前向和缓存管理占比(%)
Beauty / RQ-KM16.6315.426.6179.15
Sports / RQ-KM17.6816.217.0077.83
Toys / RQ-KM17.4016.266.0179.63
Beauty / RQ-VAE17.4516.336.3577.32
Sports / RQ-VAE17.4116.276.3976.78
Toys / RQ-VAE17.0515.906.3578.33

输出逐个核对过完全一致的只有三行 RQ-VAE,节省 6.35% 到 6.39%;RQ-KM 三行的输出和对照并不完全一样,比如 Sports / RQ-KM 上 Front-151 的搜索召回是 0.9882,对照是 0.9898,所以这三行只能当耗时参考。

预算减半,耗时却只少约 6%。原因在最后一列:前向调用和缓存管理占了 76.78% 到 79.63% 的时间。论文的剖析显示,后面几步变窄,减少的是候选和挑选的工作;4 轮前向调用,以及每轮的调用和缓存开销都还保留着。这些保留下来的开销,限制了整体能省多少。

搜索找得更全,对真实点击的预测帮助却不大。在 Sports / RQ-VAE 的 35,598 个用户上,前 10 命中用户真实下一次点击的次数,从 Beam-50 的 659 次增加到 Front-151 的 677 次。Recall@10 只从 0.0185 升到 0.0190。这说明复现模型排序和预测准用户行为是两件事:复现排序靠搜索,预测准行为还得靠模型本身。

输出变多以后呢?

前面都是只要前 10 个。作者在每个模型 256 个用户上做了探索性实验,宽度安排是在 Sports 上先试过再定的。看输出变多时会怎样。每步宽度固定 100,在 Sports 上要的结果从 10 个增加到 100 个,搜索召回从约 99% 降到 86%。这说明要的输出多了,后面几步也会开始丢结果,宽度安排得重新调。作者给的调法是:第一步保留全部可能的第一个 token,第二、三步各留输出数的 1.5 倍,最后一步留输出数。要 100 个结果时,拿每步宽 200 的束做对照;要 200 个时,拿每步宽 400 的束做对照。两种情况下,这样安排给出的候选和对照束完全一样。

反过来,如果输出仍是 10 个、只是拿来对照的束更宽,就有很多工作是多余的。作者让第一步保留全部可能的第一个 token,第二、三步各留 20 个,最后留 10 个。这样复现了每步宽 200 到 800 的束给出的前 10 名,比宽 200 的束省 10.59% 到 11.89% 的时间,比宽 800 的省约一半。省得多,是因为多出来的宽度对输出没有任何影响。

我认为它的局限

第一,宽度安排是在 Beauty 的测试集上摸索出来的,再固定下来用到 Sports 和 Toys。Beauty 的结果有在测试集上调参的成分,Sports 和 Toys 的结果更可信,好在后两者同样有效。

第二,实验规模和线上差得远。三个品类的目录只有 11,924 到 18,357 个商品,模型是 TIGER。目录到千万级、SID 结构更深时,丢失是否还都集中在第一步,要在自己的系统上重新查。输出 100 个时召回降到 86% 就是一个信号:条件一变,瓶颈位置也会变。

第三,时间收益和实现方式绑在一起。6% 左右的节省是在作者的硬件上测的:每次调用只处理一个用户,用 FP32 精度,编码用户历史的时间不算在内。线上多个用户合批解码时,前向和挑选候选各占多少时间都会变,能省多少要在自己的系统上测。

一个推荐团队可以怎么试

  1. 先离线建精确参照。抽一批用户,用现有模型把目录里每个商品的完整 SID 都打分,取前 K 名。这一步只用于诊断,不进线上链路。
  2. 统计首次剪除深度。用线上现有的束宽跑一遍,看被漏掉的精确前 K 主要掉在第几步。如果也集中在第一步,就值得往下做;如果分散在各步,说明瓶颈不同,挪宽度不一定有用。
  3. 在相同预算下把宽度挪到丢失最多的那一步,比较搜索召回和输出是否与更宽的束一致。输出完全一致时,下游排序不用重新评估。
  4. 最后在线上实现里测真实耗时,同时看前向计算占比,判断能省多少。

结论

这篇论文的价值在方法论:加宽之前先查清楚结果丢在哪一步。

维度评价
问题重要性中高。束宽直接决定生成式推荐的召回和延迟,几乎每个团队都要调
方法创新性中。思路简单,贡献在把损失定位到具体一步,并用对照实验证明
实验证据中。六组设置、完整测试集、逐用户核对输出一致,但数据集小,宽度安排在其中一个测试集上调出
工程成本低。只改每步的宽度,不重训、不改 SID
是否值得尝试值得。诊断只需抽样用户离线算一次精确打分,成本随目录变大而增加,但不碰线上服务

读完这篇,你觉得

点一下即可,无需登录;再点一次撤回。只统计匿名次数,不记录你是谁。

讨论

欢迎补充实验细节、复现经验或不同看法。需 GitHub 登录。

正在加载讨论…