在 HarmonyOS 7(API 26)的 Mate 90 Pro 模拟器上,一个离线文搜图应用内置 24 张缩略图,支持中文短语查询,返回 0~100 分的排序列表,并为每条结果附若干“命中依据”标签。本文不讨论引擎实现,而是把七张实机运行截图当作唯一证据,重建可复核的评测口径:分数是否可信、依据是否可信、改动是否可验证。样本总体只有 24 张图,不是 3 张也不是几百张,这决定了后续置信区间会较宽,榜单第 20 名这类位置的统计意义很弱。
一、从截图重建 qrels,先固定审计口径
评测起点是 qrels(query–relevance judgments)。截图提供了“冷色的复杂细节”和“明亮的蓝色风景”两次查询的完整榜单,可抄成初版标注。原则是只标截图上确实读到的项,读不到就留空;把没看清当成 0 分,会凭空制造假负例。三档相关性比连续分数更经得起人工复核,数据结构保留 None,表示截图未抄录、不参与任何指标。原文有效标注只有 6 条,故意不完整;作用不是评测出最终结论,而是固化审计口径:以后每跑一次引擎,都用同一份 qrels 比对,标注只增不改。
- from dataclasses import dataclass
- from typing import Dict, Optional
- @dataclass(frozen=True)
- class Judgment:
- query: str
- doc_id: int
- grade: Optional[int]
- QRELS: Dict[str, Dict[int, Optional[int]]] = {
- '冷色的复杂细节': {6: 2, 3: 1, 16: 1, 15: 0},
- '明亮的蓝色风景': {19: 1, 22: 1},
- }
- def effective(qrels, q):
- return {d: g for d, g in qrels[q].items() if g is not None}
复制代码
二、四个正交指标,各测一件事
只用一个平均分衡量检索质量很容易误导。这里选四个正交指标:P@5、nDCG@5、词级命中率、依据一致率。P@5 和 nDCG@5 看排序;词级命中率专门抓“一个词打满、另一个词归零,融合后却给了高分”的失效,例如只算 P@5 时这类图可能仍被算作相关;依据一致率则审计命中标签本身。实现时,P@5 只对已标注项计分,未标注项不能全算成不相关,否则指标会被系统性压低。nDCG 要使用分级增益 2^g-1,位置从 0 起时分母用 log2(i+2)。
- import math
- def precision_at_k(ranked, qrels, q, k=5):
- judged = effective(qrels, q)
- top = [d for d in ranked[:k] if d in judged]
- if not top:
- return 0.0
- return sum(1 for d in top if judged[d] >= 1) / len(top)
- def ndcg_at_k(ranked, qrels, q, k=5):
- judged = effective(qrels, q)
- def dcg(seq):
- return sum((2 ** judged[d] - 1) / math.log2(i + 2)
- for i, d in enumerate(seq) if d in judged)
- actual = dcg(ranked[:k])
- ideal = dcg(sorted(judged, key=lambda d: judged[d], reverse=True)[:k])
- return actual / ideal if ideal > 0 else 0.0
- def term_satisfaction(hits, threshold=0.15):
- if not hits:
- return 0.0
- return sum(1 for s in hits.values() if s >= threshold) / len(hits)
复制代码
三、误差预算:100 分里有多少是噪声
审计中最容易被跳过、却最有价值的一步,是问分数由哪些环节组成、每个环节带来多大不确定性。原文指出榜首 100 分至少可拆出四处量化误差,其中前两项是测量误差,后两项是模型误差。显示取整就是可复跑的敏感性来源:例如“绿 14%”在 frac/0.22 规则下,按显示值 ±0.5 个百分点复算,单项分带宽约为 61.36 到 65.91,光是显示取整就有约 ±2.3 分。因此标签写 62 分时,把 61.5 和 62.4 区分开属于过度解读。融合补偿也是误差来源:加权平均允许一项满分补偿另一项零分,改用加权几何平均后任一项归零则总分为零。原文示例中,复杂 1.00、冷 0.00,权重分别为 0.8 和 1.0,算术平均为 44.4,几何平均为 0.5;这个差距说明词级命中率必须单列。
- def weighted_mean(hits, weights):
- num = sum(weights[t] * s for t, s in hits.items())
- den = sum(weights[t] for t in hits)
- return num / den if den else 0.0
- def weighted_geometric(hits, weights, eps=1e-3):
- den = sum(weights[t] for t in hits)
- log_sum = sum(weights[t] * math.log(max(s, eps)) for t, s in hits.items())
- return math.exp(log_sum / den)
- hits = {'复杂': 1.00, '冷': 0.00}
- w = {'复杂': 0.8, '冷': 1.0}
- print('算术平均:', round(weighted_mean(hits, w) * 100, 1))
- print('几何平均:', round(weighted_geometric(hits, w) * 100, 1))
复制代码
四、置信区间与配对 A/B:让“变好”可复核
24 张图、6 条有效标注属于极小样本。“nDCG 从 0.71 升到 0.78”在这种规模下几乎肯定不显著,审计必须给出判断方法。可用配对 bootstrap 对查询重采样,同一批查询比较 A/B 两个榜单,计算差值区间;如果 95% 区间跨 0,只能说当前样本无法区分,不能写“没有差异”,更不能直接判 B 更好。原文给出 2000 轮、seed=11 的示例。样本量也可反推:若想把 nDCG 置信半宽压到 ±0.05,在观测标准差约 0.18 时,至少需要约 50 个独立查询,当前只有 2 个查询,任何“提升 7 个点”的说法都不该写进结论。
A/B 协议的最小要求是:配对而非分组,两个评分函数跑在同一批查询、同一批图上,逐查询记录差值;固定随机种子,保证 bootstrap 或后续随机采样可复现;预注册主指标,例如以 nDCG@5 为主、词级命中率为护栏,避免事后挑一个涨得最多的指标。原文的极简判定逻辑还规定,查询数少于 8 不下结论;主指标未达显著则判证据不足;护栏指标退化超过 0.02 则拒绝上线;bootstrap 与 t 检验结论不一致时,以区间更宽者为准。只盯 nDCG 会奖励“把低分项全砍掉”的粗暴改动,词级命中率能保证多词查询没有被悄悄牺牲。
五、依据一致率:给“可解释”本身打分
命中标签是该应用最特别的部分,也必须被审计。做法是把每条标签变成二元判断题:“标签声称冷色 95%,肉眼看是否成立”,由两个人独立判定,再算一致率与 Cohen’s κ;κ 会扣除随机一致的部分,比裸一致率严格。κ 低于 0.6 时,问题往往不在引擎而在标注指南:明显偏冷到底是多冷,细节丰富按整图还是按主体判定。把这些写成带例子的规则,比换算法更能提升一致率。
- def cohens_kappa(pairs):
- n = len(pairs)
- if n == 0:
- return 0.0
- po = sum(1 for a, b in pairs if a == b) / n
- pa1 = sum(1 for a, _ in pairs if a == 1) / n
- pb1 = sum(1 for _, b in pairs if b == 1) / n
- pe = pa1 * pb1 + (1 - pa1) * (1 - pb1)
- return (po - pe) / (1 - pe) if pe < 1 else 0.0
复制代码
六、结论与边界
这套审计最后要落回一句话:“可解释”不是一种质量,而是一种可审计性。它让每个错误能定位到某个词、某个百分比、某条融合规则;但定位之后是否正确,仍要靠标注、区间与配对实验来判断。qrels 只有 6 条时,指标的意义在固化口径,不在得出结论;截图上的数字是引擎输出,不是外部真值,只能作为一致性锚点。几何平均不一定优于加权平均,它会减少结果数量,对部分匹配的召回有伤害,是否采用必须由配对实验决定。四个指标、误差预算、配对 bootstrap 和护栏实验不依赖具体实现,只要能导出“查询→有序 doc_id 列表+逐项依据”,就可以迁移到其他检索应用。 |