5725 字
29 分钟
01 向量与余弦相似度:AI 如何判断"意思相近"

复习文档 · ADHD友好 · 从零开始

本章学到哪里,不学到哪里#

本章只学习:向量、点积、范数、归一化和余弦相似度之间的关系,并用两个短向量验证公式。

本章暂时不学习:Embedding 模型训练、向量数据库索引、ANN 算法、分块策略和完整 RAG。它们会使用本章的相似度思想,但不是本章的实现目标。

一句话理解#

Embedding 把文本变成固定长度的数字向量,余弦相似度比较两个向量的方向,从而作为语义检索的一个相似度信号。

它在 AI 应用中的位置#

文本
-> Embedding 模型
-> 向量
-> 余弦相似度
-> 排序相似文本
-> RAG 检索 / 推荐 / 去重 / 聚类

余弦相似度只负责“比较两个已经存在的向量”,不负责生成向量,也不负责从数据库取出文档。在 RAG 中,向量模型和向量数据库分别是它的前后环节。

最小验证例子#

下面只使用 Python 标准库 math,不需要安装额外依赖:

from math import sqrt
def cosine_similarity(a: list[float], b: list[float]) -> float:
if len(a) != len(b):
raise ValueError("两个向量的维度必须一致")
dot = sum(x * y for x, y in zip(a, b))
norm_a = sqrt(sum(x * x for x in a))
norm_b = sqrt(sum(y * y for y in b))
if norm_a == 0 or norm_b == 0:
raise ValueError("零向量没有可比较的方向")
return dot / (norm_a * norm_b)
print(cosine_similarity([3, 2], [6, 4])) # 接近 1:方向相同
print(cosine_similarity([1, 0], [0, 1])) # 0:方向垂直

验证重点不是背出小数,而是观察:同方向的向量即使长度不同,结果仍接近 1;维度不同或出现零向量时,函数应拒绝计算。


🔧 准确术语速查#

术语准确含义本章对应
Vector向量,一组有方向和长度的数字[3, 2]
Component分量,向量里的每个数字32
Dot product点积,逐位相乘再求和a1*b1 + a2*b2
Norm向量长度,多维勾股定理sqrt(x^2 + y^2)
Cosine similarity余弦相似度,比较方向接近程度dot / (norm_a * norm_b)
Normalization归一化,消除尺度影响除以长度,保留方向

一、先记住这张速查表#

概念一句话类比
向量一根箭,等于一个列表/元组去超市路线:[向东3步, 向北2步]
分量列表里每个数字每个方向各走几步
平方自己乘自己3² = 3×3 = 9
开根号平方的反操作√9 = 3(因为3×3=9)
勾股定理横²+竖²=对角线²房间宽3长4,对角线=5
norm多维勾股定理,量箭的长度所有分量平方加起来再开根号
点积逐位相乘再求和两根箭在同上方向的”重叠量”
余弦相似度点积/(长度×长度),削掉长度比方向只看口味比例,不看吃多少

二、从零开始:向量是什么#

向量 = 一根箭 = 一个列表
vec_a = [3, 2]
意思是:向右走3步,向上走2步
画出来就是一根从原点出发的箭
2 ↑
│ ● ← 箭头
│╱
1 │
─────┼──────3──→
0 1 2 3
类比:
去超市的路线 = [3, 2]
第1个分量(3) = 向东走3条街
第2个分量(2) = 向北走2条街
去医院的路线 = [1, 4]
第1个分量(1) = 向东走1条街
第2个分量(4) = 向北走4条街
两根箭指向不同方向 = 两句话说的是不同内容
关键:Embedding模型生成的向量维度永远固定!
模型输出 1024 维 → 不管你输入什么句子,输出永远是1024个数字
模型输出 1536 维 → 永远是1536个数字
维度多 = 描述更精细 = 足以区分不同语义

三、平方和开根号#

平方 = 自己乘自己#

3² = 3 × 3 = 9
2² = 2 × 2 = 4
0.9² = 0.9 × 0.9 = 0.81
(-3)² = (-3) × (-3) = 9 ← 负数平方也变正数!

开根号 = 平方的反操作#

√4 = 2 因为 2×2=4
√9 = 3 因为 3×3=9
√25 = 5 因为 5×5=25
√2 ≈ 1.414 因为 1.414×1.414≈2
平方 = "放大":3 → 3×3 → 9
开根号 = "还原":9 → "谁×自己=9" → 3

注意区分!开根号 ≠ 除法#

开根号:√9 = 3 (问谁×自己=9)
除法: 9÷3 = 3 (把9分成3份)
数字碰巧一样,但操作完全不同!
验证:√15 ≈ 3.87,但 15÷3 = 5
→ 3.87 ≠ 5,显然不是一回事

四、勾股定理 = 量对角线#

一个房间宽3米、长4米
从左下角走到右上角的对角线有多远?
● ← 右上角
╱│
对角线╱ │4米(竖边)
╱ │
左下角 ●───┘
3米(横边)
对角线 = √(横边² + 竖边²)
= √(3² + 4²)
= √(9 + 16)
= √25
= 5米
一句话记一辈子:
横² + 竖² = 对角线²

五、norm = 多维勾股定理 = 量箭的长度#

5.1 先认清”分量”和”分量平方”#

向量 A = [3, 2]
↑ ↑
这两个数字都叫"分量"(Component)
A[0] = 3 ← 第 0 个分量
A[1] = 2 ← 第 1 个分量

分量平方 = 每个分量自己乘自己:

向量 A = [3, 2]
分量平方:
3² = 3×3 = 9
2² = 2×2 = 4

为什么需要分量平方? 因为分量可能是负数(向左走、向下走),但长度不能是负数。平方让负数变正:

向量 [-3, -2] (向左3,向下2)
直接加:-3 + (-2) = -5 ❌ 长度不能是负的
平方后加:(-3)² + (-2)² = 9 + 4 = 13 ✅ 永远是正数

5.2 norm = 分量平方之和再开根号#

2维向量:norm = √(分量1² + 分量2²)
vec_a = [3, 2]
norm = √(3² + 2²) = √(9+4) = √13 ≈ 3.61
横着走了3步,竖着走了2步
箭的长度 = 3.61步

完整三步

第1步:每个分量平方
3² = 9, 2² = 4
第2步:全部加起来
9 + 4 = 13
第3步:开平方根
√13 ≈ 3.61 ← 这就是 norm(A)

为什么最后要开根号? 因为前两步(平方+求和)把数字放大了,开根号把它缩回真正的”箭长”。这是勾股定理:横²+竖²=对角线²,所以对角线 = √(横²+竖²)。

5维向量:norm = √(分1² + 分2² + 分3² + 分4² + 分5²)
vec = [0.9, 0.8, 0.7, 0.1, 0.2]
norm = √(0.81 + 0.64 + 0.49 + 0.01 + 0.04)
= √(1.99)
≈ 1.41
逻辑完全一样!只是加了5个数字而不是2个
1024维也一样:√(分1² + 分2² + ... + 分1024²)
维度再多,本质不变
就是勾股定理的扩展版

六、点积 = 逐位乘再求和#

A = [3, 2]
B = [2.8, 1.9]
点积 = 3 × 2.8 + 2 × 1.9
= 8.4 + 3.8
= 12.2
步骤拆解:
第1步:同一位置的两个数字相乘
位置0:3 × 2.8 = 8.4
位置1:2 × 1.9 = 3.8
第2步:所有乘积加起来
8.4 + 3.8 = 12.2 → 点积
乘 = 该方向上两箭的重叠程度(都强→乘积大)
加 = 所有方向的总重叠量
5维向量也一样:
A = [0.9, 0.8, 0.7, 0.1, 0.2]
B = [0.85, 0.75, 0.65, 0.15, 0.25]
点积 = 0.9×0.85 + 0.8×0.75 + 0.7×0.65 + 0.1×0.15 + 0.2×0.25
= 0.765 + 0.6 + 0.455 + 0.015 + 0.05
= 1.885
逻辑完全一样!只是乘了5次而不是2次

注意:点积不能单独判断相似性!#

A = [3, 2] 长度 ≈ 3.61
B = [2.8, 1.9] 长度 ≈ 3.38 方向一致
点积 = 12.2
C = [100, 66.7] 长度 ≈ 121 方向也一致
点积 = 433.4
单看点积:12.2 vs 433.4 → 差很多!
但A vs B 和 A vs C 方向其实一样!
→ 点积混了"长度"信息,必须削掉

七、余弦相似度 = 削掉长度比方向 = 最终答案#

7.1 公式(重要:方向别记反!)#

余弦相似度 = 点积 ÷ (norm(A) × norm(B))
↑ ↑
分子(上面) 分母(下面)
❌ 错误:norm(A) × norm(B) ÷ 点积
✅ 正确:点积 ÷ (norm(A) × norm(B))

为什么是除以而不是乘? 因为点积里”沾了”长度信息,要除掉它才能还原出”纯方向相似度”。

类比:
你称体重,但称上沾了泥(多了 2 kg)
真实体重 = 称出来的数 - 2 ← 减掉/除掉多余的部分
不是 + 2

7.2 完整流程三步#

余弦相似度 = 点积 / (norm(A) × norm(B))
= 逐位乘再求和 / (A长度 × B长度)
= 削掉长度,只剩方向

走一个完整例子#

A = [3, 2] ← "代码质量"
B = [2.8, 1.9] ← "程序维护"
C = [0.5, 4] ← "天气"
步骤1:算长度(norm = 勾股定理)
norm(A) = √(3²+2²) = √13 ≈ 3.61
norm(B) = √(2.8²+1.9²) = √11.45 ≈ 3.38
norm(C) = √(0.5²+4²) = √16.25 ≈ 4.03
步骤2:算重叠(点积 = 逐位乘再求和)
dot(A,B) = 3×2.8 + 2×1.9 = 8.4 + 3.8 = 12.2
dot(A,C) = 3×0.5 + 2×4 = 1.5 + 8 = 9.5
步骤3:削掉长度得相似度
A vs B = 12.2 / (3.61 × 3.38) = 12.2 / 12.21 ≈ 1.00 ✅
A vs C = 9.5 / (3.61 × 4.03) = 9.5 / 14.49 ≈ 0.66

结果解读#

相似度含义例子
~1.0方向完全一致”代码质量” vs “程序维护”
0.8-0.9非常相似
0.6-0.7比较相似”代码质量” vs “天气”(不同,但有微弱关联)
0.3-0.5有点关系
<0.3基本无关
0完全无关垂直方向(手电筒照墙90度)

八、三步完整对比图#

A向量 B向量
[3,2] [2.8,1.9]
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
步骤1 │ 勾股定理 │ │ 勾股定理 │
(量长度) │norm(A)= │ │ norm(B)= │
│√(3²+2²) │ │ √(2.8²+1.9²)│
│≈ 3.61 │ │ ≈ 3.38 │
└──────────┘ └──────────────┘
│ │
└───────┬───────┘
┌───────────────────────────┐
步骤2 │ 点积 │
(算重叠) │ dot = 3×2.8 + 2×1.9 │
│ = 12.2 │
└───────────────────────────┘
┌───────────────────────────┐
步骤3 │ 余弦相似度 │
(削长度) │ 12.2 / (3.61 × 3.38) │
│ = 12.2 / 12.21 │
│ ≈ 1.00 ✅ 非常相似! │
└───────────────────────────┘

九、Python 代码速查#

import numpy as np
# 两个向量
a = np.array([3, 2])
b = np.array([2.8, 1.9])
# 步骤1:算长度(norm)
len_a = np.linalg.norm(a) # √(9+4) ≈ 3.61
len_b = np.linalg.norm(b) # √(7.84+3.61) ≈ 3.38
# 步骤2:算点积
dot = np.dot(a, b) # 3×2.8 + 2×1.9 = 12.2
# 步骤3:余弦相似度
cosine = dot / (len_a * len_b) # ≈ 1.0

一句代码版#

def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

十、给 Embedding 的类比#

"如何提升代码质量" → Embedding模型 → 向量A → 一根箭(朝"编程"方向)
"今天天气真好" → Embedding模型 → 向量B → 一根箭(朝"天气"方向)
两根箭方向差很远 → 余弦相似度低 → 语义不同
两根箭方向很近 → 余弦相似度高 → 语义相近
这就是 AI 判断"意思相近"的数学原理!

✅ 检查清单#

  • 向量 = 一根箭 = 一个列表/元组
  • 分量 = 列表里的每个数字
  • 平方 = 自己乘自己;开根号 = 平方的反操作
  • 勾股定理 = 横²+竖²=对角线²(2维特殊版)
  • norm = 多维勾股定理 = 量箭的长度
  • 点积 = 逐位乘再求和 = 两根箭的重叠量
  • 余弦相似度 = 点积 ÷ (A长度×B长度) = 削掉长度比方向
  • 越接近1 → 越相似;越接近0 → 越无关
  • 同一模型输出的向量维度永远一样,不会出现长度不同

十一、深入理解:为什么除以长度能削掉倍数#

问题的核心#

A = [3, 2]
B = [100, 66.7] = 33.3 × A ← B是A放大33.3倍,方向完全相同
问:如何计算相似度,让结果不受"33.3倍"影响?

第一步:观察点积里的倍数#

np.dot(A, B)
= 3×100 + 2×66.7
= 3×(33.3×3) + 2×(33.3×2) ← 代入 B = k×A

用乘法交换律提取k:

3×(33.3×3)
= 3×33.3×3 ← 去括号(结合律)
= 33.3×3×3 ← 交换律:3和33.3换位置
= 33.3×(3×3)
= 33.3×9
2×(33.3×2)
= 2×33.3×2
= 33.3×2×2
= 33.3×(2×2)
= 33.3×4

提取公因数(分配律):

np.dot(A, B)
= 33.3×9 + 33.3×4
= 33.3 × (9 + 4) ← 提取公因数33.3
= 33.3 × 13
= k × np.dot(A, A) ← 点积里混进了倍数k!

第二步:观察norm里的倍数#

norm(B)
= √(100² + 66.7²)
= √((33.3×3)² + (33.3×2)²)
= √(33.3²×9 + 33.3²×4)
= √(33.3² × (9+4))
= √(33.3² × 13)
= 33.3 × √13
= 33.3 × norm(A)
= k × norm(A) ← norm里也混进了倍数k!

第三步:相除消掉k#

余弦相似度 = dot(A,B) / (norm(A) × norm(B))
分子:k × np.dot(A,A) = k × (3×3 + 2×2) = k × 13
分母:norm(A) × k × norm(A) = k × norm(A)² = k × (√13)² = k × 13
= (k × 13) / (k × 13) ← k在分子分母都有!
= k/k × 13/13
= 1 × 1
= 1.0 ← k被消掉了!
关键:norm(A)² = dot(A, A),等式两边严格相等!
norm(A)² = (√(3²+2²))² = 3²+2² = 9+4 = 13 = dot(A, A)
所以不需要用近似值3.61,用精确的13就能完美消掉!

一句话总结#

位置计算结果倍数k在哪
分子 dot(A,B)433.433.3 × 13 ← 这里!
分母 norm(A)3.61没有k
分母 norm(B)120.233.3 × 3.61 ← 这里!
相除结果1.0k/k=1,消了!
433.4 / (3.61 × 120.2)
≈ (33.3×13) / (√13 × 33.3×√13) ← 不用3.61,用精确的√13
= 33.3/33.3 × 13/(√13×√13) ← k/k 消掉,后面 √13×√13 = 13
= 1 × 13/13
= 1 × 1
≈ 1.0 ← 用精确值算出来就是严格的1

核心洞察:倍数k同时污染了分子和分母,一除就没了,结果只反映方向!


十二、数学定律速查#

乘法交换律#

a × b = b × a
例子:
3 × 5 = 15
5 × 3 = 15
→ 相同!
在点积中:
3 × (33.3 × 3) = 33.3 × (3 × 3)
把33.3提到前面

乘法结合律#

(a × b) × c = a × (b × c)
例子:
(2 × 3) × 4 = 6 × 4 = 24
2 × (3 × 4) = 2 × 12 = 24
→ 相同!
在norm中:
√((33.3×3)² + (33.3×2)²)
= √(33.3² × (3² + 2²))
= 33.3 × √(3² + 2²)

提取公因数(分配律逆用)#

a × b + a × c = a × (b + c)
例子:
5 × 7 + 5 × 3
= 5 × (7 + 3)
= 5 × 10
= 50
在点积中:
33.3×9 + 33.3×4
= 33.3 × (9 + 4)
= 33.3 × 13

十三、归一化的数学思想#

什么是归一化#

归一化 = 把不同尺度的东西拉到同一标准下再比较
例子1:打分
小明:85/100分
小红:42/50分
归一化:都转成百分制 → 85% vs 84%
例子2:房价
北京:800万/100平 = 8万/平
小城市:80万/100平 = 0.8万/平
归一化:每平米价格,公平比较

在向量里#

问题:两根箭长度不同,直接比不公平
A = [3, 2],长度3.61
B = [100, 66.7],长度120.2(是A的33.3倍)
但方向完全相同!
解决:除以norm,削成单位长度1
削后A = [3/3.61, 2/3.61] = [0.83, 0.55]
削后B = [100/120.2, 66.7/120.2] = [0.83, 0.55]
→ 削完完全相同!都是[0.83, 0.55]
结果:余弦相似度 = 1.0,正确反映方向相同!

一句话#

归一化 = 削掉长度的影响,只比方向
在AI中无处不在:
- 距离归一化
- 数据标准化
- 概率归一化(softmax)
- 余弦相似度
核心思想:只要标准一致,不管尺度大小

十三·补、归一化与余弦相似度的关系(为什么归一化后点积就是余弦相似度)#

1. 先回顾两个公式#

余弦相似度公式:
cosine(A, B) = dot(A, B) ÷ (norm(A) × norm(B))
↑ ↑
点积(分子) 两个范数相乘(分母)
归一化定义:
A' = A ÷ norm(A)
B' = B ÷ norm(B)

2. 归一化后范数必然是 1(数学证明)#

设 A = [a, b],norm(A) = √(a² + b²) = N
归一化后 A' = [a/N, b/N]
norm(A') = √((a/N)² + (b/N)²)
= √(a²/N² + b²/N²) ← 平方后 N² 在分母
= √((a² + b²) / N²) ← 合并分子
= √(N² / N²) ← 因为 a² + b² = N²(范数定义)
= √1
= 1 ✅
→ 任何向量归一化后,范数必然是 1
→ 任何数除以自己都等于 1,所以这是数学必然

3. 范数变 1 → 分母变 1 → 余弦相似度 = 点积#

归一化后:
norm(A') = 1
norm(B') = 1
代入余弦相似度公式:
cosine(A', B') = dot(A', B') ÷ (norm(A') × norm(B'))
= dot(A', B') ÷ (1 × 1)
= dot(A', B') ÷ 1
= dot(A', B') ✅
→ 归一化后,余弦相似度 = 点积
→ 因为分母 (1×1=1),除以 1 等于没除

4. 关键洞察:除法被”提前”了#

不归一化(除法在阶段 2,检索时做):
阶段 1:无
阶段 2:dot(A, B) ÷ (norm(A) × norm(B))
除法在这(每次检索都要做)
归一化(除法在阶段 1,灌库时做):
阶段 1:A' = A ÷ norm(A),B' = B ÷ norm(B)
↑ ↑
除法搬到这了(一次性做完,存起来)
阶段 2:dot(A', B')
没有除法,只算点积

除法没消失,只是从阶段 2 搬到阶段 1

5. 数学等价性证明#

归一化后的点积:
dot(A', B')
= dot(A÷norm(A), B÷norm(B))
= (A[0]÷norm(A))×(B[0]÷norm(B)) + (A[1]÷norm(A))×(B[1]÷norm(B))
= (A[0]×B[0])÷(norm(A)×norm(B)) + (A[1]×B[1])÷(norm(A)×norm(B))
= [(A[0]×B[0]) + (A[1]×B[1])] ÷ (norm(A)×norm(B)) ← 分母提取出来
= dot(A, B) ÷ (norm(A) × norm(B)) ← 跟原公式一样!
→ 两种做法数学结果完全相同

6. 用具体数字验证#

原始向量:
A = [3, 4] norm(A) = 5
B = [6, 7] norm(B) = √85 ≈ 9.22
做法 1:不归一化(阶段 2 做除法)
dot(A, B) = 3×6 + 4×7 = 46
余弦 = 46 ÷ (5 × 9.22) = 46 ÷ 46.1 ≈ 0.998
做法 2:归一化(阶段 1 做除法)
A' = [3÷5, 4÷5] = [0.6, 0.8]
B' = [6÷9.22, 7÷9.22] ≈ [0.65, 0.76]
dot(A', B') = 0.6×0.65 + 0.8×0.76 = 0.998
两种做法结果都是 0.998 ✅

7. 为什么要”提前”做除法?(性能优势)#

场景:知识库有 1 万个文档,用户问一个问题
不归一化(每次检索都要算):
对每个文档:dot(Q, D) ÷ (norm(Q) × norm(D))
1 万次 norm 计算 + 1 万次除法
归一化(提前算好,检索只算点积):
灌库时:D' = D ÷ norm(D) ← 每个文档算 1 次,存起来
查询时:Q' = Q ÷ norm(Q) ← 算 1 次
对每个文档:dot(Q', D') ← 只算点积,不用算 norm,不用除
省掉了:1 万次 norm 计算 + 1 万次除法

8. 类比理解#

不归一化 = 每次去超市都现量苹果重量,再算总价
顾客1:0.3kg × 10元 = 3元 ← 称重 + 乘法
顾客2:0.5kg × 10元 = 5元 ← 称重 + 乘法
(每次都要称重,慢)
归一化 = 提前把苹果标成"1斤袋",单价提前算好
阶段 1:1斤装 × 10元/斤 = 10元/袋 ← 除法(分装)在这
阶段 2:顾客买 3袋 = 30元 ← 只数袋数,不称重,快
除法(分装)没消失,只是从"每次结账"搬到"提前分装"
结账时只算袋数×单价,快很多

9. 在项目里的对应#

app/embedding.py
# 阶段 1 在这里发生(模型内部自动归一化)
model.encode(
"test",
convert_to_numpy=True,
normalize_embeddings=True, # ← 阶段 1:向量 ÷ norm,长度变 1
)
# app/tools/kb_search.py
# 阶段 2 在这里发生(ChromaDB 内部算点积)
docs = get_vector_store().similarity_search(query, k=safe_limit)
# ↑
# 阶段 2:对归一化向量算点积 = 余弦相似度

10. 关键前提:文档和查询必须一致#

文档端:normalize_embeddings=True → 存的向量长度都是 1
查询端:normalize_embeddings=True → 查的向量长度也是 1
两边一致 → 点积 = 余弦相似度 ✅
如果一边开一边不开:
文档长度 1,查询长度 3.61
dot(Q, D) ÷ (1 × 3.61) = 点积 / 3.61 ❌ 不是余弦相似度

项目里 get_query_embeddingget_document_embedding 都用 normalize_embeddings=True,保持一致。

11. 一句话总结#

归一化 = 把余弦相似度的除法"提前"到算点积之前
数学等价:dot(A÷n_a, B÷n_b) = dot(A,B) ÷ (n_a × n_b)
原因:归一化后范数都是 1,分母 (1×1=1),除以 1 等于没除
好处:除法只做一次(灌库时),检索时只算点积,快很多
前提:文档和查询必须都开归一化(或都不开),不能一边开一边不开

十四、代码 vs 数学原理#

层面做了什么看到k了吗
代码执行直接乘除❌ 看不到k
数学原理提取公因数、消掉k✅ 能看到k被消
# 代码:直接算
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 数学原理:k被消掉了
cosine = (k × dot(A,A)) / (norm(A) × k × norm(A))
= k/k × dot(A,A)/norm(A)²
= 1 × dot(A,A)/norm(A)²
# 结果一样,原理解释了"为什么长度不影响结果"

结论:代码直接乘除,但背后的数学原理就是提取公因数和消掉倍数k。


十五、完整思维导图#

对比两句话相似度
调用Embedding模型 → 获得两个向量列表
┌─────────────────┐
│ A = [3, 2] │
│ B = [100, 66.7]│ ← B = k×A,k=33.3
└─────────────────┘
├─→ 点积 dot(A,B) = 3×100 + 2×66.7
│ = 3×(k×3) + 2×(k×2)
│ = k×(3×3) + k×(2×2) ← 交换律
│ = k × (9 + 4) ← 提取公因数
│ = k × 13 ← 混进k!
├─→ norm(A) = √(3²+2²) = 3.61
├─→ norm(B) = √(100²+66.7²)
│ = √((k×3)² + (k×2)²)
│ = √(k² × (3²+2²))
│ = k × √(3²+2²)
│ = k × 3.61 ← 也混进k!
余弦 = dot / (norm(A) × norm(B))
= (k×13) / (3.61 × k×3.61)
= k/k × 13/(3.61×3.61) ← k消掉了!
= 1 × 1
= 1.0 ✅ 方向相同!

✅ 完整检查清单#

基础概念#

  • 向量 = 一根箭 = 一个列表
  • 分量 = 列表里的每个数字
  • 平方 = 自己乘自己;开根号 = 平方的反操作
  • 勾股定理 = 横²+竖²=对角线²
  • norm = 多维勾股定理 = 量箭的长度
  • 点积 = 逐位乘再求和
  • 余弦相似度 = 点积÷(长度×长度)

数学定律#

  • 乘法交换律:a×b = b×a,可以换位置
  • 乘法结合律:(a×b)×c = a×(b×c),可以换括号
  • 提取公因数:a×b + a×c = a×(b+c),把相同提出来

核心原理#

  • 点积里混进了倍数k(因为B=k×A)
  • norm(B)里也混进了倍数k
  • 相除时k/k=1,倍数被消掉
  • 结果只反映方向,不受长度影响
  • 这就是归一化的数学思想

✅ 四条理解标准#

  • 思想是什么:把文本变成向量后,用方向是否接近来判断语义是否接近。
  • 干什么:给 RAG 检索、推荐、聚类等任务提供“相似度”计算方法。
  • 为什么这么干:只看点积会被长度影响,余弦相似度通过除以长度消掉倍数。
  • 怎么干:能写出 dot / (norm_a * norm_b),并能解释点积、norm、归一化各自的作用。

⚠️ 常见坑#

现象正确做法
把点积当相似度最终答案长向量天然分数更大用余弦相似度消掉长度影响
以为开根号是除法norm 推导卡住开根号是平方的反操作
忽略分母为 0空向量计算报错或无意义真实代码里先检查 norm 是否为 0
把数学公式和代码割裂会背公式但不会写函数对照 dotnormcosine_similarity 三步实现

下一章预告:用真实 Embedding 模型跑一遍,亲眼看”代码质量”和”天气真好”的相似度到底差多少!

01 向量与余弦相似度:AI 如何判断"意思相近"
https://enkiud.com/posts/math-01/
作者
Enkidu
发布于
2026-08-10
许可协议
CC BY-NC-SA 4.0