搜索 / NOTEBOOK 课程

25 · 真实搜索任务与相关性评估

深入搜索 · 真实语料/明确子集 · 公式与单变量实验 · 共享服务

真实数据实验只读 · 已保存的实验

已有输出来自保存的教学实验,并非打开页面时重新执行。 代码和原始输出保持不变;数据与历史注释保留原始语言。

25 · 真实搜索任务与相关性评估

推荐通常从用户历史预测物品,搜索必须同时理解查询与文档。相同的排序基础设施可以复用,但任务定义、标签与分母不能混用。本章从真实数据契约开始,亲手计算MRR、NDCG与召回,再观察“未判断”如何改变解读。

先预测:一个查询有3篇相关论文,前10只找回1篇,Hit与Recall会相同吗?所有未标注论文都是真的不相关吗?

本课程共享第17章的SciFact实验,不复制数据或训练器。完整语料5,183文档,测试300查询/339个正判断;训练809查询,正式线性模型只用字典序前200。新章节的子集实验会逐格标出范围,不替代原300查询结果。

学习导航: 下一章 · 综合实验

PYTHON · 01
from pathlib import Path
import os, sys
root = Path.cwd()
if not (root / 'reclab').exists(): root = root.parent
os.chdir(root); sys.path.insert(0, str(root))
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from reclab.search_lessons import context, query_vectors, score_query, labels
from reclab.search import BM25, rrf
from reclab.retrieval import topk, metrics
ctx = context()
print('Shared search run:', ctx['receipt']['run_id'])
Notebook 保存的输出 · 本页未执行
Shared search run: 20261030T124927-d1467622

1. 把输入和输出写成契约

文档表有_id/title/text;查询是query_id → text;qrels是query_id → {相关document_idx}。原始ID保留,内部从1编号,0留给共享推荐检索函数。若忘记这个约定,语料的第一篇会被静默屏蔽。

动手:检查ID唯一性、跨表引用与查询分组,而不是仅看总行数。下面只输出数量和形状;原查询/摘要留在本机。

PYTHON · 02
summary = pd.DataFrame([{'split':s, 'queries':len(q), 'positive_qrels':sum(map(len,q.values()))} for s,q in ctx['truth'].items()])
display(summary)
print('documents / vectors:',len(ctx['corpus']), ctx['vectors'].shape)
assert ctx['corpus']['_id'].is_unique
assert not (set(ctx['truth']['train']) & set(ctx['truth']['test']))
assert all(1 <= d <= len(ctx['corpus']) for split in ctx['truth'].values() for truth in split.values() for d in truth)
Notebook 保存的输出 · 本页未执行
split queries positive_qrels
0 train 809 919
1 test 300 339
documents / vectors: 5183 (5183, 384)

2. 一项指标回答一个问题

Recall@k=找回相关数/该查询已知相关总数;Hit@k只问有没有找到;MRR只关心第一个相关结果;二值NDCG把每个相关结果按1/log2(rank+1)折扣,再除以理想排序。都是先对查询计算,再宏平均,不能把所有文档对混成一次平均。

先预测→改变量:只把第一条不相关文档换成相关文档,哪些指标变化?这是构造排名的机制验证,随后才回到真实qrels。

PYTHON · 03
truth = {'q':{1,2,3}}
for prediction in [[4,2,5],[1,2,5]]:
    display(pd.DataFrame([metrics({'q':prediction},truth,3)], index=[str(prediction)]))
rel = np.array([0.,1.,0.]); discount=1/np.log2(np.arange(3)+2)
manual_ndcg=(rel*discount).sum()/discount.sum()
assert np.isclose(manual_ndcg, metrics({'q':[4,2,5]},truth,3)['ndcg@3'])
assert metrics({'q':[4,2,5]},truth,3)['hit_rate@3']==1
assert metrics({'q':[4,2,5]},truth,3)['recall@3']==1/3
Notebook 保存的输出 · 本页未执行
recall@3 ndcg@3 mrr@3 hit_rate@3
[4, 2, 5] 0.333333 0.296082 0.5 1.0
recall@3 ndcg@3 mrr@3 hit_rate@3
[1, 2, 5] 0.666667 0.765361 1.0 1.0

3. 判断不完整时,分母来自哪里

SciFact导出仅提供正判断,没有穷尽负判断。未标注文档在本实验指标中按0处理,这是可复现的评估约定,不是对真实世界相关性的证明。known_positive_fraction@10只是已知正判断比例。

下面找出多正例查询,显示它们对宏平均的贡献。解释:为什么按所有命中数汇总再除所有正例数,不等于查询宏平均Recall?

PYTHON · 04
truth=ctx['truth']['test']
rows=[]
for r in ctx['per_query']:
    relevant=len(truth[r['qid']]); recall=r['metrics']['bm25']['recall@10']
    rows.append({'qid':r['qid'],'relevant':relevant,'recall':recall,'hits':recall*relevant})
q=pd.DataFrame(rows)
display(q[q.relevant>1].head(8))
print('macro recall:',q.recall.mean(),'pooled recall:',q.hits.sum()/q.relevant.sum())
assert np.isclose(q.recall.mean(),ctx['result']['rows'][0]['recall@10'])
Notebook 保存的输出 · 本页未执行
qid relevant recall hits
8 1029 3 1.00 3.0
9 1041 2 1.00 2.0
23 1132 2 0.50 1.0
63 1274 3 1.00 3.0
81 133 5 0.20 1.0
95 1379 4 0.50 2.0
100 141 2 1.00 2.0
107 179 4 0.75 3.0
macro recall: 0.7909444444444444 pooled recall: 0.7728613569321534

4. 用人为补判断观察偏差

只改变某个原本未判断文档的相关标签,保持排名不变。指标变化来自标签覆盖,不是检索器变好了。以下新标签明确是构造反事实,绝不写回真实qrels。

PYTHON · 05
r=ctx['per_query'][0]; qid=r['qid']
original=set(truth[qid]); prediction=[int(np.flatnonzero(ctx['corpus']['_id'].values==d)[0])+1 for d in r['top10']['bm25']]
unjudged=next((d for d in prediction if d not in original),None)
if unjudged is not None:
    display(pd.DataFrame([metrics({qid:prediction},{qid:original},10),metrics({qid:prediction},{qid:original|{unjudged}},10)],index=['original judgments','constructed extra positive']))
print('真实qrels未修改:',truth[qid]==original)
Notebook 保存的输出 · 本页未执行
recall@10 ndcg@10 mrr@10 hit_rate@10
original judgments 0.0 0.000000 0.0 0.0
constructed extra positive 0.5 0.613147 1.0 1.0
真实qrels未修改: True

5. 来源、复现与练习

docs/search_sources.json的20项输入有发布仓库固定提交、字节数和SHA。首次context()核验全部输入。模型、原始数据与逐查询输出均不进入Git;正式run的配置与代码摘要可追溯。

练习:构造两个查询,一个有1个正例,一个有5个正例,让宏平均与汇总Recall产生明显差异;再解释为什么空目标查询不能用0分悄悄混入当前协议。下一章保持这套标签不变,只改变词法检索。

继续阅读原文

本章来源

作者课程中文

王树森 · 搜索引擎技术

相关性第1–2讲

固定版本:六模块目录、9份PDF。查询处理、召回、排序、查询推荐四模块主要是目录,Lab实验为独立教学实现。

学习标记可选,仅保存在当前浏览器中。打开页面不会自动标记已学。