想象你走进一家拥有百万藏书的巨型图书馆。新来的管理员阿伟面对一个难题:读者说“我想找关于人工智能如何改变教育的书”,他该怎么在几分钟内从百万本书中找到最相关的几十本? 阿伟的解决办法很巧妙——他给每本书制作一张“标签卡片”,上面写着这本书涵盖的主题词及其重要程度。然后,当读者提出需求时,他也把需求转成同样格式的标签卡片。接下来,他只需要比较两张卡片的“相似程度”,就能排出哪本书最值得推荐。这就像给每本书和每个问题都赋予了一个独特的“数字指纹”,通过测量指纹之间的距离来判断相关性。 这个朴素的想法,正是信息检索史上大名鼎鼎的向量空间模型(VSM)。它是现代搜索引擎、推荐系统、文档分类背后的理论基石,也是理解当下炙手可热的稠密检索与语义搜索的必经之路。本文将带你从零搭建这套“标签指纹系统”,理解它如何工作、为何有效、又存在哪些局限。

岗位关联

掌握向量空间模型,你就掌握了搜索引擎“如何在毫秒间从亿万文档中挑出你最想要的那一个”的核心秘密。这是进入搜索推荐与自然语言处理领域的第一个关键里程碑,也是面试中最高频的基础考点之一。

目录
  1. 可视化演示
  2. 向量空间的几何直觉:把标签卡片变成坐标系
  3. 文档向量构建:TF-IDF权重定乾坤
  4. 查询向量表示:读者需求同样编码
  5. 余弦相似度:用角度衡量相关性
  6. 维度缩减:给词表空间瘦身
  7. 排序能力对比:从二元取舍到连续排序
  8. 现代检索演进:从稀疏词袋到语义向量
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示VSM中文档与查询的向量表示:TF-IDF权重计算、向量空间中的文档分布,以及余弦相似度排序机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

向量空间:文档与查询表示 文档表示为词频权重的高维向量 查询向量与文档向量的夹角决定相关性 夹角越小余弦值越大文档越相关 cos(θ)=(d·q)/(||d||·||q||) 余弦相似度衡量向量方向一致性

向量空间的几何直觉:把标签卡片变成坐标系

阿伟的图书馆逐渐收不住架了。给每本书手工比对标签卡片还能应付几千本,但当藏书量突破十万,手工比对彻底失效。阿伟想:能不能把这些标签卡片变成数学对象,让“相似程度”这个模糊感觉变成精确可计算的数字?

他的突破口是坐标系。我们初中就学过——平面上的任意一点,可以用它在 x 轴和 y 轴上的投影坐标唯一确定。阿伟的灵感是:如果把“每个可能的标签词”都当作一根坐标轴,那么一张标签卡片(一组词及其重要度)不就恰好是空间中的一个点吗?“人工智能”是 x 轴,“教育”是 y 轴,“医疗”是 z 轴……词表有多大,空间就有多少维。

这就是向量空间模型(Vector Space Model, VSM)的几何直觉。它由 Gerard Salton 在 20 世纪 60 年代末提出(也是 SMART 检索系统的核心)。在此框架下,每个文档都被表示成一个高维向量,向量的每一个分量对应词表中的一个词项,分量值的大小代表该词在文档中的重要程度。两篇文档的相似度,转化为它们在空间中的距离或夹角度量。

词表大小决定空间维度,这在实战中有个很直观的名字叫维度灾难——十万维的空间里,绝大多数点都是稀疏的(大部分分量为 0)。阿伟很快意识到,仅靠手工指定权重既不科学也无比低效,必须要有一套自动的、可复制的“编号”方案。下一节,我们就来看这套编号方案是如何设计的,而且它能完全自动化——权重的计算不需要人的主观判断,所有数值来自对文档自身的统计。

d⃗=(w1,1,w1,2,w1,3,…,w1,n)\vec{d} = (w_{1,1}, w_{1,2}, w_{1,3}, \dots, w_{1,n})
通俗地说:一篇文档的向量,就是它在“词项空间”中的坐标;每个词的权重就是它在对应坐标轴上的读数。n 是词表大小,通常数千到数十万。
  1. $\vec{d}$粗体向量符号 d,代表一篇文档(document)。它整体表示一个高维空间中的点,这个点的位置由所有词的权重共同决定。
  2. $w_{1,j}$第 1 篇文档中,第 $j$ 个词的权重。下标中第一个 1 表示文档编号,第二个 $j$ 表示词项编号。如果第 $j$ 个词没有出现在这篇文档中,对应的 $w$ 就是 0。
  3. $(w_{1,1}, w_{1,2}, \dots, w_{1,n})$这是一个 n 元组,将文档在每一根“词项坐标轴”上的读数依次排列。比如词表是[“人工智能”, “教育”, “医疗”],向量 (0.5, 0.3, 0.1) 表示人工智能最重要。

文档向量构建:TF-IDF权重定乾坤

阿伟需要一个自动给标签打分的系统。他观察到:一个词在一本书里出现的次数越多,它往往越能代表这本书的主题——比如一本深度学习书里,“神经网络”会出现数百次;但同时,像“的”“了”“关于”这类虚词几乎出现在每一本书里,出现次数再多也毫无区分度。

于是阿伟设计了两条打分原则。第一,词频(Term Frequency, TF)——词在文档中出现越多越重要。第二,逆文档频率(Inverse Document Frequency, IDF)——包含该词的文档越少,这个词越稀有、越有区分度。两者相乘就得到经典的 TF-IDF 权重:一个词在文档中频繁出现,同时在全部文档中很少出现时,它的权重才会高。

实践中我们通常对 TF 也做归一化,比如使用 $\log(1 + \text{tf})$,同时将整个向量除以它的长度(L2 归一化),这样文档长短差异就不会影响相似度比较。阿伟用这套方法给每本书生成了一个向量——每一个分量都是一个可解释的、有统计依据的权重数字。图书馆的“标签系统”从此自动运转,不再依赖人工。

tf-idf(t,d,D)=tf(t,d)×idf(t,D),idf(t,D)=log⁡Ndf(t)+1\text{tf-idf}(t, d, D) = \text{tf}(t, d) \times \text{idf}(t, D), \quad \text{idf}(t, D) = \log \frac{N}{\text{df}(t) + 1}
通俗地说:TF 回答“这个词在本文中热不热”,IDF 回答“这个词在全库中稀不稀”。两者都高,才能确定这个词是本文的“主题词”。log 是压缩函数,防止稀有词的逆频率值涨幅失控。加 1 是为了防止分母为 0。
  1. $\text{tf}(t, d)$词项 $t$ 在文档 $d$ 中出现的原始次数。比如“人工智能”在文档中出现了 10 次,tf = 10。为防止长文档天然词频更高,常用对数频率归一化 $\log(1 + \text{tf})$。
  2. $\text{df}(t)$词项 $t$ 出现在多少篇文档中(文档频率)。如果“医疗”只出现在全库 10% 的文档中,df 就小;如果“的”出现在所有文档中,df 就等于 N。
  3. $N$文档总数。例如 $N=10000$,某词出现在 100 篇文档中,则 $N/\text{df}=100$,取对数后约 4.6。词越稀有,这个值越高。
  4. $\log$对数运算。它让 IDF 的增长速度随稀有度增加而放缓,避免一个只在某文档出现一次的生僻词获得极端权重。
  5. $+1$平滑项。防止词在全库中都没出现过(df=0)时出现除以零的错误。

查询向量表示:读者需求同样编码

当读者走到阿伟面前说“我想找关于人工智能如何改变教育的书”时,阿伟的第一步不是直接开始翻书,而是拿出他给图书编号的那套系统,把这句话当作一篇“微缩文档”,用同样的 TF-IDF 公式算出它的向量。

这个看似简单的动作背后是 VSM 的核心设计哲学:文档和查询必须映射到同一向量空间。查询本质上就是一串文本,完全可以把它当作一篇超短文档——计算其中每个词的 TF(因为查询本来就短,词频通常是 1),再乘以全库统计出的 IDF。这里的 IDF 用的是全库文档统计的 IDF 值,而不是查询自身统计的。

查询向量构建完毕后,阿伟面前的空间是这样的:每个文档是一个点,查询也是一个点。接下来他需要一个公式,能够快速计算“点与点之间的相似程度”,并在百万个点中排出最接近查询的那个。下一节,我们介绍这个公式——它是整个 VSM 的“引擎”。

q⃗=(wq,1,wq,2,…,wq,n),wq,j=tf(q,tj)×idf(tj)\vec{q} = (w_{q,1}, w_{q,2}, \dots, w_{q,n}), \quad w_{q,j} = \text{tf}(q, t_j) \times \text{idf}(t_j)
通俗地说:查询向量和文档向量使用同一套“坐标系”。查询“人工智能如何改变教育”被映射成空间中的一个点,接下来只需测量这个点和所有文档点的距离即可。
  1. $\vec{q}$查询向量(query vector)。它是查询文本在词项空间中的坐标,与文档向量 $\vec{d}$ 处于同一高维空间。
  2. $w_{q,j}$查询中第 $j$ 个词的权重。它由查询中该词的词频(通常为 1)乘以全局 IDF 得到。
  3. $\text{tf}(q, t_j)$词项 $t_j$ 在查询 $q$ 中出现的次数。查询很短,这个值一般是 0 或 1,但也可以重复(比如查询"AI AI AI"中 AI 的 TF 就是 3)。
  4. $\text{idf}(t_j)$词项 $t_j$ 的全局逆文档频率。关键点:这个值来自整个文档集合的统计,而不是来自查询本身——查询和文档是在共享同一个全局词表坐标系。

余弦相似度:用角度衡量相关性

阿伟站在三维空间中想象:两个点(文档 A 和 文档 B)和一个查询点 Q。用什么指标来定义“相似”最合理?他试过欧氏距离(两点之间的直线距离),但很快发现一个严重问题:一篇 5000 词的长文档和一篇 200 词的短文档,即使主题完全相同,它们的向量长度也差异巨大,欧氏距离会错误地认为它们不相似。

换一个角度思考——阿伟发现,两个向量之间的夹角才是真正反映“方向一致性”的量。从原点发出两条射线分别指向两个向量,如果它们的方向趋近一致,夹角就趋近 0 度;如果方向完全相反,夹角就接近 180 度。这正是余弦相似度的几何含义。

余弦相似度的最大优点是计算友好:向量经过 L2 归一化后,余弦相似度就等价于点积,可以预先算好文档向量并缓存,每秒处理数百万文档毫无压力。阿伟的搜索引擎运行时,只需要用数据库支持的向量索引,把余弦值降序排列,前 K 条就是最终结果。

cos(θ)=q⃗⋅d⃗∥q⃗∥×∥d⃗∥=∑i=1nqi×di∑i=1nqi2×∑i=1ndi2\text{cos}(\theta) = \frac{\vec{q} \cdot \vec{d}}{\|\vec{q}\| \times \|\vec{d}\|} = \frac{\sum_{i=1}^{n} q_i \times d_i}{\sqrt{\sum_{i=1}^{n} q_i^2} \times \sqrt{\sum_{i=1}^{n} d_i^2}}
通俗地说:余弦相似度回答“两条向量指向的方向有多一致”。它只关注方向,不关注长度——所以短文档和长文档只要主题一致,余弦值就高。值域为 [-1, 1],检索场景中通常都在 [0, 1] 区间,因为 TF-IDF 权重非负。
  1. $\vec{q} \cdot \vec{d}$点积(内积)。对应位置权重相乘再求和。如果两个向量在相同词项上都有大权重,它们的点积就大。
  2. $\|\vec{q}\|$查询向量的模长(L2 范数)。计算方式是将每个分量平方后求和再开根号,即 $\sqrt{q_1^2 + q_2^2 + \dots + q_n^2}$。它表示从原点到查询点的直线距离。
  3. $\|\vec{d}\|$文档向量的模长。同样方法计算,代表文档向量的“长度”。
  4. $\sum_{i=1}^{n} q_i \times d_i$对词表从第 1 个词到第 n 个词遍历,把查询向量和文档向量的第 i 个分量相乘,然后累加。分子体现两个向量在多大程度上“同方向发力”。
  5. 除以两个模长这一步把点积归一化,消除向量长度的影响。无论文档是一句标题还是万言长文,最终结果都会被压缩到可控范围——从“绝对距离”变成“方向一致性”。

维度缩减:给词表空间瘦身

从空间几何回到阿伟的图书馆——他发现一个问题:标签体系中的词越收越多,动辄几十万个标签,但很多标签几乎没有区分能力。比如“的”“了”“吗”,几乎出现在每本书中,对任何查询都贡献不了信息。更糟的是,空间维度越高,计算越慢,数据越稀疏,相似度数值越不可靠。

于是阿伟开始“瘦身”。第一刀切掉停用词(stop words)——那些高频虚词直接不进词表。第二刀做词干化/词形归并,把“running、runs、ran“合并成”run“,大幅压缩维度。第三刀是特征选择——保留文档频率在特定区间的词:太常见的词(如”方法“)没有区分度,极罕见的词(只在一篇文档出现)容易造成过拟合,中间的才是”黄金区”。

降维后的词表通常从数十万缩到数万甚至数千。阿伟的检索系统快了 10 倍,而且检索质量不降反升——因为噪声标签被清除后,真正的主题词在向量中的相对权重变得更突出。不过需要注意平衡:维度太少会损失召回率(把相关文档漏掉),维度太多则引入噪声和计算压力。

IDF(t)=log⁡N1+df(t),保留条件: α<df(t)<β\text{IDF}(t) = \log \frac{N}{1 + \text{df}(t)}, \quad \text{保留条件: } \alpha < \text{df}(t) < \beta
通俗地说:维度缩减就是删除标签卡片上那些“每本书都有”或“几乎没人用”的标签,只保留“有区分度的中间地带词汇”。α 和 β 是经验阈值(如 5 和 N/2),用于控制保留词的数量。
  1. $\text{df}(t)$词项 t 的文档频率。它是维度缩减决策的依据——df 太小说明词太罕见、太特殊;df 太大说明词太普遍、没有区分力。
  2. $\alpha < \text{df}(t) < \beta$保留条件。α 过滤掉极端低频词(比如只有 1-2 篇文档出现),β 过滤掉高频虚词。中间地带的词保留进词表。
  3. $N$文档总数。α 和 β 的取值通常与 N 相关,比如 α=3、β=N×0.5,具体通过实验调参确定。

排序能力对比:从二元取舍到连续排序

在向量空间模型出现之前,阿伟使用的老式检索法是布尔模型:查询“人工智能 AND 教育”,文档要么匹配(返回 1)要么不匹配(返回 0)。它像一盏只有开/关两种状态的灯,无法回答“哪本书更相关”——所有匹配的文档地位相同,没有排序,导致用户面对海量结果无从选择。

VSM 的排序能力是对布尔模型的质的飞跃:它天然地支持部分匹配——一篇文档只需要在部分词项上出现信号,就能获得一个介于 0 和 1 之间的相似度分数;所有文档都能参与排序,搜索结果按分数降序呈现。这就像从开关切换发展到音量调节旋钮,用户可以享受到从“完全符合”到“部分相关”的连续体验。

从产品视角看,排序能力决定了搜索引擎的可用性。1990 年代之前,布尔检索只能服务图书馆专业用户;VSM 让普通用户输入一句话就能获得有序结果列表,“搜索引擎”才真正走向大众。这背后的原理,就是今天信息检索产品中所有“相关性排序”概念的开端。

score(q,d)=cos(θ)=∑i∈q∩dqi×di∑qi2×∑di2\text{score}(q, d) = \text{cos}(\theta) = \frac{\sum_{i \in q \cap d} q_i \times d_i}{\sqrt{\sum q_i^2} \times \sqrt{\sum d_i^2}}
通俗地说:布尔模型回答“文档到底匹不匹配”,只能给你一堆无差别的结果;VSM 回答“文档有多匹配”,能给出从 0.01 到 0.99 的精细化排序。注意分子只对查询与文档共同出现的词项求和,没有共现的词贡献为 0。
  1. $i \in q \cap d$求和只覆盖那些同时出现在查询和文档中的词项。如果某词只在文档中出现而查询中没提,它对相似度没有贡献。
  2. $\text{score}(q, d)$文档 d 对查询 q 的相关性得分。检索系统将这个分数作为排序依据,从高到低返回结果。
  3. 分子稀疏性正因为分子只统计共现词,VSM 才能在不同长度的文档之间公平比较——长文档中大量无关词不会拉低相似度,它们只是不参与分子计算。

现代检索演进:从稀疏词袋到语义向量

阿伟的图书系统已经运行得很好,但总有让他尴尬的场景:读者搜索“机器学习”,一本通篇讲“神经网络”的书却不被召回——因为 VSM 完全基于词项匹配,它看不到“神经网络”和“机器学习”在语义上的亲近关系。用户想要的是“语义相关”,VSM 给出的只是“字面相关”。

这个局限源于 VSM 的词袋假设:文档被表达成词的独立性集合,词与词之间的位置、顺序、依赖关系全部被丢弃。二十一世纪以来,研究者尝试让机器“学习”词的语义——从 Word2Vec(2013 年)到 BERT(2018 年),神经网络将每个词投影为一个低维稠密向量,使得“向量空间中距离相近的向量表达相近的语义”。这就是稠密检索(Dense Retrieval)的基本构想。

向量空间模型的框架在半个世纪后依然生命力强劲,这正是它的伟大之处。今天的搜索引擎架构中,稀疏检索(BM25)与稠密检索双路召回,都用余弦相似度打分。对当年的阿伟来说,这个概念永远改变了他的图书馆;而对今天的你来说,它依然是最基础的“检索直觉”——只不过坐标系从词表轴变成了语义轴。

sim(q,d)=v⃗q⋅v⃗d∥v⃗q∥⋅∥v⃗d∥,v⃗=Encoder(文本)\text{sim}(q, d) = \frac{\vec{v}_q \cdot \vec{v}_d}{\|\vec{v}_q\| \cdot \|\vec{v}_d\|}, \quad \vec{v} = \text{Encoder}(\text{文本})
通俗地说:VSM 的向量是“词表上稀疏的计数”,稠密检索的向量是“神经网络从语义中学出来的浓缩坐标”。两者计算相似度的方式相同,但向量的来源天差地别——后者让“意思相近但字面不同”的两段文字也能被判定为相似。
  1. $\text{Encoder}(\text{文本})$一个编码器(如 BERT),输入一段文本,输出一个固定长度的稠密向量。例如 768 维。
  2. $\vec{v}_q$ 和 $\vec{v}_d$编码器输出的查询向量和文档向量。它们比 VSM 的稀疏向量维度低得多(768 vs 50000),但每一维都不为 0,承载着压缩后的语义信息。
  3. 余弦公式复用注意相似度公式形式与 VSM 完全一致——这说明现代稠密检索不是抛弃了 VSM,而是用“更聪明的向量构造方式”取代了“手工/统计的向量构造方式”。

AI 应用场景

场景一:电商搜索引擎的相关商品排序

阶段:检索排序

解决的问题:传统电商搜索用布尔匹配,用户搜“轻薄笔记本”时,包含“轻薄”但不含“笔记本”的商品(比如“超薄本”)全部漏掉。VSM 将每个商品标题/描述构建为 TF-IDF 向量,用户查询同样向量化后,用余弦相似度计算所有候选商品的相关分并排序,实现了部分匹配和连续打分。

具体案例:京东/亚马逊的商品搜索系统。用户搜“适合学生的经济型手机”,系统将其解析为“学生”“经济”“手机”等词元,与所有商品向量的余弦相似度排序后,前 50 名展示给用户。传统布尔模型只能返回同时包含三个词的极少数商品,而 VSM 返回一个平滑排序的候选池。

场景二:文档去重与相似稿件检测

阶段:文本表征

解决的问题:资讯平台每天收到数万篇投稿,需要查出哪些稿件是重复的、哪些是洗稿。VSM 将每篇稿件转化为 TF-IDF 向量后,两两计算余弦相似度;相似度超过 0.85 的两篇就被标记为高度重复,交由人工审核。

具体案例:今日头条/微信公众号的原创保护机制。一篇 3000 字文章被改写后重新投稿,字面改了不少但 TF-IDF 权重分布高度相似,余弦相似度依然很高,系统成功拦截。这正是 VSM “关注方向而非长度”的特性的应用——即使改写后的文章更长或更短,只要核心主题词分布一致,就能被识别为相似。

场景三:智能客服的FAQ匹配系统

阶段:意图识别与回复检索

解决的问题:企业客服系统的 FAQ 库里可能有几千条问答,用户的问题五花八门,难以用规则精确匹配。将每条 FAQ 的问题部分预计算成 TF-IDF 向量,用户提问时实时向量化并计算余弦相似度,返回相似度最高、且高于阈值的那条 FAQ 作为答案。这是 VSM 在有监督数据稀缺时的快速启动方案。

具体案例:某银行智能客服上线初期,将 2000 条标准问答(如“信用卡怎么挂失?”)构建索引。用户问“我的卡丢了怎么办”,系统计算余弦相似度后命中“信用卡挂失流程”这条 FAQ,准确率约 78%。后续接入 BERT 语义模型后提升到 90% 以上,但 VSM 仍是冷启动阶段的标配。

Python 演示

python107 行
# -*- coding: utf-8 -*-"""迷你向量空间模型搜索引擎使用 TF-IDF + 余弦相似度对文档集合进行检索排序"""import mathimport refrom collections import Counter try:    import jiebaexcept ImportError:    raise ImportError("请先安装 jieba: pip install jieba")  class MiniVSMSearchEngine:    """一个极简的向量空间模型搜索实现"""     def __init__(self, stop_words=None):        self.stop_words = set(stop_words or ["的", "了", "和", "是", "在", "我", "有", "就", "不", "也", "都", "而", "及"])        self.documents = []        self.df = Counter()          # 文档频率统计        self.idf = {}        self.doc_vectors = []        # 文档的 L2 归一化向量        self.total_docs = 0     def tokenize(self, text):        """使用 jieba 分词并过滤停用词"""        return [tok for tok in jieba.lcut(text.lower())                if tok.strip() and tok not in self.stop_words]     def index(self, docs):        """构建索引:统计 df, idf 并计算所有文档向量"""        self.documents = docs        self.total_docs = len(docs)        tokenized_docs = [self.tokenize(doc) for doc in docs]         # 统计每个词的文档频率        for tokens in tokenized_docs:            for term in set(tokens):                self.df[term] += 1         # 计算 idf        N = self.total_docs        for term, df_val in self.df.items():            self.idf[term] = math.log((N + 1) / (df_val + 1)) + 1.0         # 构建文档向量(直接存储 L2 归一化向量)        self.doc_vectors = [self._build_vector(tokens) for tokens in tokenized_docs]     def _build_vector(self, tokens):        """从分词结果构建 L2 归一化的 TF-IDF 向量"""        tf = Counter(tokens)        vec = {}        for term, freq in tf.items():            if term in self.idf:                vec[term] = (1 + math.log(freq)) * self.idf[term]        norm = math.sqrt(sum(w * w for w in vec.values()))        if norm > 0:            vec = {k: v / norm for k, v in vec.items()}        return vec     def _compute_query_vector(self, query):        """把查询当作微型文档,使用同一套 idf 计算查询向量并归一化"""        tokens = self.tokenize(query)        tf = Counter(tokens)        vec = {}        for term, freq in tf.items():            if term in self.idf:                vec[term] = (1 + math.log(freq)) * self.idf[term]        norm = math.sqrt(sum(w * w for w in vec.values()))        if norm > 0:            vec = {k: v / norm for k, v in vec.items()}        return vec     def search(self, query, top_k=3):        """计算查询向量与所有文档向量的余弦相似度,返回 top_k 结果"""        query_vec = self._compute_query_vector(query)        scored = []        for idx, doc_vec in enumerate(self.doc_vectors):            # 余弦相似度 = 计算两个归一化向量的点积            score = 0.0            smaller, larger = (query_vec, doc_vec) if len(query_vec) < len(doc_vec) else (doc_vec, query_vec)            for term in smaller:                if term in larger:                    score += smaller[term] * larger[term]            scored.append((idx, score))        scored.sort(key=lambda x: x[1], reverse=True)        return [(idx, score) for idx, score in scored[:top_k] if score > 0]  if __name__ == "__main__":    docs = [        "深度学习是机器学习的一个分支,它使用多层神经网络进行学习",        "机器学习是人工智能的核心领域,关注如何从数据中学习规律",        "神经网络在图像识别和自然语言处理中取得了巨大的成功",        "人工智能正在改变医疗、教育、金融等多个行业",        "气候变化是当今世界面临的最大挑战之一,需要全球协作",        "搜索引擎使用信息检索技术帮助用户找到相关的网页",        "推荐系统通过分析用户行为来推荐用户可能感兴趣的商品",    ]    engine = MiniVSMSearchEngine()    engine.index(docs)    results = engine.search("机器学习和神经网络的关系", top_k=3)    print("查询: 机器学习和神经网络的关系")    for idx, score in results:        print(f"  [{score:.4f}] doc{idx + 1}: {docs[idx][:20]}...")
目的:这段代码完整实现了向量空间模型的核心流程——中文分词、TF-IDF 加权、L2 归一化、查询向量化与余弦相似度排序。你可以拿自己的语料替换 docs 数组,即可获得一个可用的迷你检索系统。
  1. self.df[term] += 1记录每个词出现在多少篇文档中,用于后续计算逆文档频率(IDF)。
  2. math.log((N + 1) / (df_val + 1)) + 1.0计算 IDF 的平滑版本,加 1 避免除零,末尾 + 1 保证 IDF 恒为正。
  3. vec[term] = (1 + math.log(freq)) * self.idf[term]用子线性 TF 缩放(1+log(freq))减少高频词的权重膨胀效应,再乘以 IDF 得到 TF-IDF 值。
  4. norm = math.sqrt(sum(w * w for w in vec.values()))计算向量的 L2 模长,为归一化做准备。
  5. vec = {k: v / norm for k, v in vec.items()}将向量除以模长,使其长度为 1。归一化后,余弦相似度退化为简单点积,计算更快。
  6. for term in smaller: if term in larger:只遍历词数较少的向量,节约计算时间;只有在两个向量中都出现的词才参与点积。
  7. score += smaller[term] * larger[term]累加共现词的权值乘积,这等价于两个归一化向量的点积,也即余弦相似度。
  8. scored.sort(key=lambda x: x[1], reverse=True)按相似度分数降序排序,得到最相关的文档列表。

常见误区

误区“向量空间模型把文档变成向量,所以它能理解词和词之间的语义关系。我搜'汽车'应该能查到'轿车'相关的文章。” 很多初学者把 VSM 当作语义理解工具,认为它像人一样知道“轿车”是“汽车”的一种。

正解VSM 是一个纯粹的字面词项匹配模型。它的向量坐标是词袋(bag of words)统计,不包含任何语义知识。搜“汽车”时,只要文档中不含“汽车”这个词,无论它多么深入地讨论“轿车”和“发动机”,都不会出现在结果中。这正是“词汇鸿沟”问题(vocabulary gap)——同样的语义可以用完全不同的词表达,词袋模型看不见这层关系。如果要实现语义匹配,需要走向稠密检索(如 BERT 编码的向量),把“语义相近→向量相近”作为学习目标。

为何常见:因为“向量”一词在现代 AI 中经常与“词嵌入/语义向量”联系起来,初学者容易把 VSM 的统计向量和神经网络的语义向量混为一谈。两者都叫向量,但一个是稀疏字面计数,一个是稠密语义编码。

误区“既然余弦相似度是 VSM 的标准度量,那不管什么场景,用余弦相似度就对了。” 一些初学者将余弦相似度视为万能药,不做任何分析直接套用。

正解余弦相似度只对“向量方向有意义、长度无关紧要”的场景适用。在 TF-IDF 文本检索中它很好用,因为文档长度差异不该影响主题相似度。但换一个场景——比如推荐系统中的用户评分向量(1-5 分),向量长度代表用户的评分活跃度(有些人总打 5 分,有些人总打 3 分),此时余弦相似度会忽视这种系统性偏差,可能不如皮尔逊相关系数(中心化余弦)或欧氏距离。选择相似度度量必须考虑数据的生成机制和应用目标。

为何常见:教科书和教程大量使用余弦相似度介绍 VSM,给初学者留下深刻印象,形成了“相似度=余弦”的固定思维,忽略了度量选择本身就是一个需要论证的设计决策。

误区“IDF 高意味着这个词很稀有、很有区分度,所以它一定是最重要的词。” 新手常以为 IDF 值最高的词就是文档的最佳代表词。

正解IDF 高只能说明这个词在全库中很少出现,但它不一定对查询有用。一个极端例子:某篇文档中包含一个只在本文出现的生僻错别字,这个词的 IDF 极高,但它既不是文档主题也对理解文意毫无帮助。真正有效的量化是 TF 与 IDF 的乘积——只有在一个文档中出现频率高(该文档中重要)且全库中出现频率低(全局有区分度)的词,才同时具备代表性和区分能力。单独把 IDF 拎出来看没有意义。

为何常见:IDF 的公式看起来简单直观(“越稀有越重要”),这种直觉容易被过度简化。学习者忽略了 TF 的协同作用——稀有性必须配合局部频率才有价值。

误区“停用词对检索没有作用,所以停用词表越全越好,把所有虚词都过滤掉。” 有人在使用 VSM 构建检索系统时,把停用词表扩展到数千词,认为这样能提升效果。

正解停用词过滤的本质是精度与召回的权衡。过度过滤会误伤有用词汇。比如搜索“to be or not to be“(莎士比亚名句),如果”to“和”be“都在停用词表中,查询向量就只剩一个”not“,检索结果完全偏离。现代搜索框架(如 Elasticsearch)已趋向取消手工停用词表,改用 TF-IDF/BM25 的 IDF 机制让高频词自动获得低权重——”的“即使出现在文档中也几乎不影响向量角度,自然失去干扰能力。手工过滤停用词反而是一种”硬编码的脆弱方案”。

为何常见:早期内存和计算资源有限,过滤停用词能显著缩小倒排索引规模,形成了“过滤停用词=标准操作”的思维惯性。今天资源不再是瓶颈,算法本身已能自适应词频权重。

误区“维度灾难让向量空间又大又稀疏,所以把维度降得越低,检索越快效果越好。” 初学者容易把降维等同于性能优化,认为维度越低越好。

正解维度缩减的效果是一个 U 形曲线。维度太高时噪声多、计算慢;维度太低时信息丢失严重——很多有区分度的词被删掉,两篇不同主题的文档可能因为共享了仅剩的几个高频词而获得高相似度。本质上这是一个偏差-方差权衡(bias-variance tradeoff):降维让模型更稳定(方差低)但可能丢失信号(偏差高)。实践中需要通过验证集实验寻找最优维度,而不是一味追求低维度。

为何常见:“维度灾难”概念被强调得太多,给初学者留下“维度高就是坏”的简单印象。实际上精确地说,问题在于“与有效信号相比,无关维度过多”,而不是维度数本身。

面试题

Q1. 向量空间模型(VSM)中,一篇文档被表示成什么?

单选题 · easy

  • A.一个布尔表达式,表示词是否出现
  • B.一个高维向量,每维对应词表中的一个词← 正确答案
  • C.一个概率分布,表示词的生成概率
  • D.一个字符串序列,保留词的原始顺序

答案:B。VSM 将文档表示为高维向量,每个维度对应词表中的一个词项,分量为该词的权重(如 TF-IDF)。选项 A 是布尔模型的特征;C 是概率检索模型的特征;D 保留词序,而 VSM 基于词袋假设丢弃词序。

Q2. TF-IDF 中 IDF 的作用是什么?

单选题 · easy

  • A.衡量词在单篇文档中出现频率的高低
  • B.衡量词在全部文档中的稀有程度← 正确答案
  • C.计算文档的字节长度
  • D.对查询进行分词

答案:B。IDF(逆文档频率)衡量一个词在整个文档集合中出现的稀有程度——包含该词的文档数越少,IDF 越高。选项 A 描述的是 TF 的职责;C 与检索无关;D 是预处理步骤。TF 与 IDF 相乘,让“局部高频 + 全局稀有”的词获得高权重。

Q3. 余弦相似度的值域是?

单选题 · easy

  • A.[0, 1]
  • B.[-1, 1]← 正确答案
  • C.(-∞, +∞)
  • D.[0, +∞)

答案:B。余弦相似度的数学定义是两向量夹角的余弦,值域为 [-1, 1]。当两个向量方向完全一致时为 1,完全相反时为 -1,垂直时为 0。在实际的 TF-IDF 文本检索场景中,由于权重非负,结果通常落在 [0, 1] 区间。选项 A 混淆了实际场景和理论值域。

Q4. 两个向量经过 L2 归一化后,余弦相似度等价于什么运算?

单选题 · mid

  • A.欧氏距离
  • B.曼哈顿距离
  • C.点积(内积)← 正确答案
  • D.叉积(外积)

答案:C。L2 归一化把向量模长变为 1,此时余弦公式的分母等于 1,余弦相似度退化为两个向量的点积。因此工业实现中常预先归一化文档向量,查询时直接算点积即可。欧氏距离在归一化后反而与余弦相关但不等价(距离越大,余弦越小),曼哈顿距离和叉积都没有这个性质。

Q5. 相比布尔模型,向量空间模型最核心的改进是什么?

单选题 · mid

  • A.支持布尔逻辑 AND/OR/NOT 运算
  • B.支持部分匹配和相关性排序← 正确答案
  • C.支持模糊匹配拼写纠错
  • D.支持文档分类和聚类

答案:B。布尔模型回答文档是否匹配(0 或 1),所有匹配文档地位相同;VSM 对文档连续打分,即使只匹配了查询中的部分词项也能获得一个相似度分数,从而产生排序。A 是布尔模型本身的能力;C 拼写纠错属于查询处理;D 是 VSM 的衍生应用而非与布尔模型的区别。

Q6. 在 VSM 中,为什么通常要过滤停用词?

单选题 · mid

  • A.停用词在每篇文档中出现,对区分主题没有帮助← 正确答案
  • B.停用词没有 UTF-8 编码,会导致乱码
  • C.停用词长度太长,占用大量存储空间
  • D.停用词包含敏感信息,需要清除

答案:A。停用词(如“的”“了”“和”)出现在几乎所有文档中,导致 IDF 趋近于零,对区分不同主题的文档毫无贡献。将它们从词表中移除可以降低维度、减少噪声。B、C、D 均不是真实原因——停用词有正常编码、长度通常较短、也不包含敏感信息。

Q7. 以下哪些是向量空间模型的局限性?

多选题 · mid

  • A.基于词袋假设,忽略词序信息← 正确答案
  • B.无法捕捉同义词和多义词的语义← 正确答案
  • C.高维稀疏向量带来计算和存储压力← 正确答案
  • D.无法对检索结果进行排序

答案:ABC。VSM 将文档视为词的集合,忽略顺序(A);纯字面匹配无法识别“轿车”和“汽车”的语义等价(同义词),也区分不了“苹果手机”和“苹果水果”中的“苹果”(多义词)(B);词表动辄数万导致向量高维稀疏(C)。D 错误——VSM 的核心贡献恰恰是引入了排序能力。

Q8. TF-IDF 权重设计中,以下哪些说法是正确的?

多选题 · mid

  • A.TF 衡量词在单篇文档中的出现频率← 正确答案
  • B.IDF 衡量词在全库中的稀有程度← 正确答案
  • C.TF 和 IDF 的乘积衡量词对区分文档主题的综合能力← 正确答案
  • D.一个词出现次数越多,TF-IDF 权重一定越高

答案:ABC。A 和 B 分别是 TF 和 IDF 的定义;C 是 TF-IDF 乘积的意义——结合局部重要性和全局区分度。D 错误:即使 TF 很高,如果这个词出现在全库几乎所有文档中,IDF 趋近于 0,TF-IDF 乘积仍可能很低(如“的”字)。

Q9. 查询“深度学习和机器学习的区别”,文档中同时出现“深度学习”和“机器学习”,但没有“区别”一词。VSM 处理该文档的得分情况是?

单选题 · hard

  • A.得分为 0,因为“区别”不在文档中,按布尔逻辑判定不匹配
  • B.得分较高,因为“深度学习”“机器学习”的贡献足以让文档获得正分← 正确答案
  • C.得分为 1,因为匹配了两个词且为归一化向量
  • D.得分为负,因为存在词汇鸿沟导致惩罚

答案:B。VSM 支持部分匹配——即使“区别”词项没有在文档中出现,该词在点积中贡献为 0,但“深度学习”“机器学习”共现词的权重乘积仍然贡献一个正分数。这正是 VSM 相对布尔模型的核心优势。C 错误:得分不是非 0 即 1,而是一个连续相似度;D 错误:VSM 没有负向惩罚机制。

Q10. 现代稠密检索(Dense Retrieval)与 VSM 的关系是?

多选题 · hard

  • A.稠密检索完全取代了 VSM,两者没有任何关系
  • B.稠密检索沿用 VSM 的高维稀疏向量表示方式
  • C.稠密检索使用神经网络学习语义向量,替代人工设计的 TF-IDF 权重← 正确答案
  • D.稠密检索同样用余弦相似度或点积计算相关性← 正确答案

答案:CD。C 正确——稠密检索的核心区别在于向量的来源从统计计数(TF-IDF)变为神经网络编码(如 BERT),向量维度低且稠密,能捕捉语义。D 正确——向量相似度的计算方式仍然常用余弦相似度或等价的内积。A 错误:稠密检索不是取代而是继承了 VSM 的框架思想;B 错误:稠密检索是低维稠密向量而非高维稀疏。