跳转到内容

第 9 章 关键词、词袋与 TF-IDF:建立稀疏基线

9.1 让模型写回答之前,能否先找到有用的来源?

Section titled “9.1 让模型写回答之前,能否先找到有用的来源?”

读者询问北京住宿限额。Part I 已经说明,旧回答的 600 元为什么可能与新限额 750 元冲突。在训练语言模型回答之前,可以先问一个更简单、也更容易观察的问题: 系统应该检索出哪一份给定文档? 如果便宜的词法方法已经能做好,它就是有用的基线;如果失败,具体查询也能说明,更复杂的方法究竟需要改善什么。

本章完成知识助手的第一个 项目里程碑 KA-0。它输出文档排序,以及排在首位的文档预测;没有证据时明确放弃给出预测。它不生成新回答,也不执行订单操作。比较之前,先冻结文档、问题、相关性标注和同分规则。所有数据均为虚构,并刻意控制在可以完整计算的规模内;成功案例数不能用来估计日常搜索流量上的表现。

文档集包含两个版本的住宿制度和三份短 FAQ。每条记录都保留完整说明正文和版本元数据,但首轮实验只给明确提供的 索引卡 计分,并不暗中统计正文里的全部词。卡片是作者给定的短摘要,选取它们属于任务假设,不是模型学会摘要后的结果。

表 9.1 — 五份文档及其完整索引文本。 A—E 是按固定候选顺序排列的读者标签。A、B 的金额单位都是虚构制度下的每间每晚人民币元。

卡片来源与范围完整索引文本
A旧版 v1:600 元;2025-01-01 生效,已被 B 替代北京 住宿 600
B新版 v2:750 元;2026-01-01 生效北京 住宿 住宿 750
C铁路 FAQ;标题、标签、摘要重复北京;不回答住宿限额或订单状态北京 北京 北京 火车
D状态 FAQ:处理中是虚构订单 A-104 的一种可能状态;不做实时查询订单 处理中
E取消 FAQ:订单取消需要审批;检索不会授予审批订单 审批

B 的标题与摘要都出现“住宿”,C 的标题、标签和摘要都出现“北京”。重复是这个诊断数据的一部分,不能因为某种基线处理得不好,就把它删掉。

先执行第 8 章的 search-key-v1。小型英文 tokenizer 再取连续 ASCII 字母和连续数字,忽略分隔符与标点;中文版本则在声明好的教学词典中匹配当前位置的最长词,处理连续 ASCII 字母与数字,其余内容除空白、标点外逐码点保留。因此,北京住宿 与 北京 住宿 可以得到相同的两个词表项,但这不能证明通用中文分词准确率。两种语言是作者给定、列位置对齐的平行文档集,不是自动翻译系统。

按 A—E 扫描卡片,每遇到一个新词就加入词表,依次得到 北京、住宿、600、750、火车、订单、处理中、审批,然后冻结这个顺序。查询里出现词表之外的词,不会临时增加词法列。tokenizer 可以识别“酒店”“补助”的拼写,但识别拼写,与建立同义关系或拥有文档词表中的坐标,是不同的事。

例 9.1 — 实际执行最简单的查找。 查询为 北京住宿,意图是查当前制度,相关文档为 B。数一数每张卡片含有多少个不同的查询词:A 两个都含,B 也都含,C 只含北京,D、E 都不含,得分为 [2,2,1,0,0]。

这个 关键词(keyword)基线 对每个共有词给一分,忽略重复次数与词序。同分按 A、B、C、D、E 的固定顺序排列,因此结果为 A > B > C > D > E,预测 A。它找对了主题,却选了过期来源。失败直接来自计分与同分规则,程序内部没有藏着日期偏好。

如果每张卡片都得零分,说明没有词法证据,此时不给出预测,不能仅因为 A 排在前面就选择它。诊断输出仍保存零分候选的确定顺序,但这个顺序不代表推荐。

只记有无,丢掉了 A 与 B 的一个差别:B 里“住宿”出现了两次。要保留它,可以给每个词表项一个坐标,记录出现次数。这样的 计数向量(count vector) 属于 词袋(bag of words,BoW) 表示:保留有哪些词、各出现几次,丢掉先后顺序。

表 9.2 — 完整的文档—词项计数矩阵。 行对应文档,列沿用固定词表。计数的单位是出现次数,不是概率。

卡片北京住宿600750火车订单处理中审批
A11100000
B12010000
C30001000
D00000110
E00000101

看 B 的第二格:索引卡里“住宿”出现两次,所以填 2。把五个长度为 8 的向量叠起来,就得到形状为 [5,8] 的 文档—词项矩阵(document-term matrix)。其中 12 格非零,28 格为零。词表更大时,这种表示往往很 稀疏(sparse),绝大部分坐标都是零;实现时可以只保存非零位置及其数值,省去逐个存零。相对于约定词表,稀疏并不表示缺项。

把所有正计数改成 1,就能还原关键词矩阵。B 的关键词行是 [1,1,0,1,0,0,0,0],计数行则是 [1,2,0,1,0,0,0,0]。但两者都已经忘记,一个重复词究竟出现在标题还是说明里。

例 9.2 — 比较计数向量的方向。 查询向量为 q=[1,1,0,0,0,0,0,0]q=[1,1,0,0,0,0,0,0]。与 A 比较,点积是 1(1)+1(1)=21(1)+1(1)=2,查询长度为 2\sqrt2,A 的长度为 3\sqrt3,所以余弦是 2/6≈0.8164972/\sqrt6\approx0.816497。与 B 比较,点积为 1(1)+1(2)=31(1)+1(2)=3,B 的长度平方为 1+4+1=61+4+1=6,余弦为 3/12≈0.8660253/\sqrt{12}\approx0.866025。

除以两边的长度,是为了避免仅把某张卡片的所有计数翻倍,就让相似度也翻倍。与 C 比较,点积是 3,分母为 210\sqrt2\sqrt{10},得到 0.670820。D、E 没有重合的非零坐标,得分为零。完整分数行为 [0.816497,0.866025,0.670820,0,0], BoW Count(词袋计数) 排序为 B > A > C > D > E,修正了这次查询的首选。

修正的原因是计数方向变了,并不是它理解了哪个版本更新。另一个查询 北京 处理中 想问与北京有关的订单处于处理中是什么意思,但 Count 余弦给 C 约 0.670820,给 D 却只有 0.5。重复的地名压过了有用的状态词。于是,我们还要问:一个词在多少份文档中常见,而不仅是在一份文档里出现几次?

9.5 根据一个词分布得多广,调整权重

Section titled “9.5 根据一个词分布得多广,调整权重”

这里的 词频(term frequency,TF) 直接采用原始计数:tf⁡d,j=cd,j\operatorname{tf}_{d,j}=c_{d,j},也就是词项 jj 在卡片 dd 中的出现次数。这是一种明确的 TF 定义;除以文档长度,或给计数取对数,都会得到不同变体。TF 不是下一个 Token 的条件概率。

文档频率(document frequency) 记为 df⁡j\operatorname{df}_j,数的是至少出现过一次词项 jj 的卡片数。北京在整个集合中出现五次,却只出现在三份文档里,所以它的文档频率是 3,不是 5。在许多文档中都出现的词,通常不如稀有词容易区分文档。这是一项依赖具体集合的统计考虑,并不是说稀有词天然更重要。《信息检索导论》:逆文档频率

例 9.3 — 固定逆频率的精确定义。 令 N=5N=5 表示卡片数,本章的 逆文档频率(inverse document frequency,IDF) 为

idf⁡j=ln⁡N+1df⁡j+1+1.\operatorname{idf}_j=\ln\frac{N+1}{\operatorname{df}_j+1}+1.

分子是文档总数加一,分母是含有该词的文档数加一。比值内部加一,定义了一种平滑约定;最后再加一,让所有文档都有的词仍获权重 1,而不是 0。这些是建模选择,不是从“TF-IDF”这个名称里必然推出来的公式。我们使用自然对数,只根据文档拟合权重,不使用查询的相关性标签。

表 9.3 — 全部文档频率与 IDF。 为了让后面的计算更紧凑,把三个不同权重记为 a,b,ca,b,c。

词项dfIDF 计算权重
北京31+ln⁡(6/4)1+\ln(6/4)a≈1.405465a\approx1.405465
住宿、订单各 21+ln⁡(6/3)1+\ln(6/3)b≈1.693147b\approx1.693147
600、750、火车、处理中、审批各 11+ln⁡(6/2)1+\ln(6/2)c≈2.098612c\approx2.098612

TF-IDF 坐标就是计数乘以该词的权重:

vd,j=cd,jidf⁡j.v_{d,j}=c_{d,j}\operatorname{idf}_j.

B 的住宿坐标变成 2b≈3.3862942b\approx3.386294,C 的北京坐标变成 3a≈4.2163953a\approx4.216395。未出现的词因为计数为零,乘完仍是零。查询中的未知词按固定词表规则忽略,不会临时增加坐标并随意给一个 IDF。scikit-learn 维护方文档说明了这里采用的原始计数与平滑 IDF 约定;其他已发表公式,包括经典的未平移 log⁡(N/df⁡)\log(N/\operatorname{df}),不能与本章数值混用。scikit-learn:TF-IDF 加权

表 9.4 — 长度归一化之前的完整加权向量。 每个符号都对应表 9.3,不是某个未说明的学习值。

卡片或查询北京住宿600750火车订单处理中审批
Aaabbcc00000
Baa2b2b0cc0000
C3a3a000cc000
D00000bbcc0
E00000bb0cc
北京住宿aabb000000

查询使用与文档 相同 的 IDF,长度为 a2+b2≈2.200473\sqrt{a^2+b^2}\approx2.200473。现在用第 5 章的余弦,完成它与每份文档的比较:

score⁡(q,d)=∑jvq,jvd,j∑jvq,j2∑jvd,j2.\operatorname{score}(q,d)=\frac{\sum_j v_{q,j}v_{d,j}}{\sqrt{\sum_j v_{q,j}^2}\sqrt{\sum_j v_{d,j}^2}}.

jj 遍历词表项,不是文本位置;qq 与 dd 分别表示查询和一份文档。分母消去这两个加权向量的长度,不是除以词数、文档频率或候选数。

表 9.5 — 一个 TF-IDF 查询与每张卡片的完整比较。 分子中出现权重的平方,是因为查询与文档两边都乘过 IDF。

卡片点积分子文档长度完整分母余弦
Aa2+b2≈4.842080a^2+b^2\approx4.842080a2+b2+c2≈3.040765\sqrt{a^2+b^2+c^2}\approx3.0407656.6911200.723658
Ba2+2b2≈7.708827a^2+2b^2\approx7.708827a2+4b2+c2≈4.224511\sqrt{a^2+4b^2+c^2}\approx4.2245119.2959210.829270
C3a2≈5.9259973a^2\approx5.9259979a2+c2≈4.709794\sqrt{9a^2+c^2}\approx4.70979410.3637730.571799
D0b2+c2≈2.696465\sqrt{b^2+c^2}\approx2.6964655.9334960
E0b2+c2≈2.696465\sqrt{b^2+c^2}\approx2.6964655.9334960

以 B 为例,用 7.7088277.708827 除以 2.200473×4.2245112.200473\times4.224511,实际代入未舍入值,得到 0.829270。D、E 的长度不为零,但点积为零。完全没有已知词的查询则不同:它的长度为零,余弦没有定义。应用把这种情况视为零检索证据,并放弃预测,并没有改写余弦的数学定义。

第一个查询的排序仍是 B > A > C > D > E。改善出现在 北京 处理中 上:加权查询的北京坐标为 aa,处理中坐标为 cc。与 C 相比,分子是 3a23a^2;与 D 相比,分子是 c2c^2。除以相应长度后,得到 C:0.498157,D:0.646660,反转了 Count 的结果。只出现在状态文档里的词,现在比重复地名贡献更强。

关键词:数共有的不同词
  1. 文本片段查询:北京 处理中
  2. 一组数字A=1、B=1、C=1、D=1、E=0 → A

同分规则依据候选顺序,不会识别读者在问状态。

词袋计数:比较计数向量方向
  1. 一组数字C 中北京出现三次:余弦 0.670820
  2. 一组数字D 中处理中出现一次:余弦 0.500000

C 排在前面,但它讲的是铁路差旅。

TF-IDF:先改权重,再算余弦
  1. 处理步骤IDF:北京=1.405465;处理中=2.098612
  2. 一组数字D=0.646660 > C=0.498157 → D

相关的状态文档上升了;日期和同义关系仍不在这条加权规则中。

图 9.1 — 常见词与稀有词采用不同权重,同一个查询的排序随之改变。 本书原创数据、计算与图示,CC BY-SA 4.0;由 KA-0 复现。

图 9.1 让同一个第二查询依次经过三种计分规则。加权改变了哪份来源上升,却没有因此生成同义词词典,也没有读取日期字段。

9.6 九个问题全部保留,包括不利样例

Section titled “9.6 九个问题全部保留,包括不利样例”

运行之前,相关性标注已经规定什么算成功。首位预测属于相关集合,就算成功;集合为空时,只有放弃预测才算成功。分母取 全部九个查询,包含无答案案例。这是诊断任务的成功比例,不是逐 Token 分类准确率,也不是语言模型困惑度。

表 9.6 — 固定查询与所需证据。 所有当前问题都以数据集的 2026-09-11 为参照日期。

查询文本相关卡片问题意图
1北京住宿B当前限额
2北京 处理中D与北京有关的订单处于处理中是什么意思
3750 住宿B按明确的新金额找来源
4酒店补助A 或 B找任一版本的住宿制度,不认证今天的金额
5600 北京A找历史上的 600 元规则
6订单 处理中D解释状态
7订单 审批E找审批要求,不执行操作
8北京住宿还是600元吗?B用当前制度核对旧前提
9护照换发无给定来源都不回答这个问题

表 9.7 — 实际确定性运行的全部排序。 第一张卡片就是预测;横线表示全部零分,不给预测。全零行仅在诊断存储中保留 A—E 顺序。余弦用于排序时保留十二位小数来判断同分,正文显示六位;同分按 A—E 排列。

查询KeywordBoW CountTF-IDF首位选择说明了什么
1A > B > C > D > EB > A > C > D > EB > A > C > D > E计数修正了第一次旧来源同分问题
2A > B > C > D > EC > D > A > B > ED > C > A > B > EIDF 修正了常见词干扰
3B > A > C > D > EB > A > C > D > EB > A > C > D > E都找到了明确的新金额
4———都漏掉了同义表达
5A > B > C > D > EA > C > B > D > EA > C > B > D > E都正确找到历史规则
6D > E > A > B > CD > E > A > B > CD > E > A > B > C都找到状态 FAQ
7E > D > A > B > CE > D > A > B > CE > D > A > B > C都找到审批 FAQ
8A > B > C > D > EA > B > C > D > EA > B > C > D > E都跟随问题中的旧金额选了旧版
9———都正确放弃预测

Keyword 成功 5/9,Count 成功 6/9,TF-IDF 成功 7/9。两种语言的给定数据都复现了这些矩阵和决策。改善确实发生在这份固定诊断数据上,但这些特意选取的问题不能证明后一个方法总比前一个好。

查询 8 尤其清楚地暴露了版本边界。它的已知词是北京、住宿、600,与 A 的计数方向完全一致,所以 Count 和 TF-IDF 都给 A 余弦 1。“还是”表达的是核对当前规则的需要,这里却没有相应坐标。因此,相似度很高,仍然可以选错任务所需来源。要保留版本元数据和失败案例供后续改进,不能悄悄移除 A,或重标这个问题来提高 KA-0 分数。

9.7 检索出文档,只完成了一种输出

Section titled “9.7 检索出文档,只完成了一种输出”

同一段文本可以进入不同任务。先明确输出契约,才不会把检索成功当作另一项能力已经成立。

表 9.8 — 贯穿案例中的五种任务。

任务所需输出合适的评价单位
检索排序后的文档 ID,例如先 B 后 A相关文档是否出现在要求的名次
分类整个问题的一个标签,例如差旅或订单状态留出问题的类别是否正确
序列标注每个输入位置的标签,例如金额开始、金额内部位置标签及完整标注片段
片段抽取原文中“750 元”的准确区间,连同文档和版本边界、文本与来源是否正确;原文位置很重要
生成新的回答文本,例如解释当前限额的一句话内容是否有据、是否完成任务及其他已声明标准

序列标注保留位置维,而一个整篇文档的词袋向量已经丢掉了它。常规一阶 隐马尔可夫模型(hidden Markov model,HMM) 描述隐藏状态之间的转移,以及各位置由状态产生的观测;选择一条状态序列,可以服务于标注任务。 线性链条件随机场(linear-chain conditional random field,CRF) 则直接在给定输入的条件下,对整条标签序列建模,并考虑相邻标签的关系。这里仅把它们放到任务地图上,不要求实现算法。它们都不是 TF-IDF 的别名,也不存在一种必然全面替代另一种的结论。Rabiner,1989、Lafferty 等,2001

BoW 会丢失词序。如果词表保留了人物名称,那么“甲取消乙的订单”与“乙取消甲的订单”可以拥有相同词频,却由不同的人执行动作。表示构造时已经丢弃的区别,无法从向量中重新读出。TF-IDF 只给相同坐标乘权重,因此也恢复不了词序。

可以再加入 词 N-gram 特征(word N-gram features),例如相邻词对“北京 住宿”。在这五张卡片里,共有八种不同的相邻词对;把它们加入原来的八个单词列,就得到十六列。其中 C 的“北京 北京”出现两次。这种办法保留选定的局部顺序,代价是维度更多、每个特征的观测更少;它仍会漏掉未见同义词,以及超出特征跨度的关系。

这里的 N-gram 与第 4 章的 N-gram 语言模型 不同。特征记录某个序列在文档里出现几次;语言模型则在给定上下文后,提供下一个 Token 的归一化概率分布,还需要边界规则。为了检索而统计相邻词对,并不会自动得到下一 Token 概率。

持续演进的项目位于 code/knowledge-assistant/,首版固定数据与完整输出位于 data/knowledge-assistant/。04-text-and-ka0.ipynb 检查第 8 章纯函数,打印计数与加权矩阵,再让每个查询经过三种基线。这里不使用查询标签做梯度拟合:IDF 拟合的是语料统计量,评测再读取固定相关集合。独立预期值包含逐项列出的计数和标量展开的余弦公式,因此不是让函数自己生成答案,再拿来证明自己正确。

关键词重合提供透明的初始来源选择。计数向量在固定文档—词项空间里保留重复次数,余弦控制整体大小的影响。TF-IDF 再加入明确、依赖具体文档集的权重约定,可以减轻常见词干扰。KA-0 在同一份固定数据上保留三种方法,包括同义表达、过期前提和无答案案例。任何一种分数都不会自动提供来源日期规则、片段抽取器或操作权限。

现在,表示上的一个失败已经很具体:作用相近的不同词,占据互不相关的坐标。第 10 章将问,能否让数据教会表示共享证据,以及每个词永远使用同一向量时,还会丢掉什么。

  1. 另建一个文档集变体,加入索引卡 订单 审批 审批。按原有八列写出它的 Count 行,再用同样的平滑公式,计算六份文档时北京与审批的 IDF。
  2. 对原文档集中的查询 订单 审批,计算它与 D、E 的 Count 余弦,再用 bb 和 cc 算 TF-IDF 余弦。错误 FAQ 为什么也有非零分数?
  3. 程序员计算北京的 IDF 时,用出现五次代替三份文档包含它。按既定公式会得到什么值?为什么数错了对象?
  4. 某个实现把全零查询排成 A,并当作已经回答。结合查询 4 与 9 诊断证据问题;为什么同样放弃预测,在一题里错、另一题里却对?
  5. 系统正确检索到 B,便声称已经抽出金额、生成有依据的回答并批准取消。还缺哪些输出契约与检查?
  6. 为什么从评测集合中删除 A 会抹掉有用证据?提出一种透明的后续版本处理规则,同时保留首轮对照。
  1. 新行是 [0,0,0,0,0,1,0,2]。北京仍出现在三份文档里,IDF 变为 1+ln⁡(7/4)≈1.5596161+\ln(7/4)\approx1.559616;审批现在出现在两份文档里,IDF 为 1+ln⁡(7/3)≈1.8472981+\ln(7/3)\approx1.847298。新卡中两次审批,只让含该词的文档数增加一。要公平比较这个变体,旧文档和查询都应使用重新拟合的 IDF。
  2. Count 查询与 E 的行相同,所以 E 的余弦为 1。D 只共有订单,点积 1、分母 2,余弦为 1/2。加权查询仍等于 E 的加权行,因此 E 保持 1;与 D 的余弦为 b2/(b2+c2)≈0.394276b^2/(b^2+c^2)\approx0.394276。分子非零来自共同的“订单”,不代表处理中与审批可以互换。
  3. 错误计算给出 1+ln⁡(6/6)=11+\ln(6/6)=1。五次是包含重复的集合总频次,df 却要数这个词分布在多少张卡片中。C 中三次北京,对 df 只贡献一份文档,因此正确值是 1+ln⁡(6/4)≈1.4054651+\ln(6/4)\approx1.405465。
  4. 全零说明当前表示没有重合证据,不支持选择第一个候选。查询 4 有相关住宿文档,只是同义表达未能匹配,所以放弃暴露了一次漏检;查询 9 没有相关来源,放弃则正确。区分二者的是评测相关性标注,不是零向量本身。
  5. 检索只提供来源标识。抽取还要确定金额与单位的准确边界及来源版本;生成还要给出有依据且适用的陈述。取消则另外需要已授权的操作路径与审批,找到说明审批的制度不等于获得审批。
  6. A 既用于历史问题,也用于诊断旧来源误选。未来可以根据明确的当前或历史意图与生效日期规则,过滤版本或调整排序,但应把它记录为新方法或新数据版本,保留 ka0-v1 及其错误,并先在同一批保留问题上比较,再声称改善。