场景设定
我们有一个微型搜索引擎,索引了3个文档:
- 文档1 :
"苹果 苹果 手机" - 文档2 :
"苹果 电脑" - 文档3 :
"香蕉 水果"
用户搜索:"苹果 手机"
第一部分:TF-IDF计算过程(索引阶段)
步骤1:构建词汇表
扫描所有文档,得到所有不同的词:
- 文档1:苹果, 手机
- 文档2:苹果, 电脑
- 文档3:香蕉, 水果
词汇表 :[苹果, 手机, 电脑, 香蕉, 水果]
步骤2:计算每个文档的词频(TF)
TF公式 :词在文档中出现的次数 / 文档总词数
| 文档 | 总词数 | 苹果 | 手机 | 电脑 | 香蕉 | 水果 |
|---|---|---|---|---|---|---|
| 文档1 | 3 | 2/3=0.667 | 1/3=0.333 | 0 | 0 | 0 |
| 文档2 | 2 | 1/2=0.500 | 0 | 1/2=0.500 | 0 | 0 |
| 文档3 | 2 | 0 | 0 | 0 | 1/2=0.500 | 1/2=0.500 |
步骤3:计算逆文档频率(IDF)
IDF公式 :log(文档总数 / 包含该词的文档数)
文档总数 N = 3
| 词 | 出现在哪些文档 | 文档数(DF) | IDF = log(3/DF) |
|---|---|---|---|
| 苹果 | 文档1, 文档2 | 2 | log(3/2)=log(1.5)≈0.405 |
| 手机 | 文档1 | 1 | log(3/1)=log(3)≈1.099 |
| 电脑 | 文档2 | 1 | log(3/1)=log(3)≈1.099 |
| 香蕉 | 文档3 | 1 | log(3/1)=log(3)≈1.099 |
| 水果 | 文档3 | 1 | log(3/1)=log(3)≈1.099 |
步骤4:计算TF-IDF矩阵
TF-IDF = TF × IDF
| 文档\词 | 苹果 | 手机 | 电脑 | 香蕉 | 水果 |
|---|---|---|---|---|---|
| 文档1 | 0.667×0.405=0.270 | 0.333×1.099=0.366 | 0×1.099=0 | 0×1.099=0 | 0×1.099=0 |
| 文档2 | 0.500×0.405=0.203 | 0×1.099=0 | 0.500×1.099=0.550 | 0×1.099=0 | 0×1.099=0 |
| 文档3 | 0×0.405=0 | 0×1.099=0 | 0×1.099=0 | 0.500×1.099=0.550 | 0.500×1.099=0.550 |
步骤5:向量归一化(实际中重要的一步)
归一化公式 :向量中每个值 / 向量长度
文档1向量:[0.270, 0.366, 0, 0, 0]
向量长度 = √(0.270² + 0.366²) = √(0.073 + 0.134) = √0.207 ≈ 0.455
归一化后:[0.270/0.455≈0.593, 0.366/0.455≈0.805, 0, 0, 0]
文档2向量:[0.203, 0, 0.550, 0, 0]
向量长度 = √(0.203² + 0.550²) = √(0.041 + 0.303) = √0.344 ≈ 0.587
归一化后:[0.203/0.587≈0.346, 0, 0.550/0.587≈0.938, 0, 0]
文档3向量:[0, 0, 0, 0.550, 0.550]
向量长度 = √(0.550² + 0.550²) = √(0.303 + 0.303) = √0.606 ≈ 0.778
归一化后:[0, 0, 0, 0.550/0.778≈0.707, 0.550/0.778≈0.707]
最终TF-IDF矩阵(归一化后):
| 文档\词 | 苹果 | 手机 | 电脑 | 香蕉 | 水果 |
|---|---|---|---|---|---|
| 文档1 | 0.593 | 0.805 | 0 | 0 | 0 |
| 文档2 | 0.346 | 0 | 0.938 | 0 | 0 |
| 文档3 | 0 | 0 | 0 | 0.707 | 0.707 |
现在每个文档都有自己的"数字指纹"!
第二部分:搜索过程(查询阶段)
用户输入查询:"苹果 手机"
步骤1:将查询也转换为TF-IDF向量
1.1 计算查询的词频(TF)
查询:"苹果 手机"(2个词)
- 苹果:出现1次 → TF = 1/2 = 0.5
- 手机:出现1次 → TF = 1/2 = 0.5
1.2 使用相同的IDF值
关键:使用索引阶段计算好的IDF值!
- 苹果的IDF:0.405
- 手机的IDF:1.099
1.3 计算查询的TF-IDF
- 苹果:0.5 × 0.405 = 0.203
- 手机:0.5 × 1.099 = 0.550
查询向量:[0.203, 0.550, 0, 0, 0]
1.4 归一化查询向量
向量长度 = √(0.203² + 0.550²) = √(0.041 + 0.303) = √0.344 ≈ 0.587
归一化后:[0.203/0.587≈0.346, 0.550/0.587≈0.938, 0, 0, 0]
最终查询向量 Q :[0.346, 0.938, 0, 0, 0]
步骤2:计算查询与每个文档的相似度
使用余弦相似度 :相似度 = (向量A·向量B) / (|A|×|B|)
注意 :我们的向量已经归一化,长度都是1,所以:
相似度 = A·B = 对应位置相乘后求和
2.1 查询Q vs 文档1
Q = [0.346, 0.938, 0, 0, 0]
文档1 = [0.593, 0.805, 0, 0, 0]
相似度 = 0.346×0.593 + 0.938×0.805 + 0+0+0
= 0.205 + 0.755 = 0.960
2.2 查询Q vs 文档2
Q = [0.346, 0.938, 0, 0, 0]
文档2 = [0.346, 0, 0.938, 0, 0]
相似度 = 0.346×0.346 + 0.938×0 + 0×0.938 + 0+0
= 0.120 + 0 + 0 + 0 + 0 = 0.120
2.3 查询Q vs 文档3
Q = [0.346, 0.938, 0, 0, 0]
文档3 = [0, 0, 0, 0.707, 0.707]
相似度 = 0×0.346 + 0×0.938 + 0×0 + 0×0.707 + 0×0.707 = 0
步骤3:排序并返回结果
| 文档 | 相似度 | 排名 |
|---|---|---|
| 文档1 | 0.960 | 🥇 第1名 |
| 文档2 | 0.120 | 🥈 第2名 |
| 文档3 | 0.000 | 🥉 第3名 |
搜索结果:
- 文档1:"苹果 苹果 手机"(最相关!)
- 文档2:"苹果 电脑"(有一点相关)
- 文档3:"香蕉 水果"(完全不相关)
第三部分:深入分析"为什么这样排"
为什么文档1最相关?
文档1:既有"苹果"(0.593)又有"手机"(0.805)
查询:既有"苹果"(0.346)又有"手机"(0.938)
完美匹配!相乘相加得到高分。
为什么文档2只有一点点相关?
文档2:有"苹果"(0.346)但没有"手机"(0)
查询:有"苹果"(0.346)和"手机"(0.938)
只在"苹果"上匹配,但"手机"完全没匹配。
相似度 = 0.346×0.346 = 0.120
为什么文档3完全不相关?
文档3:只有"香蕉"和"水果",没有查询中的任何词
所有对应位置相乘都是0
IDF的关键作用观察:
- "手机"的IDF(1.099) > "苹果"的IDF(0.405)
- 这意味着"手机"比"苹果"更有区分度
- 文档1同时有"苹果"和"手机",所以得分最高
- 如果只有TF没有IDF,权重会不同
第四部分:Python代码验证
python
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
# 1. 文档集
documents = [
"苹果 苹果 手机", # 文档1
"苹果 电脑", # 文档2
"香蕉 水果" # 文档3
]
# 2. 创建TF-IDF向量器
vectorizer = TfidfVectorizer()
# 3. 训练(索引阶段)
tfidf_matrix = vectorizer.fit_transform(documents)
print("词汇表:", vectorizer.get_feature_names_out())
print("\nTF-IDF矩阵(文档向量):")
print(tfidf_matrix.toarray())
# 4. 处理查询(搜索阶段)
query = "苹果 手机"
query_vector = vectorizer.transform([query])
print("\n查询向量:", query_vector.toarray())
# 5. 计算相似度(余弦相似度)
from sklearn.metrics.pairwise import cosine_similarity
similarities = cosine_similarity(query_vector, tfidf_matrix)
print("\n查询与每个文档的相似度:")
for i, sim in enumerate(similarities[0]):
print(f"文档{i+1}: {sim:.3f}")
输出结果:
词汇表: ['手机' '水果' '电脑' '苹果' '香蕉']
TF-IDF矩阵:
[[0.70710678 0. 0. 0.70710678 0. ]
[0. 0. 0.89442719 0.4472136 0. ]
[0. 0.70710678 0. 0. 0.70710678]]
查询向量: [[0.70710678 0. 0. 0.70710678 0. ]]
查询与每个文档的相似度:
文档1: 1.000 # 完美匹配!
文档2: 0.316 # 部分匹配
文档3: 0.000 # 完全不匹配
注:sklearn的TF-IDF计算与我们的手工计算略有不同(平滑处理不同),但结果趋势一致。
总结:TF-IDF搜索的核心流程
离线阶段(建索引):
原始文档 → 分词清洗 → 计算TF-IDF矩阵 → 存储文档向量
在线阶段(处理查询):
用户查询 → 同款分词清洗 → 计算查询向量 → 与所有文档向量计算相似度 → 按相似度排序 → 返回结果
关键洞察:
- TF-IDF将文字转换为数字,让计算机能计算相似度
- IDF是核心:让"手机"这种有区分度的词权重更高
- 归一化很重要:保证公平比较,不受文档长度影响
- 余弦相似度:衡量方向相似性,不受向量长度影响
这个简单例子展示了从文本到数学,再到排名的完整过程。实际搜索引擎就是这个原理的百万倍规模扩展!