9.3 迭代优化循环


9.3 迭代优化循环

本节摘要:把尺子与被测物装进循环,全书收官。循环五步:跑基线 → 改一个变量 → 全量评测 → 对比归因 → 保留或回滚;纪律是一次一变量(同时动两个参数就说不清谁立的功)、全量评测(抽样跑会放大噪声)、结果留档(history.jsonl 记下每次的参数与指标,趋势比单点重要)。evaluate.py 完整实现:加载评测集与 Retriever、金标漂移报警(ID 对不上索引就警告,不默默跳过)、三指标与延迟成组汇报、追加写入历史。坏例归因流程把 7.4 的排查表指标化成决策树:MRR 低 Recall 高 → 排序问题(重排/权重);Recall 低 → 召回问题 → 先看漏的 chunk 在不在索引(切分/嵌入)再看两路权重。回归护栏:CI 上跑评测,MRR 掉超阈值即非零退出——调参自由的前提是有门守着。最后是调参 playbook:全书每个旋钮回到循环里的标准入口。全书的管线至此首尾相接:检索系统不是调出来的,是测出来的。

学习目标

阅读完本节,你应当能够:

  1. 运行 evaluate.py 产出基线报告并读懂每个字段;
  2. 执行一次一变量的扫描(权重/深度/切分参数)并从 history 读趋势;
  3. 按 MRR 与 Recall 的组合形态做坏例归因;
  4. 把回归护栏接进 CI,说清阈值的意义与代价。

一、evaluate.py:闭环的核心

# evaluate.py — 评测驱动:跑评测集 → 算三指标 → 记历史(第 9 章的闭环核心) import json import time from pathlib import Path from metrics import mrr, ndcg, recall_at_k from retrieval import Retriever def evaluate(index_dir: str, evalset: str, embed, k: int = 5, tag: str = "", **search_kw) -> dict: """search_kw 透传 Retriever.search(recall/bm25_w/vec_w/reranker/rerank_depth)。""" queries = [json.loads(l) for l in Path(evalset).read_text(encoding="utf-8").splitlines() if l.strip()] retriever = Retriever(index_dir, embed) known_ids = {m["id"] for m in retriever.metas} stale = sum(1 for q in queries for cid in q["gold"] if cid not in known_ids) if stale: print(f"警告:{stale} 个金标 ID 不在当前索引中(金标漂移),请复核评测集(9.2)") per_rels, recalls, ndcgs, lat = [], [], [], [] for q in queries: t0 = time.perf_counter() hits = retriever.search(q["query"], top_k=k, **search_kw) lat.append(time.perf_counter() - t0) rels = [q["gold"].get(h["id"], 0) for h in hits] n_rel = sum(1 for v in q["gold"].values() if v >= 2) # 分母来自金标全集 per_rels.append(rels) recalls.append(recall_at_k(rels, k, n_rel)) ndcgs.append(ndcg(rels)) report = {"tag": tag or f"run-{time.strftime('%m%d-%H%M%S')}", "n": len(queries), "MRR": round(mrr(per_rels), 4), f"Recall@{k}": round(sum(recalls) / len(recalls), 4), "nDCG": round(sum(ndcgs) / len(ndcgs), 4), "latency_ms": round(1000 * sum(lat) / len(lat), 1), "params": search_kw} print(f"{report['tag']}: MRR={report['MRR']} Recall@{k}=" f"{report[f'Recall@{k}']} nDCG={report['nDCG']} " f"({report['latency_ms']}ms/query, n={report['n']})") with open("history.jsonl", "a", encoding="utf-8") as f: # 趋势比单点重要 f.write(json.dumps(report, ensure_ascii=False) + "\n") return report

三个设计点:金标漂移报警——仓库演进后旧 ID 会失配,默默跳过等于用残缺金标打分,9.2 的"活资产"靠这道报警维持;延迟与指标同报——9.1 的纪律落在数据结构上;history 追加写——JSONL 每行一次运行,git log 式回看趋势。

二、一次一变量:扫描的标准姿势

# sweep_weights.py — 混合权重扫描:公共参数钉死,只动一个旋钮 from evaluate import evaluate COMMON = dict(k=5, recall=30) # 钉死:top_k 与召回宽度 for w in (0.5, 0.7, 1.0, 1.5, 2.0): evaluate("index", "evalset.jsonl", embed, tag=f"bm25_w={w}", bm25_w=w, vec_w=1.0, **COMMON)

跑完看 history.jsonl:若 bm25_w=1.51.0 的 MRR 高且 Recall 不掉,则保留;两者持平取默认值(参数越少越好)。扫描切分参数(max_lines)要多一步——它要重建索引:pipeline.py --max-lines 60 重建到 index_60/evaluate("index_60", ...)索引目录按参数命名,避免"扫的是新索引、比的是旧结果"的事故。

三、坏例归因:从指标形态到责任段落

跑一遍 evaluate: │ ├─ Recall 低 ──► 漏了。取漏检查询,看金标 chunk: │ ├─ 不在 metas.jsonl 里 ──► 切分问题(3.3 症状学:没切出来/身份丢失) │ ├─ 在,但两路都没召回 ──► 嵌入问题(2.2 换模型)或分词问题(5.1 标识符拆分) │ └─ 在,单路召回 ──► 融合权重问题(本节扫描) │ ├─ MRR 低、Recall 高 ──► 排序问题: │ ├─ 未开重排 ──► 加 --rerank 20 起步(5.2) │ └─ 已重排 ──► 换重排器(交叉编码器 ↔ LLM 校准概率,5.2 取舍表) │ └─ 指标都高、用户仍不满 ──► 金标或期望问题: └─ 回 9.2:查询是否歧义?金标是否过期?holdout 上还成立吗?

归因的每一次分叉都以"看具体坏例"结束——指标告诉你哪里疼,坏例告诉你为什么疼。把修好的坏例回填评测集(9.2),循环就有了向前的齿。

四、回归护栏:给调参自由装一道门

# guard.py — 回归护栏:候选配置 vs 基线,掉点超阈值即非零退出(挂进 CI) import json import sys from pathlib import Path from evaluate import evaluate THRESHOLD = 0.02 # MRR 掉 2 个点即拦截(经验值) candidate = evaluate("index", "evalset.jsonl", embed, tag="candidate") best = json.loads(Path("best.json").read_text(encoding="utf-8")) if candidate["MRR"] < best["MRR"] - THRESHOLD: sys.exit(f"回归拦截:MRR {best['MRR']} → {candidate['MRR']}") print("护栏通过")

护栏的意义:换嵌入模型、改切分参数、升级依赖——这些"顺手"的改动是检索质量无声退化的主因(4.2 的模型版本混库是同族事故)。阈值是权衡:太紧则正常波动(50~100 条评测集上 MRR 波动 ±0.02 常见,经验值)天天报警,太松则形同虚设。配套纪律:只在 holdout 不掉、调参集涨分时更新 best.json——防过拟合的最后一道锁。

五、调参 playbook:全书旋钮的回环入口

旋钮 章节 进循环的姿势
chunk 行数上限 3.1/3.3 重建索引(按参数命名目录)→ 全量评测
嵌入模型 2.2 换模型必全量重嵌(model_ver 保险丝)→ 评测集重跑基线
召回宽度 5.1 纯参数扫描,最便宜,先扫它
混合权重 5.1 按 type 分组看指标(9.2 的配比在此兑现)
重排深度/重排器 5.2 指标与延迟一起看,depth 是买来的
查询改写开关 5.3 开/关消融,警惕 Recall 涨 MRR 掉
词法路换 Zoekt 8.1 字面类查询占比高时,子串保真是升级方向

本节要点回顾

  1. 循环五步与三条纪律:一次一变量、全量评测、结果留档(history.jsonl);
  2. evaluate.py 三件武器:金标漂移报警、指标+延迟同报、历史趋势;
  3. 归因决策树:Recall 低查召回链(切分→嵌入→分词→权重),MRR 低查排序链(重排);指标都高仍不满则回金标;
  4. 护栏 + holdout = 调参自由的前提;playbook 把全书旋钮接回循环。

至此全书闭环:切分→嵌入→索引→混合→重排→改写→评测,评测反过来校准每一环的参数。检索系统不是一次调出来的产品,而是这个循环转出来的资产——你已经拥有让它转起来的全部零件。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U