深夜的美食街上,老饕老王想找一道“苹果清甜中带着一点香蕉醇香”的菜。他没有挨家挨户翻菜单,而是闭上眼睛想象:每道菜都可以抽象成一张“食材用量表”——苹果 3 份、香蕉 1 份、盐 0.5 份……他自己心里那团渴望,其实也是一张“心愿用料表”。两张表越接近,这道菜就越可能合胃口。 向量空间模型(Vector Space Model,VSM)做的正是这件事。它把每篇文档变成一张“配料表”(文档向量),把你的查询也变成一张“配料表”(查询向量),然后在高维空间里比较两个向量的夹角:夹角越小,说明文档与查询的“口味”越一致,相关度越高。这个诞生于上世纪六七十年代的经典模型,统治了搜索引擎超过二十年。本篇用“点菜找菜”这条线索,带你拆解 VSM 的每一张配方卡:文档如何变成向量、词频和逆文档频率各管什么、余弦相似度怎么算,以及它今天为什么仍是轻量检索与教学场景的“出厂配置”。
掌握向量空间模型,等于拿到信息检索的“出厂设置”:既能理解搜索排序的原理与局限,也能在工程中用最轻量的方案快速落地检索需求,并为理解 BM25 与神经检索打下地基。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
核心思想:一切从口味空间说起
老王逛美食街的经验告诉我们一个道理:找菜的本质不是“逐字逐句读菜单”,而是把菜和心愿都抽象成可比较的“配方表”。向量空间模型的创始人 Gerard Salton 在 1960 年代把这个直觉变成了数学:文档是一张配方表,查询也是一张配方表,剩下的工作就是比较两张表有多接近。
具体来说,VSM 只做三件事。第一,把文档映射成一个向量;第二,把查询映射成相同空间里的另一个向量;第三,计算两个向量的夹角相关性,夹角越小,相关度越高。这个“空间”的每个维度都由词典中的一个词充当坐标轴,向量在每个维度上的取值,就是该词在这篇文档中的权重。
于是文档 $d$ 被写成下面的形式:
回到老王的故事:只要美食街每个档口都贴一张数字配方,再用同一套规则把心愿也画成配方,剩下的比较就能交给“夹角”了。接下来六节,就是把这张配方卡一步步拆开看。
- 向量整体 $\vec{d}$带箭头的 $d$ 表示这是“文档 d”对应的多维向量,而不是单个数字。
- 权重分量 $w_{i,d}$下标 $i$ 指向词典中第 $i$ 个词,$w_{i,d}$ 就是这个词在文档 $d$ 中的权重。
- 维度个数 $n$$n$ 等于词典大小,通常达到数万甚至数十万,决定了向量空间有多“高维”。
文档表示:先把菜谱变成配料清单
老王记菜谱有个习惯:只记“苹果 3 个、香蕉 1 根、盐半勺”,从不记“先放盐还是后放糖”。因为决定味道的是配料和用量,而不是下锅顺序。VSM 的“词袋模型”(Bag of Words)就是这个思路——把文档拆成一袋子词,无视顺序和语法,只统计每个词出现了几次。
在工程上,这一步要先做预处理:分词把句子切成词,去停用词丢掉“的、了、在”这类高频无意义词,必要时再做词干还原。然后构建词典,词典里的每一个词就是向量空间里的一个坐标轴。最后逐篇统计次数,把文档变成计数向量:
词袋向量有一个鲜明的性格:稀疏。一篇文档通常只用到词典里一小部分词,绝大多数分量为 0——就像一盘菜不可能用上超市里所有食材。稀疏高维是 VSM 后续一切工程技巧(倒排索引、稀疏存储)的出发点。
- 文档下标 $j$用 $d_j$ 表示语料库里的第 $j$ 篇文档。
- 计数分量 $c_{ij}$$c_{ij}$ 是词 $t_i$ 在文档 $d_j$ 中的出现次数——这就是最原始的“配料用量”。
- 维度个数 $m$$m$ 是词典总词数,因此每篇文档向量都有 $m$ 个分量。
TF 词频:这道菜下了多少料
同样一道苹果派,老王想知道苹果味到底有多浓。单纯看“苹果出现了几次”不够公平:一份 300 克的大份苹果塔和一份 100 克的小份苹果塔,苹果的绝对次数差了好几倍,但口味浓度可能完全一样。词频(Term Frequency,TF)解决的就是这个“浓度”问题。
TF 的经典做法是长度归一化:用词在文档中的出现次数,除以文档的总词数。这样一来,长文档天然“次数多”的偏差就被抵消了,得到的是一个 0 到 1 之间的相对浓度:
工程上还有进一步改良:对次数做子线性缩放(如 $1 + \log c$),让疯狂重复的词不至于无限霸榜——就像盐放得再多,菜也不会因此“更有营养”。但 TF 只解决了“下料足不足”,还没回答“料够不够特别”,这正是下一节 IDF 登场的原因。
- 分子 $c_{t,d}$词 $t$ 在当前文档中出现的原始次数。
- 分母 $\sum_{k=1}^{n} c_{k,d}$把文档里所有词的次数加起来,等于文档总词数。
- 比值 $tf_{t,d}$次数占比,取值在 0 到 1 之间,表示词在文档中的相对浓度。
IDF 权重:这味料有多特别
老王点菜有一条铁律:说“来点咸的”等于没说,因为每道菜都有盐;可要是说“要松露味”,整条美食街瞬间只剩一个档口够格。词的“信息量”恰恰藏在这种稀有度里。逆文档频率(Inverse Document Frequency,IDF)就是度量“稀有度”的尺子。
先定义文档频率 $df_t$:整个语料库中“包含词 $t$”的文档篇数。$df_t$ 越大,说明词越常见、越没有区分度。IDF 用总文档数除以它再取对数:
工程上为了防止某个词从未出现导致除零,常在分母加 1(如 $\log \frac{N}{df_t + 1}$)。至此,老王手里的配方卡有了两列信息:下料浓度(TF)和佐料稀缺度(IDF)。但怎么把两列信息合起来判断“方向一致”,还得靠余弦相似度。
- 文档频率 $df_t$数一数有多少篇文档包含了词 $t$——注意是“篇数”,不是“出现次数”。
- 文档总数 $N$语料里所有文档的篇数,是衡量“全”的基准。
- 稀有度比值 $\frac{N}{df_t}$比值越大,词越稀有;最小为 1(词出现在所有文档),最大为 $N$(词只出现在一篇文档)。
- 对数 $\log$把差距压缩,防止极端稀有词的权重爆炸,同时让中间地带更平滑。
余弦相似度:两道菜的默契度
有了配方,老王开始比较:他直接忽略“分量”看“方向”。一份苹果:香蕉 = 2:1 的大盘沙拉,和一份同样配比的迷你沙拉,对他来说就是同一种味道——方向一致,只是量不同。余弦相似度衡量的正是这种“方向一致性”,它对向量长度不敏感。
计算余弦相似度,需要两个向量做点积,再各自除以模长。点积衡量两向量在相同维度上的重合程度(查询里出现的词,文档里有没有、权重高不高);模长相当于向量的“总长度”。公式如下:
这解释了为什么 VSM 不用欧氏距离:两份文档一份是另一份的三倍长但配比完全相同,余弦给满分,欧氏距离却判它们“很远”。老王要的是“味道合不合”,不是“分量大不大”,所以余弦才是他的知己。
- 点积 $\vec{q} \cdot \vec{d}$把查询和文档在同一个词上的权重相乘,再对所有词求和——重合的词越多、权重越高,得分越大。
- 模长 $\|\vec{q}\|$查询向量的长度,等于各分量平方和再开根号。
- 分母 $\|\vec{q}\| \cdot \|\vec{d}\|$两个模长相乘,起到归一化作用,把不同长度的向量拉回同一尺度。
- 结果范围余弦值落在 $[-1, 1]$;检索场景权重非负,实际通常落在 $[0, 1]$。
TF-IDF 组合:合出最终权重
TF 回答“下料足不足”,IDF 回答“料够不够特别”,只用任何一个都会偏科。只看 TF,“盐、水”这类每道菜都有的原料必然霸榜;只看 IDF,全文只出现一次的错别字可能稀有到反常,却毫无意义。VSM 的标准做法是把两者乘起来,合出每个词在文档中的最终权重:
对老王来说,苹果这道主料既在下料表里分量足(TF 高),又在整条街的菜单里不算泛滥(IDF 中高),于是权重被放大;而“盐”因为家家都有,即使下料不少也被压制。一张 TF-IDF 权重向量,就是一份去粗取精的“独门配方”。
- TF 部分 $tf_{t,d}$词在本文档中的相对浓度,解决“这篇文章有多强调这个词”。
- IDF 部分 $idf_t$词的全局稀有度,解决“这个词在全语料里有多能区分文档”。
- 乘积 $w_{t,d}$两值相乘,只有“既多又特别”的词才能拿到大权重。
工程与局限:后厨的苦与天花板
如果美食街有三百个档口,老王不会挨家精算夹角——他会先让跑堂的筛出“哪些档口确实用过苹果或香蕉”,再对候选们细致比较。搜索引擎的工程实现也是如此:先用“倒排索引”粗筛,再用 VSM 精算。
倒排索引是一张“反着查”的表:从词出发,直接找到包含它的文档列表。查询“苹果 香蕉”时,取两个词的文档列表求交集,得到候选集合;再对候选逐篇算余弦相似度并排序打分:
但 VSM 的天花板也很明显:词袋模型不懂语义、不读词序——“苹果咬了牛顿”和“牛顿咬了苹果”在它眼里完全相同,同义词也无法互相认出。正因如此,它才被加入饱和与长度归一化的 BM25、以及具备语义能力的神经检索模型超越。不过,无监督、可解释、零训练成本这些优点,让它至今仍是教学与轻量场景的标配,也是理解现代搜索的起点。
- 求和符号 $\sum_{t \in q}$对查询中出现的每个词逐一处理,累加贡献。
- 查询权重 $w_{t,q}$词 $t$ 在查询向量里的 TF-IDF 权重,代表用户有多强调它。
- 文档权重 $w_{t,d}$词 $t$ 在文档向量里的 TF-IDF 权重,两者相乘得到该词的匹配贡献。
AI 应用场景
企业知识库的轻量检索
阶段:检索与排序
解决的问题:中小团队没有 GPU、没有标注数据,用 SQL 的 LIKE 模糊查询只能做关键词匹配、无法按相关度排序;引入深度模型又周期太长。VSM 无需训练、当天可上线,用 TF-IDF + 余弦相似度就能把“匹配”升级为“按相关性排序”。
具体案例:某 SaaS 公司的工单系统沉淀了上万条历史工单。工程师把工单文本分词后用 TF-IDF 向量化,用户提交新问题时同样向量化,余弦相似度取 Top 5 推荐相似工单,客服效率提升明显。推荐逻辑与本文 Python 演示代码一致,只是用在线倒排索引做了候选粗筛。
新模型评估的基线对照
阶段:模型评估
解决的问题:评估一个神经检索模型时,需要知道“不学习也能达到多少分”,否则无法判断模型的提升到底来自语义理解还是数据泄露。VSM 是天然的无监督基线。
具体案例:研究员构建对话检索系统时,先用 VSM 跑出 MRR@10 基线,再对比双塔编码器的表现。如果神经模型只比 VSM 高几个点,就要反思训练数据或模型结构是否存在问题。
文本分类的特征工程
阶段:特征表达
解决的问题:传统机器学习模型无法直接吃原始文本,需要把文本变成数值特征。TF-IDF 向量可解释、与词表一一对应,配合线性模型就是高效的轻量分类方案。
具体案例:垃圾邮件分类中,团队将每封邮件转成 TF-IDF 向量,再训练逻辑回归分类器,几分钟完成训练即可上线拦截——这与神经分类器形成“成本-效果”的梯度互补。
Python 演示
import mathimport refrom collections import Counter def tokenize(text): """按单词切分并转小写(中文场景替换为分词器即可)""" return re.findall(r'\w+', text.lower()) class VectorSpaceModel: """最小可用的向量空间模型:TF-IDF 加权 + 余弦相似度""" def __init__(self, docs): self.docs = [tokenize(d) for d in docs] self.doc_freq = Counter() for tokens in self.docs: for term in set(tokens): self.doc_freq[term] += 1 self.doc_count = len(self.docs) def idf(self, term): """IDF:文档总数除以含该词的文档数,再取对数""" return math.log(self.doc_count / (1 + self.doc_freq[term])) def tf(self, term, tokens): """TF:词在文档中出现的次数占比""" return tokens.count(term) / len(tokens) def weight(self, tokens): """把文档或查询转成 TF-IDF 权重向量(字典)""" vec = {} for term in set(tokens): vec[term] = self.tf(term, tokens) * self.idf(term) return vec def cosine(self, v1, v2): """余弦相似度:点积除以两个向量的模长乘积""" if not v1 or not v2: return 0.0 dot = sum(w * v2.get(t, 0.0) for t, w in v1.items()) norm1 = math.sqrt(sum(w * w for w in v1.values())) norm2 = math.sqrt(sum(w * w for w in v2.values())) return dot / (norm1 * norm2) if norm1 and norm2 else 0.0 def search(self, query, top_k=3): """对查询打分,返回最相关的 top_k 个文档""" q_vec = self.weight(tokenize(query)) scored = [] for i, tokens in enumerate(self.docs): score = self.cosine(q_vec, self.weight(tokens)) scored.append((score, i)) scored.sort(reverse=True) return scored[:top_k] if __name__ == '__main__': corpus = [ "apple banana apple orange", "banana orange banana orange", "apple apple apple banana", "the fox jumps over the lazy dog", ] vsm = VectorSpaceModel(corpus) query = "apple banana" print("查询:", query) for score, idx in vsm.search(query): print("文档 %d 评分: %.3f" % (idx, score))
self.doc_freq[term] += 1统计每个词出现在几篇文档中,得到 dfmath.log(self.doc_count / (1 + self.doc_freq[term]))计算 IDF,分母加 1 平滑防止除零tokens.count(term) / len(tokens)归一化 TF:出现次数除以文档总词数for term in set(tokens)先对词去重,保证每个词只算一次权重sum(w * v2.get(t, 0.0) for t, w in v1.items())点积:相同词权重相乘再累加math.sqrt(sum(w * w for w in v1.values()))计算向量模长,供分母归一化return dot / (norm1 * norm2)余弦相似度:点积除以双模长积scored.sort(reverse=True)按相似度降序排列,取前 top_k 作为检索结果
常见误区
误区我以为 TF 越大,词对相关性的贡献就越大,所以文档里重复最多的词,应该就是最能代表这篇文档的词。
正解重复最多不代表最有区分度——“的、了、我们”在每篇中文文档里都高频出现,却几乎不携带主题信息。IDF 正是为压制这类词而存在:一个词在越多的文档中出现,它的权重就越低。只有 TF 与 IDF 相乘,才能同时保证“下料足”且“料特别”,缺一不可。
为何常见:初学时先接触 TF,很容易把“出现多”直接等同于“重要”,忽略了文档频率这个全局视角。
误区我以为向量越相似,它们在空间里的“直线距离”就越近,欧氏距离和余弦相似度是一回事。
正解两者衡量的维度完全不同。余弦只看方向夹角,对向量长度不敏感;欧氏距离看的是绝对坐标差。一份文档的长文本副本与原文配比完全一致,余弦给满分,欧氏距离却判它们很远。VSM 权衡的是“口味方向”而不是“分量大小”,所以选择余弦。
为何常见:中学几何用坐标系理解距离的直觉根深蒂固,容易把“空间里离得近”直接等同于“文本相似”。
误区既然 IDF 越大说明词越稀有,那查询里只放生僻词、不放常见词,检索效果一定最好。
正解权重是 TF 与 IDF 的乘积。一个稀有词如果只在文档里出现一次,TF 很小,乘积不一定高;完全生僻的词还可能来自错别字或专名,反而误导检索。而且用户的真实查询大多是常见词,刻意回避常见词等于丢弃大多数有效信号。
为何常见:IDF“惩罚常见词”的效果令人印象深刻,让人误以为常见词毫无价值,忽略了它与 TF 的合作关系。
误区我听说它能算出“苹果和 iPhone 相似”,以为 VSM 懂得词义,甚至能理解同义词。
正解VSM 是纯词袋统计:不看词序、不读搭配、不懂语义。词典里两个不同的词是完全独立的维度,“苹果”和"iPhone"在 VSM 眼里没有任何直接联系。它捕捉的只是统计层面的共现规律,而非意义本身。
为何常见:“相似度”三个字容易让人联想到语义相似,但这里的相似度只是文本用词重叠程度的数学表达,与“含义相近”是两回事。
面试题
Q1. 向量空间模型中,一篇文档最终被表示成什么?
- A.词项的权重向量← 正确答案
- B.一段压缩的二进制
- C.一棵句法分析树
- D.一张关系数据表
答案:A。VSM 的核心就是“文档向量化”:把文档映射成词典维度上的权重向量,每个分量对应一个词的权重。二进制、句法树、关系表都不是 VSM 的表示形式,它们分别属于压缩、句法分析与数据库的范畴。
Q2. 在标准向量空间模型中,余弦相似度的取值范围是多少?
- A.恒为 0 到 1
- B.恒为 -1 到 1← 正确答案
- C.恒为 0 到正无穷
- D.恒为负无穷到正无穷
答案:B。余弦值在数学上恒落于 [-1, 1]。在信息检索中因为权重非负,实际值多在 [0, 1],但“恒为 0 到 1”是把文本场景的特殊性当成了普遍定义,B 才是通用答案。
Q3. 当查询向量与文档向量夹角为 0° 时,其余弦相似度等于多少?
- A.余弦值等于 0
- B.余弦值等于 1← 正确答案
- C.余弦值等于 -1
- D.余弦值无法计算
答案:B。cos(0°) = 1,表示两向量方向完全一致,是最高相关度。0 对应 90°(正交、无关联),-1 对应 180°(方向相反)。此题考察夹角与余弦值的对应关系。
Q4. TF(词频)主要用来刻画文档的哪个属性?
- A.词在本文档中的出现频次← 正确答案
- B.词在整个语料中的出现频次
- C.词在查询句中的出现频次
- D.词在词典中的排列位置
答案:A。TF 衡量的是词在“当前文档”内的浓度。B 是文档频率(df)的范畴,C 属于查询侧的概念,D 与权重毫无关系——词袋世界里根本没有“位置”。
Q5. IDF 公式 log(N/df_t) 中的 df_t 表示什么?
- A.包含词 t 的文档数量← 正确答案
- B.词 t 在所有文档中的总次数
- C.语料库中的文档总数
- D.查询语句中的词项总数
答案:A。df_t 是 document frequency,即“有多少篇文档包含了词 t”(一篇文章里出现多次也只算一次)。B 是总出现次数,C 是 N,D 属于查询侧,都不是 df_t 的定义。
Q6. 为什么“的、了”这类词在 VSM 中权重会被 IDF 压低?
- A.因为它们出现在绝大多数文档中← 正确答案
- B.因为它们经常出现在标题里
- C.因为它们的词频通常非常低
- D.因为它们的长度实在太短了
答案:A。这类词的 df 接近文档总数 N,IDF = log(N/df) 趋近 log 1 = 0,与 TF 相乘后权重自动归零。B、C、D 都不是 IDF 的设计依据,IDF 只关心“出现在多少篇文档里”。
Q7. 下列哪些是向量空间模型的典型特点?(选择两项)
- A.文档被表示为稀疏高维向量← 正确答案
- B.能理解词序和语法结构
- C.用向量夹角衡量相关性← 正确答案
- D.需要大量人工标注来训练
答案:AC。VSM 用词典决定维度,单篇文档只命中少量词,因此向量又高维又稀疏(A 对);相关性由向量夹角即余弦相似度衡量(C 对)。B 错误——词袋模型恰恰忽略顺序和语法;D 错误——VSM 无需训练和标注。
Q8. 为什么 VSM 常用余弦相似度而不是欧氏距离?(选择两项)
- A.余弦对向量长度不敏感← 正确答案
- B.余弦能识别同义词汇
- C.余弦只关注方向一致程度← 正确答案
- D.余弦计算结果必为整数
答案:AC。文档长短不一导致向量模长差异大,欧氏距离会被“长度”带偏;余弦通过除以双模长天然归一化,只关注方向(A、C 对)。识别同义词是语义模型的能力(B 错);余弦值是连续实数而非整数(D 错)。
Q9. 构建一个可用的向量空间模型检索系统,哪些步骤是必需的?(选择两项)
- A.分词并构建词典← 正确答案
- B.计算每个词的 TF-IDF 权重← 正确答案
- C.训练深度神经网络编码器
- D.人工为每篇文档标注标签
答案:AB。VSM 需要词典来确定维度(A),需要 TF-IDF 来确定每个维度上的权重(B)。C 是神经检索路线的做法,D 是监督学习的要求,二者都不是 VSM 的必需步骤。
Q10. 若词 t 出现在语料全部 N 篇文档中,它的 TF-IDF 权重会怎样变化?
- A.IDF 为 0,TF-IDF 恒为 0← 正确答案
- B.IDF 为 1,TF-IDF 等于 TF
- C.IDF 为 N,TF-IDF 大幅提高
- D.IDF 无法定义直接报错
答案:A。df_t = N 时,IDF = log(N/N) = log 1 = 0,任何 TF 乘以 0 都得 0——这个词在所有文档中都出现,毫无区分度,被自动屏蔽。这也解释了为什么工程上可以直接用停用词表替代这部分计算。