余弦相似度做文本推荐
用词袋向量表示句子,计算余弦相似度,找出最相似的句子。
学完本案例你将理解:搜索和推荐系统的数学基础——点积衡量「同向」,模长做归一化。
生活引入
淘宝搜索「蓝牙耳机」
你输入「蓝牙耳机」四个字,淘宝瞬间从百万商品中找出最相关的。它不是按标题里有没有完全匹配的词来排序——而是把每个商品标题转成一串数字(向量),然后比较你的搜索词和每个标题的「方向」是否一致。
方向越接近,说明意思越相近。至于标题是长是短,并不影响判断——这就是余弦相似度的直觉。
直观理解
想象两张弓射出的箭。两箭方向越一致,夹角的余弦越接近 1;两箭互相垂直,余弦为 0;两箭背道而驰,余弦为 -1。
~ 1
方向相同
两个句子意思一样
两个句子意思一样
~ 0
互相垂直
两个句子毫不相关
两个句子毫不相关
~ -1
方向相反
词袋模型中少见
词袋模型中少见
关键问题是:怎么把一段文字「变成」一个箭头?这就需要引入词袋模型。
数学定义
余弦相似度公式
\[ \cos(\theta) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \cdot \|\mathbf{b}\|} = \frac{\sum_{i=1}^{n} a_i \cdot b_i}{\sqrt{\sum a_i^2} \cdot \sqrt{\sum b_i^2}} \]分子是点积(衡量两个向量「同向」的程度),分母是两个模长的乘积(消除向量长度的影响)。
词袋模型(Bag of Words)
把一句话变成一个向量的方法:
- 确定一个词表(所有可能出现的词)
- 对于每句话,如果某个词出现了就标 1,没出现就标 0
- 得到一个长度等于词表大小的向量
这里用的是最简化的「是否出现」(one-hot 词袋),实际应用中常用 TF-IDF 或词嵌入(Word Embedding)替代。但核心思路一致:把文字变成能参与数学运算的向量。
Python 动手实践
用 5 句中文构建词袋向量,计算与查询句子的余弦相似度,找出最相似的句子。
实例
import numpy as np
# 1. 准备语料——5 句中文,包含 AI 学习与日常生活两类话题
sentences = [
"我喜欢用python学习机器学习",
"python是学习人工智能的好工具",
"今天天气很好适合出去散步",
"深度学习需要大量的数据和算力",
"散步是一种很好的放松方式",
]
# 2. 构建词表(教学简化版,手动定义可能出现的词语)
vocab_words = ["我", "喜欢", "用", "python", "学习", "机器学习", "是",
"人工智能", "好", "工具", "今天", "天气", "很", "适合",
"出去", "散步", "深度学习", "需要", "大量", "的", "数据",
"算力", "一种", "放松", "方式"]
def to_vector(sentence, vocab):
"""将句子转为词袋向量——出现过的词标记为 1"""
vec = np.zeros(len(vocab))
for i, w in enumerate(vocab):
if w in sentence:
vec[i] = 1 # 词出现了就记 1
return vec
# 5 句话 → 5 个向量
vectors = np.array([to_vector(s, vocab_words) for s in sentences])
print("RUNOOB 每句话的向量维度:", vectors.shape[1],
"(词表大小)")
# 3. 手写余弦相似度函数(不调 sklearn)
def cosine_similarity(a, b):
dot = np.dot(a, b) # 点积
norm_a = np.linalg.norm(a) # a 的模长
norm_b = np.linalg.norm(b) # b 的模长
if norm_a == 0 or norm_b == 0:
return 0.0 # 零向量没有方向
return dot / (norm_a * norm_b)
# 4. 给定查询句子,找出最相似的句子
query = "我在学习python和人工智能"
query_vec = to_vector(query, vocab_words)
sims = [cosine_similarity(query_vec, v) for v in vectors]
print(f"\nRUNOOB 查询句子: {query!r}\n")
for s, sim in sorted(zip(sentences, sims),
key=lambda x: -x[1]):
print(f" 相似度 {sim:.3f} <- {s}")
# 5. 验证:不相关的句子相似度为 0
print("\nRUNOOB 验证:两个完全不相关的句子")
v1 = to_vector("python机器学习", vocab_words)
v2 = to_vector("散步放松", vocab_words)
print(f" 'python机器学习' vs '散步放松': "
f"cos = {cosine_similarity(v1, v2):.3f}")
# 1. 准备语料——5 句中文,包含 AI 学习与日常生活两类话题
sentences = [
"我喜欢用python学习机器学习",
"python是学习人工智能的好工具",
"今天天气很好适合出去散步",
"深度学习需要大量的数据和算力",
"散步是一种很好的放松方式",
]
# 2. 构建词表(教学简化版,手动定义可能出现的词语)
vocab_words = ["我", "喜欢", "用", "python", "学习", "机器学习", "是",
"人工智能", "好", "工具", "今天", "天气", "很", "适合",
"出去", "散步", "深度学习", "需要", "大量", "的", "数据",
"算力", "一种", "放松", "方式"]
def to_vector(sentence, vocab):
"""将句子转为词袋向量——出现过的词标记为 1"""
vec = np.zeros(len(vocab))
for i, w in enumerate(vocab):
if w in sentence:
vec[i] = 1 # 词出现了就记 1
return vec
# 5 句话 → 5 个向量
vectors = np.array([to_vector(s, vocab_words) for s in sentences])
print("RUNOOB 每句话的向量维度:", vectors.shape[1],
"(词表大小)")
# 3. 手写余弦相似度函数(不调 sklearn)
def cosine_similarity(a, b):
dot = np.dot(a, b) # 点积
norm_a = np.linalg.norm(a) # a 的模长
norm_b = np.linalg.norm(b) # b 的模长
if norm_a == 0 or norm_b == 0:
return 0.0 # 零向量没有方向
return dot / (norm_a * norm_b)
# 4. 给定查询句子,找出最相似的句子
query = "我在学习python和人工智能"
query_vec = to_vector(query, vocab_words)
sims = [cosine_similarity(query_vec, v) for v in vectors]
print(f"\nRUNOOB 查询句子: {query!r}\n")
for s, sim in sorted(zip(sentences, sims),
key=lambda x: -x[1]):
print(f" 相似度 {sim:.3f} <- {s}")
# 5. 验证:不相关的句子相似度为 0
print("\nRUNOOB 验证:两个完全不相关的句子")
v1 = to_vector("python机器学习", vocab_words)
v2 = to_vector("散步放松", vocab_words)
print(f" 'python机器学习' vs '散步放松': "
f"cos = {cosine_similarity(v1, v2):.3f}")
RUNOOB 每句话的向量维度: 25 (词表大小) RUNOOB 查询句子: '我在学习python和人工智能' 相似度 0.577 <- python是学习人工智能的好工具 相似度 0.500 <- 我喜欢用python学习机器学习 相似度 0.000 <- 今天天气很好适合出去散步 相似度 0.000 <- 深度学习需要大量的数据和算力 相似度 0.000 <- 散步是一种很好的放松方式 RUNOOB 验证:两个完全不相关的句子 'python机器学习' vs '散步放松': cos = 0.000
结果很直观:包含相同关键字的句子相似度高,内容完全不相关的句子相似度为 0。
AI 中的应用场景
| AI 场景 | 如何使用余弦相似度 |
|---|---|
| 搜索引擎 | 将搜索词和文档都转为向量,按余弦相似度排序返回结果 |
| 推荐系统 | 找到与用户历史行为向量最相似的商品 |
| 语义搜索 | 用词嵌入(Word2Vec/BERT)做向量化,捕捉同义词——「蓝牙耳机」和「无线耳塞」也会匹配 |
| 人脸识别 | 把人脸图片编码为向量(Face Embedding),比较两张脸的余弦相似度判断是否同一人 |
| RAG 检索 | 大模型应用中,用余弦相似度从知识库中检索最相关的文档片段 |
GPT 等大模型中用到的「注意力机制」里,Q 和 K 的点积(QK^T)本质上也是在计算相似度——余弦相似度省略了分母的模长归一化。
