阈值扫一遍: 0.10 时答对 9/10,0.20 就掉到 4,0.30 只剩 1,0.40 之后全拒。 原因是这套向量的 Top-1 相似度只有 0.047 到 0.365。 网上抄来的 0.7、0.8,套上去等于全拒。
上一节把文件向量化了。这一节是查询那一侧:检索的四个旋钮怎么调,以及为什么阈值必须自己扫。
一、检索的四个旋钮
① k:捞几段。上一章算过,k=2 之后收益就基本到顶。 ② 阈值:相似度低于多少就不算命中。它直接决定「答」还是「拒答」。 ③ 度量:余弦还是内积。余弦会除掉向量长度,内积不会。长文档多的库,用内积会被系统性抬高。 ④ 重排:先捞 20 段,再用一个小模型重排取前 3。这是唯一能在「不加 k」的前提下提准确率的办法。 四个旋钮里,k 和度量是工程参数,阈值是业务参数。阈值该定多少, 取决于「答错」和「拒答」哪个更不能接受——这是业务决定,不是技术决定。
二、阈值必须自己扫
Demo 把阈值从 0.10 扫到 0.50,结果很极端:
| 阈值 | 答对 | 答错 | 拒答 |
|---|---|---|---|
| 0.10 | 9 | 0 | 1 |
| 0.20 | 4 | 0 | 6 |
| 0.30 | 1 | 0 | 9 |
| 0.40 | 0 | 0 | 10 |
| 0.20 就把六成正确答案拒掉了。而这不是阈值定得太高——是这套向量的相似度整体就低: | |||
| 10 个问题的 Top-1 落在 0.047 到 0.365 之间。 | |||
| 所以「阈值 0.7 才算命中」这种说法,只在特定的向量模型、特定的语料上成立。 换一套向量, | |||
| 同样的 0.7 可能一条都过不了。 | |||
| 正确做法是: | |||
| 把自己的 10 到 20 条问题跑一遍,画出「答对/答错/拒答」三条随阈值变化的线,再选那个点。 | |||
| 这个动作十分钟就能做完,比抄任何经验值都准。 |
三、为什么网上的阈值不能用
三个原因。① 向量模型不同:不同模型输出的相似度分布整体平移,有的模型天生分数就低。 ② 语料不同:文档越长、主题越分散,Top-1 的分数越低。同一套模型,问答库和合同库的分布能差一倍。 ③ 度量方式不同:余弦和内积算出来的数根本不是一回事。 有人贴的是内积分数,你拿去当余弦阈值用,一定错。 所以看到一个阈值,要先问三个问题:哪个模型、什么语料、什么度量。三个都不说, 那个数就没有参考价值。
四、测检索的两个办法
办法一:命中率。 每个问题标注正确文档,看 Top-k 里有没有。 优点是直观,缺点是它不告诉你「分数分布长什么样」。 办法二:看分数分布。 把「正确答案的分数」和「最高错误答案的分数」分成两堆画出来。 如果两堆重叠很多,说明这套向量分不开对错——这时候调阈值没用,得换向量模型或者改切块。 两个办法要一起用:命中率告诉你「能不能用」,分布告诉你「阈值有没有意义」。 两堆完全不重叠时,阈值随便定都能用;重叠一半时,怎么定都是错的。 还有一条实操建议:把每次检索的 Top-3 分数打进日志。上线后一旦有人反馈「答得不对」, 你能立刻看出是「分太低勉强答」还是「分很高但答歪了」——两者的修法完全不同。
五、动手 Demo:阈值扫一遍:超过 0.20 就把正确答案一起拒掉
对 10 个问题各算一遍与 12 段文档的相似度,取 Top-1 的分数; 然后让阈值从 0.10 扫到 0.50,统计每个阈值下「答对/答错/拒答」各几条, 并输出相似度的实际分布范围。
for q, gi, key in P.QS:
M, v = P.encode(kb + [P.norm(q)])
s = P.cos(M[:len(kb)], M[len(kb)])
detail.append((q, gi, float(s.max()), int(np.argmax(s))))
for th in (0.10, 0.20, 0.30, 0.40, 0.50):
for q, gi, smax, top in detail:
if smax < th: rej += 1 # 拒答
elif top == gi: ok += 1 # 答对
else: wrong += 1 # 答错
实跑输出:
--------------------------------------------------------------------
阈值扫一遍:超过 0.20 就把正确答案一起拒掉
--------------------------------------------------------------------
阈值 答对 答错 拒答
------------------------
0.10 9 0 1
0.20 4 0 6
0.30 1 0 9
0.40 0 0 10
0.50 0 0 10
10 个问题的 Top-1 相似度落在 **0.047 到 0.365** 之间。
**阈值只要超过 0.20,大半正确答案就被拒掉了**(0.20 一档只剩 4 条答对,0.30 只剩 1 条)。
这说明这套字面向量的相似度整体偏低:**满分根本没到 0.4。**网上抄来的 0.7、0.8,套上去等于全拒。
所以阈值不是「越高越安全」,它是在**答错和拒答之间换**。业务能接受答错,就把阈值调低;
不能接受答错,就调高,代价是更多「暂未查到」。
还有一条:**这套数只对「字二元组向量」成立。**换成真嵌入模型,相似度分布整体会平移,
网上抄来的阈值(0.7、0.8)直接套上去必然不合适——**阈值要拿自己的库、自己的问题扫一遍。**
先看最下面那行结论: 相似度落在 0.047 到 0.365 之间。这套向量的满分根本不到 0.4——因为没有一个是真正同义的表述。 所以阈值表才会那么陡:0.10 时 9 条答对,0.20 时只剩 4 条。 阈值每抬 0.1,都有一批正确答案被拦在门外。 注意「答错」那一列全是 0。 这说明在这份数据上,只要过了阈值,答的就是对的——因为 10 个问题的答案都在库里, 缺的只是「问法」。 真实场景会不一样:库里没有答案的问题,Top-1 分数可能也有 0.2, 这时候调低阈值就会开始出现「硬答」。所以要测阈值,测试集里必须包含「文档里没有答案」的问题。 诚实说明:这些数字是字二元组向量的结果,换成真嵌入模型,分布会整体上移(真模型能识别同义, 分数更高)。但「阈值要按自己分布扫」这个方法不变。
30 秒实验
- 把阈值再加一档 0.05,看答对数还能不能涨到 10。 如果 0.05 才满分,说明这套向量的区分度已经接近随机了。
- 往 QS 里加 3 条「库里没有答案」的问题(比如问发票能不能开港币),再看各阈值下的「答错」列。 这一列终于会有数了。
- 把余弦换成内积(不归一化),看分数范围怎么变。内积分数会远大于 1,之前的阈值全部作废。
常见误区
- 「阈值 0.7 才算命中」。这是最常见的抄错。不同模型、不同语料、不同度量,分布差很远。 必须自己扫。
- 「阈值调高更安全」。调高确实少答错,但会大量拒答;它是在两个错之间换,不是在消除错误。
- 「只看命中率就够了」。命中率不告诉你分数分布,看不出「这套向量分不分得开对错」。
自测三问(点开对答案)
1. 检索有哪四个旋钮? —— k(捞几段)、阈值(低于多少拒答)、度量(余弦还是内积)、重排(先多捞再精排)。 2. 为什么阈值必须自己扫? —— 因为相似度分布由向量模型、语料、度量三样共同决定,任何一项不同,阈值就不能通用。 3. 如果「正确答案分数」和「最高错误答案分数」重叠很多,该怎么办? —— 调阈值没用,要换向量模型或改切块——说明这套向量本身分不开对错。检索能用了,最后一步是把答案和出处一起交出去。引用能不能核对,决定业务方敢不敢用。
- ☐ (本节完,下一节 6.2.5 引用知识库生成回复)