现在位置: 首页 > AI 数学基础 > 正文

余弦相似度做文本推荐

用词袋向量表示句子,计算余弦相似度,找出最相似的句子。

学完本案例你将理解:搜索和推荐系统的数学基础——点积衡量「同向」,模长做归一化。


生活引入

淘宝搜索「蓝牙耳机」

你输入「蓝牙耳机」四个字,淘宝瞬间从百万商品中找出最相关的。它不是按标题里有没有完全匹配的词来排序——而是把每个商品标题转成一串数字(向量),然后比较你的搜索词和每个标题的「方向」是否一致。

方向越接近,说明意思越相近。至于标题是长是短,并不影响判断——这就是余弦相似度的直觉。


直观理解

想象两张弓射出的箭。两箭方向越一致,夹角的余弦越接近 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. 确定一个词表(所有可能出现的词)
  2. 对于每句话,如果某个词出现了就标 1,没出现就标 0
  3. 得到一个长度等于词表大小的向量

这里用的是最简化的「是否出现」(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}")
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)本质上也是在计算相似度——余弦相似度省略了分母的模长归一化。