1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# pip install langchain langchain_community
# 语义向量化的模型
from langchain_community import embeddings
import numpy as np
# 悄悄的会去拿API_KEY
model = embeddings.DashScopeEmbeddings()
# 语义向量化的计算
e1 = model.embed_query('皇帝') #假设是知识库
e2 = model.embed_query('皇后') #假设是用户提问
# print(len(e1)) # 1536维度,每个模型维度
# print(e1)
# 两句话的相似度:
# 余弦相似度计算公式:向量1 @ 向量2 / ||向量1|| * ||向量2||
# # 手搓
# # 第一步:计算向量1@向量2
# top = np.array(e1)@np.array(e2)
# # 第二部:计算向量1的模 * 向量2的模(距离原点(0点)的距离)
# distance1pingfang = 0
# # 求每个维度的平方和
# for i in e1:
# # i表示每个维度上的坐标
# distance1pingfang += i**2
# # 开根号
# distance1 = np.sqrt(distance1pingfang)
# distance2pingfang = 0
# # 求每个维度的平方和
# for i in e2:
# # i表示每个维度上的坐标
# distance2pingfang += i**2
# # 开根号
# distance2 = np.sqrt(distance2pingfang)
# bottom = distance1 * distance2
#
# # 余弦相似度 = top / bottom
# cos = top / bottom
# print(f"两句话的相似度是:{cos}")
# API实现
cos = np.dot(e1,e2) / (np.linalg.norm(e1) * np.linalg.norm(e2))
print(f"两句话的相似度是:{cos}")

# cos取值范围:0~1

这段代码的核心目的是:用数学方法计算“皇帝”和“皇后”这两个词在语义上有多接近。它先把文字转成数字(向量),然后用余弦相似度公式算出它们的相似度分数。

下面我把代码拆成三部分,重点把余弦相似度给你掰开揉碎讲明白:


1. 代码在做什么(整体流程)

  • 第1步:调用 DashScopeEmbeddings()(阿里云通义千问的向量模型),把文字“皇帝”变成一串1536维的数字列表(向量),同样把“皇后”也变成向量。
  • 第2步:计算这两个向量之间的余弦相似度,得出一个0~1之间的分数(越接近1表示语义越相似)。

2. 什么是“余弦相似度”(核心重点)

抛开复杂的数学符号,用最直白的话说:

余弦相似度,就是看两根“箭头”(向量)在空间里指向的方向是不是一致的。它不管箭头有多长(不管文字长短),只管箭头指向哪里(只管语义方向)。

  • 如果两个向量指向完全相同的方向(夹角为0度),相似度 = 1(比如“皇帝”和“皇上”)。
  • 如果两个向量指向完全相反的方向(夹角180度),相似度 = -1(极少出现)。
  • 如果两个向量方向垂直(夹角90度),相似度 = 0(完全不相关,比如“皇帝”和“苹果”)。

对于文本语义向量,由于向量各维度通常都是非负值,所以相似度基本落在 0 ~ 1 之间。

公式拆解(对应代码中的手搓部分):

余弦相似度 = (向量1 · 向量2) ÷ (|向量1| × |向量2|)

  • 分子(top):向量1 @ 向量2,即点积。把两个向量对应位置的数字相乘,再全部加起来。这个值代表“两个箭头在方向上重合的力度”。
  • 分母(bottom):|向量1| × |向量2|,即两个向量的“模”相乘。模就是向量的长度(从原点(0,0)到箭头尖端的直线距离)。代码里通过 sqrt(所有维度的平方和) 来算这个长度。
  • 为什么除以分母:为了消除文本长度(向量长度)的影响。比如“皇帝”和“中国历史上伟大的皇帝”,后者向量会很长(模很大),如果不除以长度,点积会虚高。除以长度后,只看方向,不看长短,结果才公平。

3. 对照代码,逐行看计算过程

第一步:向量化(把字变成坐标)

1
2
3
model = embeddings.DashScopeEmbeddings()
e1 = model.embed_query('皇帝') # 得到 e1 = [0.12, -0.05, 0.89, ...] 共1536个数字
e2 = model.embed_query('皇后') # 得到 e2 = [0.11, -0.04, 0.88, ...] 共1536个数字

这相当于把“皇帝”和“皇后”两个词,映射到1536维空间里的两个坐标点。

第二步:手搓余弦相似度(代码里注释掉的部分,但逻辑最清晰)

  • 算分子(top):
    1
    top = np.array(e1) @ np.array(e2)  # 对应位置相乘再相加
  • 算分母(bottom):
    1. 先算向量1的模(长度):distance1 = sqrt(0.12² + (-0.05)² + 0.89² + ...),代码里用 for 循环累加平方和再开根。
    2. 同样算向量2的模 distance2。
    3. bottom = distance1 * distance2
  • 算最终相似度:
    1
    cos = top / bottom

第三步:API快捷方式(一行搞定)

1
cos = np.dot(e1,e2) / (np.linalg.norm(e1) * np.linalg.norm(e2))
  • np.dot(e1,e2) = 点积(分子)。
  • np.linalg.norm(e1) = 直接算向量的模(长度),完全等价于上面手搓的那一堆 for 循环。

🎯 最终总结(一句话记忆法)

余弦相似度 = 两个向量的“方向一致性”。它通过“点积 ÷ 长度乘积”,把文字的语义浓缩成一个0~1的小数。在这段代码里,因为“皇帝”和“皇后”语义相近,所以算出来的相似度会很高(比如0.85以上),这就证明向量模型成功捕捉到了这两个词的内在关联。这就是AI做语义搜索、RAG检索时的底层数学原理。

你的代码里总共用了 2种 方法来实现余弦相似度的计算。

这两种方法的数学原理完全一样,区别只在于:第一种是你自己一步步手算(手搓),第二种是偷懒直接调用现成的计算工具(调包)。

我用买菜算账的大白话给你捋一遍:


方法1:手搓算法(纯手工计算)

也就是代码中被注释掉的那一大段(# 手搓 的部分)。这就像你不用计算器,拿着笔在草稿纸上一步步算:

  • 第一步(算分子):你把两个向量对应的数字挨个相乘,然后把所有的乘积加起来(np.array(e1)@np.array(e2))。这相当于算“两个箭头的重合力度”。
  • 第二步(算分母):你先分别算出两个向量的“长度”(模)。怎么算呢?就是把向量里每个数字都平方一下,全部加起来,最后再开个根号(np.sqrt)。这相当于分别量出两根箭头的物理长短。
  • 第三步(做除法):用刚才算出来的“重合力度”除以“两根箭头的长度相乘”,得出的结果就是相似度。

特点:步骤特别琐碎,甚至为了算“长度”还专门写了 for 循环。但好处是每一步逻辑都摆在你眼前,看得一清二楚。


方法2:调用现成的数学工具包(API快捷计算)

也就是代码里最后那一行没被注释掉的(cos = np.dot(e1,e2) / (np.linalg.norm(e1) * np.linalg.norm(e2)))。这就像你直接掏出一个功能强大的多功能计算器按两下:

  • 你不需要自己写循环去累加相乘了,直接用 np.dot() 这个现成函数,它就把“分子”(点积)算好了。
  • 你也不需要去写平方和开根号了,直接用 np.linalg.norm() 这个现成函数,它就直接把向量的“长度”(模)吐出来了。
  • 最后你再把它们除一下,结果就出来了。

特点:代码极其简洁,只需要一行。因为 numpy 这个库底层是用更快的语言(C语言)写的,所以这种方法不仅省事,而且计算速度比你自己写 for 循环要快得多。


总结一句话

代码里共 2种 实现:

  1. 手搓法:自己写循环算点积、算长度、算除法(看得懂原理,但啰嗦)。
  2. 调包法:直接拿现成的 dot(点积函数)和 norm(求模函数)来用(简洁高效,推荐实际使用)。

特别注意:代码里最上面的 model.embed_query() 是“向量化”(把文字变成数字)的过程,这是准备工作,不算在“计算相似度”的方法里。真正的“计算方法”,就上面这俩。😊