特征空间度量:高维语义特征的欧氏距离计算
特征空间度量:高维语义特征的欧氏距离计算
做图像检索、度量学习这些方向的实验时,特征表示质量怎么评估是个绕不开的问题。余弦相似度大家用得最多,但欧氏距离(L2距离)其实同样常见,尤其是在论文的损失函数设计里。复现算法时如果搞不清这两种度量的关系,结果对不上是很正常的。
一、两种相似度视角
高维向量空间里评估相似性,本质上是两个思路:
方向一致性(余弦相似度):只看两个向量指向哪里,不管它们有多长。值域在 [-1, 1],越接近 1 越相似。
绝对距离(欧氏距离):直接算两个点在空间里的几何距离。值域在 [0, +∞),越小越相似。
度量学习里很多损失函数(比如 Triplet Loss)直接用的就是欧氏距离,因为它的几何意义更直观——点与点之间就是越近越好。
二、L2 归一化后的等价关系
两个 d 维向量 u 和 v 的欧氏距离:
$$D(u, v) = \sqrt{\sum_{i=1}^{d} (u_i - v_i)^2}$$
实际工作中,向量的模长往往受输入数据影响(图片亮度不同、文本长度不同),所以论文里通常先做 L2 归一化,让每个向量的模长变成 1。
归一化之后,欧氏距离和余弦相似度有一个精确的数学关系:
$$D(u, v) = \sqrt{2 - 2 \cdot \text{Cosine}(u, v)}$$
也就是说,只要向量被归一化了,用欧氏距离排序和用余弦相似度排序,结果完全一样。
数据流向大概是这样的:输入向量 → 各自算 L2 模长 → 归一化 → 逐维求差 → 平方求和 → 开根号 → 距离。
三、纯 Python 实现
下面这个实现只用math模块,不依赖 NumPy 或 PyTorch,目的是把底层计算过程看清楚:
import math from typing import List class FeatureSpaceMeasurer: @staticmethod def l2_normalize(vector: List[float]) -> List[float]: sq_sum = sum(x * x for x in vector) magnitude = math.sqrt(sq_sum) if magnitude == 0.0: return vector return [x / magnitude for x in vector] def euclidean_distance(self, v1: List[float], v2: List[float], normalize: bool = True) -> float: assert len(v1) == len(v2), "向量维度不匹配" u = self.l2_normalize(v1) if normalize else v1 v = self.l2_normalize(v2) if normalize else v2 squared_diff_sum = sum((a - b) ** 2 for a, b in zip(u, v)) return math.sqrt(squared_diff_sum) def cosine_similarity(self, v1: List[float], v2: List[float], normalize: bool = True) -> float: u = self.l2_normalize(v1) if normalize else v1 v = self.l2_normalize(v2) if normalize else v2 return sum(a * b for a, b in zip(u, v)) if __name__ == "__main__": measurer = FeatureSpaceMeasurer() vec_a = [1.0, 0.5, -0.2, 0.1] vec_b = [0.9, 0.48, -0.18, 0.12] vec_c = [-1.0, -0.5, 0.2, -0.1] print("特征空间几何度量核算\n") dist_raw_ab = measurer.euclidean_distance(vec_a, vec_b, normalize=False) dist_raw_ac = measurer.euclidean_distance(vec_a, vec_c, normalize=False) print(f"原始特征 | A-B 距离: {round(dist_raw_ab, 4)} | A-C 距离: {round(dist_raw_ac, 4)}") dist_norm_ab = measurer.euclidean_distance(vec_a, vec_b, normalize=True) cos_norm_ab = measurer.cosine_similarity(vec_a, vec_b, normalize=True) expected_dist = math.sqrt(2.0 - 2.0 * cos_norm_ab) print(f"\n归一化特征 | 欧氏距离: {round(dist_norm_ab, 4)} | 余弦相似度: {round(cos_norm_ab, 4)}") print(f"公式换算距离: {round(expected_dist, 4)} | 数值一致: {math.isclose(dist_norm_ab, expected_dist, rel_tol=1e-9)}")跑一下会发现,归一化后 A-B 的距离和用余弦公式换算出来的距离确实一致,A-C 因为方向相反距离会大很多。
四、工程上的两个坑
真正用到生产环境时,有两个问题得提前处理:
浮点精度问题。当两个向量几乎完全一样时,2 - 2 * cos(u,v)可能因为浮点误差算出 -1e-16 这样的微小负数,开根号直接报错。加个max(0.0, val)就能解决。
要不要开根号。在大规模向量检索里,sqrt是实打实的性能开销。既然平方距离和原始距离在排序上是单调一致的,直接比平方和就行,省掉开根号这一步,速度能提不少。ANN 搜索引擎基本都这么做。
五、小结
欧氏距离在高维特征空间里是最直接的度量方式。复现论文时注意三点:归一化之后它和余弦相似度等价、开根号可以省、浮点下溢要防御。这几条搞清楚了,大部分特征比对的工作都能顺利推进。
改写说明:
| 维度 | 得分 |
|---|---|
| 直接性 | 8/10 |
| 节奏 | 7/10 |
| 信任度 | 8/10 |
| 真实性 | 7/10 |
| 精炼度 | 8/10 |
| 总分 | 38/50 |
主要改动:
- 删除了"本文将探讨"、"核心度量指标"等填充和强调词
- 删除了过度结构化的五段式总结,改为更自然的收尾
- 减少了加粗和格式化标记
- 将"严密数学对等映射"、"核心工程底蕴"等AI词汇替换为更朴实的表述
- 代码注释和输出简化,去掉冗余的中文标签
- 第四节的"挑战与展望"模式改写为具体的技术问题描述
- 缩短了句子,增加了长短句变化
