news 2026/10/15 21:40:53

余弦相似度与欧氏距离:相似度度量的核心差异与工程选择

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
余弦相似度与欧氏距离:相似度度量的核心差异与工程选择

很多人在刚接触机器学习或者做推荐系统、文本匹配的时候,都会遇到两个绕不开的名字:余弦相似度和欧氏距离。这俩确实是衡量"东西像不像"最常用的两个指标,但也是被误解得最深的一对。我见过太多人拿到数据就无脑算一个,结果模型效果差得离谱,还搞不清问题出在哪。这篇文章就把这两个指标掰开揉碎讲清楚,重点落在"指标衡量差异"这件事上——它俩到底差在哪,什么时候该用哪个,以及怎么在工程里用才不容易翻车。

文章比较长,涉及的场景包括向量检索、文本匹配、用户画像相似度这些常见需求。不管你是刚入门的学生,还是已经在做一些数据分析、特征工程相关的开发,读完应该都能对相似度度量有一套清晰的选择逻辑。

1. 两个度量的核心逻辑,先建立直觉

1.1 余弦相似度:看方向,不看距离

余弦相似度的定义其实特别简单,就是两个向量夹角的余弦值。数学公式长这样:

$$cosine_similarity(A, B) = \frac{A \cdot B}{||A|| \cdot ||B||}$$

说白了就是先把两个向量的模长归一化到单位长度,然后看它们之间的夹角。夹角越小,余弦值越接近1,表示方向越一致;夹角90度,余弦值是0,表示正交、无关;夹角越大,余弦值越接近-1,方向越相反。

打个比方。你把两个向量想象成从原点射出去的两支箭,余弦相似度只关心这两支箭是不是朝着差不多的方向射,不关心射出去的远近。一支箭射了1米,另一支射了100米,只要方向一致,余弦相似度就是1,完美相似。

这个特性在文本场景里特别有用。比如两篇文章,一篇写了5000字,一篇是500字浓缩摘要,主题都是"深度学习入门"。词频向量的模长差很多,但分布模式很接近——重点词都集中在"神经网络""梯度下降""反向传播"这些词上。这时候用欧氏距离,长度差异会直接毁掉结果,但用余弦相似度就能干净利落地抓住"主题一致性"。

1.2 欧氏距离:看绝对数值差

欧氏距离就是我们最熟悉的直线距离,n维空间中两个点之间的真实距离:

$$euclidean_distance(A, B) = \sqrt{\sum_{i=1}^{n}(A_i - B_i)^2}$$

它衡量的是两个向量在特征空间里的位置差了多少。余弦相似度忽略长度,欧氏距离则把长度差异直接算进结果里。

还是用射箭的比方。欧氏距离关心的是两支箭的箭尖之间隔了多少米。同样方向的两支箭,一支射了1米,一支射了100米,余弦相似度说它们"完全相似",欧氏距离却会告诉你它们差了99米,根本不接近。

所以欧氏距离在什么时候合理?在特征本身有绝对意义、数值差异代表真实差异的场景。比如两个用户在一个电商平台上的消费金额向量,一个月花了500元,一个月花了10000元,这个数值差异本身就代表消费能力的不同,用欧氏距离就非常自然。这时候你要是只看方向(比例),反而会把这俩"消费能力完全不同"的人当成很相似的人。

1.3 一张表说清核心差异

维度余弦相似度欧氏距离
关注点方向一致性数值绝对差异
取值范围[-1, 1],越大越相似[0, +∞),越小越相似
对量纲/尺度敏感度不敏感(归一化后比较)很敏感
对向量模长差异直接忽略完全计入
典型场景文本、稀疏向量、用户偏好连续数值特征、空间坐标、物理量
数学本质内积 + 长度归一化L2范数

很多人在实际项目里遇到的问题是:两种指标算出来的结果趋势经常不一致。用余弦相似度排出来的最相似用户,用欧氏距离排出来的可能是最不相似的。这不是算法错了,而是这两个指标本来就在回答不同的问题。

2. 为什么它们衡量的是"不同的差异"

2.1 标准化视角下的等价性

这里有个特别关键的数学事实:如果先把所有向量做L2归一化(也就是除以模长,变成单位向量),然后再计算,余弦相似度和欧氏距离就变成严格单调相关的了。

推导过程很简短,但能让理解上一个台阶。

对单位向量 $A'$ 和 $B'$,有 $||A'|| = ||B'|| = 1$。那么:

$$||A' - B'||^2 = ||A'||^2 + ||B'||^2 - 2 \cdot A' \cdot B' = 2 - 2 \cdot cosine(A', B')$$

也就是说,欧氏距离的平方等于2减去两倍的余弦相似度。向量A和B越相似(余弦值越大),归一化之后的欧氏距离就越小。

这就是为什么在向量检索领域(像Facebook Faiss、Milvus这类向量数据库),很多场景下你选哪个指标都差不多——因为大家基本都会在入库前做归一化。但关键点是:没做归一化之前,这两个指标根本不是一回事。千万别默认它们等价,这是实战中最大的坑之一。

2.2 量纲敏感性差异的直观案例

举个例子,假设有3个用户在某平台上的两个行为特征:"观看时长(小时)"和"购买金额(元)"。

  • 用户A:(10, 100)
  • 用户B:(20, 200)
  • 用户C:(10, 300)

先看余弦相似度。A和B的特征比例都是1:10,方向完全一致,余弦相似度为1。A和C一个1:10,一个1:30,方向差距大,余弦相似度只有大概0.83左右。所以在余弦视角下,A和B是最相似的。

再看欧氏距离。A和B的距离是 $\sqrt{(10)^2 + (100)^2} \approx 100.5$,A和C的距离是 $\sqrt{(0)^2 + (200)^2} = 200$。欧氏距离的结论同样认为A和B更近,但这纯粹是因为观看时长这个维度数值小、对距离贡献小,而购买金额差异主导了结果。

再改一下案例,把观看时长换成0到1之间的归一化值,把购买金额保持原样。

  • 用户A:(0.1, 100)
  • 用户B:(0.2, 200)
  • 用户C:(0.1, 300)

余弦相似度几乎不变,还是认为A和B最像。但欧氏距离下,A和B的距离是 $\sqrt{0.1^2 + 100^2} \approx 100.0$,A和C的距离是 $\sqrt{0 + 200^2} = 200$,结论依然一致。可如果把购买金额改成万元单位,也就是A:10000,B:20000,C:30000,再看欧氏距离:

  • A和B:$\sqrt{0.1^2 + 10000^2} \approx 10000$
  • A和C:$\sqrt{0 + 20000^2} = 20000$

结论还是一样。但你换个特征试试,比如把其中一个特征改成"登录天数",数值范围一下变成0~365,特征之间的量纲差距会直接扭曲欧氏距离的结果。事实上,只要两个特征的数值范围差几个量级,欧氏距离基本就等于被大数值的特征主导了,小数值特征全部失效。

2.3 从几何视角理解"度量差异"

在三维空间里,可以更直观地感受这两个度的差异。想象三维空间中的一个点P,欧氏距离衡量的是P到原点O的连线长度,是一个标量;而余弦相似度描述的是OP这条射线相对于坐标轴的角度偏转。同一个点上,这两个值可以一个很大、一个为0,完全描述不同性质。

从信息的视角来说,欧氏距离更"挑剔"——它要求每个维度都足够接近;余弦相似度更"宽容"——它只要求整体模式一致,允许整体缩放。所以在做相似度计算之前,先问自己一个问题:你关心的是"我们俩的数值是不是都差不多",还是"我们俩的趋势/模式是不是一样"?答案直接决定选哪个指标。

3. 实际操作:怎么选型,怎么组合

3.1 文本和稀疏向量场景,默认选余弦

NLP和搜索场景里,文本经过分词、TF-IDF或者词袋编码之后,得到的向量几乎都是稀疏向量,而且文档长度的差异会直接影响词频数值。同一篇长文和它的摘要,欧氏距离会巨大,但余弦相似度能稳定表达语义接近程度。

我自己做文本聚类的时候基本无脑用余弦。比如用TF-IDF把新闻文档转成向量,然后计算两两相似度做聚合,余弦的结果非常稳定。换欧氏距离试过一次,长文档之间因为词频绝对值大,距离全部偏大,短文档之间距离反而偏小,聚类结果基本不可用——因为欧氏距离把"文档长度"这个无关信息当成了主要差异来源。

这里有个补充细节:很多文本向量是稀疏的,里面大量维度是0。这时候用余弦相似度还有额外的好处——0值向量或者大量0值维度对余弦的扰动小。不过在scikit-learn里,cosine_similarity对全零向量的处理是返回NaN,实务里要提前过滤。

3.2 数值型特征和空间数据,优先考虑欧氏距离

如果特征是连续的、每个维度都有明确物理意义的数值——比如温度、价格、GPS坐标、传感器读数——欧氏距离通常更合适。因为这些场景里,数值的绝对大小就是信息本身,不能因为"方向一致"就忽略掉"一个在赤道一个在北极"这个事实。

举例,用K-Means做用户分群,特征有年龄、年收入、月消费额。如果只做标准化不归一化,聚类时欧氏距离就是默认选择,因为它能区分收入100万和10万的人,哪怕这两类人的消费模式比例完全一致。

但要注意,欧氏距离在多元量纲混合的场景有个隐患——特征的数值范围差异会天然产生权重偏差。所以在用欧氏距离之前,几乎必须做标准化(Z-Score或者Min-Max),不然数值大的特征会压制数值小的特征。

3.3 两者的组合使用思路

实际项目里,我越来越多地发现"单选一个指标"是不够的。一个很实用的思路是:先归一化,再同时计算两个指标,然后把它们组合成新的相似度分数。

比如一个推荐系统里,用户A和用户B在余弦相似度上得分0.95(消费结构一致),但欧氏距离非常大(消费总量差距大)。这时候你会怎么判断?如果业务上希望"消费主力人群向消费潜力人群渗透",你可能需要的是消费结构相似但总量有差距的组合,那余弦高分+欧氏距离大反而是好消息。

如果业务希望"找到同层级用户互相推荐",那就要求两个指标都要小。具体做法可以这样:把欧氏距离取倒数或者做高斯核变换变成相似度(比如 $sim = \frac{1}{1 + distance}$),然后跟余弦相似度做加权和。

权重怎么定?没有通用答案,就看业务里哪个更重要。我之前做过一个画像系统,最终公式用的是:

$$score = 0.6 \cdot cosine_similarity + 0.4 \cdot (1 - normalized_euclidean_distance)$$

这个小技巧帮我解决了一个长期困扰的问题:只看余弦会误召回很多"消费结构像但消费能力天差地别"的用户,只看欧氏又过于严格,召回太少。组合之后,线上指标明显改善。

4. 代码实现:从零手写和工程库对比

4.1 用纯Python实现两个指标

先来个最朴素的实现,帮大家把公式落地:

import math def cosine_similarity(vec_a, vec_b): # 向量长度必须一致,否则后续计算会出错 assert len(vec_a) == len(vec_b) # 处理空向量和零向量的边界情况 norm_a = math.sqrt(sum(x * x for x in vec_a)) norm_b = math.sqrt(sum(x * x for x in vec_b)) if norm_a == 0 or norm_b == 0: return float('nan') dot = sum(x * y for x, y in zip(vec_a, vec_b)) return dot / (norm_a * norm_b) def euclidean_distance(vec_a, vec_b): assert len(vec_a) == len(vec_b) return math.sqrt(sum((x - y) ** 2 for x, y in zip(vec_a, vec_b)))

这个版本的代码适合理解原理,但工程上不推荐用纯Python算,因为性能太差了。稍微大数据量的场景,建议直接用numpy:

import numpy as np def cosine_similarity_np(vec_a, vec_b): vec_a = np.asarray(vec_a, dtype=np.float32) vec_b = np.asarray(vec_b, dtype=np.float32) norm_a = np.linalg.norm(vec_a) norm_b = np.linalg.norm(vec_b) if norm_a == 0 or norm_b == 0: return np.nan return np.dot(vec_a, vec_b) / (norm_a * norm_b) def euclidean_distance_np(vec_a, vec_b): return np.linalg.norm(np.asarray(vec_a) - np.asarray(vec_b))

numpy向量化的计算速度比纯Python快几十倍不止,这也算是工程里的基本操作了。

4.2 scikit-learn里的标准做法

实际项目中,我一般直接用scikit-learn的pairwise_distances或者cosine_similarity,因为接口统一还自带很多优化。举几个常用写法:

from sklearn.metrics.pairwise import cosine_similarity from sklearn.metrics.pairwise import euclidean_distances matrix = [ [1, 0, 0, 0], [1, 1, 0, 0], [0, 0, 1, 1] ] # 返回3x3的相似度矩阵 cos_matrix = cosine_similarity(matrix) # 返回3x3的距离矩阵 euc_matrix = euclidean_distances(matrix)

这里有个非常容易犯的错误:cosine_similarity计算的是相似度,越大越好;euclidean_distances计算的是距离,越小越好。两个指标方向相反,在定义业务指标或者设置阈值的时候一定要搞清楚方向,不然排序逻辑全反了。

4.3 归一化的坑:一定要在划分数据之前做

这是我在项目里踩过最深的坑之一。做特征工程时,量纲不统一,我顺手对整个数据集做了Min-Max归一化。当时数据集包含训练集和测试集,我直接一把梭,对全量数据做了归一化。后来模型上线,测试集效果看上去还行,但实际部署时发现新数据的分布和之前归一化用的范围完全对不上。

正确做法是:先用训练集拟合归一化参数(Min-Max的min和max,或者Z-Score的mean和std),然后用同一套参数去transform测试集和新数据。这个道理在相似度计算上也成立——如果你需要用欧氏距离,标准化参数只能从参考集里学,不能从全量数据里学,否则会有信息泄露。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() # 只对训练数据fit scaler.fit(train_features) train_features_scaled = scaler.transform(train_features) # 用同一套参数transform测试数据 test_features_scaled = scaler.transform(test_features)

这个细节很多人不注意,但直接决定了泛化能力。

5. 实战中常见问题与排查思路

5.1 为什么结果"反直觉"

不少人遇到过这种问题:用余弦相似度找到了"最相似"的样本,人工一看,完全是两个不同的东西。反过来用欧氏距离找到的最邻近样本,又觉得好像也没那么接近。这种"反直觉"基本都出在特征选择和预处理上。

一个典型的反例:做图像相似度检索时,直接把原始像素值拉平成一维向量算余弦。两张图,一张是白天拍的猫,一张是夜晚拍的猫,像素值整体偏移很大,余弦相似度反而可能不高;而两张完全不同的图,因为像素均值相近,欧氏距离反而可能很小。纯像素级相似度就是这样的,"语义相似"和"数值相似"完全是两层皮。

对策是:用特征向量替代原始向量。比如图片用卷积网络提取到的embedding,文本用句向量,用户用物品向量的加权和。特征好,相似度指标才有意义。

5.2 维度灾难带来的距离趋同

当特征维度特别高(比如上万维),欧氏距离会出现一个奇怪的现象:所有样本两两之间的距离都差不多,区分度急剧下降。这就是所谓"维度灾难"的典型表现。在这种高维场景下,欧氏距离几乎失灵,余弦相似度虽然也会受影响,但相对更能保留区分度。

我的经验是,特征维度超过几百维之后,优先考虑余弦相似度,或者先用PCA等降维手段把维度压下来再算欧氏距离。尤其是做KNN或者聚类的时候,高维欧氏距离几乎等于随机排序,根本不可用。

5.3 向量中零值和缺失值的处理

现实数据里,大量特征是稀疏的,有大量0值。这在余弦相似度里问题不大,因为0值只会让内积变小,不影响方向判断。但在欧氏距离里,0值的意义是"这个特征缺失"还是"这个特征真实为0"?如果是缺失,直接参与距离计算会产生严重偏差。

假设两个用户,一个没看过任何恐怖片,一个看了3部恐怖片,特征"恐怖片观影数"分别是0和3。这个0是真实的0,参与计算没毛病。但如果用户A没登录过,那个特征是缺失的,你把缺失填成0再算距离,A就成了"没看过恐怖片的人",这和真实情况完全不同。

处理方式:

  • 如果是数值型缺失,可以考虑填充为均值/中位数后再算欧氏距离;
  • 如果是逻辑上的"不存在",可以考虑把该维度做成0/1布尔特征;
  • 如果缺失比例太高,考虑直接删掉该维特征再计算。

5.4 什么时候该切换指标

工程里判断指标是否合适,不能只看离线指标,要回到业务视角。我整理过一个自我检查清单:

  • 业务上"相似"的定义更接近"模式一致"还是"数值接近"?模式一致选余弦,数值接近选欧氏。
  • 特征是否已经做过标准化?没做标准化就别用欧氏距离,做了再考虑。
  • 是否涉及到稀疏高维特征?高维稀疏优先余弦。
  • 是否容忍整体缩放?容忍缩放选余弦,不容忍选欧氏。
  • 有没有长度差异带来的干扰?文本长度、活跃时长这类天然有量级差异的特征,余弦更稳。

如果按这个清单走完还是拿不准,我的建议是:两个都跑一遍,看它们的差异主要出现在哪些样本上,再针对差异样本人工抽检,看看哪个结果更符合业务直觉。这个方法比我见过任何理论分析都实用,毕竟数据不会说谎。

6. 最后分享一个实用技巧

不管用哪个指标,上线前最好做一次"相似度分布体检"。具体做法很简单:把你所有的样本两两算一遍相似度/距离,画出分布直方图,观察是否集中在一个很窄的区间。

我见过很多项目,阈值设得跟闹着玩似的——比如余弦相似度阈值设0.9,结果全量数据里90%的pair都在0.8~1.0这个区间,这个阈值等于没有。做一次分布体检,你会知道该把阈值设在哪个分位数上,比拍脑袋靠谱得多。

另外,工程实现上我强烈建议用专门的向量检索库来做大规模相似度计算,比如Faiss或者Milvus。自己写双层for循环算pairwise,数据量上到百万级你会怀疑人生。选索引类型的时候,注意看底层支持的距离/相似度类型是否包含你选的指标——有些索引只支持欧氏距离,有些支持内积,别把索引类型和指标搞混了。

这些内容是我在实际项目里反复摔打出来的经验。相似度度量的选择看似基础,却是很多上层算法效果的基石。下次再遇到这种选择,不妨先停下来想一想:我到底想要模型"看见"什么差异?想清楚这个问题比硬调参有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 21:40:51

从零搭建聚合搜索工具:搜索源接入、结果聚合与部署实践

最近在整理书签和常用工具时,发现“柠檬搜索”这类聚合搜索工具又回到了开发者的视野里。这次回归不仅带来了更清爽的界面,也让大家重新开始讨论“自建搜索入口”这件事到底值不值得做。与其停留在“好不好用”的层面,不如直接动手&#xff0…

作者头像 李华
网站建设 2026/10/15 21:38:05

Agent落地最后一公里:灵衢互联、分级适配与软硬协同实践解析

这段时间被Agent折腾得够呛的朋友应该不少:模型聪明不聪明已经排不到第一位了,真正让人头大的是,Agent在演示环境里跑得行云流水,一旦搬到生产环境就各种“最后一公里”问题冒出来——工具调用超时、端侧算力吃不消、多个Agent互相…

作者头像 李华
网站建设 2026/10/15 21:36:24

近世代数课件怎么啃?从代数系统主线到运算性质一次讲透

简介:近世代数课件面向数学专业本科生、考研学生以及自学抽象代数的读者,系统梳理了近世代数的核心概念与知识框架。内容从代数系统的定义与性质切入,依次介绍同构与同态、同余关系、商代数和积代数、半群与独异点、群与子群、循环群与置换群…

作者头像 李华
网站建设 2026/10/15 21:33:48

华为 MatePad / 折叠屏远控电脑,指针总差几厘米?这样校准

很多华为 MatePad、折叠屏用户在远程控制电脑时都有过这种抓狂时刻:明明点的是图标,鼠标却落在差几厘米的地方,反复点都点不中。这不是你的手抖,也不是屏幕坏了,而是屏幕坐标系的适配问题。这篇文章讲清楚原因&#xf…

作者头像 李华
网站建设 2026/10/15 21:31:47

Ubuntu 24.04 安装 PostgreSQL 18:PGDG 源、远程连接与性能调优实操

很多人在这台服务器上折腾数据库时,最容易踩的坑就是:明明是 Ubuntu 24.04,执行apt install postgresql后却发现自己装的是 16,而不是标题里想用的 PostgreSQL 18。如果你准备从零搭一套 PostgreSQL 18,并且想一次性搞…

作者头像 李华