NDCG为什么虚高:先检查理想排序的分母
候选集里的NDCG等于1,不代表检索已经找到了最好的文档。如果计算理想排序时只留下模型返回的候选,漏掉的高相关文档也从分母消失,分数就可能把“候选内排得好”误写成“完整检索做得好”。先固定评估集合,再讨论模型分数。
文章目录
下面用四份虚构文档手算NDCG@2,构造同一份结果得到1或约0.617的反例,再检查零IDCG查询、查询平均和重复文档。目标是让RAG检索评测可以复算,不把一个漂亮的小数当作答案质量证明。Read in English。
这个NDCG到底评价哪一份文档集合
scikit-learn 1.7.2的NDCG说明将折损累计增益除以最佳可能排序的增益。关键在于“最佳可能”是相对于什么集合:完整固定的评估集合,还是某次检索留下的候选?两种口径可以分别有用途,但不能不加说明地混为一谈。
本文固定判断集合J={A,B,C,D},相关等级依次为3、2、1、0;等级是人为教学设定,不是真实评测数据。每个查询有自己的一套相关性标注。模型只返回有序ID,评估器从独立的truth表取等级,不能把模型自己的相似度分数当作真实相关性。
这里选择线性gain(r)=r,并使用从1开始的位置折损log₂(i+1):DCG@K=Σᵢ₌₁ᴷ rᵢ/log₂(i+1)。有些口径使用2ʳ−1作为增益;它会更强调高等级文档,数值不能直接与线性版本比较。不能只记录“NDCG@10”就认为所有实现的定义相同。
同一个结果,为什么会从0.617变成1
假设系统返回[B,C]。位置1的折损分母是1,位置2是log₂3,因此DCG@2=2+1/log₂3,约2.630930。固定完整集合时,理想顺序为[A,B],IDCG@2=3+2/log₂3,约4.261860。相除得到约0.617320。
若先删除未召回的A与D,只用候选[B,C]计算理想排序,理想顺序也成为[B,C]。分子没有变化,分母却降为同样的2.630930,于是NDCG变成1。这里没有改进检索,也没有移动任何一份文档;变化只有评估问题。
候选内评估可以回答“给定这些候选,重排是否接近它们的最佳排列”。端到端评估则还要看更好的文档是否被找来。比较两个召回器时,若它们各自使用自己的候选作为理想集合,甚至可能把召回更差的一方也各自归一化成1。
分母固定,才看得见漏召回的代价
在固定J下,让系统改为[A,B],NDCG@2就是1;只返回[B]时,缺少的第二个位置贡献0,而理想分母仍来自J。不能为了列表短,就把IDCG也自动缩到返回条数。K与系统返回多少条是不同输入。这里的缺位按零增益处理;它不是把未返回文档按某个未知的低分顺序补到列表尾部。
即使没有同分,也不能在每个系统返回列表上重新采样标签:判断集合应在比较之前确定,并与查询ID、文档ID绑定。重新运行检索时只替换排序结果,保持truth不变,才是在回答同一个问题。
当K大于判断集合大小,本文计算到集合中可用的文档即可;不存在的理想项与缺少的返回位置都没有正增益。实现也会拒绝重复ID,防止同一份高相关文档在多个位置反复领分。是否按文档、页面还是chunk去重,应与相关性标注的实体一致。
真实RAG往往只有部分文档被标注,不能假装知道整个语料库的理想排序。应写清判断池如何构建、哪些文档有标签、未判断结果如何处理。本文遇到未知ID直接报错,迫使调用方明确政策,而不是悄悄将“没有标注”当成“已确认不相关”。
如果采用未判断文档计0的约定,也要在所有系统间固定同一判断池与协议,并说明覆盖局限。一个系统找到了判断池之外的有用文档,离线指标未必识别得出来;这属于证据缺口,不能拿来编造精确的真实质量排序。
IDCG为0的查询,如何计入平均
若某查询的所有等级都是0,DCG与IDCG都为0,数学上不能直接做0/0。本文约定这类查询的分数为0,并在汇总中保留。它是一项显式政策,不是由代数唯一推出的答案。
设一条正常查询得到1,另一条零IDCG查询按上述政策得到0,宏平均为0.5;若排除后一条,平均变成1。系统结果没有改变,分数却翻倍。报告时至少给出总查询数、零IDCG查询数与排除规则,不能只挑一条更高的汇总数。
还有一个常见的口径变化:先算每条查询的DCG/IDCG再平均,与先把所有DCG、IDCG分别求和再相除,不是一回事。例子中一条查询得到3/3,另一条得到0/1,宏平均为0.5,合并后相除则为3/4=0.75。后一种会按IDCG重新加权查询,不能仍称为相同的等权宏平均。
十九项检查锁住评估约定
程序只使用Python标准库,输入已经排好序的ID列表,不加载模型,也不执行向量检索。它核对手算分子分母、理想排序、漏位、零增益、候选分母反例、顺序反转、宏平均差异,以及重复ID、未知ID、非法K和非法等级。
# Linear-gain, explicit-ranking teaching evaluator; not a search-engine run.
from math import log2,fsum,isclose
checks=0
def check(ok):
global checks
assert ok
checks+=1
def dcg(grades,k):
return fsum(g/log2(i+2) for i,g in enumerate(grades[:k]))
def ndcg(ranking,truth,k):
if type(k) is not int or k<1:
raise ValueError('k must be a positive integer')
if any(type(g) is not int or not 0<=g<=3 for g in truth.values()):
raise ValueError('grades must be integers in 0..3')
if len(set(ranking))!=len(ranking):
raise ValueError('duplicate document IDs')
if any(d not in truth for d in ranking):
raise ValueError('unjudged document: policy required')
actual=dcg([truth[d] for d in ranking],k)
ideal=dcg(sorted(truth.values(),reverse=True),k)
return (actual/ideal if ideal else 0.),actual,ideal
truth={'A':3,'B':2,'C':1,'D':0}
s,actual,ideal=ndcg(['B','C'],truth,2)
check(isclose(actual,2+1/log2(3)))
check(isclose(ideal,3+2/log2(3)))
check(isclose(s,actual/ideal) and 0<s<1)
check(ndcg(['A','B'],truth,2)[0]==1.)
check(isclose(ndcg(['B'],truth,1)[0],2/3))
check(ndcg([],truth,2)[0]==0.)
check(ndcg(['D'],truth,2)[0]==0.)
check(ndcg(['B','C'],{'B':2,'C':1},2)[0]==1.)
check(ndcg(['C','B'],truth,2)[0]<s)
zero=ndcg(['Z'],{'Z':0},1)[0]
check(zero==0.)
check((1.+zero)/2==.5) # Includes zero-IDCG query under this policy.
a=ndcg(['A'],{'A':3},1);b=ndcg([],{'B':1},1)
check((a[0]+b[0])/2==.5)
check((a[1]+b[1])/(a[2]+b[2])==.75)
for ranking,labels,k in [(['A','A'],truth,2),(['X'],truth,2),(['A'],truth,0),(['A'],truth,True),(['A'],{'A':-1},1),(['A'],{'A':1.5},1)]:
try:ndcg(ranking,labels,k)
except ValueError:check(True)
else:check(False)
print(checks,'ranking-contract checks passed')
print('DCG',actual,'IDCG',ideal,'full-pool NDCG',s)
print('candidate-only NDCG',ndcg(['B','C'],{'B':2,'C':1},2)[0])
这是显式排序的小型评估器,与接受完整y_true、y_score矩阵的库接口并不相同。模型分数若存在同分,必须先按项目约定确定如何形成有序列表;此处不实现对所有同分排列求期望。站内top-k同分与召回率讨论了稳定排序和同分处理,不能把忽略同分当作无影响的性能优化。
对K截断、判断集合和去重政策做变更时,保留这组小反例作为验收,比只看一批真实查询的平均分更容易定位定义变化。代码不是用来声称某个RAG系统已经取得0.617或1,这些数字都来自手工构造的数据。
NDCG解释排序,不能替代回答证据
相关文档靠前,对检索阶段有意义,但NDCG不会检查生成器是否引用了正确句子、是否忠实于文档、是否遗漏关键限制。评估完整RAG时,应把检索相关性与回答证据分别观察,避免把离线排序分数当成回答正确率。
同样,扩大候选窗口可能改变被评估结果,却不能由这四份文档推出真实延迟或收益。对比实验需要固定查询集、标注、K、增益函数、判断池和汇总规则,再报告实际差异。已有Cross-Encoder重排解决的是候选排序流程,本文解决的是如何检查这条流程的计分口径。
如果这组分母反例与可复算代码帮你发现了检索评测虚高,欢迎在文末自愿支持。可以直接带回项目的是固定判断集合、空查询政策与查询级计分记录。
来源与补发说明
NDCG, scikit-learn 1.7.2 · DCG, scikit-learn 1.7.2
参照原计划日期前已有的scikit-learn 1.7.2版本定义;本文使用线性增益和显式ID排序,标准库实现、数据与两图为原创,不是调用该库或运行检索系统。保留2026年10月22日14:00北京时间文章日期,实际补发时间另行记录,不表示此前已经公开。
支持
如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。
二维码可点击放大。更多支持方式见支持页面。


