跨模态查询、融合与候选边界
上一章训练出了一个真实但受限的图文编码器。本章把它放回资料助手,而不是停在训练损失。目标是给定查询和由真实文件建立的索引,返回可追溯候选及每一步排名。编码器现在冻结:加载检查点后关闭参数梯度并进入评估模式。索引读取实际PNG重新算图像向量,查询读取实际字符串算文本向量;不从评测答案表取向量,不从生成caption表直接做视觉判断。
索引有两类条目。文档条目来自原件逐行字段,含主题、字段、值与字符位置;图像条目来自实际像素,含固定规则提取的颜色、形状、大小与三十二维学习表示。词法分支可以检索两类条目的文本,而学习分支只比较图像向量。当前模型训练的文字仅是几何短语,没有学过借阅台的开放时间,因此不能把学习分支称为通用文档语义检索;文档问题在这一路失败正是能力边界的显式表现。
查询前先用同一有限词语法提取单位。只有存在匹配单位时才执行神经检索;完全没有域内词时返回空学习排名。这个门控防止把一个全空袋向量产生的任意方向当作有意义语义信号,但它不会消除语法漏洞:包含几何词与未知属性的混合查询仍能产生图像候选,后面的支持检查必须拒绝未支持属性。检索器不应悄悄删去“材质”二字后给出看似完整答案。
学习索引保存同一个检查点产生的单位图像向量,查询向量也是单位向量,点积即余弦,不需要再除图像长度。若更换权重或预处理,旧索引必须重建;同为三十二维不构成空间兼容证明。课程把检查点SHA写进索引与每条查询轨迹,回答阶段也核对原件散列,分别保证表示版本可追查与引用原件没有变化。这两个验证对象不同,不能用其中一个代替另一个。
算例一:两套排名为何不能直接加原分数
设某文档BM25得十二而图像余弦零点八,数字更大没有表示文档证据强十五倍,因为两者不是同一量尺。若将BM25整体乘十,词法内部排序完全不变,但直接加分融合的结果可能大变。排名融合只使用各自列表中的位置,避开这种任意缩放;代价是丢掉分数间隔信息,例如第一名远胜第二名也只体现为相邻名次。它是一项明确取舍,而不是将不同分数校准为概率。
令词法排名A、B,学习排名B、A,名次从一开始,常数取六十。每个列表向条目贡献一除六十加名次,两个条目都得到一除六十一加一除六十二,约零点零三二五二二四七四。它们精确并列,最终按稳定ID升序决定位置。若C只出现在一路第一名,则仅得一除六十一,约零点零一六三九三,仍低于双路支持的二项之和。
本项目在融合前使用所有正BM25条目和所有可编码图像条目,最后才取融合前五。资料缺词时不会为了凑列表而加入零分词法文档。常数越大,相邻靠前名次的差异越小,是否被多路召回往往更有影响;常数接近零时头部名次差异更明显。六十是沿用原始RRF研究的约定,不是经当前测试集调出的最优数,当前成绩不构成所有库都应取六十的证明。
from rag_lab.retrieval import reciprocal_rank_fusion
s=reciprocal_rank_fusion([['A','B'],['B','A']],constant=60)
assert abs(s['A']-(1/61+1/62))<1e-15
assert s['A']==s['B']
print(sorted(s.items(),key=lambda x:(-x[1],x[0])))算例二:候选正确但重排不能创造缺失证据
查询是“红色 小 圆形”,候选一是红色大圆形,候选二是红色小方形,候选三是红色小圆形。确定性覆盖重排数查询属性中有多少出现在实际像素短描述,前两者各命中三分之二,第三者三项全中为一,所以第三者被放到前面。这不是训练的交叉编码器:它没有联合理解图片与句子的神经网络,只把已有可测属性与明确查询单位比对。该区分应写在报告里,不能把透明规则改名成高级模型。
若正确图片原本没进入前五,覆盖重排看不到它,再强的排序规则也不能补回这张图片。若查询是“红色 小 圆形 材质”,重排仍可能给三项全中的图片满分,因为材质不属于words函数的域内单位;因此覆盖为一也不能授权回答材质。回答器另外检查去掉已知单位之后是否还剩未支持内容,发现“材质”便拒绝。这说明排名与支持核验是两个不同合同,不能只靠一个相关性分数。
文档重排采用另一个显式规则:主题和字段都逐字出现在查询时覆盖得分为二,否则为零;图像覆盖在零到一之间。这种跨模态优先级是人工约定,只适用于当前的主题字段查询语法。复杂混合问题如“照片里的设备在哪里借用”没有实现实体连接,不能凭这个分数假装支持跨资料推理。要扩展必须先定义对应数据与评价题,再设计关系检索,课程当前选择如实暴露缺口。
排序轨迹保存词法前五、神经前五、融合候选原分数、每条BM25分和余弦,以及覆盖重排后的顺序。不要混淆这些层:当前报告中的hit与MRR使用最终candidate_scores中的顺序,已经经过覆盖重排,严格应称“候选加重排后的检索指标”。原始两路前五仍在trace里可追溯,但报告没有单独汇总纯融合未重排的MRR,不能将现有数值标为纯RRF效果。
计算成本也要按实际算法说明。设图像条目数为I,维数为D,查询编码完成后线性扫描点积需要约I乘D次乘加,保存图像表示约I乘D个浮点数。当前I为二十四、D为三十二,直接扫描足够。若改近似最近邻,召回可能丢失真实高分项;若使用量化,分数可能改变。这些是新的近似假设,需要和精确扫描基线比,不是只换数据库名称就自动保持结果。
本次固定三十九题中,词法与混合最终各三十九题符合标注,学习分支三十题符合标注,九道文档题全失败。正确项包含六道应拒答题,因此不能把三十除三十九说成三十张图片找到;图像题共二十四道。混合没有超过强词法基线,不能用“融合更智能”作为结论。实际证据只说明这一小语料中保留词法可弥补当前学习分支没有文档能力的缺口。
还有一个重要控制:索引构建后临时删除training-pairs和评测查询表,重新建索引应得到相同entries。测试直接这样做,证明运行时没有读标签表来替代感知。这个检查不能证明训练数据绝不泄漏到参数,因为参数本来就学习二十对训练图文;它只是区分“真正学习后的前向计算”与“检索时偷读预制答案”。两种来源的证据需要分别保存。
到这里项目已经完成从实际文件到候选的闭环,但还没有完成回答。下一章将把候选组织为完整证据单位,重新核验原件,保留冲突与未知,并输出精确引用。检索相关只是进入这个下一阶段的资格,尤其不能把图片相似误读为能够推断其重量、材质、真实身份或许可状态。
TRY IT YOURSELF
练习一:一次缺席
A在词法第一、学习列表缺席;B在两路均第二。常数六十时比较RRF得分。
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习二:版本失配
新编码器仍输出三十二维,直接查询旧索引为什么没有等价保证?应保留哪些对照?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习三:评价层次
原始融合把正确项放第二,覆盖重排放第一,最终回答正确。可以说纯RRF的hit一为一吗?
提示 1
提示 2
对照参考解答
BUILD THE MECHANISM
实现融合与完整证据选择
实现RRF与整条证据选择,继续使用项目实际Markdown来源。 下载本课完整项目,在项目根目录运行;只编辑 learner/starter.py,已有构造框架由项目提供。
输入与输出 rank从1开始、缺项贡献0、并列按稳定ID;同一路重复ID拒绝。
证据预算为quote的Unicode字符数,超长项跳过,不能截断原件。
# rrf
def rrf(rankings,k=60):
raise NotImplementedError('fusion')
# full_evidence
def full_evidence(candidates,max_characters):
raise NotImplementedError('fusion')提示 1
先手算两路中每个ID的1/(k+rank)。
提示 2
累加后按(-score,id)排序,不用分数裁决事实真伪。
提示 3
首项装不下时继续试后项;两个字段的完整性另由证据门核对。
独立检查 .venv/bin/python -m learner.check --stage fusion --output runs/learner-fusion.json
迁移检验 加入一个低排名同主题冲突源,分别讨论融合排名与来源裁决;排序高不能消除冲突。
参考实现与解释
# rrf
def rrf(rankings,k=60):
if type(k)is not int or k<1:raise ValueError('positive integer k')
score={}
for ranking in rankings:
if len(set(ranking))!=len(ranking):raise ValueError('duplicate id in ranking')
for rank,identity in enumerate(ranking,1):score[identity]=score.get(identity,0.)+1/(k+rank)
return sorted(score.items(),key=lambda item:(-item[1],item[0]))
# full_evidence
def full_evidence(candidates,max_characters):
if type(max_characters)is not int or max_characters<0:raise ValueError('nonnegative integer budget')
selected=[];used=0
for row in candidates:
cost=len(row['quote'])
if used+cost<=max_characters:selected.append(dict(row));used+=cost
return selected