top-k同分:检索结果与召回率为何会变

黎 浩然/ 20 10 月, 2026/ 大语言模型/LARGELANGUAGEMODEL/LLM, 教程/TUTORIAL, 计算机/COMPUTER/ 0 comments

top-k结果中的分数已经排好序,不代表并列分数对应的文档ID也固定。尤其第k名落在一组同分候选中时,返回的成员可能有多个合法答案;它们的分数列表相同,检索召回率却可能不同。解决方法是先明确并列规则,再决定评测是否把这些合法答案视作等价。

文章目录
  1. 排好分数,与固定文档不是一项保证
  2. 第3名的边界,能改变召回率
  3. 用边界计数给出可解释范围
  4. 稳定排序还需要稳定的第二关键字
  5. 参考实现:先定义语义,再测性能
  6. 把并列全收,不再是固定k
  7. 复现一次排名,记录哪些证据
  8. 来源与补发说明

这篇针对“torch.topk同分索引为何不稳定、RAG检索如何复现排名”的搜索问题,给出一个六文档反例、召回率范围推导和可执行的参考排序器。它不把随机种子当作排序契约,也不把教学数字当作某个真实检索系统的成绩。Read in English。

排好分数,与固定文档不是一项保证

PyTorch 2.8的topk文档指出,sorted=True保证返回的k个元素本身有序,但同分元素的索引不保证稳定。这个接口定义并不表示每次运行一定变化;它表示调用者不能把观察到的一次同分顺序当作保证。

还有三个不同的稳定目标:同一候选输入反复运行,结果是否相同;候选到达顺序变化后,结果是否相同;设备、版本或数值精度变化后,结果是否相同。只通过第一项实验,并不能自动回答后两项。若候选分数本身变了,问题更已经超出“同分怎么排序”。

先保存选取前的候选ID和完整分数,再比较结果。日志里都显示0.82,不一定是存储值精确相等;显示格式可能把0.821与0.824都截成同一文本。反过来,量化或离散评分也确实可能制造大量相同值。诊断应依据参与排序的值,而不是屏幕上格式化后的字符串。

第3名的边界,能改变召回率

构造六个虚拟文档:A得9分,B、C、D都得8分,E得7分,F得6分。取k=3时,A必须入选,而B、C、D中任选两个都符合“选出最高三分”的要求。于是集合可以是{A,B,C}、{A,B,D}或{A,C,D},分数列表都为[9,8,8]。

再设相关文档真值G={A,B,D},定义Recall@3=选中相关文档数/3。三组合法结果分别是2/3、1、2/3。这里分母是固定真值集合的大小,不是返回数量。分数未变、模型未变,仅边界同分选择不同,就能出现三分之一的召回差异。

这是刻意放大的教学反例,不代表真实项目一定有同等波动。它提醒评测者:比较模型之前,先检查第k名分数的同分组是否跨过截断边界。如果所有同分候选都在边界之内或之外,成员集合的歧义不在这里;但组内顺序仍可能影响MRR等顺序敏感指标,需要另行定义。

必选:A=9分
边界:B=C=D=8分,选2个
三种集合:ABC / ABD / ACD
相关集合ABD → 召回2/3或1
原创边界示意:同一分数列表[9,8,8]对应不同成员与召回率。

用边界计数给出可解释范围

令a个候选严格高于边界分数,边界同分组有t个候选,需要从中取s=k−a个。严格高分区有rₕ个相关文档,同分组有rₜ个相关文档,固定真值集合大小为g>0。能取到的同分相关文档最少为max(0,s−(t−rₜ)),最多为min(s,rₜ)。

所以不额外规定同分规则时,这个查询的召回范围为[rₕ+max(0,s−(t−rₜ))]/g至[rₕ+min(s,rₜ)]/g。推导很直接:最坏情况先取所有可用的不相关候选,剩余名额才被相关候选占据;最好情况反过来。本例a=1、t=3、s=2、rₕ=1、rₜ=2、g=3,得到[2/3,1]。

范围不是置信区间,也不估计未知相关性。它使用完整候选分数与已有真值,只描述精确同分边界的选择歧义。这里假设0≤s≤t且真值集合固定。如果近似检索根本没有召回某篇候选,这个公式无法把它补回来。候选生成变化、相关性标注缺失和同分选择必须分开记录。

稳定排序还需要稳定的第二关键字

torch.sort的stable=True保留等价元素原有顺序;Python排序说明也定义了这种稳定性。若上游候选顺序来自并发分片返回,保留原顺序仍可能让最终文档随到达顺序变化。稳定算法不等于稳定输入。

一种明确的参考契约是“分数降序,再按唯一文档ID升序”。本例固定选A、B、C;它在候选、分数与ID规则不变时可复现,却不是相关性最优的保证。ID只是消除歧义的规则,不能证明B比D更相关。若产品有更合理的次级业务分数,应写出完整优先级与最后的唯一键。

不要给分数偷偷加一个与ID相关的小ε来“打破并列”。如果两个真实分数差距比ε还小,这会反转原本不相等的排名。使用分数与ID的词典序键,可以把并列规则与主分数分开。用“差值小于ε就相等”的两两比较还可能失去传递性,例如0、0.75ε、1.5ε相邻两对接近,首尾却不接近。若业务选择分桶或四舍五入,应该承认这重新定义了哪些分数视作并列,并记录桶规则。

候选资格:过滤规则与快照
主键:实际分数降序
次键:唯一ID升序
分别核验分数、成员、顺序
原创排名契约图:固定候选和复合键,再比较评测结果。

参考实现:先定义语义,再测性能

下面的排序器限定唯一字符串ID、有限浮点分数与0≤k≤候选数,按(-score,ID)排序。它遍历六个候选的全部720种输入排列,确认输出不随到达顺序变化,并枚举边界选择的三种成员集合。

# Synthetic retrieval ranking; no PyTorch or search-server benchmark.
from itertools import permutations
from fractions import Fraction
from math import isfinite

rows = [('A',9.), ('B',8.), ('C',8.), ('D',8.), ('E',7.), ('F',6.)]
gold = {'A','B','D'}

def rank(rows, k):
    if not 0 <= k <= len(rows):
        raise ValueError('k outside candidate count')
    ids = [doc for doc, score in rows]
    if len(set(ids)) != len(ids):
        raise ValueError('duplicate document ID')
    if not all(isfinite(score) for doc, score in rows):
        raise ValueError('nonfinite score')
    return tuple(doc for doc, score in sorted(rows, key=lambda r: (-r[1],r[0]))[:k])

def recall(ids):
    return Fraction(len(set(ids) & gold), len(gold))

assert rank(rows,3) == ('A','B','C')
assert all(rank(list(p),3) == ('A','B','C') for p in permutations(rows))
sets = {frozenset(['A'] + list(p[:2])) for p in permutations(['B','C','D'])}
assert len(sets) == 3
assert min(map(recall,sets)) == Fraction(2,3)
assert max(map(recall,sets)) == 1
inclusive = {doc for doc,score in rows if score >= 8}
assert len(inclusive) == 4 and recall(inclusive) == 1
assert rank(rows,0) == ()
assert rank(rows,len(rows)) == ('A','B','C','D','E','F')
for bad,k in [(rows,7), ([('A',1.),('A',2.)],1), ([('A',float('nan'))],1)]:
    try:
        rank(bad,k)
    except ValueError:
        continue
    raise AssertionError('invalid input accepted')
print('11 ranking checks passed, including all 720 input permutations')

十一项检查包含八个断言与三个非法输入拒绝场景,代码已在本地执行。它没有调用torch.topk,没有测试GPU内核,也不是近似向量检索的性能测评。全排序参考实现按n个候选需要O(n log n)次量级的比较,保留O(n)量级辅助空间;字符串比较成本还与ID长度有关。

大候选集可用支持同一复合键的部分选择方法优化,但先用参考实现核对语义。只对某次topk已选出的k个结果再按ID排序,无法恢复边界处已经被排除的同分文档。若底层只返回截断后的结果,调用层通常缺少足够信息重建完整同分组;需要上游提供更强排序契约或足够候选,不能靠事后整理宣称问题已解决。

把并列全收,不再是固定k

另一个可解释策略是保留所有不低于第k名分数的候选。本例返回A、B、C、D,召回为1,但返回数是4。这个策略可以合法使用,前提是准确说明查询实际返回数量与评测定义,不能把它直接当作原来只返回3篇的同一项成绩。

同分全收会改变后续重排或上下文预算,尤其边界组很大时。若系统只能给生成器三段上下文,仍然需要一个选三段的规则。把歧义挪到下一阶段,并没有移除它。比较实验应固定最终可见结果和资源约束,避免让一组多取文档后看起来“召回更高”。

此前的RAG权限过滤顺序讨论候选资格;本文讨论合法候选内部的并列排序。先确定候选集合,再固定评分与并列契约,最后计算指标,三步都应在实验记录中可追溯。

复现一次排名,记录哪些证据

保留查询与候选快照的版本标识、候选ID、实际参与排序的分数表示、k、次级键和指标真值版本;涉及用户数据时仅在有授权的本地记录中保存,公开示例使用虚构ID。若分数来自不同精度或不同归一化路径,应把这些变化独立列出,而不是统称“top-k随机”。

验收时分别比较分数序列、成员集合和完整顺序。分数一样而成员不同,优先查截断边界;成员一样而顺序不同,检查指标是否对顺序敏感;候选集合已经不同,先返回候选生成阶段。这种分层比只对比最后一个Recall数字更容易定位原因。

如果这个反例、范围推导与参考排序器帮你减少了检索评测的误判,欢迎通过文末支持区自愿支持。它们的价值在于让下一次模型对比有可核对的排名契约,而不是保证某个模型得到更高分。

来源与补发说明

torch.topk · torch.sort · Python sorting

按原计划时段前已有的PyTorch 2.8版本定义核对,推导和图示为原创。本次补发保留10月20日23:30北京时间文章日期,实际公开时间另行核验。执行了标准库示例,没有运行PyTorch集成测试或设备性能测量。

支持

如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*