4、向量相似度与距离度量:余弦相似度、欧氏距离、曼哈顿距离、内积、Jaccard相似度、适用场景对比

聊到向量数据库,有个绕不开的话题——怎么判断两个向量“像不像”?

说白了,这就是相似度度量。我刚开始接触向量检索时,觉得不就是算个距离嘛,有啥难的?结果第一次上线推荐系统,召回结果全是错的。嗯,后来才发现,选错度量方式,整个系统就废了。

这一章,我把工作中最常用的六种度量方法掰开揉碎讲清楚。每种我都会给出代码示例,再聊聊实际场景里踩过的坑。

4.1 余弦相似度(Cosine Similarity)

余弦相似度,算的是两个向量方向上的差异。它只看夹角,不看长度。

公式很简单:

cos(A, B) = (A · B) / (||A|| × ||B||)

取值范围是 [-1, 1]。1 表示方向完全一致,-1 表示方向完全相反。

核心特点:对向量的模长不敏感。哪怕一个向量是另一个的十倍长,只要方向相同,余弦相似度就是 1。

我在做文本检索时特别喜欢用它。为什么?因为文档长度差异太大了。一篇 500 字的新闻和一篇 5000 字的报告,词频向量长度差很多,但主题可能完全一样。余弦相似度正好能忽略长度,抓住语义方向。

import numpy as np

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

# 示例
vec1 = np.array([1, 2, 3])
vec2 = np.array([2, 4, 6])  # 方向相同,长度翻倍
print(cosine_similarity(vec1, vec2))  # 输出 1.0

个人经验:做文本 embedding 检索时,我默认先用余弦相似度。它和 BERT、Sentence-BERT 这类模型的输出天然匹配。我曾经试过用欧氏距离替代,结果 top-10 召回里混进了大量短文本噪声,后来果断换回余弦。

4.2 欧氏距离(Euclidean Distance)

欧氏距离就是我们最熟悉的“直线距离”。

d(A, B) = sqrt( (a1-b1)² + (a2-b2)² + ... + (an-bn)² )

取值范围是 [0, +∞)。0 表示完全重合。

欧氏距离对向量的每个维度都敏感。换句话说,它既看方向,也看长度。

我记得有一次做图像特征检索,用余弦相似度怎么调都召不回相似图片。后来发现,图像特征向量的模长本身就携带了重要信息——比如亮度、对比度。换成欧氏距离后,效果立竿见影。

def euclidean_distance(a, b):
    return np.sqrt(np.sum((a - b) ** 2))

vec1 = np.array([1, 2, 3])
vec2 = np.array([2, 4, 6])
print(euclidean_distance(vec1, vec2))  # 输出约 3.74

避坑指南:我曾经在一个高维稀疏向量场景里直接用欧氏距离,结果所有样本的距离都差不多,完全拉不开区分度。高维空间下,欧氏距离会遭遇“维度灾难”,建议先降维再用。

4.3 曼哈顿距离(Manhattan Distance)

曼哈顿距离也叫 L1 距离。想象你在网格状街道上走路,只能横平竖直地走,不能穿楼。

d(A, B) = |a1-b1| + |a2-b2| + ... + |an-bn|

它比欧氏距离更鲁棒。为什么?因为值运算对异常值不那么敏感。欧氏距离平方后,异常值会被放大,曼哈顿距离不会。

我个人的习惯是:当特征中有明显噪声或离群点时,优先考虑曼哈顿距离。比如用户行为数据,偶尔会有刷单导致的异常点击,用曼哈顿距离能减少这些异常的影响。

def manhattan_distance(a, b):
    return np.sum(np.abs(a - b))

vec1 = np.array([1, 2, 3])
vec2 = np.array([2, 4, 6])
print(manhattan_distance(vec1, vec2))  # 输出 6

4.4 内积(Dot Product)

内积就是向量对应位置相乘再求和。

dot(A, B) = a1×b1 + a2×b2 + ... + an×bn

取值范围是 (-∞, +∞)。

内积同时受向量方向和长度影响。两个向量方向越一致、长度越大,内积越大。

你想想看,什么场景下内积最常用?推荐系统!用户向量和物品向量的内积,直接代表用户对物品的偏好程度。长度越大,偏好越强。

注意:内积不是归一化的度量。如果向量长度差异很大,内积结果会被长度主导,而不是语义相似度。所以使用前一定要确认你的业务逻辑是否需要考虑长度。

def dot_product(a, b):
    return np.dot(a, b)

vec1 = np.array([1, 2, 3])
vec2 = np.array([2, 4, 6])
print(dot_product(vec1, vec2))  # 输出 28

4.5 Jaccard 相似度

Jaccard 相似度比较特殊。它算的是两个集合的交集与并集之比。

J(A, B) = |A ∩ B| / |A ∪ B|

取值范围是 [0, 1]。0 表示没有共同元素,1 表示完全相同。

它不关心向量中每个维度的具体数值,只关心“有没有”。所以特别适合处理二值向量(0/1 向量)。

我在做标签系统时用过它。比如用户 A 的标签是 {体育, 科技, 音乐},用户 B 的标签是 {科技, 音乐, 美食},Jaccard 相似度就是 2/4 = 0.5。

def jaccard_similarity(set_a, set_b):
    intersection = len(set_a & set_b)
    union = len(set_a | set_b)
    return intersection / union

set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}
print(jaccard_similarity(set1, set2))  # 输出 0.333

个人经验:Jaccard 在稠密向量上表现一般。它最适合稀疏的二值特征,比如用户是否购买过某商品、文档是否包含某关键词。我曾经用它做协同过滤的相似度计算,效果比余弦好。

4.6 适用场景对比

说了这么多,到底什么时候用哪个?我整理了一张表,你直接拿去用。

度量方法 适用场景 对长度敏感 对异常值敏感 典型应用
余弦相似度 文本、语义检索 否 中 BERT embedding、文档相似度
欧氏距离 图像、数值特征 是 高 图像检索、聚类
曼哈顿距离 含噪声数据 是 低 异常检测、用户行为
内积 推荐系统 是 高 矩阵分解、协同过滤
Jaccard 相似度 二值特征、标签 否 低 标签匹配、协同过滤

选度量方式没有银弹。我一般会先问自己三个问题:

  • 向量长度有没有业务含义?有的话用欧氏或内积,没有的话用余弦。
  • 数据里有没有异常值?有的话优先曼哈顿或 Jaccard。
  • 特征是稠密还是稀疏?稠密用余弦/欧氏,稀疏二值用 Jaccard。

4.7 知识体系总览

下面这张图把六种度量的核心逻辑串起来了。你可以把它当作选型时的快速参考。

向量相似度与距离度量知识体系 向量度量方法 余弦相似度 欧氏距离 曼哈顿距离 内积 Jaccard 相似度 选型核心原则 • 文本/语义 → 余弦相似度(忽略长度,关注方向) • 图像/数值 → 欧氏距离(长度和方向都重要) • 含噪声数据 → 曼哈顿距离(对异常值鲁棒) 常见应用场景 • 推荐系统 → 内积(用户偏好 × 物品属性) • 标签匹配 → Jaccard(二值特征,只看有无) • 高维稀疏 → 余弦(避免维度灾难)

4.8 实战中的选择建议

最后,我分享几条实战经验:

  • 先归一化再选度量。很多情况下,把向量归一化到单位长度后,余弦相似度和内积就等价了。我习惯先做 L2 归一化,再根据业务选。
  • 不要迷信单一度量。我在一个项目里同时用了余弦和欧氏,然后取加权平均,召回率提升了 12%。
  • 测试!测试!测试!拿一小批标注数据,跑一遍不同度量的召回结果,用 NDCG 或 MAP 评估。数据不会骗人。

重要提醒:向量数据库(如 Milvus、Qdrant)在创建索引时就需要指定度量类型。一旦建好索引,切换度量的成本很高。所以上线前一定要想清楚,最好用真实数据做对比实验。

嗯,这一章的内容就到这儿。六种度量方法,每种都有它的脾气。选对了,事半功倍;选错了,事倍功半。希望你能从我的经验里少走些弯路。