3. 向量空间模型:词袋模型、TF-IDF与向量化表示

好,咱们今天聊聊文本相似度计算里最基础、也最绕不开的一个话题——向量空间模型。说白了,就是把文字变成数字,让计算机能“算”出两段话像不像。

我个人习惯把这一步叫做“文本的数学化”。你想想看,计算机不认识“苹果”和“香蕉”,但它认识 [1, 0, 0] 和 [0, 1, 0]。所以,一切相似度计算的前提,就是先把文本扔进一个向量空间里。

3.1 词袋模型(Bag of Words)

词袋模型,名字很形象。就像你往袋子里扔单词,不管顺序,只管有没有、有多少。

核心思想: 忽略语法和词序,把文档看成一组词的集合。每个词的出现次数,就是它的特征值。

举个例子:

  • 文档A:“我喜欢苹果”
  • 文档B:“苹果喜欢我”

在词袋模型下,这两句话的向量表示是完全一样的。因为词袋只关心“我”、“喜欢”、“苹果”这三个词各出现了一次。顺序?不存在的。

重要理解: 词袋模型丢失了语序信息。这在很多场景下是致命的。比如“猫追老鼠”和“老鼠追猫”,向量一模一样,但意思完全相反。

数学表示:

假设我们有 N 个文档,词汇表大小为 M。那么每个文档就是一个 M 维的向量。第 i 个文档的第 j 个维度,就是词汇表中第 j 个词在该文档中出现的次数。

# 一个简单的词袋向量示例
# 词汇表: ["我", "喜欢", "苹果", "吃", "香蕉"]
# 文档: "我喜欢吃苹果"
# 向量: [1, 1, 1, 1, 0]

我的经验: 我在做早期文本分类项目时,直接用了词袋模型。结果发现“的”、“了”、“是”这些高频词严重干扰了分类效果。嗯,那时候我才意识到,光数词频是不够的。

3.2 TF-IDF 原理

词袋模型有个明显的问题:它认为每个词的重要性是一样的。但实际呢?“的”出现100次,和“算法”出现1次,哪个更能代表文档主题?显然是后者。

TF-IDF 就是来解决这个问题的。它由两部分组成:

3.2.1 TF(词频)

TF 衡量一个词在文档中出现的频率。公式很简单:

TF(t, d) = (词 t 在文档 d 中出现的次数) / (文档 d 的总词数)

为什么要除以总词数?归一化。不然长文档天然就有更高的词频,这不公平。

3.2.2 IDF(逆文档频率)

IDF 衡量一个词的“稀缺性”。如果一个词在很多文档中都出现,那它的区分度就很低。比如“的”几乎每篇都有,那它的 IDF 就接近于0。

IDF(t) = log(总文档数 / 包含词 t 的文档数 + 1)

注意那个 +1,是为了防止分母为0。你想想看,如果某个词在所有文档中都没出现过,那分母就是0,数学上就炸了。

3.2.3 TF-IDF 最终公式

TF-IDF(t, d) = TF(t, d) * IDF(t)

这个乘积的含义很直观:一个词在文档中出现的次数越多(TF高),同时在其他文档中出现的越少(IDF高),那它对这篇文档的代表性就越强。

我曾经踩过的坑: 有一次做新闻分类,直接用 sklearn 的 TfidfVectorizer 默认参数。结果发现“中国”、“美国”这类词的 IDF 被压得很低,因为几乎每篇新闻都有。后来我调整了 max_df 参数,把出现在超过80%文档中的词过滤掉,效果才上来。

3.3 向量化表示方法

有了 TF-IDF 这个工具,我们怎么把文本变成向量呢?

3.3.1 基于词袋的向量化

这是最直接的方法。每个维度对应一个词,值就是该词的 TF-IDF 权重。

from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
    "我喜欢吃苹果",
    "他喜欢吃香蕉",
    "苹果和香蕉都是水果"
]

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)

# 查看词汇表
print(vectorizer.get_feature_names_out())
# 输出: ['苹果' '香蕉' '喜欢' '吃' '水果' '他' '和' '我' '都']

# 查看第一个文档的向量
print(X[0].toarray())
# 输出: [0.5, 0, 0.5, 0.5, 0, 0, 0, 0.5, 0]

3.3.2 向量维度问题

你想想看,如果词汇表有10万个词,那每个文档就是一个10万维的向量。这太稀疏了,大部分维度都是0。

常见处理方式:

  • 降维: 用 PCA、LSA 等方法把高维向量压缩到几百维
  • 特征选择: 只保留出现频率最高的前 N 个词(比如5000个)
  • 停用词过滤: 去掉“的”、“了”、“是”等无意义词

核心要点: 向量化表示的质量,直接决定了后续相似度计算的上限。你给模型喂垃圾,它只能吐出垃圾。

3.4 相似度计算示例

有了向量,计算相似度就简单了。最常用的是余弦相似度:

from sklearn.metrics.pairwise import cosine_similarity

# 假设我们有两个文档的 TF-IDF 向量
vec1 = [[0.5, 0.3, 0.2, 0]]
vec2 = [[0.4, 0.3, 0.1, 0.2]]

similarity = cosine_similarity(vec1, vec2)
print(f"相似度: {similarity[0][0]:.4f}")
# 输出: 相似度: 0.9487

0.9487 意味着这两个文档在向量空间中非常接近。说白了,它们的内容大概率是相关的。

我建议: 在实际项目中,不要只看余弦相似度的绝对值。更重要的是看相对排名。比如在1000个候选文档中,找出最相似的Top-10。这样更实用。

3.5 小结与思考

向量空间模型,尤其是词袋模型和 TF-IDF,是文本相似度计算的基石。虽然现在有 BERT、GPT 这些深度学习模型,但 TF-IDF 在很多场景下依然能打——速度快、可解释性强、资源消耗低。

嗯,这里要注意:TF-IDF 假设词与词之间是独立的。但实际语言中,“苹果”和“手机”经常一起出现,这种共现信息 TF-IDF 是捕捉不到的。这就是为什么后来有了 Word2Vec、GloVe 这些词嵌入方法。

不过那是后面章节的内容了。先把 TF-IDF 吃透,你就能解决80%的文本相似度问题。剩下的20%,我们慢慢聊。