第 9 章 关键词、词袋与 TF-IDF:建立稀疏基线
9.1 让模型写回答之前,能否先找到有用的来源?
Section titled “9.1 让模型写回答之前,能否先找到有用的来源?”读者询问北京住宿限额。Part I 已经说明,旧回答的 600 元为什么可能与新限额 750 元冲突。在训练语言模型回答之前,可以先问一个更简单、也更容易观察的问题: 系统应该检索出哪一份给定文档? 如果便宜的词法方法已经能做好,它就是有用的基线;如果失败,具体查询也能说明,更复杂的方法究竟需要改善什么。
本章完成知识助手的第一个 项目里程碑 KA-0。它输出文档排序,以及排在首位的文档预测;没有证据时明确放弃给出预测。它不生成新回答,也不执行订单操作。比较之前,先冻结文档、问题、相关性标注和同分规则。所有数据均为虚构,并刻意控制在可以完整计算的规模内;成功案例数不能用来估计日常搜索流量上的表现。
9.2 先固定真正参与索引的文本
Section titled “9.2 先固定真正参与索引的文本”文档集包含两个版本的住宿制度和三份短 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.3 先数共有的不同关键词
Section titled “9.3 先数共有的不同关键词”例 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 排在前面就选择它。诊断输出仍保存零分候选的确定顺序,但这个顺序不代表推荐。
9.4 用词袋保留重复次数
Section titled “9.4 用词袋保留重复次数”只记有无,丢掉了 A 与 B 的一个差别:B 里“住宿”出现了两次。要保留它,可以给每个词表项一个坐标,记录出现次数。这样的 计数向量(count vector) 属于 词袋(bag of words,BoW) 表示:保留有哪些词、各出现几次,丢掉先后顺序。
表 9.2 — 完整的文档—词项计数矩阵。 行对应文档,列沿用固定词表。计数的单位是出现次数,不是概率。
| 卡片 | 北京 | 住宿 | 600 | 750 | 火车 | 订单 | 处理中 | 审批 |
|---|---|---|---|---|---|---|---|---|
| A | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
| B | 1 | 2 | 0 | 1 | 0 | 0 | 0 | 0 |
| C | 3 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| D | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
| E | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
看 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 — 比较计数向量的方向。 查询向量为 。与 A 比较,点积是 ,查询长度为 ,A 的长度为 ,所以余弦是 。与 B 比较,点积为 ,B 的长度平方为 ,余弦为 。
除以两边的长度,是为了避免仅把某张卡片的所有计数翻倍,就让相似度也翻倍。与 C 比较,点积是 3,分母为 ,得到 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 定义;除以文档长度,或给计数取对数,都会得到不同变体。TF 不是下一个 Token 的条件概率。
文档频率(document frequency) 记为 ,数的是至少出现过一次词项 的卡片数。北京在整个集合中出现五次,却只出现在三份文档里,所以它的文档频率是 3,不是 5。在许多文档中都出现的词,通常不如稀有词容易区分文档。这是一项依赖具体集合的统计考虑,并不是说稀有词天然更重要。《信息检索导论》:逆文档频率
例 9.3 — 固定逆频率的精确定义。 令 表示卡片数,本章的 逆文档频率(inverse document frequency,IDF) 为
分子是文档总数加一,分母是含有该词的文档数加一。比值内部加一,定义了一种平滑约定;最后再加一,让所有文档都有的词仍获权重 1,而不是 0。这些是建模选择,不是从“TF-IDF”这个名称里必然推出来的公式。我们使用自然对数,只根据文档拟合权重,不使用查询的相关性标签。
表 9.3 — 全部文档频率与 IDF。 为了让后面的计算更紧凑,把三个不同权重记为 。
| 词项 | df | IDF 计算 | 权重 |
|---|---|---|---|
| 北京 | 3 | ||
| 住宿、订单 | 各 2 | ||
| 600、750、火车、处理中、审批 | 各 1 |
TF-IDF 坐标就是计数乘以该词的权重:
B 的住宿坐标变成 ,C 的北京坐标变成 。未出现的词因为计数为零,乘完仍是零。查询中的未知词按固定词表规则忽略,不会临时增加坐标并随意给一个 IDF。scikit-learn 维护方文档说明了这里采用的原始计数与平滑 IDF 约定;其他已发表公式,包括经典的未平移 ,不能与本章数值混用。scikit-learn:TF-IDF 加权
表 9.4 — 长度归一化之前的完整加权向量。 每个符号都对应表 9.3,不是某个未说明的学习值。
| 卡片或查询 | 北京 | 住宿 | 600 | 750 | 火车 | 订单 | 处理中 | 审批 |
|---|---|---|---|---|---|---|---|---|
| A | 0 | 0 | 0 | 0 | 0 | |||
| B | 0 | 0 | 0 | 0 | 0 | |||
| C | 0 | 0 | 0 | 0 | 0 | 0 | ||
| D | 0 | 0 | 0 | 0 | 0 | 0 | ||
| E | 0 | 0 | 0 | 0 | 0 | 0 | ||
| 北京住宿 | 0 | 0 | 0 | 0 | 0 | 0 |
查询使用与文档 相同 的 IDF,长度为 。现在用第 5 章的余弦,完成它与每份文档的比较:
遍历词表项,不是文本位置; 与 分别表示查询和一份文档。分母消去这两个加权向量的长度,不是除以词数、文档频率或候选数。
表 9.5 — 一个 TF-IDF 查询与每张卡片的完整比较。 分子中出现权重的平方,是因为查询与文档两边都乘过 IDF。
| 卡片 | 点积分子 | 文档长度 | 完整分母 | 余弦 |
|---|---|---|---|---|
| A | 6.691120 | 0.723658 | ||
| B | 9.295921 | 0.829270 | ||
| C | 10.363773 | 0.571799 | ||
| D | 0 | 5.933496 | 0 | |
| E | 0 | 5.933496 | 0 |
以 B 为例,用 除以 ,实际代入未舍入值,得到 0.829270。D、E 的长度不为零,但点积为零。完全没有已知词的查询则不同:它的长度为零,余弦没有定义。应用把这种情况视为零检索证据,并放弃预测,并没有改写余弦的数学定义。
第一个查询的排序仍是 B > A > C > D > E。改善出现在 北京 处理中 上:加权查询的北京坐标为 ,处理中坐标为 。与 C 相比,分子是 ;与 D 相比,分子是 。除以相应长度后,得到 C:0.498157,D:0.646660,反转了 Count 的结果。只出现在状态文档里的词,现在比重复地名贡献更强。
- 文本片段查询:北京 处理中
- 一组数字A=1、B=1、C=1、D=1、E=0 → A
同分规则依据候选顺序,不会识别读者在问状态。
- 一组数字C 中北京出现三次:余弦 0.670820
- 一组数字D 中处理中出现一次:余弦 0.500000
C 排在前面,但它讲的是铁路差旅。
- 处理步骤IDF:北京=1.405465;处理中=2.098612
- 一组数字D=0.646660 > C=0.498157 → D
相关的状态文档上升了;日期和同义关系仍不在这条加权规则中。
图 9.1 让同一个第二查询依次经过三种计分规则。加权改变了哪份来源上升,却没有因此生成同义词词典,也没有读取日期字段。
9.6 九个问题全部保留,包括不利样例
Section titled “9.6 九个问题全部保留,包括不利样例”运行之前,相关性标注已经规定什么算成功。首位预测属于相关集合,就算成功;集合为空时,只有放弃预测才算成功。分母取 全部九个查询,包含无答案案例。这是诊断任务的成功比例,不是逐 Token 分类准确率,也不是语言模型困惑度。
表 9.6 — 固定查询与所需证据。 所有当前问题都以数据集的 2026-09-11 为参照日期。
| 查询 | 文本 | 相关卡片 | 问题意图 |
|---|---|---|---|
| 1 | 北京住宿 | B | 当前限额 |
| 2 | 北京 处理中 | D | 与北京有关的订单处于处理中是什么意思 |
| 3 | 750 住宿 | B | 按明确的新金额找来源 |
| 4 | 酒店补助 | A 或 B | 找任一版本的住宿制度,不认证今天的金额 |
| 5 | 600 北京 | A | 找历史上的 600 元规则 |
| 6 | 订单 处理中 | D | 解释状态 |
| 7 | 订单 审批 | E | 找审批要求,不执行操作 |
| 8 | 北京住宿还是600元吗? | B | 用当前制度核对旧前提 |
| 9 | 护照换发 | 无 | 给定来源都不回答这个问题 |
表 9.7 — 实际确定性运行的全部排序。 第一张卡片就是预测;横线表示全部零分,不给预测。全零行仅在诊断存储中保留 A—E 顺序。余弦用于排序时保留十二位小数来判断同分,正文显示六位;同分按 A—E 排列。
| 查询 | Keyword | BoW Count | TF-IDF | 首位选择说明了什么 |
|---|---|---|---|---|
| 1 | A > B > C > D > E | B > A > C > D > E | B > A > C > D > E | 计数修正了第一次旧来源同分问题 |
| 2 | A > B > C > D > E | C > D > A > B > E | D > C > A > B > E | IDF 修正了常见词干扰 |
| 3 | B > A > C > D > E | B > A > C > D > E | B > A > C > D > E | 都找到了明确的新金额 |
| 4 | — | — | — | 都漏掉了同义表达 |
| 5 | A > B > C > D > E | A > C > B > D > E | A > C > B > D > E | 都正确找到历史规则 |
| 6 | D > E > A > B > C | D > E > A > B > C | D > E > A > B > C | 都找到状态 FAQ |
| 7 | E > D > A > B > C | E > D > A > B > C | E > D > A > B > C | 都找到审批 FAQ |
| 8 | A > B > C > D > E | A > B > C > D > E | A > 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
9.8 稀疏词袋还保不住什么?
Section titled “9.8 稀疏词袋还保不住什么?”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 拟合的是语料统计量,评测再读取固定相关集合。独立预期值包含逐项列出的计数和标量展开的余弦公式,因此不是让函数自己生成答案,再拿来证明自己正确。
9.9 本章小结
Section titled “9.9 本章小结”关键词重合提供透明的初始来源选择。计数向量在固定文档—词项空间里保留重复次数,余弦控制整体大小的影响。TF-IDF 再加入明确、依赖具体文档集的权重约定,可以减轻常见词干扰。KA-0 在同一份固定数据上保留三种方法,包括同义表达、过期前提和无答案案例。任何一种分数都不会自动提供来源日期规则、片段抽取器或操作权限。
现在,表示上的一个失败已经很具体:作用相近的不同词,占据互不相关的坐标。第 10 章将问,能否让数据教会表示共享证据,以及每个词永远使用同一向量时,还会丢掉什么。
9.10 习题
Section titled “9.10 习题”- 另建一个文档集变体,加入索引卡
订单 审批 审批。按原有八列写出它的 Count 行,再用同样的平滑公式,计算六份文档时北京与审批的 IDF。 - 对原文档集中的查询
订单 审批,计算它与 D、E 的 Count 余弦,再用 和 算 TF-IDF 余弦。错误 FAQ 为什么也有非零分数? - 程序员计算北京的 IDF 时,用出现五次代替三份文档包含它。按既定公式会得到什么值?为什么数错了对象?
- 某个实现把全零查询排成 A,并当作已经回答。结合查询 4 与 9 诊断证据问题;为什么同样放弃预测,在一题里错、另一题里却对?
- 系统正确检索到 B,便声称已经抽出金额、生成有依据的回答并批准取消。还缺哪些输出契约与检查?
- 为什么从评测集合中删除 A 会抹掉有用证据?提出一种透明的后续版本处理规则,同时保留首轮对照。
9.11 参考解答
Section titled “9.11 参考解答”- 新行是
[0,0,0,0,0,1,0,2]。北京仍出现在三份文档里,IDF 变为 ;审批现在出现在两份文档里,IDF 为 。新卡中两次审批,只让含该词的文档数增加一。要公平比较这个变体,旧文档和查询都应使用重新拟合的 IDF。 - Count 查询与 E 的行相同,所以 E 的余弦为 1。D 只共有订单,点积 1、分母 2,余弦为 1/2。加权查询仍等于 E 的加权行,因此 E 保持 1;与 D 的余弦为 。分子非零来自共同的“订单”,不代表处理中与审批可以互换。
- 错误计算给出 。五次是包含重复的集合总频次,df 却要数这个词分布在多少张卡片中。C 中三次北京,对 df 只贡献一份文档,因此正确值是 。
- 全零说明当前表示没有重合证据,不支持选择第一个候选。查询 4 有相关住宿文档,只是同义表达未能匹配,所以放弃暴露了一次漏检;查询 9 没有相关来源,放弃则正确。区分二者的是评测相关性标注,不是零向量本身。
- 检索只提供来源标识。抽取还要确定金额与单位的准确边界及来源版本;生成还要给出有依据且适用的陈述。取消则另外需要已授权的操作路径与审批,找到说明审批的制度不等于获得审批。
- A 既用于历史问题,也用于诊断旧来源误选。未来可以根据明确的当前或历史意图与生效日期规则,过滤版本或调整排序,但应把它记录为新方法或新数据版本,保留
ka0-v1及其错误,并先在同一批保留问题上比较,再声称改善。
9.12 参考文献
Section titled “9.12 参考文献”- Manning、Raghavan、Schuetze,《信息检索导论》:词频、IDF、把查询作为向量。支持文档内计数与集合统计的区别,以及向量空间比较;小语料与全部分数由本书原创。
- Sparck Jones,1972,《词项专指性的统计解释及其在检索中的应用》。给出原始统计专指性论证。本章另行声明具体平滑变体,不把这个精确公式直接归于该论文。
- scikit-learn:TF-IDF 词项加权。说明与本章所选约定相符的原始 TF、平滑 IDF 和长度归一化。项目独立实现小型计算,不依赖 scikit-learn 包。
- Rabiner,1989,HMM 教程。第 261 页定位观测、状态序列和参数估计三个问题,本章只用来说明任务位置。
- Lafferty、McCallum、Pereira,2001,CRF。引言与第 3 节解释条件序列标注。链接为作者提供的 PostScript 原论文,本书未复用其中图示、基准分数或实现。