ETHZ-信息检索笔记-全-
ETHZ 信息检索笔记(全)
001:引言
在本节课中,我们将要学习信息检索的基本概念及其历史发展脉络。我们将从人类存储和检索信息的古老方式开始,一直探索到现代搜索引擎和大型语言模型(LLM)的出现。通过了解这段历史,我们可以更好地理解当前信息技术的核心挑战与机遇。


从午餐闲聊到信息检索
想象一下,你和朋友在食堂吃午饭,聊天时突然想知道爱因斯坦的出生年份。你会怎么做?


以下是当今人们可能采取的一些行动:
- 使用维基百科。
- 用智能手机或电脑上的谷歌等搜索引擎搜索。
- 在YouTube上搜索。
- 去询问教授。
- 去图书馆查找。
- 使用ChatGPT、Perplexity等大型语言模型。


在课堂上,大多数学生选择了使用搜索引擎。这个简单的例子展示了我们获取信息方式的演变。如今,我们不仅可以直接在搜索引擎中得到答案摘要,还能使用集成了计算器功能的搜索框,甚至与能够进行对话的LLM交互。
上一节我们看到了现代信息检索工具的便捷性,本节中我们来看看这些工具是如何从历史中一步步发展而来的。
信息存储与检索的历史



信息检索的历史,也是一部人类知识存储技术演进的历史。

前数字时代:从图书馆到百科全书
在计算机和互联网出现之前,知识主要存储在实体媒介中。
- 图书馆与分类法:大型图书馆如亚历山大图书馆,致力于收集和保存知识(如纸莎草卷轴)。后来出现的杜威十进制分类法等系统,帮助人们在海量书籍中按主题分类查找。
- 百科全书:例如《大英百科全书》,它以多卷本书籍的形式按字母顺序组织知识,并配有索引,功能上类似于一部巨大的字典。


这些方式的共同缺点是更新困难且成本高昂。
数字时代的黎明:从本地到网络

随着技术进步,知识存储开始数字化。
- 本地数字存储:例如《Encarta》数字百科全书,它将海量知识存储在CD-ROM上,用户可以在本地电脑上安装和搜索。
- 万维网的诞生:1989年,蒂姆·伯纳斯-李在瑞士日内瓦的欧洲核子研究中心(CERN)发明了万维网。它结合了互联网和超文本的概念,通过链接将全球信息连接起来,引发了信息传播的革命。


搜索引擎的崛起
万维网的出现催生了对有效信息检索工具的需求。
- 早期搜索引擎:在谷歌之前,已有如AltaVista、Yahoo等搜索引擎。Yahoo的特色是人工对网站进行分类目录导航。
- 谷歌的革命:拉里·佩奇和谢尔盖·布林在斯坦福大学创建了谷歌。其革命性在于PageRank算法,它通过分析网页间的链接关系来评估网页重要性,从而对搜索结果进行更有效的排名。

从实体书籍到全球互联的网络,信息存储和检索的方式发生了翻天覆地的变化。接下来,我们将视角转向支撑这些技术的另一个基础领域:数据库。


数据处理技术的演进
信息检索系统背后离不开数据处理技术的支持。数据库的发展史可以看作是为管理不同“形状”的数据而不断演进的历史。
关键里程碑
数据处理的关键演进节点包括:
- 1970年:关系型数据库:埃德加·科德提出了关系模型。用户无需直接操作文件和目录,而是通过表(Table)、行(Row) 和列(Column) 来查看和管理数据,实现了数据独立性。其核心操作可以用关系代数描述,例如选择操作:
σ_{条件}(表名) - 1980-1990年代:面向对象与数据库:随着C++、Java等面向对象编程语言的兴起,出现了连接对象与关系型数据库的技术(如ORM),以解决两者之间的“阻抗不匹配”问题。
- 2000年代:NoSQL与大数据:由于数据量剧增且格式多样(非结构化或半结构化),传统关系型数据库难以应对。于是出现了键值存储(Key-Value Store)、文档存储(Document Store)、图数据库(Graph Database) 等NoSQL系统,以及用于处理海量数据的MapReduce等分布式计算框架。

数据的“形状”


数据可以大致分为以下几种结构或“形状”:
- 表(Tables):高度结构化,如关系型数据库中的数据。
- 树(Trees):半结构化,如XML、JSON格式的数据。
- 图(Graphs):表现实体间复杂关系,如社交网络。
- 立方体(Cubes):用于多维数据分析,如商业智能。
- 向量(Vectors):代表非结构化数据(如文本、图像)经过特征提取后的数值表示,是现代AI和语义搜索的基础。
这些结构构成了从非结构化到高度结构化的数据光谱。本课程将重点关注非结构化数据(尤其是文本)的检索,因为这是当前互联网和人工智能应用的核心。






了解了数据的形态,我们自然要问:我们面对的数据规模究竟有多大?
大数据时代的挑战:容量、吞吐量与延迟




我们正处于数据爆炸的时代。数据量的单位已经从MB、GB发展到TB、PB,甚至EB、ZB。例如,arXiv.org这样的学术论文库每月新增提交量已达五位数。


然而,巨大的数据量带来了核心挑战。我们可以通过三个关键指标来审视存储硬件的发展:
- 容量:存储设备能保存的数据总量,单位是字节(Byte)。
- 吞吐量:读写数据的速度,单位是字节/秒(Byte/s)。
- 延迟:从发出请求到开始接收到数据所需的时间,单位是秒(s)或毫秒(ms)。

对比1956年的IBM RAMAC 305硬盘(容量5MB,延迟600ms)和现代的超大容量硬盘(如100TB SSD),可以发现:
- 容量增长了数千万倍。
- 吞吐量也增长了数万倍。
- 但延迟的改善相对有限(从几百毫秒到几毫秒或更低)。



用简单的比喻来说:我们现在拥有了一个无比巨大的仓库(高容量),货物进出仓库的整体搬运速度也很快(高吞吐量),但是想立刻找到并拿到某一件特定货物(低延迟) 却仍然不那么容易。这种容量/吞吐量增长与延迟改善之间的不匹配,是现代大规模信息检索系统面临的根本性技术挑战之一。


本节课中我们一起学习了信息检索的广阔图景。我们从一次日常的信息查询出发,回顾了人类存储和检索信息方式的历史演变,从古老的图书馆到现代的搜索引擎与LLM。接着,我们探讨了支撑这些应用的数据库技术的发展,以及数据的不同结构形态。最后,我们指出了在大数据时代,存储系统在容量、吞吐量和延迟之间存在的显著不平衡,这构成了未来信息检索技术需要解决的核心问题。这门课程后续将深入探讨如何从海量非结构化数据(特别是文本)中高效、准确地检索信息。
002:引言 (2/2) 📚

概述
在本节课中,我们将继续探讨信息检索领域面临的核心挑战,即大数据带来的容量、吞吐量和延迟问题。我们将通过一个生动的例子来理解这些问题的严重性,并初步了解解决这些问题的关键技术思路,例如并行化。最后,我们将概览本学期的课程安排和学习方法。
大数据带来的挑战
上一节我们介绍了存储容量、吞吐量和延迟之间的巨大差距。具体来说,单位体积的存储容量增长了近千亿倍,而吞吐量仅增长了四万倍,延迟仅改善了六千倍。即使采用了固态硬盘(SSD),这个问题依然存在。


因此,我们面临两个主要问题:
- 存储容量与吞吐量之间的差距。
- 吞吐量与延迟之间的差距。
对数坐标图可以更直观地展示这些差距。
一个生动的例子
为了展示我们所处的困境,这里有一个例子。假设一本书的容量是60万字。一个普通人今天的阅读速度大约是每分钟1000字。从图书馆书架上取一本书的延迟大约是1分钟。


通过简单的除法,你可以计算出读完这本书需要的时间:600,000 字 / 1,000 字/分钟 = 600 分钟,即10小时。

现在,让我们快进两个世纪,到2225年。假设人类在这两百年里的进化速度,与过去70年硬盘的进化速度相同(采用实际数据)。那么我们会得到:


- 书籍容量:55千万亿字(55 quadrillion words)。
- 阅读速度:每分钟4000万字。
如果进行计算,55,000,000,000,000,000 字 / 40,000,000 字/分钟 ≈ 1,375,000,000,000 分钟,这相当于大约2613年。这就是我们面临的巨大麻烦,因为硬盘的实际发展正是如此。
解决方案:并行化
那么,我们如何解决这个问题呢?显然我们已经找到了解决方案,否则今天的所有技术都无法正常工作。

我们发现的第一个解决方案是并行化。

因为如果你进行并行处理,例如将一本书的每个章节分发给地球上的每一个居民,或者某个社交网络的每个用户,你可以在10小时内完成阅读,因为你只是并行地分发了任务。
我们将在几周后学习如何做到这一点。通过并行化的力量,我们将能够索引那些甚至无法装在你笔记本电脑上的大型数据集。
这也是为什么当你去数据中心时,看到的并不是一台超级计算机,而是成千上万台看起来普通的机器。
当然,还有另一种解决吞吐量和延迟问题的方法,叫做批处理。但这属于大数据范畴,也引出了我们课程的范围。
课程范围与关联
顺便一提,你见过这些词吗:数据(Data)、信息(Information)、知识(Knowledge)、智慧(Wisdom)?数据科学是一个非常广阔的领域。
但我们不会在这里讲授人工智能或机器学习。我们将专注于数据库,我把机器学习留给了其他课程。我们关注的是数据的存储和管理,因为这是历史发展的脉络。
不过,本课程与机器学习和人工智能仍有联系。事实上,如果你看我们课程使用的教材《信息检索导论》,你会发现书中有一半的章节实际上是关于机器学习的,其中很多内容你会在机器学习课程中看到。
我尽量不过多重叠,因为没人想上两次相同的课。
教学团队与课程安排
这是助教团队。我们有胡安和乔什,你们会在习题课上认识其他助教。
现在,我来简要说明本学期我们将要学习的内容。显然,今天除了讲故事什么也没做,但真正的课程从下周开始。
以下是本学期的课程大纲:
基础部分(学期前几周)
我们将学习信息检索系统早期阶段的基础知识。
布尔信息检索
下周,我们将学习信息检索的基础,即最早、最朴素的检索系统。这被称为布尔信息检索,因为一切都基于真/假逻辑,即判断文档是否包含某个词。我们会看到它实际上已经相当强大,但也存在一些局限。我们将在实践练习中实现它。
词汇表与倒排索引
然后,我们将学习被称为“词项词汇与倒排列表”的章节。我更愿意称之为分词(Tokenization),因为这更酷。词项词汇基本上就是如何处理一本书或一个文档,将其分解成有意义的、可以索引的单位。这就是分词。我们还会探讨不同语言(如英语、中文)的分词差异及其对系统设计的影响。
容错检索
接着是容错检索。这是搜索引擎“您是不是要找”(Did you mean)功能背后的所有秘密。当你输入内容并出现拼写错误时,它会提示你。这就是拼写纠正和容错检索。
进阶主题
在掌握了基础知识后,我们将在此基础上构建更高级的主题。
索引构建与扩展
首先,如果索引时间太长,我们能更快地完成吗?答案是肯定的。其次,如果你想索引整个互联网,而它无法装在你的笔记本电脑上,我们还能做到吗?显然可以,谷歌就做到了。我将展示这是如何实现的。这就是扩展(Scaling Up) 部分。


索引压缩
然后是索引压缩。扩展意味着在相同时间内做更多的事。但比这更好的是压缩数据,因为压缩后需要的内存更少,读取时间也更短。这也是扩展的好方法。我们将学习一些统计学和信息论的知识,了解如何利用熵的力量,将更大的索引压缩到更小的硬盘中。


排序检索
接下来是排序检索。这也是我最喜欢的课程之一,因为我们将使用线性代数。它将充满向量、点积和余弦相似度等概念。你可以将其视为ChatGPT的“史前史”,因为其底层是相同的线性代数结构。但与现代大语言模型(LLM)相比,我们的向量空间可能更直观作为起点。这也是为什么我们有向量数据库以及对SQL进行扩展以处理向量的原因。



评分优化
然后我们将进行优化。这次不是针对索引,而是针对在线查询,即当人们在谷歌或ChatGPT上输入内容并获取结果时。我们将学习如何优化评分(Scoring)。评分决定了你得到的结果的顺序。在学期初的布尔查询中,我们不进行排序,结果要么在,要么不在。但在排序检索中,有了向量空间和评分优化,就有了排名。这就是为什么在谷歌中,你会得到与你搜索最匹配的前10个结果。
系统评估
然后我们会用一周时间学习评估。这当然会与机器学习联系起来,因为有些概念是相似的,比如假阳性、假阴性、精确率、召回率等。这些都是评估系统是否在做正确工作的方式。
概率信息检索
接着是我本学期绝对最喜欢的课程:概率信息检索。不要被这个名字吓到,我会花很多时间设计这堂课,让它不那么可怕。我会假设你们都学过概率论,并可能花半小时带大家回顾一下概率论的定义和贝叶斯公式。然后,借助数学和贝叶斯公式的力量,我们将构建一个基于概率的食品信息检索系统。
语言模型
然后是语言模型。这不是指大语言模型,而是指大语言模型的“祖先”,即上世纪90年代可以构建的那种模型,你甚至可以向高中生解释。它们比ChatGPT简单得多,但这是开始学习语言模型的好方法,以便之后可以推广到更复杂的模型。
PageRank与谷歌
如果我们有时间,我可能会讲讲PageRank和谷歌,因为在可扩展性方面也很有趣:如何下载整个网络并对页面进行排名。
课程深度
这门课程包含学士和硕士两个层次的内容。我认为对于三年级本科生来说,可以接受一些硕士水平的挑战,这很令人兴奋,也能让我们超越基础。
这些周次之间是有联系的。我们在某些地方会做一些假设,当我们学到概率论和语言模型时,我们就能理解为什么做那些假设,并能用概率论来证明它们。
课程组织与学习建议
现在介绍每周的课程安排。每周五上午10:10到12:00上课。希望大家尽可能来教室,如果不能,也可以通过Zoom参与。课程会被录制并放在YouTube上。
每周五下午4点到5点是习题课(第一周除外)。我们将从平缓的Python介绍开始。会有助教负责,你们可以提问。
有一本教材,你们应该认真阅读相关章节,进行自学。会有理论练习和动手编程练习。动手编程是理解知识的最好方式。

夏季学期会有书面考试,大约在8月,时长3小时。考试设计得让你们有充足时间检查和修改。

加分机会
好消息是,你们可以通过完成计分测验在最终成绩中获得额外加分。在三个指定的周次,Moodle上会有计分测验。如果你通过其中两个,就能在考试中获得额外加分。这能激励大家在学期中学习。
我们希望大家不仅为了考试,更是为了长期掌握知识。参加讲座并在学期中学习,有助于大脑进行长期记忆。加分点就是为了激励大家在学期中学习材料。
学习资源与平台
这是曼宁、拉格哈万和舒策写的教材。其中一半是机器学习内容(本课程不涉及),另一半是本学期重点。请务必阅读,我每周会在Moodle上给出对应章节。
我们会有Python编程作业。如果你还不会Python,可以学习,对于计算机科学学生来说,学习一门新语言很容易。

我们使用Moodle服务器放置所有材料:幻灯片、练习、Notebook、教材章节指引。上面还有论坛和Element聊天室。你可以在论坛提问,助教团队和我都会回答。Element聊天室是另一个平台,你可以用ETH账号登录,与团队或其他同学交流。

我们还有一个Jupyter Notebook服务器,助教每周会在上面提供Notebook。
请记得在MyStudies上注册课程和考试。
总结
本节课我们一起学习了信息检索面临的核心数据挑战,并通过一个跨越世纪的例子深刻体会了其严重性。我们了解到并行化是解决大规模数据处理的关键思路之一。最后,我们详细了解了本学期的课程路线图,从基础的布尔检索、分词,到进阶的索引扩展、压缩、排序检索、概率模型和语言模型。课程提供了讲座、习题课、教材、编程实践和在线交流平台等多种学习资源,并设有计分测验作为学期中的学习激励。希望大家能积极参与,为长期的知识积累打下坚实基础。

下周,我们将正式进入课程内容,从布尔信息检索开始。谢谢大家,下周见。
003:布尔检索 (1/2) 📚


在本节课中,我们将要学习信息检索的基础——布尔检索模型。我们将从最简单的文本搜索开始,逐步抽象出文档和术语的概念,并学习如何通过布尔逻辑(AND, OR, NOT)来组合查询。最后,我们会初步了解如何评估一个检索系统的性能。


课程回顾与引入

上一周我们介绍了信息检索的基本概念。本节中,我们来看看如何具体实现一个简单的检索系统。
信息检索本质上是一个数据库问题。我们处理的数据形态多样,包括论文、树、图、立方体和向量。其中,向量是处理文本、音频、视频等非结构化数据的新方法。本课程的重点是文本数据,因此我们会看到大量的向量、矩阵和线性代数应用。

我们今天要解决的问题是:如何从大规模的非结构化数据集合中,找到满足特定信息需求的内容。这不仅仅是搜索一个小文本文件,而是可能针对整个互联网或一个大型图书馆的所有书籍内容。
首先,我们通过一个问题来回顾一下:杜威十进制分类法是什么?
以下是几个选项:
- 它是谷歌用于索引网络的核心算法。
- 它是世界上许多图书馆用来排序和检索图书的基于数字的系统。
- 它是数学家用来对集合进行分类的标准。
正确答案是第二个选项:它是一个被许多图书馆使用的基于数字的分类系统。这种分类法对信息检索也至关重要,因为文档和查询通常都被编码为数字。
一种朴素的方法
现在,让我们从一个朴素的方法开始思考。假设你下载了亚瑟·柯南·道尔爵士的全部作品(现在是公共领域资源),并想在其中查找特定的单词。你会怎么做?
一个直接的想法是使用文本处理器(如Word)的查找功能,或者使用命令行工具。例如,我们可以使用 grep 命令。


假设我们想查找单词 “lawyer”。命令如下:
grep lawyer conan_doyle.txt
这将输出文件中所有包含 “lawyer” 的行。
那么,如何查找同时包含 “lawyer” 和 “penang” 的行呢?我们可以使用管道符连接两个 grep 命令:
grep lawyer conan_doyle.txt | grep penang
现在,我们得到了同时包含这两个词的行。
更复杂一点,如果我们想要查找包含 “lawyer” 和 “penang” 但不包含 “silver” 的行,可以使用 -v 参数来排除:
grep lawyer conan_doyle.txt | grep penang | grep -v silver
这样,我们就得到了包含前两个词且不包含第三个词的行。
至此,我们已经有了一种查询语言。它是一种基于布尔逻辑的查询语言,允许我们使用 AND、OR、NOT 来组合单词构成查询。并且,我们也有了一个非常朴素的实现方式。
然而,这种方法存在一些明显的缺陷。
以下是 grep 方法的一些潜在缺点:
- 它无法对结果进行排序。
- 它不支持邻近语义查询(例如,查找两个相邻的单词)。
- 它难以扩展到非常大的数据集(因为它是线性复杂度)。
- 它可能无法返回所有匹配项(存在假阴性的可能)。
grep 支持通配符和正则表达式,例如 .* 可以匹配任何字符序列。但主要的短板在于无法处理海量数据、无法进行邻近搜索以及无法对结果相关性进行排序。而这正是现代搜索引擎(如谷歌)价值万亿美元的核心所在。
建立抽象模型:文档与术语
在编写代码或构建索引之前,我们需要进行抽象,简化问题并思考我们工作的模型。
在信息检索的上下文中,第一个核心概念是文档。文档是检索的目标,可以是网页、书籍或任何文本单元。
第二个核心概念是术语。术语是查询的基本单位,例如 “lawyer”、“Switzerland”。但请注意,术语不一定是一个简单的字典单词,它也可能是 “New Forest”(包含空格)或 “Sherlock” 这样的专有名词。如何确定术语是一个复杂的问题,我们将在下周详细讨论。目前,我们假设存在一个术语词典。
接下来,我们需要考虑文档和术语之间的关系。
考虑一个德语文档(不理解德语也没关系,这有助于抽象思考)。文档和术语之间的关系可以有三种表达方式:
- 包含关系:一个术语是否包含在文档中。这体现了集合论的语义,文档是一个集合,术语可能是其成员。关系表示为:
术语 ∈ 文档。 - 计数关系:一个术语在文档中出现的次数。例如,“Herzly” 出现了两次。这引入了基数的概念,关系表示为:
术语 在 文档 中出现 n 次。 - 顺序关系:术语在文档中出现的顺序。关系表示为:
术语 A 出现在 术语 B 之前。
这三种关系对应着三种不同的数据抽象:
- 列表:包含所有信息(包含性、出现次数、顺序)。可以完全重构原始文档。
- 包:包含包含性和出现次数,但丢失了顺序信息。无法重构原始文档的流畅阅读顺序。
- 集合:只包含包含性信息,丢失了出现次数和顺序。这是最简化的抽象。
在本课程中,每周我们都会使用这三种抽象中的一种。今天,我们使用集合抽象,只关心一个术语是否出现在文档中。

布尔检索模型与关联矩阵
现在,我们开始考虑如何实现这个系统。我们需要一种数据结构来编码每个术语与每个文档之间的包含关系(是/否)。
最直接的方法是使用一个二维表格,即一个矩阵。在这个关联矩阵中:
- 每一行对应一个术语。
- 每一列对应一个文档。
- 矩阵中的每个元素
M[i, j]是一个布尔值(1或0),表示术语i是否出现在文档j中。
用数学公式表示,这是一个从“术语×文档”的笛卡尔积到布尔值的函数:
f: Terms × Documents → {0, 1}


假设我们有6个术语(T, U, V, W, X, Y)和10个文档(D1-D10),关联矩阵可能如下所示(其中1表示包含,0表示不包含):

| 术语/文档 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | D9 | D10 |
|---|---|---|---|---|---|---|---|---|---|---|
| T | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| U | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 |
| V | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| W | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
| X | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Y | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
根据这个矩阵,我们可以看出:
- 文档 D1 是术语 T 和 X 的集合:
{T, X} - 文档 D2 是术语 T 和 V 的集合:
{T, V} - 文档 D3 是术语 T, U, V, X 的集合:
{T, U, V, X}

执行布尔查询

有了这个矩阵,我们就可以执行查询了。
- 查询 Y:查找包含术语 Y 的文档。只需查看 Y 所在的行,值为1的列即是结果:D5, D6, D7, D8。
- 查询 U:查找包含术语 U 的文档。结果是:D3, D5, D6, D7, D8, D10。
- 查询 NOT U:查找不包含术语 U 的文档。即 U 行中值为0的列:D1, D2, D4, D9。
对于组合查询:
- 查询 U AND Y:查找同时包含 U 和 Y 的文档。对 U 行和 Y 行进行逻辑与(AND)操作(按位与)。结果是:D5, D8。
- 查询 U OR Y:查找包含 U 或 Y 的文档。对 U 行和 Y 行进行逻辑或(OR)操作(按位或)。结果是:D3, D5, D6, D7, D8, D10。
从数学角度看,可以将 OR 视为一种“加法”,将 AND 视为一种“乘法”。那么,整个系统可以看作是在一个布尔半环上运行的模块。一个查询可以表示为一个行向量(例如,查找 U 和 Y 就是 [0, 1, 0, 0, 0, 1]),通过与关联矩阵进行(基于布尔操作的)矩阵乘法来得到结果向量。这展示了线性代数在信息检索中的有趣应用。

系统评估:精确度





在介绍更高效的索引方法之前,我们先简要讨论一下如何评估检索系统。这涉及到在机器学习中常见的概念:假阳性和假阴性。
你可能会问:布尔操作总是正确的,给定一个查询,计算结果就是结果,怎么会有错误呢?
原因在于信息需求与查询之间存在差距。用户脑海中的研究意图(信息需求)与他们实际在键盘上输入的查询词可能不完全匹配。
例如,一个用户的信息需求可能是:“研究2011-2013年全球变暖对南尤伊大陆蝴蝶生态系统发展的影响”。而她实际输入的查询可能是:“butterfly climate Ui south”。
因此,系统返回的结果可能:
- 包含一些与查询匹配但与信息需求无关的文档(假阳性,即无关结果被返回)。
- 遗漏一些与信息需求相关但与查询不匹配的文档(假阴性,即相关结果未被返回)。
为了衡量检索质量,我们引入精确度的概念。
假设有一个文档集合,系统返回了一个结果子集。在所有返回的文档中:
- 真正例:相关且被返回的文档。
- 假正例:不相关但被返回的文档。
精确度 定义为返回的相关文档数量占所有返回文档数量的比例:
Precision = (Number of True Positives) / (Total Number of Retrieved Documents)
精确度衡量了返回结果的相关性有多高。例如,如果返回10个文档,其中8个是相关的,那么精确度就是 80%。


总结



本节课中,我们一起学习了布尔检索模型的基础知识。我们从最简单的文本搜索工具 grep 出发,指出了其在大规模检索中的局限性。接着,我们建立了信息检索的抽象模型,明确了文档和术语作为核心概念,并引入了集合、包、列表三种抽象层次。
我们重点介绍了基于集合抽象的布尔检索模型,使用关联矩阵来表示术语-文档关系,并演示了如何执行 AND、OR、NOT 等布尔查询。最后,我们探讨了由于信息需求与查询不匹配而导致的评估问题,并引入了精确度作为衡量检索结果相关性的重要指标。

在下一节,我们将继续学习布尔检索,探讨比关联矩阵更高效的索引结构——倒排索引,并介绍另一个重要的评估指标:召回率。
004:布尔检索 (2/2) 📚
在本节课中,我们将学习如何评估检索系统的质量,并深入探讨布尔检索的核心数据结构——倒排索引。我们将了解其构建过程、查询执行方式以及相关的性能优化策略。
评估检索质量:精确率与召回率
上一节我们介绍了布尔检索的基本概念,本节中我们来看看如何衡量一个检索系统的效果。这通常通过两个核心指标来完成:精确率和召回率。
为了理解这两个指标,我们引入一个混淆矩阵。它根据文档是否相关以及是否被系统返回,将所有情况分为四类:
- 真正例:文档相关且被系统返回。
- 假正例:文档不相关但被系统返回。
- 假反例:文档相关但未被系统返回。
- 真反例:文档不相关且未被系统返回。
精确率 衡量的是系统返回的结果中有多少是真正相关的。其公式为:
精确率 = 真正例 / (真正例 + 假正例)
召回率 衡量的是所有相关的文档中,有多少被系统成功返回。其公式为:
召回率 = 真正例 / (真正例 + 假反例)
在机器学习等领域,假正例常被称为第一类错误,假反例常被称为第二类错误。
高精确率的系统返回的结果中垃圾信息较少,而高召回率的系统则很少遗漏相关文档。然而,在实践中同时获得完美的精确率和召回率几乎是不可能的,通常需要根据具体应用场景在两者之间做出权衡。例如,在搜索引擎中,用户通常更关注第一页结果的精确率,但也希望重要的相关结果不被遗漏。
从矩阵到倒排索引
我们之前用布尔矩阵来表示文档和词项的关系。然而,对于现实中的大规模文档集(例如百万级文档和数十万词项),这个矩阵将极其庞大且稀疏(绝大部分值为0)。直接存储这个矩阵是空间低效的。
解决这个稀疏性问题的关键在于,我们只需要存储值为1的位置(即包含某个词项的文档)。更关键的是,为了高效处理以词项为起点的查询,我们应该按词项来组织数据,而不是按文档。
以下是构建索引的几种思路,其中正确的是:
- 为每个词项列出包含它的文档列表。
这种结构被称为标准倒排索引或倒排文件。它“反转”了文档包含词项的直观视角,变为词项指向包含它的文档。
倒排索引的结构与术语
一个标准倒排索引主要包含两部分:
- 词典:所有词项的列表。
- 记录表:每个词项对应的文档ID列表。
让我们明确几个关键术语:
- 词项:索引中的基本单位(如“apple”)。
- 文档:被检索的基本单元,有一个唯一ID(如文档4)。
- 记录项:一个
(词项, 文档ID)对,表示该文档包含该词项。在实际存储中,由于同一列表内的记录项共享词项,通常只存储文档ID。 - 记录表:一个词项对应的所有记录项(文档ID)的列表。
- 记录:所有记录表的统称。
- 文档频率:附加在每个词项记录表前的数字,表示包含该词项的文档数量,即记录表的长度。这是一个非常重要的元数据。
因此,整个索引 = 词典 + 记录。
构建倒排索引
构建倒排索引的过程可以概括为以下步骤,我们通过一个简单例子来说明:
- 文档获取:收集需要索引的文档。
文档1: “I did enact Julius Caesar.” 文档2: “I did enact Julius Caesar.” 文档3: “So let it be with Caesar.” - 词条化:将文档文本分割成独立的词条(例如,根据空格和标点分割)。
文档1: [I, did, enact, Julius, Caesar] 文档2: [I, did, enact, Julius, Caesar] 文档3: [So, let, it, be, with, Caesar] - 语言预处理:进行标准化处理,如统一小写、去除标点等(实际应用更复杂)。
文档1: [i, did, enact, julius, caesar] 文档2: [i, did, enact, julius, caesar] 文档3: [so, let, it, be, with, caesar] - 创建词项-文档ID对:为每个词条生成
(词项, 文档ID)对。(i,1), (did,1), (enact,1), (julius,1), (caesar,1), (i,2), (did,2), (enact,2), (julius,2), (caesar,2), (so,3), (let,3), (it,3), (be,3), (with,3), (caesar,3) - 排序:将所有对按词项排序,词项相同时按文档ID排序。
- 合并:将相同词项的对合并,形成词项及其对应的文档ID列表。
- 生成词典和记录表:最终形成倒排索引结构。
be -> [3] caesar -> [1, 2, 3] did -> [1, 2] enact -> [1, 2] i -> [1, 2] it -> [3] julius -> [1, 2] let -> [3] so -> [3] with -> [3]
使用倒排索引执行查询
有了倒排索引,执行布尔查询就变得非常高效。
- 单词查询:例如查询“caesar”。只需在词典中找到“caesar”,返回其记录表
[1, 2, 3]即可。 - AND 查询:例如查询“julius AND caesar”。需要获取“julius”的记录表
[1, 2]和“caesar”的记录表[1, 2, 3],然后计算它们的交集。结果是[1, 2]。 - OR 查询:例如查询“julius OR caesar”。需要计算两个记录表的并集。结果是
[1, 2, 3]。
交集算法(合并算法)可以在线性时间内完成。它使用两个指针遍历两个已排序的记录表,比较当前指针所指的文档ID:
- 如果ID相等,则加入结果集,两个指针同时前进。
- 如果ID不相等,则将较小ID的指针前进。
- 重复直到任一列表遍历完毕。
并集算法类似,但无论ID是否相等,都会输出较小的ID(相等时只输出一次),然后移动相应指针。
查询优化与复杂查询
对于包含多个AND操作的复杂查询,例如 A AND B AND C,由于AND操作满足交换律,我们可以选择最优的计算顺序以提升效率。最佳策略是:
优先处理记录表最短(文档频率最低)的词项。
因为先与短列表求交,可以快速缩小中间结果集的大小,减少后续比较操作。我们可以直接从索引的文档频率信息中获得各词项记录表的长度。
对于更一般的布尔查询(包含AND、OR、NOT),可以利用逻辑代数知识将其转换为合取范式——即多个OR子句的AND连接。例如 (A OR B) AND (C OR D)。
- 对于每个OR子句,可以估算其结果大小上限(各词项记录表长度之和)。
- 执行查询时,先计算文档频率小的OR子句,有助于优化整体性能。
- 对于NOT操作,无需预先计算“不包含某词项”的巨大列表,只需在求交或求并算法中做逻辑取反即可。
总结


本节课中我们一起学习了布尔检索系统的评估与核心实现。我们首先引入了精确率和召回率这两个关键指标来量化检索质量。接着,为了解决大规模数据下的存储与效率问题,我们深入探讨了标准倒排索引这一核心数据结构,包括其组成部分(词典、记录表)、构建过程以及基于它的高效查询执行算法(交集/并集合并算法)。最后,我们还了解了如何通过查询重排(如先处理短记录表)和利用合取范式来处理复杂布尔查询,以实现性能优化。倒排索引是传统信息检索系统的基石,理解其原理对后续学习至关重要。
005:词汇表 (1/3) 📚

在本节课中,我们将开始学习一个新的主题:词汇表与索引前的文档预处理。我们将看到,这个过程远比上周我们简化的版本要复杂得多。

回顾:标准倒排索引
上一节我们介绍了信息检索的核心数据结构——标准倒排索引。本节中,我们来看看它的具体构成。




我们之前看到的模型被称为 标准倒排索引。它之所以“倒排”,是因为它不再列出文档中包含哪些词项,而是反过来,列出每个词项出现在哪些文档中。这种结构使得从海量文档集合中快速检索文档成为可能。




其背后的查询语言基于布尔逻辑。用户输入几个用 AND、OR、NOT 等操作符分隔的词语,系统就会返回一个满足条件的文档集合。这种系统在早期(如60-70年代的法律全文检索)非常流行。

我们将文档视为一个词项集合,忽略了词序和词频。这让我们可以用一个由0和1组成的向量来表示文档,其中1表示词项存在。从更抽象的视角看,这形成了一个词项-文档二部图,或者一个巨大的、稀疏的邻接矩阵。
为了节省空间,我们引入了倒排记录表的概念。一个记录是 (词项, 文档ID) 对。在标准倒排索引中,所有具有相同词项的记录被组织在一起,共享同一个词项,后面跟着一列文档ID。


基于这个结构,我们可以高效地执行布尔查询。例如,对于查询 A AND B,我们需要找到同时包含词项A和B的文档,即两个记录表的交集。对于查询 C OR D,我们需要找到包含词项C或D的文档,即两个记录表的并集。
文档预处理的复杂性
上一节我们回顾了索引的基本原理,但上周的讲解过于简化。本节中,我们将深入探讨构建索引前必须完成的、复杂的文档预处理流程。
实际上,从原始文档到可索引的词项,需要经过多个步骤:
- 收集文档:首先需要确定什么算作一个“文档”。
- 词条化:将文档文本切分成独立的词条(Tokens)。
- 语言学预处理:对词条进行规范化处理。
- 构建索引:基于处理后的词条创建倒排索引。


挑战一:文档编码与解码


第一个挑战是读取和解码文档。文档本质上是字符序列,但存储在计算机中是二进制比特。我们需要知道使用哪种字符编码来正确解读这些比特。
以下是关于编码的一个重要辨析:
- ASCII:一种早期的英文字符编码。
- UTF-8:一种基于Unicode的、可变长度的编码,现已成为网络标准。
- MacRoman:苹果电脑早期使用的一种编码。
- Unicode:这不是一种编码。它是一个全球字符集标准,为每个字符分配一个唯一的编号(码点)。UTF-8、UTF-16、UTF-32才是具体的编码方案,它们定义了如何将Unicode码点转换为字节序列。
最佳实践是始终使用UTF-8编码,以避免国际化时出现乱码问题。解码时,我们需要自动检测或通过元数据获知文档的编码格式,否则就可能出现乱码。
此外,文档可能不是纯文本,而是PDF、Word或包含XML/HTML实体(如 < 代表 <)的格式,这都需要特殊的解析和处理。
挑战二:界定文档单元
“文档”的界定并非总是显而易见的。
- 过大需要拆分:例如,一个包含多年邮件的压缩包,应该将每封邮件视为独立的文档。
- 过小需要合并:例如,用LaTeX编写的学位论文,每个章节是单独文件,但索引时应将它们视为一个完整的文档单元。
构建词汇表:词条化的难题
在成功读取文档后,我们需要从中提取文本并构建词汇表,即所有唯一词项的集合。这首先涉及到词条化——将文本流切分成一个个词条。这个过程远非按空格分割那么简单。
以下是一些英语中的棘手案例,说明了词条化的复杂性:

- 标点符号:句点
.在缩写(如U.S.A.)、电子邮件或小数点中不应被移除。撇号‘在isn’t中连接了单词。 - 连字符:用法不固定,如
state-of-the-art(应视为一个概念)与learn-it-all-by-heart(可能应分开索引)。 - 复合词:随时间演变,如
web site->website,data base->database。 - 专有名词:如
San Francisco、New York University,应作为一个整体处理。 - 数字与日期:电话号码、电子邮件地址、日期格式(月/日/年顺序不统一)都需要特殊处理。




多语言带来的挑战




不同语言的语法和书写系统给词条化和预处理带来了巨大挑战。以下是几个例子:
- 英语:动词变位相对简单(如
want,wants,wanted),但需要将不同形式归一化。 - 法语/意大利语/德语:动词变位非常丰富,同一个动词可能有六种甚至更多书写形式,大大增加了词汇表大小。
- 德语:以复合词著称,可能形成极长的单词,词条化困难。
- 汉语:使用成千上万个字符,没有空格分隔词,需要专门的分词技术。
- 北萨米语等:某些语言中,整个句子可能写成一个单词,包含了主语、谓语、宾语、时态等多种语法信息。
这些例子都表明,通用的、简单的空格分割式词条化是行不通的,必须进行针对性的语言学预处理。

核心术语辨析


在深入讨论前,明确一系列相关术语至关重要。它们可以从两个维度来理解:
- 是否经过语言学处理:原始形式 vs 规范化后形式。
- 是否与文档及位置关联:关联特定位置 / 仅关联文档 / 不关联文档。







以下是六个核心概念:
- 位置型词条:原始词条,并记录其在文档中的具体位置。
- 非位置型词条:原始词条,仅知道它属于某个文档,不记录位置。
- 未归一化词型:原始词条,脱离具体文档上下文,即词汇表中的原始形式。
- 归一化词型:经过语言学处理(如词干还原、转为小写)后的词条形式,脱离具体文档上下文。这通常就是我们所说的词项。
- 位置型记录:归一化词型与具体文档、具体位置的关联。
- 非位置型记录:归一化词型与具体文档的关联(不记录位置)。我们在标准倒排索引中提到的“记录”通常指这种。
注意:在日常交流或文献中,这些术语的使用可能不严格。例如,“词条”可能指位置型词条,也可能泛指任何切分单元;“记录”可能特指非位置型记录。理解这两个维度有助于我们根据上下文准确判断其含义。

本节课中我们一起学习了构建信息检索系统词汇表初期所面临的巨大复杂性。我们回顾了标准倒排索引,然后深入探讨了从文档解码、文档单元界定,到多语言词条化所遇到的各种挑战,并厘清了与词条、词型、记录相关的一系列核心术语。理解这些预处理步骤的复杂性,是构建高效、准确检索系统的基石。在接下来的课程中,我们将继续探讨如何处理这些挑战。
006:词汇表 (2/3) 📚
在本节课中,我们将继续探讨构建信息检索系统词汇表时的关键概念与优化技术。我们将了解如何通过忽略常见词、对词汇进行归一化以及应用现代分词算法来提升索引的效率和效果。
停用词优化 🛑
上一节我们介绍了词汇表的基本结构,本节中我们来看看一种经典的索引优化方法:停用词。
停用词是指在文档集合中出现频率极高、但对区分文档内容几乎没有帮助的词语,例如英文中的 “the”、“a”、“and”、“is” 等。由于它们几乎出现在所有文档中,在查询时返回的结果集过大,失去了筛选意义。
因此,一个常见的优化是在构建索引时完全忽略这些停用词。这样做有两个好处:
- 节省存储空间:索引文件体积减小。
- 提升查询速度:查询时无需遍历这些超长且无意义的倒排列表。
以下是历史上一个著名的停用词列表(Reita‘s list)示例:
- a
- an
- and
- are
- as
- at
- be
- by
- for
- from

注意:随着存储和计算资源的极大丰富,现代搜索引擎(如Google、Bing)已逐渐减少甚至不再使用停用词列表。但理解这一概念对于掌握信息检索的发展历程和基础优化思想仍然非常重要。

词汇归一化:等价类与词干提取 🔄
词汇归一化的核心目标是将表面形式不同但含义相同的词项归为一类,从而提升召回率。这主要通过创建等价类来实现。
等价类
在数学上,等价类是通过满足自反性、对称性和传递性的等价关系对元素进行的分组。在信息检索中,我们将非归一化词型(如 “USA”、“U.S.A”、“usa”)通过一组规则映射到同一个归一化词型(如 “usa”)上,这些归一化词型就构成了等价类。
创建等价类的规则可能包括:
- 移除特定字符(如 “.”、“-”)。
- 移除变音符号(如 “cliché” -> “cliche”)。
- 大小写转换(如 “Apple” -> “apple”)。
词干提取
词干提取是一种基于规则的、机械式的创建等价类的方法。它通过截断词尾的字母,将不同的词汇形式归结到同一个“词干”上,而不需要理解语言。
最著名的算法是波特词干提取器。它应用一系列预定义的转换规则。
例如,应用规则后:
analysis->analysifeatures->featurrealized->realiz
词干提取对英语等语言效果很好,但对于德语、法语等有复杂词形变化的语言,更有效的方法是词形还原。词形还原需要基于词典和语言知识,将词汇还原为其基本形式(词典原形),例如将 “computing”、“computed”、“computation” 都还原为 “compute”。
核心要点:词汇归一化非常复杂,没有放之四海而皆准的简单规则,需要根据具体语言和应用场景进行精细调整。
现代分词算法:字节对编码(BPE) 🤖
传统的分词严重依赖空格等分隔符,对于德语复合词或中文等无空格语言存在局限。现代大语言模型(如ChatGPT)采用的分词方法(如BPE)是语言中立的。
字节对编码 最初是一种压缩算法,其核心思想是迭代地合并训练文本中最频繁出现的相邻符号对,并将合并后的新符号加入词汇表。
以下是BPE算法的简化步骤:
- 初始化词汇表为所有基本字符(如字母)。
- 在训练文本中统计所有相邻符号对的频率。
- 将频率最高的符号对合并为一个新符号,加入词汇表。
- 在文本中用新符号替换所有该符号对的出现。
- 重复步骤2-4,直到词汇表达到预设大小。
假设初始文本为字符序列,目标词汇表大小为6,合并过程可能如下:
初始: a, b, a, a, b, a, b, a, a, ...
第1步: 合并 ‘a a‘ -> ‘aa‘
第2步: 合并 ‘a b‘ -> ‘ab‘
第3步: 合并 ‘aa ab‘ -> ‘aaab‘
...
最终词汇表: {a, b, aa, ab, aaab, ...}
优势:
- 语言中立:可以处理任何字符序列,包括未知语言或代码。
- 平衡粒度:能产生介于字符和单词之间的子词单元,有效处理未登录词。
影响:这也是为什么LLM有时会“数不清”单词中的字母(例如“strawberry”),因为其分词结果可能不是按单个字母进行的。
查询优化:跳表指针 ⏩
最后,我们回到倒排索引的查询执行优化。当处理AND查询(求两个倒排列表的交集)时,如果两个列表长度相差悬殊,线性遍历比对效率较低。
观察发现,在遍历长列表寻找与短列表匹配的文档ID时,可能会经历一长串不匹配的ID。跳表指针 就是为了加速这一过程而引入的辅助数据结构。
跳表指针是预先在长倒排列表中设置的“捷径”指针,允许算法跳过一段不可能产生匹配的文档ID区间,直接跳到下一个可能的匹配点附近。
例如,列表A为 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...],列表B为 [1, 3, 10, 14, ...]。当比对到A中的4时,如果有一个跳表指针能直接指向10,就可以跳过4到9之间的无效比较。
关键问题:跳表指针的最佳间隔(“跳跃”长度)是多少?这需要在指针的存储开销和查询加速收益之间取得平衡,我们将在后续课程中探讨。

本节课中我们一起学习了信息检索中词汇处理的进阶知识。我们了解了通过移除停用词来优化空间与时间,探讨了通过等价类、词干提取和词形还原来进行词汇归一化的复杂性与方法,并深入了解了现代大语言模型所采用的、语言中立的BPE分词算法。最后,我们预览了用于加速倒排列表求交操作的跳表指针概念。这些技术共同构成了高效、智能的信息检索系统的基础。
007:索引优化与短语查询
在本节课中,我们将要学习如何优化标准倒排索引以处理更复杂的查询,特别是短语查询。我们将探讨两种主要的索引扩展方法:二元词索引和位置索引。





上一节我们介绍了在长倒排列表中使用跳表指针来加速查询。本节中我们来看看如何扩展索引以支持短语查询。






短语查询的挑战
布尔查询系统(使用 AND、OR、NOT)虽然精确,但难以获得数量适中的相关结果。短语查询允许用户搜索一个完整的句子(例如 “ETH Zurich”),而不仅仅是独立的单词。现代搜索引擎通常用双引号(如 "ETH Zurich")来触发短语搜索。


标准倒排索引无法直接支持短语查询,因为它只记录了单词是否出现在文档中,而没有记录单词之间的位置关系。
解决方案一:二元词索引
为了解决位置信息缺失的问题,第一种方案是修改词汇表的结构。
其核心思想是:在构建索引时,不再将单个词项作为词汇表条目,而是将连续的两个词(二元词)作为基本单位。索引过程使用一个大小为2的滑动窗口遍历文档。
以下是一个构建二元词索引的示例过程:
文档文本: “help ETH Zurich”
滑动窗口: [“help”, “ETH”], [“ETH”, “Zurich”]
索引条目: “help ETH” -> 文档ID, “ETH Zurich” -> 文档ID
现在,要查询短语 "ETH Zurich",我们只需在索引中查找二元词 "ETH Zurich",即可获得精确匹配的文档列表。
处理更长的短语
对于更长的短语(如 "ETH Zurich flexibly react"),我们可以将其分解为多个重叠的二元词:
查询短语: “ETH Zurich flexibly react”
分解为: [“ETH”, “Zurich”], [“Zurich”, “flexibly”], [“flexibly”, “react”]
然后,我们查找所有包含这些二元词的文档,并取它们的交集。




然而,这种方法会产生误报。例如,一个文档包含句子 “help ETH Zurich to introduce techniques to flexibly react”,它包含了所有目标二元词,但并非精确的目标短语。





以下是减少误报的几种思路:
- 使用更大的K元词索引(如三元词)。
- 在二元词索引查询后,进行结果后过滤。
- 使用更小的词对窗口(这实际上会增加误报,因此不正确)。


虽然增加K值可以减少误报,但会导致词汇表大小呈指数级增长(从二元词的平方级到三元词的立方级),在实践中不可行。因此,常见的折中方案是使用二元词索引进行初步筛选,再通过后处理步骤过滤掉误报。
解决方案二:位置索引
第二种方案不改变词汇表,而是扩展倒排记录项本身的内容。
在位置索引中,每个倒排记录项不仅包含文档ID,还包含该词项在文档中出现的所有位置信息。一个典型的位置记录项结构如下:
词项 -> (文档ID, 词频, [位置1, 位置2, ...])
例如,词项 “Zurich” 在文档C中出现了3次,位置分别是4、7、11,则记录为:(C, 3, [4, 7, 11])。
现在,要执行短语查询 "ETH Zurich",算法步骤如下:
- 分别获取词项
“ETH”和“Zurich”的倒排列表。 - 找出同时包含这两个词项的文档(文档ID交集)。
- 对于这些候选文档,检查
“Zurich”出现的位置是否恰好比“ETH”的某个出现位置大1(即相邻)。
这种索引结构称为位置索引,它提供了完整的词序信息,可以精确支持短语查询以及近似查询(如“两个词在5个单词内出现”)。

索引变体总结
本节课中我们一起学习了标准倒排索引的几种重要变体:
- 带跳表指针的索引:通过添加跳转指针加速长列表的遍历,跳表间隔通常设置为列表长度的平方根作为经验值。
- K元词索引:将连续的K个词作为索引单元,以支持短语查询,但会显著增加索引大小。
- 位置索引:在倒排记录项中存储词项位置,既能支持短语查询,也为后续的排名模型(如向量空间模型)提供了基础数据(词频)。


这些扩展使得倒排索引从一个简单的布尔过滤器,演变为能够支持复杂信息检索需求的核心数据结构。
008:容错检索 (1/3) 🔍



在本节课中,我们将要学习信息检索中的容错检索技术,特别是如何执行近似查询和拼写检查。首先,我们会回顾之前学过的核心概念,然后深入探讨如何高效地在词汇表中查找词项。
课程回顾 📚






上一节我们介绍了标准倒排索引及其查询执行。本节中,我们来看看如何高效地管理词汇表。



我们之前学习过,可以将文档集合视为词的集合、词袋或词序列。我们使用布尔查询语言来搜索词项的存在与否,并输出一个无序的文档子集。






标准倒排索引是一个优化结构,它将词项(词汇表)与对应的倒排记录表关联。每个倒排记录表包含文档ID,而文档频率则记录了每个词项出现在多少篇文档中。


以下是构建索引的关键步骤:
- 文档收集与分词:确定文档单元并进行分词。
- 语言预处理:可能包括词干还原、去除停用词等。
- 构建索引:创建词项到倒排记录表的映射。


为了优化,我们引入了跳表指针来加速合并操作,以及位置索引来记录词项在文档中的具体位置。




词汇表查找 🔎
现在,我们聚焦于词汇表本身。词汇表是倒排索引左侧所有词项的集合。当执行查询时,我们需要快速判断一个词项是否存在,并定位其倒排记录表。
一个直观的想法是将词汇表存储为排序列表。人类可以快速在排序列表中查找,因为我们可以利用字母顺序进行二分查找。但对于计算机,我们需要高效的数据结构来支持这种查找。
哈希表方案


第一种方案是使用哈希表。我们通过一个哈希函数将词项映射到一个数组的特定位置。
公式:位置 = hash(词项)
哈希表的优点是查找复杂度理论上为 O(1)。然而,它存在两个主要缺点:
- 不支持范围查询:例如,查找所有以“A”到“C”开头的词项会非常低效。
- 哈希冲突:现实中哈希函数可能不完美,导致多个词项映射到同一位置,需要额外处理。
树形结构方案
由于哈希表的局限性,实践中更常使用树形结构,这与关系型数据库中的索引类似。
以下是树的基本概念:
- 节点:树中的元素。
- 叶节点:没有子节点的节点。
- 节点高度:从该节点到最远叶节点的路径长度。
- 节点深度:从根节点到该节点的路径长度。
- 二叉树:每个节点最多有两个子节点。
一种特殊的二叉树是二叉搜索树。在这种树中,对于任意节点,其左子树中的所有值都小于该节点的值,其右子树中的所有值都大于或等于该节点的值。

代码(查找逻辑伪代码):
function search(node, key):
if node is null or node.key == key:
return node
if key < node.key:
return search(node.left, key)
else:
return search(node.right, key)


在二叉搜索树中查找一个值的复杂度为 O(log n)。我们可以将词项存储在二叉搜索树的节点中,并将节点与对应的倒排记录表关联,从而实现对数时间的词项查找。

为了进一步优化,我们通常采用以下两种策略:
- 词项仅存储在叶节点:内部节点仅用于指引搜索方向(向左或向右)。
- 内部节点复用词项:内部节点的值可以直接使用某个词项,其语义是“小于该词项的向左,大于或等于的向右”,而无需虚构一个分隔值。
总结 🎯
本节课我们一起学习了容错检索的基础部分。我们回顾了信息检索的核心组件——倒排索引,并重点探讨了如何高效管理词汇表以支持快速查询。我们分析了哈希表方案(O(1)查找但不支持范围查询)和树形结构方案(支持范围查询且效率为O(log n)),并指出了实践中对二叉搜索树的优化方向。

下一节,我们将继续深入,学习在实际大规模系统中广泛使用的 B+树 结构。
009:容错检索 (2/3)






在本节课中,我们将学习如何利用B+树高效地组织词汇表,并在此基础上实现支持通配符查询的检索系统。我们将从理论数据结构过渡到实际系统构建,理解如何通过额外的索引结构来应对复杂的查询模式。





从理论到实践:B+树简介

上一节我们介绍了基本的索引结构。本节中我们来看看如何利用更高效的数据结构来优化磁盘访问。

在理论计算机科学或算法与数据结构课程中学到的知识,为构建真实系统提供了理论基础。一个典型的例子是,二叉搜索树在理论上能以对数时间查找术语,但在涉及磁盘读写时效率不足。
这是因为磁盘以数据块为单位进行读写,通常大小为4KB。即使只需要读取几个字节,也必须读取整个块。如果索引结构(如二叉搜索树)的节点大小远小于一个磁盘块,就会导致大量资源浪费,因为每次读取都只利用了块的一小部分。
因此,在数据库和信息检索系统中,人们不使用二叉搜索树,而是使用允许更多子节点的树结构,即B树。理想情况下,节点的大小应设计为与磁盘块大小匹配,从而高效利用磁盘资源。




B+树的结构与约定
以下是B+树的核心概念和约定。

B+树是B树的一种变体。两者的主要区别在于数据存储的位置:
- B树:键值(术语)存储在所有节点(包括内部节点和叶子节点)中。
- B+树:键值(术语)仅存储在叶子节点中。内部节点仅存储用于导航的键。
B+树有以下几个关键约束:
- 所有叶子节点深度相同:树是平衡的。
- 子节点数量有区间限制:每个节点(除根节点外)的子节点数量在一个可配置的区间
[d+1, 2d+1]内,其中d是一个参数。这保证了节点大小与磁盘块对齐。 - 根节点是特例:根节点的子节点数量可以少于下限
d+1,但必须至少为2。
一个 (d+1, 2d+1) 的B+树,意味着每个内部节点的键数量在 [d, 2d] 之间(因为n个子节点对应n-1个键)。叶子节点的键数量也遵循同样的区间 [d, 2d]。
代码示例:节点子节点数量区间
# 对于一个参数为 d 的 B+ 树
min_children = d + 1
max_children = 2 * d + 1
# 对应的键数量区间为
min_keys = d
max_keys = 2 * d
需要提醒的是,关于B树的定义存在多种约定。本课程采用上述约定,但查阅其他资料时可能会看到不同的参数化方式。



B+树的“Plus”(增强)特性在于,所有叶子节点通过一个单向链表连接起来。这形成了一个“高速公路”,使我们能够在线性时间内遍历所有按字母顺序排序的术语,这对于处理范围查询非常有用。





B+树的构建与操作
构建和操作B+树的算法较为复杂,涉及插入和删除时的节点分裂与合并,以维持树的平衡和约束。其核心思想是弹性伸缩:当插入导致节点键数超过上限 2d 时,将其分裂为两个节点(分别包含约 d 和 d+1 个键);当删除导致节点键数低于下限 d 时,可能需要与兄弟节点合并。这些操作可能会向上传播,导致树的高度增加或减少。
在实际硬件中,为了效率,通常将B+树的上部几层加载到内存中,而将下部(叶子节点及部分内部节点)存储在磁盘上。这样,查询时只需将必要的磁盘块读入内存,从而高效利用磁盘I/O。
利用B+树实现通配符查询

现在我们有了一个用B+树组织的高效可搜索词汇表,可以支持更复杂的查询,例如通配符查询。用户可以使用 * 符号来匹配任意字符序列。
通配符可以出现在术语的末尾(如 comput*)、开头(如 *ics)或中间(如 sta*t)。我们来看看如何利用现有结构高效处理这些模式。
1. 后缀通配符查询(如 BA*)
这是最容易处理的情况。我们利用B+树叶子的链表这一“增强特性”。
- 步骤:在词汇表B+树中定位查询范围的下界(
BA)和上界(BB,不包含)。从下界节点开始,沿着叶子链表遍历,直到达到上界,即可收集所有匹配的术语。 - 效率:只需一次树搜索定位起始点,然后线性遍历链表,无需回溯树结构。
2. 前缀通配符查询(如 *AC)
直接处理效率低下(需要扫描所有术语)。解决方案是构建一个额外的索引结构。
- 方法:创建所有术语的反向版本(如
"cat"变为"tac"),并用另一个B+树索引这些反向术语。 - 查询时:将查询
*AC反转为CA*,然后在反向索引中执行一个后缀通配符查询,即可高效找到所有以"AC"结尾的原始术语。
3. 中缀通配符查询(如 *MT*)
这留给读者作为思考题。存在利用旋转索引等更高级数据结构的解决方案。
4. 两侧通配符查询(如 ZU*CH)
可以通过分解查询并结合上述索引来解决,但需要注意可能产生的误报。
- 方法:将查询分解为
ZU*(在标准索引中处理)和*CH(在反向索引中处理为HC*)。分别得到两个术语集合后,取它们的交集。 - 注意:在倒排列表级别取交集可能导致误报(返回的文档包含
ZU...和...CH,但并非同一个词)。更好的方法是在术语级别取交集,即先找出同时满足两个通配符模式的术语,再用这些术语去查询倒排索引。对于剩余的潜在误报,可以进行后过滤。
通用解决方案:轮排索引
为了更通用、高效地处理任意位置的通配符查询(特别是中缀查询),可以引入轮排索引。
- 构建:对于词汇表中的每个术语,在其末尾添加一个特殊结束符(如
$),然后生成该字符串的所有循环旋转。例如,"plant$"的旋转包括"plant$","lant$p","ant$pl"等。将所有术语的所有旋转用另一个B+树索引起来。 - 查询:将通配符查询转换为轮排索引上的一个前缀查询。例如,查询
P*T可以转换为在轮排索引中查找以T$P开头的旋转,这对应了所有以P开头、以T结尾的原始术语。 - 优化:在轮排索引中,旋转条目不直接指向倒排列表,而是指向原始术语。这样节省了空间,在通过轮排索引找到匹配的术语后,再通过主索引查找其倒排列表。




本节课中我们一起学习了B+树如何作为高效索引结构的基础,并探讨了如何通过构建标准索引、反向索引和轮排索引等多种辅助结构,来实现对后缀、前缀和中缀通配符查询的支持。关键在于理解空间(存储额外索引)与时间(查询速度)之间的权衡,以及根据查询模式选择合适的索引策略。
010:容错检索 (3/3)





在本节课中,我们将学习信息检索中的容错检索技术,特别是如何利用k-gram索引和编辑距离等技术来处理拼写错误和通配符查询。
概述
上一节我们介绍了通配符查询和轮排索引。本节中,我们将深入探讨k-gram索引,并学习如何利用它以及编辑距离、Jaccard系数和Soundex算法来实现拼写校正。
什么是归一化类型?
首先,我们回顾一个核心概念。归一化类型是指词元的等价类。例如,动词的不同变体(如“compute”、“computing”、“computed”)或名词的不同形式,我们希望将它们视为同一类型进行分组处理。
k-gram索引
为了处理更复杂的通配符查询(例如同时包含开头和结尾通配符的查询),我们引入k-gram索引。这与之前学过的双词索引原理相似,但操作对象从文档中的词变成了词中的字母。
k-gram是指长度为K的字符序列。我们通过在词的首尾添加特殊符号(如$),然后使用一个大小为K的滑动窗口划过该词,来生成所有的k-gram。


例如,对于单词“computer”,其2-gram(bigram)生成过程如下:
$c, co, om, mp, pu, ut, te, er, r$
以下是构建k-gram索引的步骤:
- 为词汇表中的每个词生成其所有k-gram。
- 为这些k-gram建立一个倒排索引,其中每个k-gram指向包含它的所有词的列表。
通过这个索引,我们可以高效地查找包含特定字符序列的词,从而支持复杂的通配符查询。

拼写校正

用户输入查询时可能出现拼写错误。搜索引擎需要能够识别并纠正这些错误。一种常见的方法是计算编辑距离(莱文斯坦距离)。
编辑距离

编辑距离衡量的是将一个字符串转换为另一个字符串所需的最少单字符编辑操作次数(插入、删除、替换)。
直接为查询词计算它与词汇表中所有词的编辑距离效率太低。因此,我们通常先使用k-gram索引进行预筛选。
利用k-gram进行预筛选
其核心思想是:拼写相似的词,会共享大量的k-gram。
具体步骤如下:
- 为查询词生成其k-gram集合。
- 在k-gram索引中查找这些k-gram,得到包含这些k-gram的所有候选词。
- 计算查询词与每个候选词之间的Jaccard系数,以衡量相似度。
Jaccard系数的计算公式为:
Jaccard(A, B) = |A ∩ B| / |A ∪ B|
其中,A和B分别是查询词和候选词的k-gram集合。系数越接近1,说明两个词越相似。
我们可以高效地计算这个系数,因为:
- 分子
|A ∩ B|是共享k-gram的数量,可以从索引查询结果中直接统计。 - 分母
|A ∪ B|可以利用公式|A| + |B| - |A ∩ B|计算,其中|A|和|B|可以预先存储或快速计算。
在获得高Jaccard系数的候选词后,我们再对这批数量较少的候选词计算精确的编辑距离,从而确定最可能的正确拼写。
其他拼写校正方法
除了基于k-gram和编辑距离的方法,还有两种常见的拼写校正思路。
基于上下文的方法


有时,一个词本身正确,但在特定上下文中是错误的。例如,句子“I am a graduate form ETH”中的“form”应为“from”。通过分析词周围的上下文(例如使用双词索引查看搭配),可以判断并纠正此类错误。这种关注上下文关联的思想,也是现代大语言模型(LLM)中“注意力”机制的基础。
基于发音的方法:Soundex算法
Soundex算法基于词的发音进行编码和匹配,适用于拼写错误但发音相似的情况。它将每个词转换成一个由1个字母和3个数字组成的指纹码,过程如下:
- 保留单词的首字母。
- 将后续的字母根据特定的发音规则映射为数字(例如,元音和
h、w、y映射为0;b、f、p、v映射为1等)。 - 去除连续重复的数字。
- 去除所有0。
- 如果长度不足4,用0补足;如果超过4,则截断。
例如,“computer”和拼错的“cmputer”会生成相同的Soundex指纹码。通过为Soundex指纹码建立索引,可以快速找到发音相似的词。

总结
本节课我们一起学习了容错检索的几种关键技术:
- k-gram索引:通过将词切分为定长字符序列来构建索引,有效支持了复杂的通配符查询。
- 拼写校正:结合k-gram索引预筛选、Jaccard系数相似度计算和编辑距离精确匹配,实现了高效准确的拼写错误纠正。
- 其他方法:了解了基于上下文的校正和基于发音的Soundex算法。

这些技术共同构成了信息检索系统处理用户输入不准确、提高检索召回率的重要基础。
011:索引构建 (1/3) 🏗️

在本节课中,我们将要学习如何构建大规模信息检索系统的索引。我们将从回顾标准倒排索引开始,然后深入探讨硬件限制如何影响索引构建的性能,并介绍第一代索引构建算法——基于块的排序索引。
回顾:标准倒排索引 📚
上一节我们介绍了处理拼写检查和通配符查询的各种技术。本节中,我们来看看构建索引的核心数据结构。

标准倒排索引是一个高效的数据结构,用于快速查找包含特定词汇的文档。它主要由三部分组成:





- 词典:存储所有唯一的词汇项,通常使用哈希表或B+树等数据结构增强。
- 文档频率:记录每个词汇项出现在多少个文档中,即对应倒排记录表的长度。
- 倒排记录表:对于每个词汇项,存储包含它的所有文档的ID列表。
在索引中,我们通常不重复存储词汇项本身,而是用整数ID来指代,以节省空间。
硬件视角下的性能瓶颈 💻


为了构建更大规模的索引,我们必须考虑代码运行的硬件环境。数据库领域的专家通常将硬件抽象为四个核心组件:CPU、内存、磁盘和网络。系统性能瓶颈几乎总是由其中之一引起。
理解存储层次结构至关重要,不同层级的存储设备在容量、访问速度和成本上存在巨大差异:
- CPU缓存:速度极快(纳秒级),但容量很小(几MB),成本高昂。
- 内存:速度较快(几十纳秒),容量较大(可达TB级),成本较高。
- 磁盘:速度较慢(毫秒级),容量很大(TB到PB级),成本较低。
- 磁带:速度非常慢(小时级),容量极大(PB级),成本极低。
过去几十年的发展趋势是:存储容量飞速增长,但读取速度和访问延迟的改善相对缓慢。这导致了一个核心矛盾:数据量增长的速度远快于我们读取数据的速度。
磁盘访问优化:块读取

磁盘访问慢的主要原因是机械部件(如旋转盘片和移动磁头)带来的延迟。如果每次只读取一个字节,大部分时间都会浪费在等待上。


因此,操作系统和数据库系统不会进行单字节读写,而是以块为单位(例如64KB)进行读写。这样,在等待一次延迟后,可以连续读取大量数据,从而显著提高吞吐效率。
平衡负载:压缩技术

当处理海量数据时,磁盘I/O往往成为瓶颈,而CPU可能处于空闲状态。此时,可以使用压缩技术来转移负载:

- 压缩数据,减少需要从磁盘读取的字节数,从而减轻磁盘I/O压力。
- 压缩后的数据需要CPU进行解压缩,这为闲置的CPU提供了工作。


关键在于找到平衡点:足够的压缩以缓解磁盘瓶颈,但又不能过度压缩导致CPU成为新的瓶颈。
压缩倒排记录对 🔧

在构建索引的初始阶段,我们会在内存中生成大量的(词项, 文档ID)对。为了在有限的内存中容纳更多数据,我们需要压缩这些数据对。
一个简单有效的压缩方法是:用整数ID代替词项字符串。
假设我们有一个1GB的文本集合,包含1亿个词项位置。平均每个词项长约10字节,文档ID用4字节整数表示。那么:


- 每个未压缩的
(词项, 文档ID)对占用:10字节 + 4字节 = 14字节。 - 1亿个这样的对占用:
100M * 14字节 ≈ 1.4 GB。
如果我们为每个唯一词项分配一个整数ID(例如,“ETH”=1,“Zurich”=2),并维护一个从ID到词项的映射表。那么:


- 每个压缩后的
(词项ID, 文档ID)对占用:4字节 + 4字节 = 8字节。 - 1亿个这样的对占用:
100M * 8字节 = 800 MB。
通过这种替换,我们实现了近一倍的压缩率,使得同样大小的内存可以处理更多数据。词项到ID的映射表本身很小(通常只有几十万条记录),内存开销可以忽略。



第一代算法:基于块的排序索引 🧱

直接使用磁盘构建索引速度太慢,而内存容量有限。基于块的排序索引算法巧妙地结合了内存的速度和磁盘的容量。

BSBI算法的核心思想是:将整个文档集合分成若干批次,每个批次的大小刚好能被内存容纳。


以下是算法的主要步骤:






- 分批处理:将文档集合分割成多个块。依次将每个块读入内存。
- 块内处理:对于当前内存中的块:
- 解析文档,生成
(词项ID, 文档ID)对。 - 在内存中将这些数据对按词项ID为主键、文档ID为次键进行排序。
- 将排序后的结果作为一个中间文件顺序写入磁盘。
- 解析文档,生成
- 合并中间文件:当所有块都处理完毕,磁盘上会留下多个已排序的中间文件。
- 使用多路归并算法(类似于归并排序的第二阶段)线性地合并这些文件。
- 将最终合并后的结果写入磁盘,形成完整的倒排索引。







这个算法的优势在于,它最大限度地减少了昂贵的随机磁盘I/O。对每个块,它只进行两次顺序磁盘操作(一次读入,一次写出)。最后的归并阶段也是高效的顺序读写。






算法的时间复杂度为 O(T log T),其中T是词项位置的总数,主要开销来自每个块内部的排序。
总结与预告 📝

本节课中我们一起学习了构建大规模索引的初步方法。我们首先回顾了标准倒排索引的结构,然后从硬件角度分析了性能瓶颈,并介绍了通过压缩(词项, 文档ID)对来节省内存的技巧。最后,我们详细讲解了第一代索引构建算法——基于块的排序索引,它通过分批处理和在内存中排序,有效地利用内存和磁盘构建索引。




在下一节课中,我们将介绍性能更优的第二代算法——单遍内存索引,它将在BSBI的基础上进行重要改进,实现更高效的索引构建。
012:索引构建 (2/3) 📚
在本节课中,我们将继续学习如何为大规模文档集构建索引。上一节我们介绍了基于磁盘的索引构建方法,本节中我们将探讨更高效的“单次内存索引”技术,并了解如何利用分布式计算框架(如MapReduce)来处理超大规模数据,例如整个互联网。最后,我们会讨论当文档集动态更新时,如何维护索引。


从BSBI到SPIMI:效率提升 🔄





上一节我们介绍了基于磁盘的索引构建方法。该方法将文档集分批处理,生成(词项ID, 文档ID)对,最后合并排序。这种方法存在一个效率问题:它需要在内存中存储大量重复的词项字符串,占用了宝贵的内存空间。
因此,我们有了一个改进版本:单次内存索引。其核心思想与BSBI类似,也是将文档集分区并逐个处理。但关键区别在于,SPIMI在内存中直接为每个分区构建一个迷你标准倒排索引,而不是生成成对的ID。
以下是SPIMI的构建步骤:
- 处理分区:顺序读取当前分区的文档。
- 动态构建迷你索引:对于文档中的每个词项,将其插入到内存中的迷你倒排索引字典中(保持字典有序),并将当前文档ID追加到该词项的倒排记录表末尾。
- 写入磁盘:处理完一个分区后,将这个已排序的迷你倒排索引写入磁盘。
- 重复与合并:对所有分区重复步骤1-3。最后,使用归并排序的合并阶段,线性合并所有磁盘上的迷你索引,生成最终的全局倒排索引。
SPIMI的优势在于:
- 无需词项ID:直接使用词项字符串,避免了分配和管理全局词项ID的复杂度。
- 高效的文档ID追加:由于文档是按顺序处理的,文档ID单调递增,因此向倒排记录表末尾追加新ID是常数时间复杂度
O(1)。 - 高效的内存利用:每个词项在内存字典中只存储一次,而非在大量配对中重复存储。


其算法复杂度主要分为两部分:
- 构建迷你索引:复杂度为
O(T log M),其中T是词条总数,M是唯一词项数。由于M远小于T,且log M增长缓慢,这部分开销相对较小。 - 合并索引:复杂度为
O(T),是线性的。








因此,总体复杂度给人的感觉是 O(T),这对于处理大数据非常有利。


分布式索引构建:MapReduce简介 🌐
当数据集合(如整个互联网)无法存储于单机磁盘时,我们需要使用分布式集群。Google提出的MapReduce框架正是为此而生,它也是构建网络搜索引擎的核心技术。
MapReduce包含三个阶段,可以形象地理解为“收集宝可梦并计数”的过程:





- Map阶段:数据被分割成多个分区,分发到集群的不同机器上并行处理。每台机器处理自己分到的数据块,输出中间键值对。例如,处理文档时,输出
(词项, 文档ID)对。 - Shuffle阶段:框架将所有Map任务输出的中间结果进行“洗牌”,确保所有相同键(如相同词项)的数据被传输到同一台机器上。这是最复杂且网络通信密集的阶段。
- Reduce阶段:收到同一键所有数据的机器进行并行归约操作。例如,将同一个词项的所有文档ID收集起来,排序,形成该词项的倒排记录表。
应用到网络索引构建:
- Map任务:下载并处理一部分网页,对于网页中的每个词项,输出
(词项, 文档ID)。 - Shuffle:将所有
(词项, 文档ID)对按照词项分组,发送到对应的Reduce机器。 - Reduce任务:每台机器负责若干个词项,它将收到的所有文档ID排序合并,最终写出整个倒排索引的一部分。



通过这种方式,可以利用成百上千台机器的力量,在几天内为整个互联网构建索引。

动态索引维护:应对变化 📈

互联网是不断变化的,新网页出现,旧网页被删除或修改。因此,索引需要支持动态更新。完全重建索引(耗时数天)不现实,我们需要更精细的策略。







一种常见的方法是使用辅助索引:
- 保持一个大的、相对静态的主索引,涵盖历史数据。
- 将所有新增的文档索引到一个较小的、常驻内存的辅助索引中。
- 执行查询时,需要同时查询主索引和辅助索引,合并结果。
- 当辅助索引增长到一定大小时,将其与主索引合并,然后创建一个新的空辅助索引。
对于文档删除,直接在庞大的主索引中物理删除记录效率极低。通常的解决方案是维护一个无效位向量(或删除位图):
- 为每个文档分配一个二进制位(0表示有效,1表示已删除)。
- 当文档被删除时,只需将其对应的位标记为1。
- 查询时,从索引中获取结果后,用位向量过滤掉已标记删除的文档ID。


这种“标记删除,定期清理”的策略在许多系统(如版本控制系统Git)中都有应用。





关于辅助索引与主索引的合并策略,如果每次都将辅助索引(大小为N)合并到不断增长的主索引中,总的合并工作量会达到 O(T^2 / N),是平方级复杂度,这是不可接受的。在课间休息后,我们将揭晓如何解决这个复杂度问题。


本节课中我们一起学习了更高效的SPIMI索引构建方法,了解了利用MapReduce框架进行分布式索引构建的原理,并探讨了如何通过辅助索引和无效位向量来维护动态变化的索引。下节课我们将继续解决动态索引合并带来的复杂度挑战。
013:索引构建 (3/3) 📚



在本节课中,我们将要学习如何高效地构建大规模索引,特别是面对持续流入的新数据时。我们将介绍一种名为对数合并或LSM树的巧妙方法,它能显著改善构建效率。最后,我们会将所学知识与其他索引类型和分布式计算框架联系起来。


从低效到高效:问题的提出 🧩
上一节我们讨论了简单的周期性合并方法,其构建复杂度是二次方的,这对于大数据场景来说是完全不可接受的。
那么,我们能否做得更好?答案是肯定的。
引入对数合并(LSM树) 🌲
这里有一种巧妙的方法,它出现在许多不同的技术中,例如信息检索和宽列数据库。这种方法被称为对数合并,其数据结构称为日志结构合并树。
它专门解决了当新数据持续到达,需要不断合并时的效率问题。
以下是LSM树的核心思想:
我们从零开始,假设初始索引为空。随着新网页内容的创建,我们收集到N个倒排记录项。我们将其构建成一个标准的小型索引,并存储在磁盘上,这被称为第0层。
当内存再次被相同数量(N)的新倒排记录项填满时,我们不会创建第二个独立的索引。相反,我们会将内存中的新索引与磁盘上现有的第0层索引合并,形成一个大小为2N的更大索引,并将其作为第1层索引存回磁盘。这个过程称为压实。
随着数据持续流入,这个模式会继续下去。我们总是将内存中的数据“刷新”到第0层。当需要将第i层索引与第i+1层索引合并时,如果目标层已存在索引,则继续向上合并,每次合并都会使索引大小翻倍(形成2的幂次方大小)。
你可能会注意到,这就像在用二进制计数,也类似于游戏 2048——我们不断地合并相同大小的“方块”。
LSM树的优势分析 📈

这种结构带来了显著的改进。对于一个总共有T个记录项、内存块大小为N的系统:
- 索引数量:我们最多拥有 log₂(T/N) 个索引(即T/N以2为底的对数)。这远优于之前简单方法中的 T/N 个索引。
- 构建复杂度:构建的总体复杂度变为 O(T log (T/N)),这比二次方复杂度要好得多。
- 查询复杂度:查询时,我们需要检查所有层级的索引,因此查询时间为 O(log (T/N))。

这是一种典型的权衡:通过增加查询时的开销(需要查询多个索引),我们换来了构建时的高效性。在计算机科学中,你通常无法同时拥有“鱼与熊掌”。
技术的应用场景与扩展 🔄
值得注意的是,许多大型搜索引擎实际上会定期(例如每4小时)重建整个互联网索引或刷新部分索引。LSM树等技术在互联网规模下可能不是首选,但在公司内部数据库等场景中,它们极其有用。
此外,我们所讨论的构建大规模索引的技术(分块处理、合并)并不仅限于标准倒排索引。它同样适用于其他类型的索引,例如:
- 按词索引
- 带位置的索引
- 轮排索引
无论是单机顺序处理,还是使用MapReduce在机器集群上并行处理,核心思想都是相同的。MapReduce的map和reduce函数完全可由用户自定义,这正是其强大和通用之处。更新的技术如Apache Spark可以看作是MapReduce的演进,它更友好且支持多轮计算。
目前我们构建的索引是按文档ID排序的,这非常利于进行布尔查询。然而,这还没有解决网页排名的问题。我们返回的是一组网页,而非根据相关性排序的前10个结果。排名和安全控制是我们后续课程将要探讨的内容。


总结与展望 🎯



本节课中我们一起学习了:
- LSM树(对数合并) 的原理,它通过将索引组织成多个层级(大小为2的幂次方)来高效处理持续的数据流入。
- 其核心优势是将索引构建复杂度从二次方降低到 O(T log (T/N)),同时查询复杂度为对数级。
- 这种索引构建范式具有通用性,可应用于多种索引类型和分布式计算框架。
- 我们目前构建的是支持布尔查询的索引,真正的搜索引擎还需要排名算法来返回最相关的结果。



这对应着教科书中的第4章,建议大家结合阅读以加深理解。接下来,我们将进入下一个重要主题:索引压缩。
014:索引压缩 (1/3) 📚



在本节课中,我们将要学习索引压缩的基本概念和动机。我们会回顾之前学过的索引结构,并引入两个重要的经验法则——齐普夫定律和希普斯定律,它们描述了文本集合的统计特性,是理解压缩可能性的基础。
课程回顾 📝
上一节我们介绍了处理动态集合和高效合并索引的技术。本节中,我们来看看到目前为止课程所涵盖的全部内容。

我们至今为止关注的都是布尔查询。在这种查询中,我们有一个文档集合和一个针对某些词的查询(可能包含 AND、OR、NOT 操作),查询会返回一个文档子集,但没有特定的排序。

我们了解到,为了实现高效查询,需要构建的结构被称为标准倒排索引。其结构如下:
- 左侧是词汇表,包含所有词项,通常组织成字典结构(如B树或哈希表),以便在对数时间内访问任何词项。
- 紫色方框内是文档频率,即每个词项出现在多少个文档中。
- 蓝色方框内是文档ID,也称为倒排记录。每个蓝色方框代表一个
(词项, 文档ID)对,但由于同一行的词项相同,我们只记录文档ID。
我们学习了如何用字典结构组织词汇表,并为其添加了许多功能来处理通配符查询(包括词尾、词首和中间通配符)和拼写校正。我们还学习了如何进行短语搜索,这需要双词索引或位置索引。
位置索引在非位置倒排记录的基础上,增加了更多信息。对于每个文档ID,还会记录该词项在文档中出现的所有位置(即词项频率 tf)。例如,一个词项在文档13中出现在第4、7、11个位置,那么它的词项频率就是3。区分文档频率 df(多少文档包含该词)和词项频率 tf(在某文档中出现多少次)至关重要,因为它们在后续的文档排序中会起到核心作用。
我们还介绍了k-gram索引(例如,当k=3时是trigram索引)。其思想与处理文档中词的双词索引类似,但滑动窗口作用于词的字母上,而不是文档中的词上。这主要用于处理通配符查询和拼写校正。

在索引构建方面,我们学习了压缩技术,用于减少 (词项, 文档) 对的大小,这在BSBI(基于块的排序索引构建)等场景中非常有用,特别是当这些数据在内存中流动时。BSBI是第一种允许处理无法完全装入内存的大规模集合的技术,其核心思想是分块处理。随后我们学习了SPIMI(单遍内存索引)技术,它不再使用 (词项, 文档) 对,而是直接为每个块构建迷你倒排索引,最后进行合并。
我们还探讨了如何处理不断变化的文档集合(动态索引),这比静态集合更具挑战性。解决方案包括使用辅助索引结构来存储较新的数据,以及采用对数合并策略(而非简单的逐条合并)来高效处理周期性索引合并。
压缩的动机 💡
前面我们学习了如何扩展系统以索引更大的集合。但需要意识到,一遇到问题就急于增加机器或购买更大集群并非总是良策。许多初创公司常犯这个错误。




在考虑扩展之前,你应该花时间审视是否可以对现有代码和存储方式进行优化。例如,优化代码、减少内存占用。我的一个学生马克只用了一台8GB内存的笔记本电脑就完成了他的硕士论文。资源限制迫使他写出更高效的代码。当这些优化后的代码最终运行在亚马逊的集群上时,其性能远超未优化的版本,从而节省了资源和成本,并更快地获得了结果。事实上,对于许多项目,你最初可能根本不需要集群,单机优化就足够了。
这就是我引入本节课内容的精神:在尝试构建更大的索引之前,可以先尝试优化索引的存储空间。我们将学习压缩词典和倒排记录表的大小。这样做的直接好处是,在压缩后,内存中可以容纳更多数据,从而可以用更少、更大的分区来构建索引,使得构建过程更快。
但在开始压缩之前,我们需要了解一些统计数据,因为数据的分布特性是决定如何压缩的关键。接下来,我将介绍两个在实践中非常有效的经验法则。
文本集合的统计定律 📊
为了清晰地讨论,我们引入信息检索中常用的符号约定:
N:集合中的文档数量(例如,图书馆的藏书数,搜索引擎的网页数)。T:位置词元的总数。即所有文档中所有词出现的总次数,也反映了集合的总体大小。T和N通过平均文档长度这个常数因子相关联。M:词项的数量。即经过规范化、词形还原等处理后,标准倒排索引左侧黄色方框的数量,也就是唯一词项的数量。
我们需要探究的核心问题是:M(词项数)如何随 T(总词元数)增长?换句话说,随着向图书馆或互联网添加更多文档,我的倒排索引的“高度”会如何增长?
通过对真实数据(如维基百科)进行统计分析,并在双对数坐标下进行线性回归拟合,我们发现存在以下关系:
log(M) = b * log(T) + a
这等价于:
M = k * T^b
经验表明,指数 b 的值约为 1/2。因此:
M = k * sqrt(T)
这个关系被称为希普斯定律。它是一个经验发现,表明词项数量大约与总词元数的平方根成正比。
接下来是第二个定律。我们关注每个词项的集频率,即该词项在整个集合中出现的总次数。例如,“the”这个词可能出现数亿次,而一些生僻词可能只出现几次。
问题是:这些集频率是如何分布的?我们将所有词项按其集频率降序排列,并绘制其排名与频率的关系图。通过对真实数据的双对数坐标进行线性回归拟合,我们发现:
log(frequency) = c * log(rank) + d
其中斜率 c 约为 -1。这意味着:
frequency ∝ 1 / rank
这个关系被称为齐普夫定律。它表明,一个词项的频率与其排名成反比。
这两个定律为我们提供了关于文本数据统计特性的重要洞察,是设计压缩算法的基础。
已有技术的压缩效果 🔧
实际上,我们之前学过的一些技术已经起到了压缩作用。例如:
- 移除数字:减少了词汇表的大小。
- 大小写折叠:将不同大小写的词视为同一词项。
- 移除停用词:如“the”、“a”、“an”等。
- 词干还原:将词汇还原为其词干形式。
我们可以量化这些技术对索引不同部分的影响:
- 对词项数
M(词典大小)的影响:- 移除数字:轻微减少。
- 大小写折叠:减少约17%。
- 移除停用词:影响微乎其微(因为停用词数量相对总词项数很少)。
- 词干还原:减少约17%。
- 对非位置倒排记录数(倒排记录表存储)的影响:
- 移除停用词:效果显著(因为停用词出现频率高,关联的文档ID多)。
- 其他技术:效果较小。
- 对位置倒排记录数(位置索引存储)的影响:
- 移除停用词:效果比在非位置倒排记录中更显著。
由此可见,我们已经掌握的技术对压缩是有帮助的,但这还不够。在接下来的课程中,我们将深入探讨更多用于压缩词典和倒排记录表的高级技术。
总结 ✨

本节课中我们一起学习了索引压缩的动机和理论基础。我们回顾了标准倒排索引及其变种,引入了描述文本集合统计特性的希普斯定律和齐普夫定律,并分析了已有预处理技术(如移除停用词、词干还原)对索引不同部分的压缩效果。理解这些统计规律是设计高效压缩算法的第一步,它们告诉我们数据中存在大量可压缩的冗余信息。在接下来的课程中,我们将基于这些知识,学习具体的词典和倒排记录表压缩技术。
015:索引压缩 (2/3) 🗜️














在本节课中,我们将学习如何压缩标准倒排索引,以在尽可能小的空间中存储尽可能多的数据。我们将从压缩词典结构开始,然后探讨如何压缩倒排列表。

压缩词典 📚




上一节我们介绍了齐普夫定律和希普斯定律,它们描述了词汇表规模和词项频率的分布规律。本节中,我们来看看如何利用这些规律来压缩词典。


词典通常以B+树等数据结构存储,以支持高效的词项查找。一个典型的B+树节点包含多个键值和指向子节点的指针。

以下是压缩词典的几种技术:

- 固定宽度存储:为每个词项分配固定长度的字节(例如20字节)。这种方法简单,但会浪费空间,因为许多词项的实际长度远小于分配的长度,而极长的词项又无法存储。
- 动态分配存储:为每个词项精确分配其所需的字节数。这解决了固定宽度的问题,但需要额外的指针来记录每个词项在内存中的起始和结束位置。
- 块指针与长度编码:为了减少指针数量,我们可以每K个词项存储一个指针。对于这K个词项中的每一个,我们额外存储一个字节来表示其长度。这样,我们通过增加少量长度信息,大幅减少了指针的开销。
- 前端编码:由于词典中的词项是按字母顺序排序的,相邻词项通常有共同的前缀。前端编码技术通过存储共享前缀和剩余后缀来进一步压缩数据。例如,对于“automate”、“automatic”、“automation”,可以编码为“automate” + 特殊字符 + “tic” + 特殊字符 + “ion”。

通过综合运用这些技术,我们可以显著减少词典的存储空间。例如,在一个示例数据集中,固定宽度存储需要11.2MB,而结合动态分配、块指针和前端编码后,可以压缩到5.9MB,几乎将内存利用率提高了一倍。
然而,压缩数据通常需要CPU进行更多的计算来解压缩,这体现了计算机科学中常见的空间与时间权衡。我们需要根据实际应用场景,在存储空间和查询速度之间做出合适的折衷。


压缩倒排列表 🔢
完成词典压缩后,我们转向一个更有趣的数学问题:如何压缩倒排列表。本质上,倒排列表是一个按文档ID递增排序的整数列表。
间隙编码
由于文档ID是递增的,我们可以不存储原始ID,而是存储相邻ID之间的间隙。对于频繁出现的词项,其文档ID通常很接近,间隙值会很小,这为压缩提供了可能。
变长字节编码

直接为间隙分配固定位数(如4位)只对间隙很小的频繁词项有效。我们需要一种弹性的编码方式:小整数占用空间少,大整数可以占用更多空间。这引出了变长字节编码。
变长字节编码的核心思想是将一个整数分割成多个“包”,每个包包含若干数据位和一个续位。续位为0表示后面还有包,为1表示这是最后一个包。

以下是编码步骤:
- 将整数转换为二进制。
- 将二进制位按固定长度(如7位)分组,最后一组不足则补零。
- 为除最后一组外的所有组添加续位0,为最后一组添加续位1。






例如,使用包大小为7位(即1个续位+7个数据位):
- 编码数字
4(二进制100):补零至7位得到0000100,添加续位1,最终编码为10000100。 - 编码数字
270(二进制100001110):分组为0000010和0001110。第一组加续位0,第二组加续位1,最终编码为00000010 10001110。





这种编码是一种前缀码,就像电话号码系统一样,解码时无需额外信息就能明确每个编码的边界。我们可以将包大小作为参数,得到不同的编码方案(如UTF-8、UTF-16本质上也是参数化的变长编码)。



现在,我们可以将倒排列表中的文档ID转换为间隙,再使用变长字节编码对这些间隙进行压缩,从而高效地存储倒排列表。





总结 📝
本节课中我们一起学习了倒排索引的压缩技术。
- 对于词典,我们探讨了固定宽度、动态分配、块指针和前端编码等方法,通过利用词项排序和前缀共享特性,有效减少了存储开销。
- 对于倒排列表,我们引入了间隙编码和变长字节编码。通过存储文档ID间的间隙,并对这些间隙使用基于前缀码的弹性编码,实现了对整数列表的高效压缩。





这些压缩技术在节省大量存储空间的同时,也引入了额外的计算开销,是典型的空间换时间策略。下一节我们将继续探讨更高效的压缩算法。
016:索引压缩 (3/3) 📚

在本节课中,我们将学习如何将大型有序整数列表压缩到尽可能小的空间里,即使用尽可能少的比特位。我们将深入探讨两种关键的压缩技巧:差值编码和变长编码,并最终介绍一种近乎最优的压缩方法——Gamma编码。

回顾与目标 🎯
上一节我们介绍了如何通过编码数字之间的差值(而非数字本身)来压缩有序列表,并利用变长编码(如可变字节编码)来弹性地表示这些差值,使得小数字占用少量比特,大数字占用更多比特。
本节中,我们将探讨如何进一步优化压缩,并引入一种理论上更优的编码方案。
压缩的权衡:CPU vs. 磁盘 ⚖️
在计算机系统中,性能瓶颈通常出现在四个地方:CPU、内存、磁盘或网络。在信息检索的索引压缩场景中,瓶颈通常是磁盘I/O速度。
压缩数据可以减少从磁盘读取的比特数,从而加快读取速度。虽然解压缩需要CPU时间,但如果CPU原本处于空闲状态,那么这种“用CPU时间换取磁盘I/O时间”的权衡就是非常有益的。我们的目标是在CPU和磁盘负载之间找到一个平衡点。
可变字节编码的参数选择 🔧

可变字节编码是一种参数化编码,需要选择“数据包”的大小(例如,7位或8位)。以下是选择参数时需要考虑的两个因素:


- CPU与磁盘的平衡:如果数据包太大,压缩率不高,磁盘读取负担重;如果数据包太小,压缩率高但CPU解压负担重。需要通过实验找到最佳平衡点。
- 整数分布:如果数据中大部分是较小的整数,则应选择更激进的压缩(更小的数据包);如果包含许多大整数,则可能需要更大的数据包。
引入Gamma编码:比特级的优化 ✨







我们能否做得比可变字节编码更好?答案是肯定的。有一种称为Gamma编码的方法,它可以在比特级别进行操作,并且从信息论角度看是近乎最优的。




在介绍Gamma编码之前,我们先了解其基础组件:一元编码。
一元编码(温度计编码)
一元编码非常简单:要编码一个正整数 N,就写入 N 个 1,后面跟一个 0。
公式:
一元编码(N) = ‘1’ * N + ‘0’
例如,数字 3 的一元编码是 1110。

这种编码是前缀码,可以通过寻找比特流中的 0 来解码。虽然它看起来非常浪费空间,但对于某些特定的整数分布(如几何分布),它实际上是最优的。



Gamma编码详解 🧠




Gamma编码由Peter Elias提出,它巧妙地结合了一元编码和二进制表示。编码一个大于0的整数 x 的步骤如下:


- 将
x转换为二进制,去掉最高位的1(因为对于任何大于0的整数,二进制表示的最高位总是1)。剩下的部分称为“尾数”。 - 计算尾数的长度(比特数),记作
L。 - 使用一元编码来编码
L。这构成了编码的“前缀”部分。 - 将前缀和尾数连接起来,就得到了Gamma编码。
代码描述:
def gamma_encode(x):
if x <= 0:
raise ValueError("Gamma编码只适用于正整数")
# 步骤1: 获取二进制表示并去掉最高位的1
binary = bin(x)[3:] # bin(5) -> '0b101', [3:] 得到 '01'
# 步骤2: 计算长度L
L = len(binary)
# 步骤3: 对L进行一元编码
prefix = '1' * L + '0'
# 步骤4: 组合
return prefix + binary
# 示例:编码 5 (二进制 101)
# 尾数: ‘01’, 长度 L=2
# 前缀: ‘110’
# Gamma编码: ‘11001’




特性:
- 变长与弹性:小整数编码短,大整数编码长。
- 前缀码:无需额外分隔符即可解码。
- 总是奇数长度:编码结果总是由奇数个比特组成。
- 近似长度:Gamma编码的长度大约是原始二进制表示长度的两倍。




对于数字 0,可以通过编码 x+1 来间接支持。

信息论基础:熵与最优编码 📉


为了理解Gamma编码为何优秀,我们需要一点信息论知识。考虑一个场景:Alice需要根据一个已知的概率分布(随机变量 X)向Bob发送随机整数,她希望平均使用的比特数最少。
信息量与熵
- 信息量(自信息):观察到事件
x的“惊喜”程度。其概率P(x)越小,信息量越大。
公式:I(x) = -log₂(P(x)) - 熵(香农熵):随机变量
X的平均信息量,即编码所需平均比特数的理论下限。
公式:H(X) = Σ [ -P(x) * log₂(P(x)) ](对所有可能的x求和)


示例:
- 总是输出
0的确定性分布:熵为0,无需发送任何信息。 - 均匀分布
{0,1,2,3}:每个概率为1/4,熵为2。最优编码就是2位二进制(00,01,10,11)。 - 几何分布(概率按
1/2的幂次递减):熵也为2,但需要更智能的编码(如Gamma编码)来接近这个下限。






Gamma编码的性能评估 📊



我们可以计算特定编码方案在给定分布下的平均长度,并与熵(理论最优值)进行比较。

- 可变字节编码(8位包):对于几何分布,平均长度约为
8比特,远高于熵2。 - 一元编码:对于特定的几何分布,它恰好能达到最优(平均长度等于熵
2),但对于其他分布(如均匀分布)则性能很差。 - Gamma编码:其强大之处在于通用性。对于任何概率分布,Gamma编码的平均长度都不会超过该分布熵的
3倍。这意味着无论数据如何分布,Gamma编码都能保证接近最优的压缩效果。




总结 🏁
本节课我们一起深入学习了索引压缩的高级技术。





- 我们首先回顾了通过差值编码和变长编码来压缩有序整数列表的基本原理。
- 我们讨论了压缩中CPU时间与磁盘I/O时间的经典权衡,以及如何根据整数分布选择编码参数。
- 我们学习了作为Gamma编码基础的一元编码。
- 我们详细剖析了Gamma编码的构造方法:它结合了一元编码前缀和二进制尾数,是一种高效的前缀码。
- 为了从理论上评估编码效率,我们引入了信息论中的熵的概念,它代表了无损编码的平均比特数下限。
- 最后,我们比较了不同编码的性能。Gamma编码以其通用性脱颖而出,它能保证对于任意整数分布,其压缩效果都在理论最优值的3倍以内,是实践中非常强大和可靠的选择。

通过掌握这些编码技术,我们能够更有效地存储和检索大规模数据,在系统资源之间做出明智的权衡。
017:排序检索 (1/3)
概述
在本节课中,我们将从布尔检索模型过渡到排序检索模型。我们将学习两种主要的排序方法:基于元数据的参数化搜索,以及基于词频和文档频率的向量空间模型。排序检索的核心目标是为查询返回一个按相关性排序的文档列表,而非简单的文档子集。
课程回顾
上一节我们介绍了布尔检索及其高效实现方式。本节中,我们来看看如何对检索结果进行排序。
到目前为止,我们已经学习了以下内容:
- 布尔检索:使用 AND、OR、NOT 等操作符,基于标准倒排索引检索文档子集。
- 索引结构:字典可以使用哈希表或 B+ 树实现。倒排索引可以扩展为双词索引或位置索引以支持短语查询。
- 索引构建:介绍了 BSBI 和 SPIMI 等索引构建算法。
- 动态更新:通过辅助索引和日志结构合并树(LSM 树)来处理集合的动态更新。
- 词汇增长:Heaps‘ Law 描述了文档集合增长时,不同词项数量的增长规律。
- 索引压缩:通过差值编码和 Gamma 编码等技术,可以大幅压缩倒排索引的存储空间。
现在,我们进入本周的核心主题:排序检索。
参数化搜索与区域加权
首先,我们介绍一种基于文档元数据进行排序的方法。
在图书馆或在线书库中,文档不仅包含正文,还包含标题、作者、出版社、页数、语言等元数据。这些元数据非常有价值,可以用于构建专门的搜索界面。
将元数据查询视为关系数据库问题,可以使用 SQL 查询进行过滤。为了结合全文搜索,我们可以构建一个混合系统:既包含传统的关系型数据库索引(如 B+ 树),也包含针对文档正文的标准倒排索引。
为了对结果进行排序,我们可以进一步为不同的文本区域(如标题、摘要、正文)建立独立的倒排索引。更高效的方法是将区域信息整合到一个共享的倒排索引中。

以下是两种整合方式:
- 将区域作为词项的一部分,例如
(information, title)。 - 在倒排记录表中存储区域信息,例如
(文档ID, [区域列表])。

利用这种结构,我们可以为文档计算评分。为每个区域分配一个权重 g_i,如果词项出现在该区域,则对应的布尔值为 1,否则为 0。文档对于某个词项的得分是所有权重 g_i 在其出现区域上的和。
这实际上就是权重向量 g 和布尔向量 s 的点积:
score = g · s = Σ (g_i * s_i)
示例:
假设区域权重向量 g = [标题: 0.3, 摘要: 0.2, 正文: 0.5]。
- 文档1:词项“information”仅出现在正文中。得分 = 0.5。
- 文档4:词项“information”出现在标题和正文中。得分 = 0.3 + 0.5 = 0.8。
- 文档5:词项“information”出现在摘要和正文中。得分 = 0.2 + 0.5 = 0.7。
对所有文档计算得分后,按分数降序排序,即可得到排名结果(文档4 > 文档5 > 文档1)。
对于多词项查询,可以将每个词项在所有区域上的得分相加,得到文档的总分。这个过程可以在合并倒排列表时动态计算,称为累加评分。
与机器学习的联系:区域权重向量 g 的值可以人为指定,也可以通过机器学习方法从训练数据中学习得到。训练数据包含查询-文档对及其人工标注的相关性标签。通过最小化预测得分与真实相关性之间的误差(如最小二乘法),可以学习出最优的权重。
向量空间模型:词袋与词频
接下来,我们介绍另一种更强大的排序方法——向量空间模型。
我们首先将文档视为词袋,即忽略词序,但记录每个词项在文档中出现的次数,这称为词项频率。
词项频率定义为词项 t 在文档 d 中出现的次数,记为 tf(t, d)。我们在之前介绍的位置索引中已经见过它。
一个简单的评分想法是:将一个文档中所有词项的频率相加,总和高的文档排名靠前。
示例:
- 文档A:
tf(“foo”, A)=5,tf(“bar”, A)=0,tf(“baz”, A)=2。总分 = 7。 - 文档B:
tf(“foo”, B)=1,tf(“bar”, B)=4,tf(“baz”, B)=1。总分 = 6。
根据这个规则,文档A的排名高于文档B。
然而,这个简单方法存在缺陷。它平等对待所有词项。实际上,像“bar”这样在整体集合中出现较少的稀有词项,可能比“foo”这样常见的词项更具区分度。如果一个文档包含了稀有词项,它可能更相关。


因此,我们需要一个改进的加权方案:不仅考虑词项在文档中的频率(tf),还要考虑词项在整个集合中的稀有程度。这引出了逆文档频率的概念,我们将在下一节详细探讨。
总结
本节课中,我们一起学习了排序检索的初步概念:
- 参数化搜索:通过为文档的不同区域(如标题、正文)分配权重,并计算权重向量与词项出现向量的点积,来实现基础的文档评分和排序。这种方法可以与机器学习结合以优化权重。
- 向量空间模型基础:将文档表示为词袋,并引入词项频率作为基本的统计特征。我们认识到仅使用词频进行加权的不足,为引入逆文档频率以衡量词项重要性做好了铺垫。

下一节,我们将深入探讨向量空间模型的核心:tf-idf 加权方案及其计算。
018:排序检索 (2/3) 📚








在本节课中,我们将学习如何改进排序检索系统,通过引入词项权重来解决之前遇到的问题。我们将从简单的频率统计开始,逐步深入到向量空间模型的核心概念,并探讨其数学基础和实现思路。



概述
上一节我们介绍了基于词项频率的简单排序方法。本节中,我们将看到如何通过为词项分配权重来改进排序结果,特别是如何处理罕见词项和常见词项。我们将引入逆文档频率的概念,并最终将文档和查询都表示为向量,从而在数学框架下计算它们的相似度。
权重计算:从频率到IDF
我们之前观察到,在排序时,罕见词项“bar”没有得到足够的重视。为了解决这个问题,我们需要为词项“foo”和“bar”添加不同的权重。
一种直观的想法是使用集合频率,即一个词项在整个文档集合中出现的总次数。例如,在一个包含三个文档的集合中:
- “foo” 出现了4次。
- “bar” 出现了5次。
- “foobar” 出现了5次。
然而,集合频率有一个问题。以“foobar”为例,虽然它出现了5次,但这5次都集中在同一个文档(文档2)中。这可能意味着“foobar”是一个非常专业的术语,只出现在少数特定文档里,本质上它仍然是一个罕见词项。因此,仅用集合频率作为权重并不完全合理。
那么,我们应该使用什么频率呢?答案是文档频率。
文档频率与集合频率不同,它统计的是包含某个词项的文档数量,而不是该词项出现的总次数。在上面的例子中:
- “foo” 出现在2个文档中(文档1和2),DF=2。
- “bar” 出现在所有3个文档中,DF=3。
- “foobar” 只出现在1个文档中,DF=1。
这样,“foobar”就被正确地识别为最罕见的词项,而“bar”则是最常见的词项(可能是一个停用词)。文档频率正是我们之前在标准倒排索引的紫色框中见过的数据。


但是,对于权重而言,方向是反的:文档频率越高(词项越常见),我们希望的权重反而越低。因此,我们需要取其倒数。
我们通过以下公式进行转换和调整:
IDF(t) = log(N / DF(t))
其中,N是文档集合中的文档总数,DF(t)是词项t的文档频率。

这个公式被称为逆文档频率。取对数是为了平滑数值:当词项出现次数从0到1000时,差异巨大;但从1000到2000时,差异的重要性就降低了。对数函数可以放大小数值之间的差异,同时压缩大数值之间的差异。

TF-IDF:词项权重

现在,我们将词项频率和逆文档频率结合起来,就得到了著名的TF-IDF权重。
具体计算步骤如下:
- 计算每个词项-文档对的词项频率。
- 计算每个词项的逆文档频率。
- 将对应的TF和IDF相乘,得到TF-IDF权重。
以下是一个计算示例:
假设我们为“foo”、“bar”、“foobar”计算了IDF权重,分别为5、10、3。然后将文档-词项矩阵中的每个TF值与对应词项的IDF权重相乘。
原始TF矩阵:
文档A: [foo:5, bar:1]
文档B: [foo:0, bar:4]
文档C: [foo:2, bar:1]
IDF权重:
foo: 5, bar: 10, foobar: 3
计算TF-IDF:
文档A: [5*5=25, 1*10=10] -> 总和 35
文档B: [0*5=0, 4*10=40] -> 总和 40
文档C: [2*5=10, 1*10=10] -> 总和 20
现在,对于查询“foo bar”,我们计算其与每个文档的TF-IDF向量点积(或简单求和,这里假设查询词项权重为1),文档B的得分(40)最高,成为了排名第一的文档,这符合我们的预期。因为“bar”是罕见词项,在文档B中出现了4次,因此获得了更高的权重。
TF-IDF评分系统在信息检索中非常流行,它同时考虑了词项在文档中的局部重要性(TF)和在整个集合中的全局重要性(IDF)。
向量空间模型 🧮
虽然TF-IDF在数学上很清晰,但要实现它来处理数十亿文档和数百万词项,我们需要更高效的抽象模型。这就是向量空间模型。
在旧系统中,我们将文档视为词项的集合(布尔向量)。现在,我们使用词袋模型,并将文档视为数值向量,其中每个坐标对应一个词项,坐标值可以是TF、TF-IDF或其他权重。这样,一个文档就变成了高维空间(维度数等于词项数)中的一个点或向量。
更具体地说,我们通常使用TF-IDF值作为向量坐标。所有文档向量都位于这个高维空间的第一象限(坐标均为非负实数)。为了方便比较,我们通常将文档向量归一化为单位长度(即模长为1)。归一化意味着我们只关心向量的方向(词项之间的相对频率比例),而不关心其绝对长度。例如,将同一本书复制100遍,其归一化后的向量与原书是相同的。
归一化后的所有文档向量都位于一个单位超球面上。在这个空间中,一个点的位置主要由其角度决定。
那么,如何衡量两个文档的相似度呢?直觉告诉我们,两个向量之间的夹角越小,它们的方向越接近,内容就应该越相似。在数学上,两个归一化向量的点积恰好等于它们夹角的余弦值:
cos(θ) = (v · w) / (||v|| * ||w||)
由于向量已归一化(模长为1),点积 v · w 就直接等于余弦相似度。
这个发现非常强大:我们将文档内容的相似度比较,转化为了高维空间中向量夹角的计算。
查询即向量
更关键的是,我们的查询也可以被表示为同一个向量空间中的向量。用户输入的查询词被转换成TF-IDF向量(同样,查询向量通常非常稀疏,大部分坐标为零)。
于是,信息检索的任务变得清晰而优美:
- 将所有文档表示为归一化的TF-IDF向量。
- 将用户查询也表示为一个向量(通常不归一化,或按需处理)。
- 计算查询向量与每个文档向量的点积(即余弦相似度)。
- 按照点积分值从高到低对文档进行排序,返回最相关的文档。
在向量空间模型中,相关性排名直接对应于查询向量与文档向量夹角的余弦值。余弦值越接近1,夹角越小,文档与查询越相似,排名就越靠前。





实现挑战与思路 💻


数学模型很完美,但实现起来面临巨大挑战:我们需要计算一个查询向量与数十亿个文档向量(每个向量可能有数十万维)的点积。
幸运的是,查询向量通常是极度稀疏的,只包含少数几个非零值(对应查询中的几个词)。因此,每次点积计算只需要对这几个非零坐标进行运算,而不需要遍历所有维度。
接下来,我们需要高效地处理数十亿次这样的点积运算。这引导我们回到倒排索引的结构。

观察点积公式:
score(q, d) = Σ (TF-IDF(t, q) * TF-IDF(t, d) / ||d||)
对于查询q,我们只需要对查询中的每个词项t,找到包含t的文档d,然后累加 TF-IDF(t, q) * TF-IDF(t, d) / ||d|| 即可。这本质上是一个证据累积的过程。



为了实现它,我们需要在倒排索引中存储必要的信息:
- 在倒排列表的每个帖子(文档ID)中,存储该词项在该文档中的词项频率。
- 在词典部分(原来的紫色框,存储文档频率DF),我们可以存储
N/DF用于计算IDF。注意,这里通常不预先计算对数log(N/DF),而是将N/DF存储起来,把是否取对数的选择权留给具体的评分函数,这样更灵活且节省计算。

这样,当处理一个查询时,我们可以:
- 对于查询中的每个词项,取出其倒排列表。
- 从词典中获取该词项的
N/DF值,计算查询词项本身的权重。 - 遍历倒排列表,对于列表中的每个文档,获取其存储的TF值,结合IDF计算文档中该词项的TF-IDF,然后与查询词项的权重相乘,并累加到该文档的总分中。
- 最后,对所有文档的得分进行排序。
如何高效地管理这些中间得分并进行排序,将是下一讲的内容。
总结




本节课中,我们一起学习了排序检索的核心改进方法:
- 引入了逆文档频率的概念,用于给罕见词项更高的权重,从而形成了TF-IDF评分机制。
- 将文档和查询抽象为高维向量空间中的点,建立了向量空间模型。
- 利用向量点积(余弦相似度)来数学化地衡量查询与文档的相关性。
- 探讨了将这一数学模型与倒排索引结合以实现高效检索的基本思路。


通过将文本信息转化为数学向量,我们为复杂的信息检索问题提供了一个强大而优雅的解决方案框架。
019:排序检索 (3/3) 📚

概述
在本节课中,我们将继续学习向量空间模型,并深入探讨其实现细节。我们将了解如何高效计算文档与查询之间的相似度,以及如何通过不同的权重方案来调整检索效果。
向量空间模型回顾
上一节我们介绍了向量空间模型的基本概念,即将文档和查询表示为向量,并通过计算向量间的夹角余弦值来衡量相似度。具体来说,我们通常使用 TF-IDF 值来构建这些向量。
TF-IDF 的计算公式为:
TF-IDF(t, d) = TF(t, d) * IDF(t)
其中:
TF(t, d)是词项t在文档d中的词频。IDF(t)是词项t的逆文档频率,常用公式为log(N / df(t)),N是文档总数,df(t)是包含词项t的文档数。


相似度计算与实现
本节中我们来看看如何高效地计算查询向量与所有文档向量的内积(即标量积),这是排序的基础。
证据累积策略
计算标量积本质上是一个求和过程:对每个文档,将其向量与查询向量对应维度的乘积相加。由于索引规模庞大,我们需要高效的累积策略。以下是两种主要的实现方式:

1. 按词项累积
这种方法一次处理查询中的一个词项。
- 遍历该词项的倒排记录表。
- 对于记录表中的每个文档,计算该词项对应的
TF-IDF乘积(查询的TF-IDF× 文档的TF-IDF)。 - 将这个乘积累加到该文档对应的“分数桶”中。
- 处理完一个词项后,再处理下一个词项,继续向各文档的分数桶中累加。
2. 按文档累积
这种方法一次处理一个文档。
- 对于一个文档,找出查询中所有出现在该文档中的词项。
- 分别计算这些词项的
TF-IDF乘积,并将它们累加,得到该文档的总分数。 - 处理完一个文档后,再处理下一个文档。
两种方法在数学上是等价的,最终都会得到每个文档与查询的标量积。选择哪种策略取决于系统实现的具体优化考虑。
排序与复杂度
得到所有文档的分数后,我们需要按分数降序排列,以返回最相关的文档。


- 完全排序 的复杂度是
O(N log N),其中N是文档数量。 - 然而,用户通常只需要查看前
K个结果(例如前10个)。此时,我们可以使用优先队列(堆)。 - 构建一个包含所有文档分数的堆需要
O(N)时间。 - 从堆中提取前
K个最高分文档需要O(K log N)时间。 - 由于
K是常数,总体复杂度可以视为O(N),这比完全排序更高效。
权重方案变体 🍽️
我们之前介绍的 TF-IDF 是标准方案,但它并非唯一选择。信息检索领域有一个经典的权重方案集合,称为 SMART 表示法,它允许我们像点菜一样组合不同的权重计算方式。
SMART 表示法使用三个字母的代码来描述一个向量的权重计算规则,格式为:ddd.qqq
- 前三位
ddd代表文档向量的权重计算规则。 - 后三位
qqq代表查询向量的权重计算规则。 - 每位字母分别对应:词频处理、文档频率处理、向量归一化处理。


以下是常见的代码选项:
词频处理
n(natural): 直接使用原始词频,即weight = tf。l(logarithm): 使用对数词频,weight = 1 + log(tf),用于抑制高频词的影响。a(augmented): 使用增强词频,公式为0.5 + 0.5 * (tf / tf_max),用于平滑处理。b(boolean): 二值化,词频大于0则为1,否则为0。weight = 1 if tf > 0 else 0。
文档频率处理
n(none): 不使用文档频率,即weight = weight * 1。t(idf): 使用标准的逆文档频率,weight = weight * log(N / df)。p(probabilistic idf): 使用概率型的逆文档频率变体。
归一化处理
n(none): 不进行归一化。c(cosine): 进行余弦归一化,即向量除以其欧几里得范数(长度),这是我们之前使用的标准方法。
标准组合
最常用的标准组合是 lnc.ltc。
- 文档向量:
l(对数词频),t(逆文档频率),c(余弦归一化)。 - 查询向量:
l(对数词频),t(逆文档频率),c(余弦归一化)。
在实际系统或习题中,如果使用非标准方案,通常会给出对应的代码提示,无需记忆所有公式,但需要理解标准方案并能根据代码查表计算。


总结
本节课我们一起学习了向量空间模型的具体实现和扩展。
- 我们探讨了计算文档-查询相似度的两种证据累积策略:按词项累积和按文档累积。
- 我们了解了使用优先队列高效获取前K个结果,其复杂度可降至
O(N)。 - 我们介绍了 SMART 权重表示法,知道了除了标准的 TF-IDF 外,还存在多种对词频、文档频率和归一化的变体处理方式,这些变体可以通过特定的字母代码进行组合和配置。
020:优化 (1/2) 🚀
在本节课中,我们将学习如何优化向量空间模型的计算效率。我们将探讨如何利用向量数据库和SQL来简化向量运算,并介绍一系列用于加速相关性排序的实用技巧。
向量数据库与SQL扩展
上一节我们介绍了基于向量和点积的检索模型。本节中我们来看看如何利用数据库技术来高效地处理这些向量运算。

随着大语言模型(LLM)和ChatGPT的兴起,向量计算变得极其重要。因此,数据库工程师设计了专门的向量数据库,就像为表格、树、图设计数据库一样。





以下是一个使用PostgreSQL扩展创建向量表的SQL示例。它创建了一个表,其中一列可以存储向量。
CREATE TABLE docs (
id INTEGER,
tfidf_vector VECTOR(10000)
);



这段代码创建了一个包含两列的表:id是文档编号,tfidf_vector是一个包含10000个分量的向量,每个分量对应一个词的TF-IDF值。可以将其理解为每一行存储一个文档的TF-IDF向量。
将向量存入SQL表后,查询变得非常有趣,因为向量扩展提供了额外的运算符。




使用SQL进行向量距离计算
存储向量后,我们可以使用SQL直接计算向量之间的距离,例如查找与查询向量最接近的文档。
以下是计算欧几里得距离(L2距离)的查询示例:
SELECT id FROM docs
ORDER BY tfidf_vector <-> '[0.1, 0.2, ...]'::vector
LIMIT 10;
运算符 <-> 用于计算L2距离。然而,我们之前课程中使用的相似度度量是余弦相似度。
计算余弦距离的运算符略有不同:
SELECT id FROM docs
ORDER BY tfidf_vector <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
运算符 <=> 用于计算余弦距离,它衡量的是向量之间的夹角。
此外,系统还支持其他距离度量方式:
- 内积:使用
#<运算符。 - L1距离:使用
<+>运算符,计算各分量绝对值的和。 - 汉明距离等。
使用SQL的好处在于,开发者无需关心底层实现细节,数据库系统会对其进行高度优化,从而快速完成点积等计算。
相关性分数计算的优化技巧
尽管SQL接口很简单,但其背后是大量的优化工作。现在,我们来回顾并优化之前介绍的相关性分数计算公式。
初始的分数计算公式为:
score(q, d) = Σ (tf_t,q * idf_t² * tf_t,d) / (|q| * |d|)
以下是我们可以进行的一系列优化步骤:
1. 消除查询向量长度
因为查询向量长度 |q| 对所有文档都是一个常数,除以它不会改变文档的排序顺序,所以可以直接移除。
2. 延迟处理文档向量长度
我们不需要在累加每个词的贡献时都除以文档长度 |d|。可以先完成所有词的累加,最后再为每个文档统一除以一次其长度。
3. 惰性创建累加器
我们只需要为那些至少包含一个查询词的文档创建“桶”(累加器)。对于不包含任何查询词的文档,可以完全忽略。
4. 简化查询词项权重
在大多数搜索场景中,用户输入的查询词不会重复。因此,可以假设查询中的词项频率 tf_t,q 非0即1。当它为1时,可以直接累加;为0时则跳过。这简化了计算。
5. 合并逆文档频率
公式中,逆文档频率 idf_t 在查询侧和文档侧各出现一次,是冗余的。我们可以调整“SMART”记法中的查询权重,只保留文档侧的 idf_t。
经过这些优化,我们需要计算的核心简化为:
score(q, d) = Σ (idf_t * tf_t,d) / |d|
即对每个文档,我们累加其包含的所有查询词的 TF-IDF 值,最后再除以文档长度。
近似检索与预选策略
除了公式优化,我们还可以通过近似检索来大幅提升效率,即不计算所有文档的精确分数。
1. 忽略停用词
停用词(如“and”、“the”)的IDF值很低,对最终分数贡献微乎其微,但其倒排记录表非常长。忽略它们可以避免大量不必要的累加操作。

2. 文档预选
我们不必为集合中的每个文档计算分数。可以只预选那些可能相关的文档(例如,包含较多查询词的文档)进行计算。这被称为近似Top-K检索。




由于TF-IDF模型本身就是对现实世界相关性的一种近似,因此这种近似检索通常是可接受的。



高级优化技巧工具箱


以下是业界常用的一些高级优化技巧:


冠军列表法
为每个词的倒排记录表,不按文档ID排序,而是按词项频率 tf_t,d 降序排列。我们只保留每个词项频率最高的前 R 个文档(即“冠军”),并仅在这些文档中计算分数。参数 R 可以根据词的稀有度动态调整。
但这种方法带来一个问题:不同词的“冠军列表”文档顺序不同,导致无法使用之前高效的合并算法。解决方案是同时维护按文档ID排序的原始列表和按词频排序的冠军列表,查询时使用后者进行预选。
联合排序法
将词按IDF降序排列(稀有词在前),将文档按词频降序排列。这样,高IDF词和高词频文档的交集区域(矩阵左上角)包含了贡献最大的TF-IDF值。我们可以优先计算这个区域的点积,并设定阈值提前终止计算,而不必遍历所有词和文档。
这种方法通常采用按词遍历的方式来实现累加。
分层索引法
将每个词的文档列表分为多个层级(例如:高频层、中频层、低频层)。查询时,首先只在最高频层中搜索。如果返回的文档数量不足(例如不足10个),则扩展到下一层继续搜索,以此类推。这样可以保证在多数简单查询中速度极快,同时对复杂查询也有兜底策略。
聚类法
利用机器学习中的聚类技术。预先将所有文档向量聚类,并选出每个簇的“代表”点。对于一个新的查询向量,首先找到它所属的簇(即距离最近的“代表”点),然后只与该簇内的文档计算相似度。这相当于将搜索范围缩小到了一个相关文档子集。

本节课中我们一起学习了信息检索系统的核心优化策略。我们了解了如何利用向量数据库和SQL来简化向量运算,并深入探讨了从公式简化、近似检索到冠军列表、分层索引等多种实用优化技巧。这些方法共同作用,使得大规模搜索引擎能够快速响应用户查询。下一节,我们将继续探讨更多优化技术及其实现细节。
021:优化 (2/2) 🚀

在本节课中,我们将学习如何为向量数据构建高效的索引,并回顾如何将本学期所学的所有组件组合成一个完整的信息检索系统。我们还将简要探讨向量表示在大型语言模型(LLM)中的新应用。
向量索引:让SQL查询变得飞快 🗂️


上一节我们介绍了多种优化技巧。本节中我们来看看,工程师们如何将这些技巧封装起来,使得开发者无需关心底层细节,只需编写简单的SQL语句就能获得极快的查询速度。

其核心是为向量数据创建索引。就像在关系型数据库中为列创建索引(如哈希索引、B树索引)可以加速查询一样,我们也可以为向量创建专门的索引。



以下是在SQL中创建向量索引的示例代码:
CREATE INDEX my_index ON my_table USING ivfflat (my_vector_column) WITH (distance_function = 'l2');
或者使用另一种索引:
CREATE INDEX my_index ON my_table USING hnsw (my_vector_column) WITH (distance_function = 'l2');
创建索引后,之前提到的向量相似度查询(例如 SELECT ... WHERE distance < ...)将会变得非常快速。



两种核心向量索引原理






以下是两种常见向量索引的高层原理介绍。

IVF Flat 索引:基于聚类的思想 🧩
IVF Flat(倒排文件与扁平压缩)索引的核心思想与我们之前讨论的聚类密切相关。





其工作原理是:
- 将所有文档向量通过聚类算法分成多个组(簇)。
- 当一个新的查询向量到来时,系统首先确定它属于哪个(或哪几个)簇。
- 然后,只在该簇内部的向量中进行精确的相似度计算(如计算点积或距离)。




这种方法大大减少了需要直接比较的向量数量,从而提升了查询效率。简而言之,IVF Flat 利用了聚类思想来缩小搜索范围。



HNSW 索引:跳表指针的泛化 🛣️
HNSW(分层可导航小世界)索引可以看作是我们在倒排索引中学过的跳表指针思想在向量空间中的泛化和多层扩展。
想象一个多层的交通网络:
- 底层:包含所有向量,如同所有地方道路,需要缓慢遍历。
- 中间层:如同高速公路,连接点更少,可以快速跨越长距离。
- 顶层:如同航线,连接关键枢纽,能以最快速度到达大致区域。


HNSW 的工作方式如下:
- 查询从顶层开始,在稀疏连接的网络中快速导航,接近目标区域。
- 然后下降到下一层,在更密集的连接中进一步细化搜索。
- 最终到达底层,在局部范围内找到最相似的向量。

这个过程就像乘坐飞机到大城市,再换乘火车到附近城镇,最后骑自行车到达目的地。上层追求速度但召回率较低(找到的结果少),下层则进行精细搜索,召回率高。





总结一下:IVF Flat 与聚类相关,而 HNSW 是跳表指针思想在向量数据上的多层应用。
构建完整的信息检索系统 🏗️




现在,我们已经学习了足够多的组件,可以将它们组装成一个完整的信息检索系统。让我们从架构层面俯瞰整个流程。
一个完整系统的架构通常包含以下环节:


1. 文档处理与索引构建
流程始于读取原始文档。以下是处理步骤:
- 语言预处理:对文档进行分词、去除停用词、词干提取或词形还原等操作,将文本转化为词元。
- 构建索引:根据系统需求,可以构建一种或多种索引。我们学过的索引包括:
- 标准倒排索引
- 双词索引
- K-gram索引
- 轮排索引
- 域索引
- 位置索引
- 用于近似Top-K检索的索引


2. 查询处理与执行
当用户提交查询时,系统会进行以下操作:
- 查询解析与扩展:可能包括拼写纠正(利用轮排索引、K-gram或Soundex算法)。
- 查询索引:根据查询类型,系统可能会查询一个或多个索引。
- 支持的查询类型:系统可以处理多种查询:
- 布尔查询(使用交集、并集算法)
- 通配符查询
- 短语查询(利用双词索引或位置索引)
- 类似图书馆系统的结构化字段查询(如标题、作者)
- 向量空间模型查询(计算点积或余弦相似度)



3. 结果生成与展示
获取候选文档后,系统需要生成最终结果:
- 评分与排序:对于布尔模型,返回匹配的文档集;对于向量空间模型,使用如
score(q, d) = Σ (TF-IDF(t, d) * TF-IDF(t, q))的公式进行累积评分并排序。 - 结果展示:为了友好地展示结果,系统通常需要:
- 文档缓存:存储文档原始内容,用于生成摘要片段(Snippet),就像谷歌搜索中显示关键词周围文本一样。
- 参数化与机器学习:评分中的权重等参数可以通过机器学习模型进行优化和调整,这超出了本课程范围,但指出了系统可扩展的方向。

通过串联以上所有组件,我们就得到了一个功能完整的信息检索系统架构。当然,构建这样的系统需要大量工程实践,本课程的编程练习旨在让大家逐一接触这些核心环节。



延伸:从文档向量到词嵌入(Embedding) 🤖

在本课程中,我们一直将向量视为文档或查询的表示(例如TF-IDF向量)。但在当今的大型语言模型中,向量的含义得到了扩展。
这种扩展的概念称为词嵌入。在LLM中:
- 向量表示的是词、短语或更广泛概念的“语义”。
- 语义相近的词语在向量空间中位置接近。
- 更强大的是,向量空间中的方向也可以蕴含语义关系(例如,“国王”-“男人”+“女人”≈“女王”)。
尽管解释不同,但底层技术——向量表示、相似度计算、索引检索——与我们学习的向量空间模型一脉相承。如果你对此感兴趣,可以进一步学习LLM相关的课程。
总结



本节课中我们一起学习了:
- 向量索引:了解了IVF Flat和HNSW这两种索引的高层思想,它们让向量查询在数据库层面得以高效执行。
- 系统集成:回顾了如何将语言预处理、多种索引、查询处理和结果排序等模块组合成一个完整的信息检索系统架构。
- 前沿联系:简要了解了词嵌入概念,看到了经典信息检索技术与当前大语言模型在向量表示上的联系。



接下来,我们将进入新的部分,探讨概率信息检索和语言模型。
022:评估 (1/2) 📊
在本节课中,我们将要学习如何评估信息检索系统的性能。我们已经了解了布尔检索和向量空间模型,现在需要一种客观的方法来衡量系统是否能返回用户真正需要的相关文档。评估是优化系统、做出设计决策的关键。
从布尔检索到排序检索 🔄
上一节我们介绍了布尔检索模型,其中文档被视为词的集合,查询返回一个无序的文档子集,结果只有“是”或“否”。

我们使用标准的倒排索引来处理这类查询。索引的左侧是词汇表(词典),可以使用哈希表或B+树等数据结构来高效查找词项。

然而,在之前的课程中我们看到,我们可以将文档建模为“词袋”,用非负数值向量而非布尔向量来表示文档。这样,文档就存在于一个非常高维(例如50万维)的向量空间第一象限中。这种模型允许我们对输出结果进行排序,并返回前10个结果进行分页展示。
为什么需要评估? ❓
构建了这样的系统后,我们需要判断其好坏。系统是否能为用户返回有意义的结果?这需要测试。
一个检索系统有许多可调整的参数,例如:
- 是否使用停用词?
- 是否排除数字?
- 是否使用“冠军列表”?如果使用,列表长度多少最优?
- 是否使用词干还原或词形归并?
- 在后期处理中,是否对查询词项加权?
- 应该使用哪种“SMART”表示法(那六个字母)?如何优化?
- 是否进行归一化?使用L1还是L2范数?
- 对于查询权重和文档权重,应该使用哪种权重方案?
为了做出这些决策,我们必须进行量化,需要有客观的方法来评估。我们系统的核心目标是优化相关性,即给予用户他们寻找的相关文档。

评估方法与数据集 📁
为了评估系统,我们需要建立评估方法。这类工作大约始于20世纪60年代,与早期计算机化信息检索系统的发明同期。研究人员通过会议讨论如何构建和评估系统。
他们提出的方法是创建评估数据集。构建这样的数据集需要真实用户(通常是领域专家)的参与。具体流程是:准备一组文档和一组信息需求,然后请专家判断每个文档对于每个信息需求是否相关,得到“是”或“否”的判断。
早期著名的数据集包括Cranfield数据集(约1400个文档,225个信息需求)和更大的TREC数据集(近200万个文档,450个信息需求)。构建这些数据集非常耗时,因为需要人工进行相关性判断。

信息需求 vs. 查询 💡

这里需要再次强调信息需求和查询的区别。
- 查询是用户输入到搜索框中的文本(例如“ETH Zurich”)。
- 信息需求是用户脑海中想要寻找的东西(例如,Alice想为女儿找高等教育机构,Bob想找苏黎世的以太坊加密货币信息)。
一个查询可能对应不同的信息需求,而一个信息需求也可能通过不同的查询来表达。因此,一个在数学上完全正确执行查询的系统,未必能满足用户真正的信息需求。评估的核心是衡量系统满足信息需求的能力。
核心评估指标:精确率与召回率 ⚖️
当系统返回结果时,有些结果与用户的信息需求相关(相关结果),有些则不相关(不相关结果)。同时,数据集中还存在未被系统返回的相关文档。
以下是评估中常用的核心指标:
精确率衡量返回结果中相关文档的比例。
精确率 = 返回的相关文档数 / 返回的总文档数
召回率衡量所有相关文档中被系统返回的比例。
召回率 = 返回的相关文档数 / 数据集中所有相关文档数
通常,精确率和召回率之间存在权衡:
- 一个追求高精确率的系统可能只返回它非常确信的文档,但会漏掉一些相关文档(召回率低)。
- 一个追求高召回率的系统可能返回大量文档以确保覆盖所有相关项,但其中会混杂许多不相关文档(精确率低)。
我们需要根据具体应用场景在两者之间找到平衡。
其他评估指标 📈

除了精确率和召回率,还有其他指标:

特异度衡量系统避免返回不相关文档的能力,可以看作是“不相关文档的召回率”。
特异度 = 未返回的不相关文档数 / 数据集中所有不相关文档数
准确率衡量所有判断(返回与不返回)中正确的比例。
准确率 = (返回的相关文档数 + 未返回的不相关文档数) / 总文档数
但准确率在信息检索中可能具有误导性。例如,一个什么都不返回的系统会有很高的准确率(因为大部分不相关文档都没返回),但这显然不是一个好系统。同样,一个返回所有文档的系统召回率为100%,但精确率极低。
结合精确率与召回率:F值 ➕
由于需要同时考虑精确率和召回率,我们使用一个结合两者的指标:F值(F-measure)。它本质上是精确率和召回率的加权调和平均数。
调和平均数的公式为:
F = 2 * (精确率 * 召回率) / (精确率 + 召回率)
更通用的加权形式(Fβ值)允许调整对精确率或召回率的偏重:
Fβ = (1 + β²) * (精确率 * 召回率) / (β² * 精确率 + 召回率)




当β=1时,即为F1值,是精确率和召回率的调和平均数。调和平均数的特点是,如果两者中任何一个值很低,都会显著拉低整体分数,这符合我们的评估意图。




排序检索系统的评估 📊➡️




对于返回排序列表的检索系统,评估方式需要调整。我们可以模拟用户逐条查看结果的过程,并绘制精确率-召回率曲线。
绘制方法如下:
- 从排名第一的结果开始。
- 每查看一个新结果,就根据当前已看到的所有结果,计算此时的精确率和召回率。
- 在图上标出该点(召回率为X轴,精确率为Y轴)。
- 继续查看下一个结果,重复步骤2和3,直到看完所有相关文档或整个列表。



这样会得到一条曲线。通常,随着看到的文档增多(召回率增加),精确率会呈下降趋势。一个优秀的系统曲线会尽可能维持在较高位置。


为了标准化比较,通常采用11点插值平均精确率:在召回率分别为0.0, 0.1, 0.2, ..., 1.0的11个点上,取对应精确率的平均值(对于未达到的召回率点,使用其后续达到的最高精确率进行插值)。这提供了一个单一数值来概括系统在排序检索中的性能。

总结 🎯







本节课我们一起学习了信息检索系统评估的基础知识。我们首先理解了评估的必要性,以及如何通过构建带有人工相关性判断的数据集来进行评估。我们重点区分了信息需求与查询的关键概念。


接着,我们深入学习了核心评估指标:精确率、召回率及其权衡关系,并了解了特异度、准确率等其他指标及其局限性。为了综合衡量性能,我们引入了F值作为精确率和召回率的调和平均。



最后,我们将这些概念扩展到排序检索场景,学习了如何绘制和分析精确率-召回率曲线,以及如何使用11点插值平均精确率来量化排序系统的整体性能。这些评估工具是我们优化和比较不同检索算法的基础。
023:评估 (2/2) 📊



在本节课中,我们将继续学习信息检索系统的评估方法。我们将深入探讨如何评估排序检索系统的性能,并介绍几种关键的评估指标和曲线。




上一节我们介绍了精确率和召回率的基本概念。本节中,我们来看看如何将这些概念应用于返回排序结果的检索系统,例如向量空间模型。








排序检索的评估



在向量空间模型中,系统会返回前K个最相关的文档(例如前10个)。为了评估其性能,我们需要随着用户不断点击“下一页”来查看更多结果,并持续计算当前的精确率和召回率。


以下是评估过程的步骤:
- 系统返回排序后的结果列表。
- 用户从第一个结果开始查看。
- 每查看一个新结果(或一组结果),就计算一次当前的精确率和召回率。
- 将计算出的点连接起来,形成一条评估曲线。




这条曲线通常不平滑。为了得到更标准的表示,我们进行“平滑”处理:对于每个召回率水平(如0.0, 0.1, 0.2, ..., 1.0),取该点之后所有精确率的最大值作为该点的精确率。这样就得到了11点插值平均精确率曲线。
平均精确率与均值平均精确率
为了用一个单一数值来概括系统的性能,我们可以计算曲线下的面积,这被称为平均精确率。
公式:
AP = Σ (P(k) * rel(k)) / (总相关文档数)
其中,P(k)是在查看第k个文档时的精确率,rel(k)是一个指示函数,当第k个文档相关时为1,否则为0。



由于评估通常涉及多个查询,我们需要对多个查询的性能进行平均。这就引出了均值平均精确率。
公式:
MAP = (Σ AP_q) / Q
其中,AP_q是对于查询q的平均精确率,Q是查询的总数。
MAP是学术界广泛使用的核心评估指标。
R精确率
另一种简化的评估方法是R精确率。其思想是:对于一个查询,如果已知总共有R篇相关文档,那么只考察系统返回的前R篇结果,并计算此时的精确率。
有趣的是,当考察的结果数量恰好等于相关文档总数R时,此时的精确率与召回率在数值上相等。
在评估曲线上,R精确率对应于精确率-召回率曲线与直线y = x的交点。



ROC曲线

接收者操作特征曲线是另一种评估二元分类器(可类比检索系统)性能的工具。它描绘了在不同判定阈值下,真正例率(即召回率)与假正例率之间的关系。







公式:
- 真正例率
TPR = TP / (TP + FN)(即召回率) - 假正例率
FPR = FP / (FP + TN)
一个理想的系统,其ROC曲线应尽量靠近左上角(高TPR,低FPR)。对角线表示一个随机猜测的系统,而曲线如果位于对角线下方,则意味着系统性能比随机猜测还差;但值得注意的是,这样的系统其实可以通过反转其预测结果(将相关视为不相关,反之亦然)来变成一个有效的系统。
其他评估考量
除了检索质量,评估一个信息检索系统还需考虑其他维度:
- 索引构建速度:构建整个索引需要多长时间。
- 搜索速度/延迟:用户提交查询后,得到结果需要多长时间。对于网页搜索引擎,通常要求在毫秒级。
- 查询语言表达能力:系统支持何种查询(如布尔查询、短语查询、自然语言查询等)。
- 可扩展性:系统能处理多大尺寸的文档集合。




本节课中我们一起学习了评估排序检索系统的核心方法。我们掌握了如何通过精确率-召回率曲线、平均精确率、MAP、R精确率和ROC曲线来量化系统的性能。理解这些指标对于设计和改进任何信息检索系统都至关重要。从下节课开始,我们将进入更高级的主题:概率信息检索。
024:概率信息检索 (1/4) 🎯
在本节课中,我们将学习概率信息检索的基础知识。这是一种基于概率论和贝叶斯规则的检索模型,旨在更精确地估计文档与查询的相关性。我们将从回顾概率论的基本概念开始,逐步构建概率检索模型。
上一节我们回顾了布尔检索和向量空间模型等传统检索系统。本节中,我们将正式进入概率信息检索的领域,首先需要回顾一些核心的概率论知识。
概率论基础回顾 📊
概率论为我们提供了一套量化不确定性的数学框架。在信息检索中,我们可以利用它来估计一个文档与用户查询相关的“可能性”。
样本空间与事件


我们首先定义一个样本空间(通常记作 Ω)。它包含了所有可能的基本结果或“可能世界”。样本空间中的每个元素 ω 称为一个基本事件。

概率函数 P 为每个基本事件分配一个非负的权重,并且所有权重之和为1。这表示每个基本事件发生的可能性。

[
P: \Omega \rightarrow [0, 1] \quad \text{且} \quad \sum_{\omega \in \Omega} P(\omega) = 1
]
事件是样本空间的子集,即某些基本事件的集合。事件 E 的概率是其包含的所有基本事件概率之和。
[
P(E) = \sum_{\omega \in E} P(\omega)
]
事件运算与规则
以下是关于事件的一些基本运算和规则。
- 补事件:事件 E 不发生的概率是其补集的概率。
[
P(\neg E) = 1 - P(E)
] - 赔率:事件 E 发生的赔率是其概率与补事件概率的比值。
[
O(E) = \frac{P(E)}{P(\neg E)} = \frac{P(E)}{1 - P(E)}
] - 事件并集:两个事件 E 和 F 至少有一个发生的概率。
[
P(E \cup F) = P(E) + P(F) - P(E \cap F)
] - 互斥事件:如果两个事件不能同时发生(即 E ∩ F = ∅),则它们的并集概率可以简化为概率之和。
[
P(E \cup F) = P(E) + P(F)
] - 分割规则:任何事件 E 都可以被另一个事件 F 分割成两部分。
[
P(E) = P(E \cap F) + P(E \cap \neg F)
]
条件概率与独立性
当我们获得新信息时,需要更新对事件发生可能性的估计,这就引入了条件概率。
事件 E 在已知事件 F 发生条件下的概率,称为条件概率,计算公式如下:
[
P(E|F) = \frac{P(E \cap F)}{P(F)}
]
这可以理解为,在事件 F 发生的“世界”中,重新归一化后事件 E 发生的比例。
将上述公式变形,我们得到链式法则:
[
P(E \cap F) = P(F) \cdot P(E|F)
]
这个法则可以推广到多个事件。
如果事件 F 的发生与否不影响事件 E 发生的概率,则称 E 和 F 是独立的。数学上表示为:
[
P(E|F) = P(E)
]
结合链式法则,独立性意味着两个事件同时发生的概率等于各自概率的乘积:
[
P(E \cap F) = P(E) \cdot P(F)
]
上一节我们回顾了条件概率和链式法则。本节中,我们将看到如何利用它们来解决一个经典问题,并引出概率检索的核心工具——贝叶斯规则。
贝叶斯规则:概率的“翻转” 🔄
贝叶斯规则是概率论中一个强大且直观的工具,它允许我们根据新的证据来更新假设的概率。
考虑一个具体问题:
- 事件 R:今天下雨。
- 事件 U:我今天带了伞。
- 已知:
P(U|R) = 0.5(下雨时,我有一半概率忘带伞),P(R) = 0.5(每两天下一场雨)。 - 问题:如果今天我带了伞(U 发生),那么下雨(R)的概率是多少?即求
P(R|U)。
直接计算 P(R|U) 很困难,但我们很容易知道 P(U|R)。贝叶斯规则提供了在这两者之间转换的桥梁。

公式推导
根据链式法则,事件 R 和 U 同时发生的概率有两种等价的表达方式:
[
P(R \cap U) = P(R) \cdot P(U|R)
]
[
P(R \cap U) = P(U) \cdot P(R|U)
]
令两者相等:
[
P(U) \cdot P(R|U) = P(R) \cdot P(U|R)
]
由此得到贝叶斯规则:
[
P(R|U) = \frac{P(R) \cdot P(U|R)}{P(U)}
]
其中,P(R) 称为先验概率(在知道带伞证据前的估计),P(U|R) 称为似然(假设下雨时,观察到带伞的可能性),P(U) 是证据(观察到带伞)的总概率,P(R|U) 称为后验概率(在获得带伞证据后,对下雨概率的更新估计)。

在信息检索中的应用

在信息检索的语境下:
- 事件 R 可以类比为“文档 D 与查询 Q 相关”。
- 事件 U 可以类比为“我们在文档 D 中观察到了查询词 T”。
- 目标:计算
P(相关 | 文档特征),即在观察到文档包含某些词后,该文档与查询相关的概率。

贝叶斯规则使我们能够利用相对容易统计的数据(如词项在相关/不相关文档中的分布),来估算我们真正关心的、但难以直接获取的相关性概率。这正是概率检索模型的核心思想。

本节课中我们一起学习了概率信息检索的数学基础。我们从回顾样本空间、事件、条件概率等核心概念开始,重点推导并理解了贝叶斯规则及其直观含义——如何利用新证据(如文档内容)来更新我们对假设(如文档相关性)的信念。下一节,我们将具体探讨如何将这些概率原理应用于文档排序的实际问题中。
025:概率信息检索 (2/4)
在本节课中,我们将学习概率信息检索的核心思想,并利用贝叶斯定理和随机变量的概念,构建一个理论上可行的检索系统模型。我们会从简单的例子入手,逐步推导出用于文档排序的关键公式。
回顾:贝叶斯定理与先验概率
上一节我们介绍了概率论的基础。本节中我们来看看如何应用贝叶斯定理进行推理。
我们想知道“下雨”的概率,条件是“我带了伞”。这可以用贝叶斯定理来表达:
公式:
P(Rain | Umbrella) = [P(Umbrella | Rain) * P(Rain)] / P(Umbrella)


现在只需要计算这三个数值:
P(Umbrella | Rain)是下雨时我带伞的概率,假设为0.95。P(Rain)是下雨的先验概率,假设为0.5。P(Umbrella)是我带伞的总概率。假设统计一年有271天带伞,则概率为271/365 ≈ 0.742。
代入计算:
P(Rain | Umbrella) = (0.95 * 0.5) / 0.742 ≈ 0.64
这意味着,在知道我带了伞这个新信息后,“下雨”的概率从先验的 0.5 更新为了后验的 0.64。这种从先验概率开始,结合新证据更新为后验概率的推理方式,称为贝叶斯推理。

贝叶斯公式的威力在于可以估计那些无法直接测量的事物。例如,著名的德雷克公式用于估算银河系内可能存在的外星文明数量,它就是一连串概率的乘积,是链式法则的典型应用。
核心概念:随机变量

理解了概率更新后,我们需要一个关键工具来描述文档和查询,这就是随机变量。
随机变量既不是“随机”的,也不是传统意义上的“变量”。它是一个确定的函数,将样本空间 Ω 中的每一个基本事件(或结果),映射到一个我们感兴趣的值的集合上。

图示解释:
假设样本空间有一些基本事件,随机变量 X 是一个函数,它将每个事件映射到一个形状集合 {蓝色方块, 红色五边形, 绿色三角形} 中的某个形状。
通过这个函数,我们可以将样本空间上的概率“转移”到形状集合上。某个形状的概率,等于所有映射到该形状的基本事件的概率之和。
重要提示:
永远要记住,随机变量背后总有一个样本空间 Ω。在表示概率时,务必明确写出随机变量。建议使用 P(X = x) 的清晰形式,避免使用容易引起混淆的 P(x)。
我们可以同时定义多个随机变量,例如 X 和 Y,并讨论它们的联合概率 P(X=x, Y=y)。同样,条件概率定义为:
P(X=x | Y=y) = P(X=x, Y=y) / P(Y=y)

构建概率检索模型
现在,我们将利用随机变量为信息检索系统建立一个概率模型。
理想实验:南极洲的“小精灵”基地
想象一个理想实验:在南极洲有一个基地,里面有无数“小精灵”。每个小精灵面前有一台电脑,屏幕上显示一个文档和一个查询。小精灵是领域专家,他的任务是根据文档是否满足查询的信息需求,按下“相关”(1) 或“不相关”(0) 的按钮。
每个小精灵代表一个基本事件。我们可以定义三个随机变量:
- D:一个函数,将每个小精灵映射到他看到的文档。
- Q:一个函数,将每个小精灵映射到他看到的查询。
- R:一个函数,将每个小精灵映射到他按下的按钮(1 或 0)。
在这个设定下,P(R=1 | D=d, Q=q) 就有了明确的含义:对于所有看到文档 d 和查询 q 的小精灵,其中按下“相关”按钮的比例。这个值衡量了文档 d 对查询 q 的相关性概率。
从理想模型到实际系统
如果这个“小精灵”基地真的存在,我们就可以构建一个完美的搜索引擎:
- 用户输入查询 q。
- 对于网络上的每一个文档 d,计算概率
P(R=1 | D=d, Q=q)。 - 按照这个概率值从高到低对所有文档进行排序,并返回给用户。

这就实现了一个概率排序检索系统。我们也可以设定一个阈值(如 0.5),实现一个布尔检索系统:只返回那些多数小精灵认为相关的文档。

当然,建造这个基地是不现实的。但我们可以把它作为一个思想实验,进行数学推导,目标是得到一个不需要小精灵也能计算的、近似的公式。
关键推导与独立性假设
为了推导出可计算的公式,我们需要对文档进行建模并引入一些假设。
文档的表示:词项集合
我们将文档和查询表示为词项集合(忽略词频)。对于一个固定的词项顺序,文档可以表示为一个布尔向量,其中每一维对应一个词项,1 表示词项出现在文档中,0 表示未出现。

因此,文档随机变量 D 可以分解为一组布尔随机变量 D1, D2, ..., Dt,其中 D_k = 1 表示文档包含第 k 个词项。


二元独立性假设 (BIM)
现在,我们做出一个核心假设:在给定查询 Q 和相关性 R 的条件下,文档中各个词项的出现与否是相互独立的。这称为二元独立性假设。

这意味着,一个特定文档 d 出现的概率,可以分解为其各个词项出现与否的概率的乘积:
公式:
P(D=d | R=r, Q=q) = Π<sub>k</sub> P(D_k = d_k | R=r, Q=q)
其中 d_k 是文档向量在第 k 维的值(0 或 1)。
应用贝叶斯定理进行变换
我们的目标是计算 P(R=1 | D=d, Q=q) 用于排序。直接计算它很困难,但我们可以使用贝叶斯定理进行变换:
P(R=1 | D=d, Q=q) = [P(D=d | R=1, Q=q) * P(R=1 | Q=q)] / P(D=d | Q=q)
同样地,对于 R=0 也有:
P(R=0 | D=d, Q=q) = [P(D=d | R=0, Q=q) * P(R=0 | Q=q)] / P(D=d | Q=q)


简化排序依据:胜率 (Odds)
由于排序只需要相对顺序,我们可以计算相关与不相关的胜率,并对其取对数简化计算:
O(R=1 | D=d, Q=q) = P(R=1 | D=d, Q=q) / P(R=0 | D=d, Q=q)
将贝叶斯公式代入,分母 P(D=d | Q=q) 被消去,得到:
O(R=1 | D=d, Q=q) = [P(D=d | R=1, Q=q) / P(D=d | R=0, Q=q)] * O(R=1 | Q=q)
代入独立性假设
现在,将二元独立性假设代入上式中的概率比值部分:
P(D=d | R=1, Q=q) / P(D=d | R=0, Q=q) = Π<sub>k</sub> [P(D_k = d_k | R=1, Q=q) / P(D_k = d_k | R=0, Q=q)]
最终,我们得到用于排序的公式。排序时,我们可以忽略与具体文档 d 无关的常数项 O(R=1 | Q=q),专注于计算每个词项贡献的乘积(实践中常取对数变求和)。
核心排序公式(取对数后):
Score(d, q) ∝ Σ<sub>k</sub> log [ P(D_k = d_k | R=1, Q=q) / P(D_k = d_k | R=0, Q=q) ]
这个公式的意义在于:我们将文档相关性概率的估计,转化为了对文档中每个词项在相关/不相关文档中出现概率比值的求和。这大大简化了计算,并为实际系统设计奠定了基础。
总结


本节课中我们一起学习了概率信息检索的基础框架。
- 我们首先用贝叶斯定理说明了如何利用新证据更新概率信念。
- 然后明确了随机变量是确定性函数这一关键概念,并建立了清晰的概率表示法。
- 通过“南极洲小精灵”的思想实验,我们将信息检索问题形式化为一个概率计算问题:
P(相关 | 文档, 查询)。 - 为了得到可计算的模型,我们引入了文档的词项向量表示和二元独立性假设。
- 最后,通过一系列贝叶斯变换和化简,我们将复杂的相关性概率计算,推导为基于各词项概率比值的求和公式,为构建实用的概率检索模型扫清了道路。

下一节,我们将继续深入,探讨如何从实际数据中估计公式中的概率参数,并最终得到一个完整、可运行的概率信息检索系统。
026:概率信息检索 (3/4) 📊


在本节课中,我们将继续学习概率信息检索。我们将从回顾基本概念开始,通过一系列数学推导,最终得到一个可用于实际文档排序的简洁公式。本节课的核心在于理解如何将概率模型转化为可计算的评分机制。


回顾与基础概念



上一节我们介绍了概率信息检索的基本框架和二元独立模型。本节中,我们来看看如何通过一系列假设和数学变换,将复杂的概率公式简化。



我们有一个核心公式,用于计算文档 D 对于查询 Q 的相关性概率比:




P(R=1 | D, Q) / P(R=0 | D, Q)



在二元独立模型的假设下,这个比值可以分解为各个词项的乘积。




关键假设与公式推导

为了简化计算,我们引入了几个关键假设。
假设一:非查询词项的影响可忽略
我们首先假设,对于不在查询 Q 中的词项 K,它在相关文档和非相关文档中出现的概率是相等的。这意味着这些词项对于区分文档相关性没有贡献,因此在乘积中它们的因子为1,可以从计算中移除。
结果:我们只需要考虑出现在查询 Q 中的词项。
假设二:概率与具体查询无关
接下来,我们做了一个更强的简化假设:词项 K 在(非)相关文档中出现的概率(即 P_K 和 U_K)不依赖于具体的查询 Q。虽然这在直觉上并不完全合理(不同查询的相关文档集不同),但实践表明,这样做可以极大地简化公式,并且最终构建的系统效果良好。




结果:我们去掉了概率下标中的 Q,得到通用的 P_K 和 U_K。

公式重组与常数分离
基于以上假设,我们对公式进行重组和变换。核心技巧是乘以并除以相同的因子,目的是将公式中依赖于具体文档 D 的部分与不依赖的部分分离开。
经过变换后,公式变为两部分乘积:
- 一个仅依赖于查询和词项本身,而与具体文档
D无关的常数因子。由于我们在对文档进行排序时,对所有文档都乘以相同的常数不会改变排序结果,因此这部分可以忽略。 - 一个仅针对同时出现在查询和当前文档中的词项进行求积的因子。
当前公式:
∏_{K ∈ Q ∩ D} [ (P_K / U_K) * ((1 - U_K) / (1 - P_K)) ]
从乘积到求和:引入对数
我们得到的仍然是一个乘积公式。然而,在信息检索的实际系统中(如倒排索引),我们更习惯于进行“证据累加”,即求和操作。
解决方案:对公式取对数。对数函数是单调递增的,取对数不会改变文档的排序顺序,但能将乘积巧妙地转化为求和。
取对数后的公式:
RSV = Σ_{K ∈ Q ∩ D} log [ (P_K / (1 - P_K)) / (U_K / (1 - U_K)) ]
= Σ_{K ∈ Q ∩ D} log( O(P_K) ) - log( O(U_K) )
其中 O(p) = p / (1-p) 称为事件发生概率 p 的几率。

检索状态值 (RSV):我们定义 RSV 为文档的得分。它等于查询与文档共同包含的词项所对应的 C_K 值之和,其中 C_K = log( O(P_K) / O(U_K) )。

重要洞见:这个 RSV 计算方式,本质上就是遍历查询词项的倒排列表,为包含这些词项的文档累加一个权重 C_K。这完全符合我们之前学习的高效检索模式。它甚至可以看作查询向量(由0和1组成)与文档向量(在词项 K 处的分量为 C_K,否则为0)的点积。
估计概率 P_K 与 U_K
现在,我们需要为每个词项 K 估计两个关键概率:
P_K:词项K出现在相关文档中的概率。U_K:词项K出现在非相关文档中的概率。
我们可以通过收集真实数据的统计量,并用最大似然估计来近似这些概率。
考虑以下列联表,其中 S 是相关文档总数,s 是相关文档中包含词项 K 的数量。
词项 K 出现 |
词项 K 不出现 |
总计 | |
|---|---|---|---|
| 相关文档 | s |
S - s |
S |
| 非相关文档 | df_K - s |
(N - df_K) - (S - s) |
N - S |
| 总计 | df_K |
N - df_K |
N |
N:文档集合中的文档总数。df_K:包含词项K的文档数量(文档频率)。
根据此表,最大似然估计为:
P_K ≈ s / SU_K ≈ (df_K - s) / (N - S)
然而,直接使用这些估计存在两个问题:
- 需要已知
s和S:这通常需要通过用户反馈(相关性判断)来获得,在系统初始阶段是未知的。 - 数据稀疏性:在小规模样本中,
s或(df_K - s)可能为0,导致概率估计为0或1,进而使C_K计算出现无穷值。
解决方案:平滑与进一步假设
为了解决上述问题,研究者提出了巧妙的平滑技术和假设。


平滑技术:在所有的统计计数上加上一个小的常数(例如0.5),以避免出现零概率估计。平滑后的估计变为:
P_K ≈ (s + 0.5) / (S + 1)U_K ≈ (df_K - s + 0.5) / (N - S + 1)


简化假设:为了完全消除对未知量 s 和 S 的依赖,我们可以做出两个实用的假设:
- 假设三 (Croft & Harper):假设相关文档中词项出现与否是等可能的,即
P_K ≈ 0.5。这意味着O(P_K) ≈ 1,其对数值为0。 - 假设四:对于大多数查询,相关文档只占整个文档集的极小部分(
S << N)。因此,非相关文档的统计可以近似用整个文档集的统计来代替,即U_K ≈ df_K / N。
结合假设三和四,权重 C_K 可以简化为:
C_K ≈ log( (1 - U_K) / U_K )
≈ log( (N - df_K) / df_K )
这个公式具有深刻的直观意义:一个词项的权重,与其文档频率 df_K 成反比。词项出现在越多的文档中(df_K 越大),其区分能力越弱,权重 C_K 越小(甚至为负值)。这与我们之前学习的 IDF(逆文档频率) 概念在精神上完全一致!


总结


本节课中我们一起学习了概率信息检索模型的核心推导过程。
- 我们从概率排序原则和二元独立模型出发。
- 通过引入非查询词项无关和查询无关性的假设,大幅简化了概率计算。
- 通过巧妙的数学变换和对数运算,将概率乘积模型转化为实用的求和评分模型 (RSV),实现了高效的“证据累加”。
- 为了估计模型参数(
P_K和U_K),我们引入了基于统计的估计方法。 - 针对数据稀疏和参数未知的问题,我们采用了平滑技术和两个关键的简化假设。
- 最终的简化结果惊人地得出了一个类似于 IDF 的权重公式,这揭示了概率模型与经典向量空间模型之间的深刻联系。

这个推导过程展示了如何将严谨的概率论框架,通过一系列合理的工程化近似,转化为一个高效、可计算、且在实践中非常有效的检索算法。概率信息检索为现代检索模型奠定了重要的理论基础。
027:概率信息检索 (4/4) 🎯





在本节课中,我们将深入探讨概率信息检索模型的推导过程,并揭示其与向量空间模型的内在联系。我们将看到,通过一系列合理的假设,概率模型最终可以推导出我们熟悉的逆文档频率(IDF)概念。

上一节我们介绍了概率信息检索的基本框架和列联表。本节中,我们将通过数学推导,展示概率模型如何自然地引出向量空间模型中的核心权重。
现在,我们将揭示一个关键结果。我们将展示,经过一系列推导,我们将回到几周前讨论过的概念。
我们将会使用这个假设。请记住我们之前所做的假设:列联表不依赖于查询。即 P(K) 与 P(K|Q) 相同。你可以用“皮卡丘”来记忆:Pikachu -> P(K) -> P(K|Q)。
我们随后还做了其他几个假设。这里的核心思想是:对于不相关文档,我们假设其词项分布与整个互联网(或文档集)的背景分布一致,因此我们使用文档频率来估计。对于相关文档,由于我们通常缺乏先验信息,我们简单地假设其出现概率为 1/2。
这就是我们得到的结果。如果我们进行一些平滑处理,甚至可以近似地用 N(文档总数)来代替这里的某些项。对于大多数非停用词来说,其文档频率相对于 N 非常小。因此,我们可以将 N 代入。
现在,我们可以将这个结果代入 C(K) 的公式中,即结合 P(K) 和 U(K)。我们得到以下推导:
log(1) = 0,这一项消失了。然后我们得到 log(文档频率 / N)。为了去掉对数前的负号,我们将其移入对数内部,取倒数。于是我们得到:
log(N / 文档频率)
这非常不可思议。谁对这个结果感到惊讶?log(N / 文档频率) 是什么?我们在哪里见过它?是的,我们见过。那就是逆文档频率(IDF)。

这之所以不可思议,是因为我们做了所有这些假设,但最终得到的求和项中,恰好包含了逆文档频率。
我们刚刚通过数学推导所做的工作,就是为向量空间模型中使用 IDF 提供了理论依据。这就是我们所完成的。它从概率检索的角度证明,在向量空间模型的向量中加入 IDF 权重,本质上与按照相关性概率降序排列文档是一致的。这是一个非常重要的结果。
这基本上对应了向量空间模型中没有使用词频(TF)的情况。这相当于在 SMART 表示法中,词频权重设为 0 或 1(布尔值)。如果在查询端和文档端都这样做,并且文档端使用 IDF 而不进行归一化,那么我们得到的结果就完全等同于概率信息检索模型的检索状态值。一切都联系起来了:概率信息检索和向量空间模型。

这个结果非常棒。对于词频,我们同样需要为其提供理论依据,这将是下一讲的内容。届时,词频也会以类似的方式“从帽子中变出来”。我们将为两者都找到理论支撑。
当我们说只使用布尔值而非词频时,这类似于 SMART 表示法中的 BTN 编码。也许不完全准确,因为 IDF 应该只加在文档一侧。但核心思想是:我们遍历查询中的词项,遍历包含该词项的文档(倒排记录表),并将 IDF 值累加到每个文档的“桶”中,从而得到检索状态值。这就是我们之前学过的:从交集算法,到一次一词、一次一文档的遍历方法。我们只是在调整每个“词项”(数学意义上的)所累加的值,在这里就是 IDF。
能够回到我们之前熟悉的概念,感觉非常好。
现在,我想以另一种不同的参数估计方法来结束本节,这不同于在黑板上进行的理论推导。这是一种估计 P(K) 和 U(K) 的不同方法,被称为 OKAPI BM25 方法。BM 代表“最佳匹配”(Best Matching),它是一类结合了词频、IDF 等因素的检索系统,而不仅仅是布尔方法。它被称为 BM25(我不确定为什么是 25)。
以下是他们的做法。关键区别在于,他们认为之前对相关文档中词项概率使用 1/2 的假设过于简化,可能不够精确。他们对此并不满意。


因此,他们提出,与其使用固定的 1/2,不如利用真实用户的反馈来评估它。在现实中,我们有使用系统的真实用户。如果你是一个拥有用户的搜索引擎,你可以向用户提问。
其工作方式是:用户向系统输入一个查询。查询被执行后,我们展示结果。在初始阶段,你可以使用 1/2 来启动系统。但在更一般的步骤 S,我们将其称为 P_K(S)。我们向用户展示结果,并请用户标记相关文档。这是人工工作。你在许多网站上见过类似功能:“您觉得这个页面有用吗?”
他们要求用户标记相关文档。这意味着文档被分为相关和非相关两类。
基于此,我们可以进行统计,再次使用最大似然估计。我们收集用户的反馈,查看所有被标记为相关的文档,统计其中包含词项 K 的比例,从而计算出一个更新的值 R。我在这里加上 S+1 表示这是一个更好的值,因为它考虑了用户反馈。然后我们重复这个过程,不断循环更新。
我提到过平滑。同样,我们可以加入 1/2 和 1 来进行平滑处理,以应对可能没有足够用户数据使得统计量可靠的情况。
另一些人则不是用 1/2 平滑,而是用前一个值进行平滑。谁听说过卡尔曼滤波器?这是一种平滑方法,常用于 GPS 或导航坐标。当你有一系列位置点时,GPS 并不十分精确,如果直接使用下一个点,路径可能会跳跃。卡尔曼滤波器通过将新值与先前值进行插值来平滑移动路径。
这里的方法有点类似。你有一个即将计算出的 P_K 新值,但你不是直接跳转到它,也不是简单地加 1/2 平滑,而是赋予前一个值一定的惯性(权重),这样你就不会偏离前一个值太远,从而保持平滑。这就是为什么我们在这里加上 P_K(S)。κ(Kappa)只是一个希腊字母表示的常数参数。你可以设置任何你想要的 κ 值,或者使用机器学习等方法来优化它。
我们就这样不断更新新值。这类似于贝叶斯更新。还记得“雨伞和下雨”的例子吗?当我给你“我带了伞”这个新信息时,它如何影响“正在下雨”的概率?从步骤 S 到 S+1 就变成了一个贝叶斯更新。因此,这种卡尔曼滤波器式的方法与此类似。现在,我更新了这些后验概率(步骤 S 的先验,步骤 S+1 的后验)。然后我再次重复,持续更新。如果我幸运地拥有一个拥有大量用户的流行搜索引擎,那么这个估计的质量将会不断提高。这就是它的工作原理。
这是概率信息检索的最后一页幻灯片。大家有什么问题吗?有没有不清楚的地方想问?请不要犹豫,因为如果你有不清楚的地方,很可能其他人也不清楚。提问从来都不是坏事。有任何问题吗?……如果在 Zoom 上没有,那么如果之后还有问题,请在论坛上提问,或者给我们发邮件、发消息,我们很乐意回答。你们也可以互相提问,因为教是最好的学。当你教别人时,也会迫使自己发现知识盲点,从而学得更扎实。
那么,关于概率信息检索,我们就讲到这里。

再次强调,本节课最重要的信息是:我们重新得到了 IDF 的概念。我们回到了带有逆文档频率的向量空间模型。

从历史上看,向量空间模型出现得稍晚一些。先是布尔检索,然后是概率信息检索(60、70、80年代),向量空间模型出现得比概率检索稍晚。我认为 SMART 系统是最早的向量空间模型之一,但时间上相差并不太远。



本节课中,我们一起学习了概率信息检索模型的完整推导。我们看到了如何通过“不依赖查询”和“相关文档中词项概率为 1/2”等假设,将概率排序函数简化为对 IDF 的求和,从而在理论上将概率模型与向量空间模型联系起来。我们还简要介绍了利用用户反馈动态更新概率参数的 OKAPI BM25 方法。理解这种联系,有助于我们更深入地把握不同信息检索模型背后的统一思想。
028:语言模型 (1/3) 🧠
在本节课中,我们将要学习语言模型的基础概念。我们将从计算机科学中的经典语言模型入手,理解其核心思想,并将其与当今流行的大语言模型进行对比。通过本节课,你将掌握如何将文档视为词序列,并利用概率模型来生成或评估文档。
从有限状态自动机到语言模型
上一节我们介绍了概率检索模型。本节中,我们来看看语言模型,我们将从一个经典的计算概念——有限状态自动机开始。
在理论计算机科学中,“语言”指的是一组字符串的集合。一个有限状态自动机可以用来识别(或“接受”)属于某个特定语言的字符串。例如,在编程中,编译器检查你的代码语法是否正确,本质上就是在做这件事。
以下是一个简单的有限状态自动机示例,它接受所有以“BAA”开头,后跟任意数量“A”的字符串(即正则表达式 BAA A*)。

它的工作原理如下:
- 你从起始状态(节点1)开始。
- 你逐个“消耗”输入字符串中的字母(例如 B, A, A, A...)。
- 根据当前状态和看到的字母,沿着标有该字母的边转移到下一个状态。
- 如果你最终到达了接受状态(带双圈的节点),那么这个字符串就被该语言接受。
语言模型:概率化的视角
理解了自动机如何识别语言后,我们现在将其概念应用到信息检索中。关键的一步是进行认知转换:将文档视为词序列,将词视为字母。
因此,在信息检索的语境下:
- 字母表 (Σ) 对应 词汇表(所有可能的词)。
- 字符串 对应 文档(一个词的序列)。
- 语言 对应 所有可能文档的集合。
一个语言模型就是在这个“所有可能文档的集合”上定义的一个概率分布。它给每个可能的文档分配一个概率,所有文档的概率之和为1。
用公式表示,即:
P(D = d) = p_d, 且 Σ_{d ∈ 所有文档} p_d = 1
其中 D 是一个代表文档的随机变量,d 是某个具体的文档。

作为生成器的语言模型
一个有趣的观点是,我们可以将识别语言的自动机“反转”过来,用作文档生成器。为此,我们需要为状态转移边赋予概率。
例如,假设从起始状态出发,选择B或A的概率各为1/2,并且在每个状态都有一定概率(如1/4)停止生成。那么,生成文档“BAAA”的概率可以计算为:
P(“BAAA”) = P(选B) * P(选A) * P(选A) * P(选A) * P(停止) = (1/2) * (1/2) * (1/2) * (1/2) * (1/4) = 1/64


这样,我们就有了一个基于统计的简单文档生成器。生成某个文档的概率越高,说明这个文档越符合该语言模型的“风格”或统计规律。










链式法则与文档概率分解
为了构建实用的语言模型,我们需要计算生成一个特定文档 d(例如词序列 [w1, w2, w3])的概率。这可以通过概率论中的链式法则来实现。



链式法则告诉我们,一系列事件同时发生的概率,可以分解为一系列条件概率的乘积:
P(A, B, C) = P(A) * P(B|A) * P(C|A, B)







将其应用于文档生成,生成文档 [w1, w2, w3] 的概率可以分解为:
- 首先生成至少一个词的概率。
- 接着,在已生成至少一个词的条件下,第一个词恰好是
w1的概率。 - 然后,在已有第一个词
w1的条件下,生成长度至少为2的概率。 - 接着,在已有第一个词
w1且长度至少为2的条件下,第二个词恰好是w2的概率。 - 以此类推,直到生成所有词,并最终停止。



这个过程可以形式化地表示为一系列条件概率的乘积。








马尔可夫假设与N元语法模型





然而,上述完整的链式法则在实际中难以应用,因为计算第 n 个词的概率时,需要考虑前面所有 n-1 个词的历史,这需要巨大的内存和计算量。
为了解决这个问题,我们引入一个简化假设:马尔可夫假设。它假设一个词出现的概率只依赖于它前面有限的 n-1 个词,而不是整个历史。
这引出了 N元语法模型:
- 一元语法 (Unigram):
P(w_i),词的出现相互独立。n=1 - 二元语法 (Bigram):
P(w_i | w_{i-1}),词的概率只依赖前一个词。n=2 - 三元语法 (Trigram):
P(w_i | w_{i-2}, w_{i-1}),词的概率依赖前两个词。n=3
例如,在二元语法模型下,生成文档 [w1, w2, w3, w4] 的概率近似为:
P(d) ≈ (1 - p_stop) * P(w1) * (1 - p_stop) * P(w2|w1) * (1 - p_stop) * P(w3|w2) * (1 - p_stop) * P(w4|w3) * p_stop
其中 p_stop 是生成停止的概率。
请注意:这是经典语言模型与大语言模型(LLM)的一个关键区别。经典模型基于马尔可夫假设,上下文窗口有限。而像ChatGPT这样的大语言模型使用注意力机制,能够考虑更长远、甚至双向的上下文依赖,因此能力强大得多。
总结


本节课中我们一起学习了语言模型的基础。我们从有限状态自动机出发,理解了语言作为字符串集合的概念。接着,我们将其与信息检索结合,把文档看作词序列,并定义了文档上的概率分布。我们探讨了如何将语言模型视为生成器,并使用链式法则分解生成概率。最后,为了模型的可行性,我们引入了马尔可夫假设,从而得到了N元语法模型,这是现代大语言模型发展的重要历史基石。



下一节,我们将继续深入,探讨如何从数据中估计这些概率,并应用语言模型到信息检索任务中。
029:语言模型 (2/3) 🧠
在本节课中,我们将继续学习语言模型,特别是从一元模型(Unigram Model)过渡到二元模型(Bigram Model),并探讨如何利用这些模型进行信息检索。我们将通过简单的例子和公式,理解模型如何生成文本以及如何计算文档生成查询的概率。


上一节我们介绍了基于链式法则和马尔可夫假设的语言模型基本公式。本节中,我们来看看当我们将马尔可夫阶数参数 n 设置为0时,会发生什么。
当 n=0 时,我们得到一个非常特殊的模型,称为一元模型。在这个模型中,生成下一个词的概率完全不依赖于之前的任何词。这意味着我们只需要一个简单的概率查找表。





一元模型的概率公式可以简化为:
P(D) = P_stop * ∏_{k=1}^{L} [ (1 - P_stop) * P(w_k) ]
其中,P(w_k) 是词 w_k 独立出现的概率,P_stop 是生成过程停止的概率。
从自动机的视角看,一元模型对应一个非常简单的两状态自动机:一个状态不断根据概率表 P(w) 生成新词,另一个状态是接受文档的终止状态。
现在,让我们思考一个具体问题:在一元模型下,生成一个特定顺序的文档(例如,“信息”、“检索”、“讲座”)的概率是多少?
根据链式法则和一元模型的独立性假设,其概率为:
P(“信息”,“检索”,“讲座”) = (1-P_stop)*P(“信息”) * (1-P_stop)*P(“检索”) * (1-P_stop)*P(“讲座”) * P_stop


然而,一元模型有一个重要特性:它不关心词的顺序。这与我们之前学过的向量空间模型中的“词袋”假设一致。因此,我们更常从“词袋”的视角,而非“词序列”的视角来看待一元模型。





从有序的词序列模型转换到无序的词袋模型,需要进行一些数学处理。我们通过一个简单的例子来理解这个过程。


假设我们不是生成词,而是生成猫,并且只有“黑猫”和“白猫”两种颜色。我们生成4只猫,黑猫概率是2/3,白猫概率是1/3。


以下是生成特定颜色序列(有序)的概率计算:
- 生成序列【黑,黑,黑,黑】的概率是
(2/3)^4。 - 生成序列【黑,白,白,白】的概率是
(2/3) * (1/3)^3。



但如果我们只关心最终有多少只黑猫和白猫(无序的词袋),就需要考虑所有能产生相同颜色组合的序列。例如,要得到“1只黑猫,3只白猫”这个组合,有4种不同的排列方式。
计算特定组合(词袋)的总概率,需要将一种特定顺序的概率乘以可能的排列数。这个排列数由多项式系数给出。






多项式系数公式为:
排列数 = (总数量)! / ( (颜色1的数量)! * (颜色2的数量)! * ... )
对于两种颜色的情况,这就是二项式系数。






因此,得到“1只黑猫,3只白猫”这个组合的概率是:
概率 = [4! / (1! * 3!)] * [(2/3)^1 * (1/3)^3]

将这个过程推广到词汇表,我们就得到了一元语言模型下的词袋概率公式。对于一个长度为 L、已知词频 tf(t, d) 的文档 d,其生成概率为:
P(d) = P_stop * (1-P_stop)^L * [ L! / (∏_t tf(t, d)! ) ] * ∏_t [ P(t) ^ tf(t, d) ]
这个公式本质上是多项分布。





理解了简单的一元模型后,我们来看看更复杂的模型。当我们将马尔可夫阶数参数 n 设置为1时,就得到了二元模型。

在二元模型中,生成下一个词的概率依赖于前一个词。这意味着我们的概率查找表从一维变成了二维:对于每一个可能的“前一个词”,都需要一个完整的概率分布来生成“当前词”。

二元模型的概率公式如下:
P(D) = P_stop * P(w1) * (1-P_stop) * P(w2|w1) * (1-P_stop) * P(w3|w2) * ... * P_stop
其中,P(w_k | w_{k-1}) 表示在前一个词是 w_{k-1} 的条件下,生成 w_k 的概率。






由于词与词之间有了依赖关系,二元(及更高阶)模型无法再简化为无视顺序的词袋模型,我们必须考虑词序。



学习了如何构建语言模型来生成文档后,一个关键的问题是:这如何应用于信息检索?
其核心思想是进行一个“思想实验”:
- 为文档库中的每一个文档
d构建一个语言模型(例如,统计其词频或词对频率)。 - 当用户输入一个查询
q时,我们问:如果随机选择一个文档模型,并用它来生成文本,恰好生成查询q的可能性有多大? - 我们认为,那个最有可能生成查询
q的文档,就是与查询最相关的文档。
这便将信息检索问题转化为了一个估计概率 P(文档模型 生成 查询) 的问题。这个概率通常称为查询的似然。我们的目标是找到使这个似然值最大的文档。
本节课中我们一起学习了:
- 一元模型:词生成完全独立,对应词袋假设,其概率计算基于多项分布。
- 二元模型:词生成依赖于前一个词,概率计算需要考虑条件概率表。
- 语言模型用于信息检索:通过计算查询在文档模型下的生成似然来评估相关性,即“哪个文档最有可能生成这个查询?”。

在接下来的课程中,我们将深入探讨如何具体计算这个生成似然,并解决相关的概率计算问题。
030:语言模型 (3/3) 🧠
在本节课中,我们将学习如何将语言模型应用于信息检索系统,构建一个名为“查询似然模型”的检索框架。我们将看到,这个基于概率的模型最终如何推导出我们熟悉的向量空间模型中的核心元素。
概述
上一节我们介绍了如何使用语言模型生成文档。本节中,我们来看看如何利用这个生成过程来构建一个信息检索系统。核心思想是:给定一个用户查询,找出哪个文档最有可能“生成”这个查询。


查询似然模型
在检索场景中,用户在一个搜索框中输入查询,这个查询称为 Q。
想象一个场景:一个小精灵从整个文档库中随机挑选一个文档。然后,它花费一小时编写一个Python脚本,为该文档构建条件概率表(即语言模型)。接着,它使用这个模型生成了该文档。而最不可思议的事情发生了:这个小精灵刚刚重新生成了查询 Q。这个概率有多大?
因此,我们提出这个问题:在已知生成的文档是查询 Q 的条件下,哪个文档被挑选并生成 Q 的可能性最高?

这为我们提供了一个信息检索系统:如果我们能计算特定文档 D 生成查询 Q 的概率,并按概率降序排列文档,我们就构建了一个检索系统。我们只需返回概率最高的文档。


这种构建的系统称为 查询似然模型。

应用贝叶斯定理

当我们遇到一个难以直接计算的条件概率时,可以使用神奇的贝叶斯定理进行转换。
具体来说,我们想知道在给定查询 Q 的条件下,文档 d(我们想要排序的那个)被选中的概率。我们将为每个文档 d 计算这个概率,然后排序。
根据贝叶斯公式,我们可以进行如下转换:
P(d|Q) = P(Q|d) * P(d) / P(Q)
现在,神奇的事情发生了:
- P(Q) 是一个常数,它不依赖于文档 d,因此可以忽略,因为它不会改变排序顺序。
- P(d) 是文档的先验概率。我们可以假设先验是均匀的,即每个文档被随机选中的可能性相同。因此,P(d) 可以视为
1 / (文档总数),这也是一个常数。
于是,我们只剩下:
P(d|Q) ∝ P(Q|d)
这非常棒,因为 P(Q|d) 我们知道如何计算!这正是我们在本讲座中花费一小时解释的内容:使用多项式分布和链式概率来计算生成查询的概率。
核心计算公式
以下是我们用于计算 P(Q|d) 的核心公式(以一元模型为例):
P(Q|d) = Multinomial(Q) * ∏_{t ∈ V} P(t|d)^{c(t, Q)} * P_stop^{|Q|} * (1 - P_stop)^{L_d}

让我们解析这个公式:
- Multinomial(Q):基于查询 Q 的多项式系数,这是一个常数。
- ∏_{t ∈ V} P(t|d)^{c(t, Q)}:这是核心部分。对词汇表 V 中的每个词项 t,取 P(t|d)(词项 t 在文档 d 模型中的生成概率)的 c(t, Q) 次幂(即 t 在查询 Q 中出现的次数),然后将所有结果相乘。
- P(t|d) 来自对文档 d 的统计分析,最简单的方法是使用词项频率:
P(t|d) = tf(t, d) / L_d,其中L_d是文档 d 的长度。 - P_stop^{|Q|} * (1 - P_stop)^{L_d}:与生成过程长度相关的概率,在文档长度固定或忽略时可作为常数处理。





我们需要为每个文档 d 评估这个公式的值,然后根据该值对文档进行排序,值最高的文档排名最前。


转化为证据累积形式

我们将进行一些转换,尝试将其变为类似证据累积(如向量空间模型中点积)的形式。



首先,我们有一个大的乘积项,但证据累积通常使用求和。因此,我们对整个表达式取对数:
log P(Q|d) = log(Multinomial(Q)) + ∑_{t ∈ V} c(t, Q) * log P(t|d) + |Q| * log P_stop + L_d * log(1 - P_stop)
接下来,我们区分哪些部分依赖于文档 d,哪些是常数。
log(Multinomial(Q))、|Q| * log P_stop是常数,与文档 d 无关。L_d * log(1 - P_stop)依赖于文档长度 L_d,可能变化,但为了简化焦点,我们有时假设文档长度相近或将其影响单独处理。
最重要的部分是中间的求和项:
∑_{t ∈ V} c(t, Q) * log P(t|d)
这看起来非常酷,因为它开始与我们之前学过的内容联系起来。这是一个求和项,其中:
- 第一个因子
c(t, Q)依赖于查询(即查询中的词项频率)。 - 第二个因子
log P(t|d)依赖于文档(即文档模型中的对数概率)。



这本质上是一个标量积(点积)!我们在向量空间模型中见过类似的结构:一个向量代表查询(基于词项频率),另一个向量代表文档(基于词项权重,例如对数概率)。
我们刚刚重新发现(或重新推导出了)向量空间模型!现在的理论依据是:词项频率为我们提供了生成特定文档的概率。

与TF-IDF的联系


回顾我们两周前的工作:
- 在概率信息检索中,通过对贝叶斯规则和相关概率进行处理,我们得到了 IDF。
- 在语言模型中,通过类似的推导,我们得到了 TF(词项频率)。

因此,TF-IDF 在向量空间模型中的出现得到了理论上的衔接和解释。语言模型为使用词项频率提供了高层次的理由:词项频率给出了生成特定查询的概率估计。



模型变体与平滑




当然,实际中还有更多变体,这类似于我们之前见过的“SMART”系统选项菜单。
其中一个重要概念是平滑。想象你基于文档计算概率,可能会遇到一个问题:文档可能太小,以至于无法生成查询中的所有词。





例如,查询是“information retrieval”,但有些文档根本不包含“information”或“retrieval”这些词。如果你基于这样的文档构建模型,那么这些词的概率就是0,导致生成整个查询的概率为0,仅仅因为文档样本太小。
为了解决这个问题,我们可以进行平滑。基本思想是给概率估计增加一个小的常数,或者使用整个文档集的集合频率进行插值。就像烹饪一样,你混合一点文档词项频率和一点集合词项频率,得到一个更平滑的概率分布,避免过多的零概率。
其他建模方法
查询似然模型是从文档生成模型,然后计算生成查询的概率。还有其他方法:
- 文档似然模型:反过来,基于查询生成一个模型,然后计算该模型生成每个文档的概率。由于查询通常很短,这需要大量的平滑处理。
- 模型比较法:为每个文档和查询分别生成一个语言模型,然后计算模型之间的距离(例如,计算两个概率分布之间的KL散度或其他距离度量)。与查询模型最接近的文档模型对应的文档排名最高。
总结

本节课中,我们一起学习了如何将语言模型应用于信息检索,构建了查询似然模型。通过贝叶斯定理和一系列推导,我们展示了该模型如何最终简化为一个类似于向量空间模型中点积的形式,从而为 TF(词项频率) 的使用提供了理论依据。我们还简要讨论了平滑的重要性以及其他基于语言模型的检索方法。
恭喜你完成了本学期所有核心内容的学习!概率检索和语言模型是课程中较为进阶的部分,但现在我们明白了 TF 和 IDF 从何而来以及为何有效。
031:总结 (1/2) 🎓
在本节课中,我们将对本学期所学的信息检索知识进行一次全面的回顾与总结。我们将从高层次梳理所有核心概念,理解它们之间的联系,并简要展望这些知识如何通向现代大型语言模型。
课程回顾:从布尔检索到向量空间
上一节我们介绍了本课程的总结框架,本节中我们来看看本学期所涵盖的广阔知识体系。你们已经掌握了大量的新知识。
现在再看第一周课程中的这张幻灯片,你们应该会感到熟悉,因为你们已经理解了其背后的含义。

我们学习了最早的布尔查询系统。
接着,我们探讨了语言处理,发现将数据构建成索引并非易事。
我们学习了如何处理容错检索,如何评估系统,以及如何为更大规模的集合扩展索引和搜索引擎的构建。
我们看到了数据压缩技术,这能帮助我们在扩展时节省资源成本。
然后,我们进入了通常属于硕士阶段的高级信息检索领域,包括向量空间模型、概率信息检索和语言模型。
你们看到了后两者与向量空间模型的联系:概率模型为逆文档频率提供了理论依据,而语言模型则为词项频率提供了理论依据。正是这两者共同构成了向量空间模型中的 TF-IDF。



所以,我喜欢说这实际上是一门数据库课程,因为我们学习了如何存储、查询以及高效地查询数据,使用特定的查询语言并获得结果。
从某种意义上说,这是关于第五种数据形态的课程。我们学过数据库设计和建模中的表格,接下来在大数据课程中还会看到立方体、树和图。而在这门课中,我们处理的是文本。
但我想说,一个重要的新启示,尤其是在过去五年甚至十年里,是将文本视为向量非常高效。这一点在向量空间模型中已初现端倪,而随着像 ChatGPT 这样的大型语言模型的出现,使用向量和线性代数来处理文本和自然语言的方法得到了验证。
历史演进:从布尔系统开始
然而,在发展到这一步之前,历史上最早的信息检索系统并未使用向量或此类先进技术。最初的尝试只使用布尔逻辑。
这意味着,你可以编写一个查询,指定文档应包含或不包含哪些词项,使用合取、析取等逻辑运算符。我们有一个非常简单的查询语言:AND、OR、NOT。
它可以应用于一个输入文档集合,然后查询的输出就是该集合的一个子集,没有排名,没有特定顺序,只是返回一些文档。
实际上,曾有基于此的商业产品,尤其在图书馆和法律事务所中用于文本研究。
核心抽象:文档、词项与索引
接着我们看到,“文档”的概念在只有书籍和卷轴的古代似乎显而易见,但经历了数字化后,文档可以是网页、电影、声音、论文等许多不同的事物。



因此,我们有了文档这个抽象概念,它是构成集合的基本单位。我们有一个文档集合。
然后是词项,这是对语言世界的抽象。人类语言本质上基于一些可以在句法、语音、词汇等层面分析的标记。在信息检索中,我们喜欢在词的层面进行分析。
同样,我们还有词项上下文的概念,起初看似简单(只需看空格之间的内容),但实际上我们了解到这要复杂得多。



一旦完成了分词和所有预处理工作,我们就得到了词项。现在我们有了一个非常抽象的模型:一个文档集合,文档包含或不包含我们感兴趣的词项。我们可能关心词项出现的次数,也可能关心它们出现的顺序。但从数学角度看,仅用集合、文档、词项(词袋、词表、语义)这种高层次视角来看待问题,就蕴含着巨大的力量。
对于布尔检索,我们可以使用词项集合模型,即一个词项要么属于一个文档,要么不属于。
这意味着任何文档都可以用一个布尔向量来表示。1 表示该词项在文档中,0 表示不在。重要的是,向量的结构是每个位置对应一个词项。我们有一个任意的词项顺序,每个词项对应向量中的一个位置,其值为 1 或 0。这与后来的语言模型不同,在旧系统中,每个词项确实对应向量中的一个位置。
我们研究了几种表示方法,其中一种是词项-文档关联矩阵,行代表词项,列代表文档。实际上,每一列都对应前面提到的那个向量。一个文档的布尔向量就是矩阵中代表该文档的那一列。
然而,为了执行查询,这种表示效率极低,存在大量零值,占用大量内存和时间。
因此,为了高效处理查询,我们构建了所谓的索引。索引并非信息检索独有,它是数据库系统中的通用概念,指在原始数据(集合)之外额外构建的数据结构。构建索引需要空间和时间成本,但一旦建成,就能基于它高效处理查询。
在标准倒排索引中,我们有词项(黄色部分)、文档频率(紫色部分,即包含该词项的文档数量)以及倒排记录表(蓝色矩形)。这里的倒排记录是非位置性的,是 (文档ID, 词项) 对。
一旦有了这个索引,我们就可以将布尔查询映射到索引操作上。例如,对于 AND 操作,我们求倒排记录表的交集;对于 OR 操作,我们求并集。由于倒排记录表是有序的,我们可以用线性时间完成这些操作,只需用指针同时遍历它们即可。NOT 操作也能高效执行。
这一切在有了词项之后很美妙,但难点在于如何获取词项。我们花了整整一周时间来探讨如何将文档映射到包含/不包含哪些词项,并发现由于人类语言的复杂性,这相当棘手。
收集文档、分词、语言预处理、构建索引,所有这些实际上都非常复杂,但我们现在掌握了一些工具来完成这些工作。
关键概念辨析与索引优化
我几年前制作了这张幻灯片,因为很容易在众多术语中迷失。我想借此澄清,根据你指的是处理前还是处理后的概念(“处理”指经过语言预处理,如识别不同时态下的相同词项),以及是否与特定文档(甚至位置)绑定,我们大致有六种概念。

如果它是原始的且与特定位置绑定,我们称之为位置型词元。
如果与文档绑定但无位置信息,则是非位置型词元。
如果尚未处理且不与任何特定内容绑定,可以称为词或未归一化词型。
经过处理后,我们基本上就有了位置型倒排记录、非位置型倒排记录和词项(即归一化词型)。词项对应索引中的黄色部分,是处理后的结果。非位置型倒排记录对应蓝色部分,而位置型倒排记录则对应如果我们把索引变成位置索引时的蓝色部分。


我们看到,有些词项因为其倒排记录表几乎包含整个集合而可以完全跳过,这样能节省空间。这在计算资源和存储空间有限的六七十年代尤为重要。这些词被称为停用词,它们因出现过于频繁而无助于区分文档,故被移除。
我们还看到,作为将词元处理为词项的一部分,我们可以使用查询扩展,既可以在索引端扩展,也可以在查询端扩展。当然,在拥有大型语言模型的今天,这看起来有些原始,因为 LLM 可以直接利用嵌入向量来完成这类事情。
在算法和机械层面,我们有词干提取器,它允许你将词语分组到不同类别以获得词项。同样,这现在已被使用嵌入向量的 LLM 所取代。
我们看到,如果倒排记录表非常稀疏,可以使用跳表指针,让你直接跳转到其他位置。更准确地说,当你想计算一个非常稀疏的倒排记录表与另一个不那么稀疏的表的交集时,你可能希望直接跳转到相关记录。例如,如果一个倒排记录表包含 [1, 11],另一个包含 [1, 5, 9, 11],使用跳表指针,你可以直接从 1 跳到 5,再跳到 9,然后到 11,而无需遍历中间项。这是跳表指针的思想。
这与我们几周后看到的多层跳表概念相关。你可以有更多层级,这类似于选择自行车、火车、飞机等不同交通工具。在向量数据库中,当寻找接近查询的特定向量时,这种多层跳表结构非常有用。尽管一个用于倒排记录表,一个用于向量数据库,但从理论计算机科学的角度看,它们是相同的“跳跃”思想。如果你想用花哨的术语,它们基本上就是带有额外单链表或双链表层的链表,你可以在图结构中上下移动。



索引的变体与高效查找
接着我们看到,我们可以改进标准倒排索引(之所以叫“标准”,是因为我们可以修改它,产生不那么标准的变体)。
其中一种变体是使用词对甚至三词、四词组合作为索引项,这被称为双词索引、三词索引或 K 词索引。它的一个问题是可能返回误报,因此需要额外的工作来确保不会返回错误结果。你可以通过使用更长的窗口来减少误报,但更长窗口的问题是会扩大空间需求。当然,这个问题后来被 LLM 的注意力机制解决了。但了解历史视角和过去面临的挑战总是很重要的。
另一种处理短语查询的方法是位置索引。这次,我们拥有的不是非位置型倒排记录,而是位置型倒排记录。这意味着对于文档中的每个词项,我们不仅知道它是否出现,还知道它在文档中出现的所有位置。这让我们能够处理短语查询。
索引的目标是高效处理查询。为了高效查找词项,我们需要能够在标准倒排索引中高效地查找这些黄色部分的词项。这意味着标准倒排索引左侧的这些黄色词项被组织在某种数据结构中。
这又回到了理论计算机科学的算法和数据结构。我们有两类主要的数据结构来实现这种查找:哈希表(查找速度极快,但不能处理范围查询)和树结构(更准确地说,在数据库背景下是 B 树 或 B+ 树)。B+ 树是二叉搜索树的现代版本,它允许有多个子节点(如10个、20个)。这样做的主要原因与硬件有关:从磁盘读取数据时,你希望每次读取都能获取一个完整的块。如果只用二叉搜索树,每次读取的数据量可能很少,无法充分利用磁盘带宽,导致延迟效率低下。因此,哈希表和 B+ 树是最常见的索引结构。当然,还有更多结构,甚至在数据库中,人们也开始使用机器学习来学习定位词项的结构,而不是手动构建。



B+ 树看起来像这样。重要的是,在 B+ 树中,词项和倒排记录表位于树的底部(叶子节点),而所有非叶子节点仅用于引导查找,告诉你该向左还是向右走。实际与倒排记录表关联的词项位于叶子节点。
处理复杂查询:通配符、拼写检查与距离度量
接着我们看到,除了布尔查询和短语查询,我们还可以执行通配符查询。根据通配符的位置,处理难度不同。如果通配符在末尾,处理起来相对容易;如果在开头,则需要一个镜像索引;如果在中间,则可以使用轮排索引的技巧,即在词项后添加特殊符号(如$),然后旋转词项,使得通配符在末尾的情况转换为在开头的情况,从而构建包含所有旋转形式的索引。
另一种方法是计算 k-gram。k-gram 的概念与 K 词索引完全相同,只不过单位从“词”变成了“字母”。例如,“computer”的 3-gram(假设首尾添加了 $ 符号)可能是 $co, com, omp, mpt, pte, ter, er$。通常 2-gram 或 3-gram 很常用。这对于拼写检查也很有用,当你想纠正拼写错误时,k-gram 会很有帮助。
另一种度量词间接近程度的方法是编辑距离,即计算两个词之间需要多少编辑步骤(插入、删除、替换)才能相互转换。这可以通过动态规划高效解决,即填充一个二维网格,最终得到右上角的数字,即两个词之间的编辑距离。这也被称为 Levenshtein 算法。值得一提的是,你在 GitHub 上比较两个分支差异时使用的算法也是这个,只不过比较的单位从字母变成了代码行。
另一种计算词间相似度的方法是 Jaccard 系数,它计算两个词 k-gram 集合的交集大小与并集大小的比值。注意,这不是距离,而是一个相似度系数,因为当两个词完全相同时,其值为 1;完全不相同时为 0。你可以通过数学变换将其转换为距离。
还有一种寻找发音相近的词的方法是 Soundex 算法,它基于词的发音进行编码,但这主要针对英语语言。现在有了人工智能,我们有更好的方法将词映射到语言,但 Soundex 是历史上的先驱。
硬件考量与索引构建
从硬件角度看,我们了解到,要想让任何东西运行得快,别无选择,必须理解硬件、内存、磁盘的速度和成本。你需要花时间了解这些。
缓存和 RAM 比磁盘或磁带昂贵得多,但也快得多,不过容量也小得多。你需要在脑海中建立关于价格、速度和容量的层次结构,并知道前两者需要电力来维持数据,而后两者即使断电数据也能保存。
一旦了解了这些,你就能设计出更高效的索引构建算法。我们看到的一个技巧是用词项ID 替代词项本身,就像给文档分配文档ID一样。在索引构建过程中,(词项, 文档) 对可以转换为 (词项ID, 文档ID) 对。
第一代索引构建算法是 BSBI,它分批处理集合,每次处理一个分区,创建 (词项ID, 文档ID) 对,最后合并它们。
第二代算法是 SPIMI,它改进了 BSBI,不使用词项ID技巧,而是直接为每个批次构建部分标准倒排索引,最后再合并。这适用于数据无法完全放入内存但可以放入磁盘的情况。
如果数据连磁盘也放不下,那就需要使用 MapReduce,这是大数据方法,用于索引整个互联网,利用数据中心集群。同样,你处理的是 (词项, 文档ID) 对,在数据中心环境下,不需要词项ID,因为有充足的空间。谷歌或必应等搜索引擎就是这样构建的。
处理动态数据与压缩技术


我们遇到的下一个问题是,互联网每天都在变化,网页被编辑,新内容被添加。如果你想维护一个搜索引擎,它必须持续更新以反映互联网的最新状态。
处理这个问题的一个技巧是对数合并。你持续用最新数据构建一个新的、较小的索引,然后将其合并到更底层的索引中。这有点像考古学和化石分层,你以对数方式进行合并,即每次合并较小的索引,当某个大小的索引积累到一定数量时,就将它们合并,使索引大小翻倍。越往右,数据越旧。这是一种从算法复杂度角度处理索引定期更新的技巧。
接着我们看到,能够扩展并购买更多机器固然好,但成本高昂,尤其是对于初创公司。实际上,更高效的方法是尝试压缩数据,仅用一台机器完成任务。许多初创公司的第一个原型只需要一台机器。
为此,我们研究了一些统计规律。基于实验的 Heaps‘ Law 告诉我们,随着集合不断增长,添加越来越多的书籍,唯一词项的数量如何增长。我们发现,它大致与集合大小的平方根成正比,这有些令人惊讶。
与之相关的第二定律是 Zipf‘s Law,它描述了词项的集频如何演变。如果按集频降序排列词项并绘制其集频图,会发现它近似服从一条双曲线,即集频与排名的倒数成正比。
基于这些规律,我们找到了压缩方法。我们可以压缩黄色部分的词典,也可以压缩蓝色部分的倒排记录表。一种朴素的方法是可变字节编码,可以节省一些空间,但更好的方法是 Gamma 编码,它是弹性且自适应的。
记住,关键技巧在于倒排记录表是递增的整数排序列表,我们只需要编码整数之间的间隙,而不需要编码实际的整数本身,因为间隙总是正数。
顺便提一下,出于好奇,我实际上已经将这种方法扩展到了编码任何十进制数。它只是在指数部分使用 Gamma 编码,你可以用这种方式编码所有十进制数,并且可以直接在比特位层面比较两个数的大小。但这不属于课程内容。
排序检索与向量空间模型
接着,我们进入了需要对结果进行排序的新一代检索系统。这对人们来说更熟悉,因为使用谷歌、必应等现代搜索引擎时,你会得到前10个结果,点击“下一页”得到接下来的10个,依此类推。我们需要对文档进行排序并计算得分。
我们看到,可以使用关系数据库和基于图书元数据的表单来实现这一点(图书馆过去就是这样做的),但这有些局限,因为你无法触及图书的全文内容。
但如果我们深入全文,查看图书的实际内容,我们可以基于词项频率 和文档频率(更准确地说,是逆文档频率)来计算得分。这样,我们可以将文档表示为向量,每个词项对应一个坐标,其值为 TF-IDF(词项频率与逆文档频率的乘积)。
然后,我们可以使用点积 来计算文档向量与查询向量之间的相似度得分。这是一个了不起的洞见:你可以使用线性代数来计算得分。这意味着文档现在是由数字组成的向量,而不仅仅是布尔值。你拥有一个向量空间,所有文档和查询都是这个空间中的点或向量。现在的美妙之处在于,定位那些最接近查询的文档,它们就是你的结果,越接近的文档在输出中排名越高。
技巧在于使用向量之间的夹角,这可以通过点积来计算,如果你愿意,还可以将其归一化。点积之所以受计算机科学家喜爱,是因为它就是一个大的求和运算。
实现方式是:遍历倒排记录表,将 xi * yi 累加到对应的“桶”(即文档得分)中,然后找出得分最高的那些,它们就是结果。
我们看到,尽管 TF-IDF 是教科书上的标准方法,但它有很多变体。你可以只用 TF 不用 IDF,可以使用 TF 的对数,甚至可以使用布尔值。这些变体可以用一组六个字母的代码来编码,以说明在查询和文档中使用了哪种约定,这被称为 SMART 表示法,因为 SMART 是第一个实现该功能的系统名称。
所有这些催生了向量数据库。我认为大型语言模型也在一定程度上推动了向量数据库的热潮。人们意识到,向量在技术上变得如此重要,以至于我们可以像构建关系数据库、图数据库一样构建专门的向量数据库。这是一个重要的扩展领域,例如在 PostgreSQL 中,你可以创建向量,插入它们,然后在 SQL 中计算 L2 距离(欧几里得距离)、L1 距离(曼哈顿距离)、内积(点积),如果对内积进行归一化,就得到了我们在向量空间模型中一直使用的余弦距离。所有这些都可以通过向量数据库非常高效地执行,目前有大量关于如何高效实现的研究。
概率模型与语言模型:TF-IDF 的理论基石
但是,我们能够使用向量空间并将文档视为数字向量的洞见,实际上源于概率信息检索。虽然我们在课程最后才学习它,但历史上它出现得更早。
因为人们试图通过概率来对文档按相关性进行排序,通过 Elizondo 实验 等方法,实际上可以推导出一个公式,而这个公式正是向量空间模型中点积运算的基础。最初的洞见就来自这种方法。
然后我们意识到,这给出了 IDF 的理论依据。而 TF 的理论依据则来自语言模型。这就是历史上我们如何从概率方法推导出向量空间模型和点积运算的过程。
接着是语言模型,这是我们上周学习的内容,它比 LLM 简单,只生成文档中的词项。你可以计算生成某些词项的概率,我们做了一些假设(如 P_stop, 1-P_stop),以及我们只“记忆”过去两个文档的情况,这给出了生成文档的概率。
我们看到,在没有任何记忆的情况下,我们想要将文档分组到那些拥有完全相同词项但顺序不同的“词袋”中。这很重要,因为计算有多少种组合、多少种顺序,能让我们精确地写出公式,从而将词序列方法转化为词袋方法,两者之间的区别就在于需要乘以的这些因子。
最后,我们通过一个思想实验推导出一个检索模型:你收到一个查询,随机选取一个文档,基于该文档创建一个语言模型,用它生成一些文本,恰好生成了这个查询。然后你问自己:如果发生了这种情况,我最可能选取的是哪个文档?这恰恰给了我们一种基于此对文档进行排序的方法。并且,惊喜的是,我们又得到了一个点积运算,但这次是基于词项频率。正如我们从概率方法中得到 IDF 一样,我们从语言模型这里得到了 TF,这两者共同构成了向量空间模型中的 TF-IDF。



系统评估指标
为了评估系统,我们使用了精确率、召回率、特异度。对你们来说,这些已不再神秘。至于 F1 分数,实际上没什么人用。我还没发现全世界有谁在用“穷举率”或“哭喊率”这样的术语。这里我想说的是,除了精确率、召回率、特异度这三者,其他指标并不常用。为了完整性,F1 分数是精确率和召回率的调和平均数,在实践中确实有时会用到。
对于排序系统,我们有这样的曲线:你不断点击“下一页”,每次更新精确率和召回率,绘制出的曲线就是精确率-召回率曲线。
另一种来自电子学的方法是 ROC 曲线,它也能给出系统性能的一些信息。你希望 ROC 曲线尽可能接近左上角,这表示系统性能好。
通向大型语言模型
现在,我们已经完成了所有内容。我想现在过渡到大型语言模型。你们明年春季可以选修 LLM 课程。
首先,第一个区别是规模。本学期我们看到的系统规模较小,但 LLM 之所以“大”,是因为它们在规模上走向了极端:处理的词元数量、连接的规模都是海量的,涉及整个互联网。这是信息检索与 LLM 的第一个区别。
第二个区别是分词。在信息检索中,我们保持简单,主要使用空格分词(尽管语言预处理仍然适用)。但在 LLM 中,我们不再关心空格。LLM 可以“吞下”任何东西,你可以对任何内容进行分词。这使用了信息论和熵的考虑来寻找最优的分词方式。即使你训练一个不懂“街道”这个词的 LLM,你仍然可以用“街道”来调整它,它就能处理它。因此,LLM 的分词能力更强大,词元不再仅对应于由空格分隔的“词”。
第三个区别是向量表示的含义。在传统信息检索中,当使用向量时,向量的每个坐标对应一个词项(例如,第一个数字对应第一个词项,第二个数字对应第二个词项)。你的向量可能有50万或100万个坐标,每个词项对应一个布尔值或数字。在 LLM 中,情况不再如此。向量对人类来说不再具有直观含义。我们仍然使用向量、向量数据库、点积和邻近度计算,但向量的坐标不再对应具体的词项,它对应的是某种抽象的东西。我们只是将文档和文本放入那个向量空间中,但以一种我们并不真正理解的方式。当然,这是一个鸡生蛋蛋生鸡的问题:我们如何创建一个向量空间并以我们不理解的方式放置文档?这就是机器学习的魔力。我们实际上是通过学习来构建的。我们创建拥有数十亿、数万亿权重(参数)的系统(神经网络),这些神经网络学习如何将文档放置到向量空间中,如何放置这些点。这是在满足我们所有目标约束的条件下学习出来的,它学会了以一种恰好能工作、能理解或创造数据语义的方式来放置点。因此,左边(传统 IR)没有机器学习,我们可以通过编程实现;右边(LLM)我们使用机器学习将点放入向量空间,这种方式对人类来说没有直观意义,但恰好能理解或创造数据的语义。

另一个区别是注意力机制。在传统的语言模型中,我们有马尔可夫链,只关注前面两三个或四个词项。但在 LLM 中,我们不再这样。我们关注所有内容,甚至包括“未来”的内容,而不仅仅是过去。因此,我们有了注意力机制。你不再仅仅看过去的几个词项,而是关注其中的所有内容。例如,如果你有一个代词如“我”、“你”、“他”、“她”,你会将其与可能指代的人或名词的其他位置关联起来。这就是注意力机制的一部分。当然,在生成内容时,有一个技巧是屏蔽未来的部分,因为你无法知道未来。所以在生成时,你使用一个掩码来隐藏未来部分,只基于已有文本进行生成。

最后,所有这些意味着,我们现在不仅可以用于搜索和排序,还可以用于翻译(甚至是实时翻译)、生成文本、总结等。搜索也变得非常有用,当你在互联网上搜索某个内容时,它实际上可以“理解”你在寻找什么(就我之前提到的查询扩展而言,例如“电梯”和“升降机”)。现在,这种语义搜索几乎是原生完成的。


以上是 LLM 课程的“开胃菜”,该课程由 Ryan Cotterell、Marco Baroni 和 Thomas Hofmann 教授,通常每年开设。如果你们想继续深入学习大型语言模型,明年可以在硕士阶段选修。

课程总结与后续安排

本节课中,我们一起回顾了信息检索课程的核心内容,从基础的布尔检索、索引构建,到高级的向量空间模型、概率模型和语言模型,最后展望了这些知识如何构成现代大型语言模型的基础。我们涵盖了文档与词项的抽象、各种索引结构与优化技术、查询处理与扩展、系统评估方法,以及硬件和规模化的考量。
现在,我们即将进入课间休息。休息之后,我将解释考试模式、注意事项,然后为神秘游戏的幸运(也是能力出众的)获胜者颁发奖品,接着进行问答游戏。

我们15分钟后见。
032:课程总结与考试说明 📚

在本节课中,我们将一起回顾苏黎世联邦理工学院信息检索课程的核心内容,并详细了解期末考试的形式、规则以及备考建议。本节内容基于课程总结讲座整理而成。
考试形式与规则 💻
上一节我们回顾了课程的核心知识点,本节中我们来看看期末考试的具体安排。
期末考试是计算机化考试,将在苏黎世联邦理工学院的机房进行。考试使用学校提供的计算机,所有必要软件已预装完毕,考生无需担心安装问题。考试系统基于Windows和Moodle平台。



考生需要使用自己的ETH账户密码登录考试系统。在监考人员发出开始指令后,考试正式开始。考试时长约为三小时,但题目设计旨在让考生能在更短的时间内完成,以消除时间压力。




考试包含大约60道题目。以下是主要的题型:
- 多项选择题
- 判断对错题(即判断多个陈述的真假)
- 可能需要填写文本或数字答案的简答题


请注意,本课程考试不包含任何编程任务。



考试辅助工具与注意事项 📝







了解了考试的基本形式后,我们来看看考试中可用的辅助工具和一些重要规则。







考试电脑上将尝试提供计算器工具,以帮助考生进行简单计算,减少压力。如果使用Moodle内置的计算器,请注意其运算符优先级可能异常,建议使用括号来确保计算顺序正确。






对于非英语母语的考生,考试电脑上通常安装有翻译工具。考生也可以携带通用字典(如英德、英汉字典),但不得携带信息检索专业的专用词典或包含往年试题答案的材料。



如果考试过程中电脑出现死机等故障,请立即举手示意。现场有技术支持人员(TA)提供帮助。因故障损失的时间会在考试结束后单独补回。



考试结束时,即使你已完成答题,也请保持安静,因为其他考生的考试时间可能尚未结束。在监考人员允许后,方可离开考场。


考生可以提前半小时到达考场,并在开考前五分钟内找到座位准备。监考人员会提前说明考试注意事项。




备考资源与建议 📖


在考试准备期间,充分利用以下资源将对你大有裨益。





课程提供了全面的学习材料来帮助你备考。核心资源包括课程幻灯片和授课录像,你可以根据自己的节奏(例如1.25倍或1.5倍速)反复观看。强烈推荐阅读指定的教科书,以深化对理论知识的理解。





虽然考试不考编程,但完成课程中的实践练习(编程作业)能强制你思考知识的结构,是检验理解程度的有效方式。课堂上的即时问答(Clicker Questions)旨在促进互动和思考,其题型与考试题目并不完全相同,但有助于巩固概念。





在整个备考期间,教学团队(教授和助教)都会通过Moodle论坛、Element聊天工具等渠道为大家答疑解惑。请务必充分利用这些支持。


课程结束与祝福 ✨






随着本节内容的结束,本学期的信息检索课程也即将画上句号。


感谢大家本学期的积极参与。教学团队非常享受与大家共同度过的时光。预祝各位在考试中取得优异成绩,我们八月考场上见!
本节课中我们一起学习了期末考试的具体形式、可用工具、重要规则以及有效的备考策略与资源。请合理安排复习计划,充分利用教学支持,祝你成功!

浙公网安备 33010602011771号