返回

曼哈顿距离与余弦距离:Qdrant中分数不同但结果相同?

Ai

为什么曼哈顿距离和余弦距离得分不同,但返回的文本块却相同?

在使用 Qdrant 数据库和客户端进行文档嵌入时,你可能会遇到一个情况:使用曼哈顿距离(Manhattan distance)构建向量集合时得到的分数比使用余弦距离(Cosine distance)时更高,但返回的文本块却完全相同。 刚接触RAG(Retrieval-Augmented Generation),不太理解这是为什么。别急,下面我们来捋一捋。

问题原因分析

根本原因在于曼哈顿距离和余弦距离这两种距离度量方式的不同计算方法,导致了评分机制上的差异,进一步导致分数在数值上的巨大差别。但这 不影响 返回最相近的结果的 相对顺序, 这也是返回文本相同的原因。

  • 余弦距离(Cosine Distance): 衡量的是两个向量之间夹角的余弦值。它关注的是向量的方向,而不考虑向量的长度(幅度)。余弦值的范围在 -1 到 1 之间,值越接近 1,表示两个向量的方向越相似;越接近 -1,表示方向越相反;0 表示两个向量正交(不相关)。 在Qdrant里,做了归一处理,因此范围变成了[0,2], 因此上面例子中相关性很低。

  • 曼哈顿距离(Manhattan Distance): 也叫城市街区距离,想象一下在曼哈顿的街区,你从一个路口走到另一个路口,不能直接穿过大楼,只能沿着街道走。曼哈顿距离就是这样,计算的是两个向量在各个维度上的差值的绝对值之和。数值越小,距离越小。与余弦距离不同,曼哈顿距离同时考虑了方向和幅度。

所以,虽然两种距离度量方式都用于衡量向量的相似度,但它们的计算方法和数值范围完全不同。这意味着,对于相同的两个向量,余弦距离和曼哈顿距离计算出的分数通常会不同,甚至可能差别很大。但更重要的是,这种差别是两种标准下的,只要每个向量之间可以比较,那最终通过排序得到的最近距离仍然会返回一样的文本块,仅仅是不同计算标准下的“分数”表示不同罢了。

解决方案

既然已经知道分数不同是由距离计算方式导致的, 而且并不影响实际结果(返回的文本块相同)。 所以,真正在意的并不是绝对数值本身。 你真正需要关注的应该是如何根据自己的应用场景选择合适的距离度量方式。下面提供选择和使用不同度量方式时的几点建议。

1. 理解并选择合适的距离度量

  • 如果更关注向量的方向: 选余弦距离。比如,比较两篇文章的主题相似度,这时文本长度可能差异很大,但主题(方向)可能相似。

  • 如果向量的幅度和方向都很重要: 选曼哈顿距离或欧几里得距离。 比如,比较两个城市的人口和经济指标,这时数量级和具体数值都很重要。

  • 经验法则 : 一般做语义相似度搜索, 大概率会使用 Cosine 余弦距离

在 Qdrant 中,你可以在创建集合时通过 distance 参数指定距离度量方式。

from qdrant_client import QdrantClient, models

client = QdrantClient(":memory:")  # 或连接到你的 Qdrant 实例

# 使用余弦距离
client.create_collection(
    collection_name="my_collection_cosine",
    vectors_config=models.VectorParams(size=128, distance=models.Distance.COSINE)
)

# 使用曼哈顿距离
client.create_collection(
    collection_name="my_collection_manhattan",
    vectors_config=models.VectorParams(size=128, distance=models.Distance.MANHATTAN)
)

# 默认距离是 `Cosine`.  

2. 分数归一化 (Normalization)

如果一定要比较不同距离度量方式下的分数,或者希望分数在一个更直观的范围内,可以考虑对分数进行归一化。 归一化可以将分数映射到一个特定的范围(如 0 到 1)。注意,虽然我们上面说不用太纠结这个不同算法下的“绝对值”大小,但是为了更好展示和观察,还是可以将这个值归一处理到比如[0-1]的范围的。

  • 余弦距离归一化: 因为Qdrant对余弦相似度做了处理,让范围在[0-2]之间, normalized_score = 1 - cosine_distance / 2,可以将余弦距离转换成 0 到 1 之间的相似度分数。数值越大越相似。

  • 曼哈顿距离归一化: 由于曼哈顿距离没有固定的上限,可以根据数据集的特点进行归一化。例如,可以除以最大可能的曼哈顿距离(基于向量的维度和取值范围)或所有样本中的最大曼哈顿距离。 或者,可以采用 normalized_score = 1 / (1 + manhattan_distance). 数值越大,代表距离越小。

import numpy as np

def normalize_cosine_score(cosine_score):
  """将 Qdrant 的余弦距离得分归一化到 [0, 1] 范围。"""
  return 1 - cosine_score / 2

def normalize_manhattan_score(manhattan_distance, max_possible_distance=None):
    """将曼哈顿距离归一化。
    
       可以自定义最大距离. 或者用 1/(1+manhattan_distance).
    """
    if max_possible_distance is None:
         return 1 / (1 + manhattan_distance)

    return 1 - (manhattan_distance / max_possible_distance)

# 示例
cosine_score = 0.17464592
normalized_cosine = normalize_cosine_score(cosine_score)
print(f"Normalized Cosine Score: {normalized_cosine}")  #更符合越大越相似的习惯

manhattan_distance = 103.86209
#假设128维,每维最大值差值是1。 那么理论最大值就是128,可以这么归一
normalized_manhattan = normalize_manhattan_score(manhattan_distance, 128)
print(f"Normalized Manhattan Score (max_dist=128): {normalized_manhattan}")
# 或采用这个通用型
normalized_manhattan2 = normalize_manhattan_score(manhattan_distance)
print(f"Normalized Manhattan Score (1/(1+d)): {normalized_manhattan2}")

3. 关注相对排名而非绝对分数

大部分情况下, 只需要保证向量数据库使用了同一种度量方法, 并关注最相似结果的相对排名即可, 而不是具体分数值。 分数本身只是一个相对的参考。 Qdrant 返回的结果是按照相似度排序的,最相似的结果排在最前面。 即使分数不同,只要返回的文本块顺序相同,就说明两种距离度量方式在这个场景下给出了相同的相似度排序结果。

4. 进阶技巧: 距离度量结合使用

某些场景下,还可以组合距离来获取跟精细化对比:

  1. 先用 Cosine 找出比较相关的 (比如 top 50)
  2. 再用其他距离精排

Qdrant 支持自定义查询, 提供了很强灵活性。以下是一个进阶用法的例子,结合使用过滤和不同的距离计算方法。

from qdrant_client import QdrantClient, models

client = QdrantClient(":memory:")

# 创建一个同时支持两种距离的集合
client.create_collection(
     collection_name="my_mixed_collection",
    vectors_config={
        "cosine": models.VectorParams(size=4, distance=models.Distance.COSINE),
        "manhattan": models.VectorParams(size=4, distance=models.Distance.MANHATTAN),
    },
)
#插入点数据
client.upsert(
    collection_name="my_mixed_collection",
    points=[
         models.PointStruct(id=1, vector={"cosine":[0.1, 0.2, 0.3, 0.4], "manhattan": [0.1, 0.2, 0.3, 0.4]}),
         models.PointStruct(id=2, vector={"cosine": [0.4, 0.3, 0.2, 0.1], "manhattan": [0.4, 0.3, 0.2, 0.1]}),
         models.PointStruct(id=3, vector={"cosine": [0.2, 0.4, 0.1, 0.3],"manhattan":  [0.2, 0.4, 0.1, 0.3]})
    ]
)
# 使用Cosine进行初筛, 用Manhattan精排 (用Filter)
result = client.search(
    collection_name="my_mixed_collection",
    query_vector=("cosine", [0.11, 0.22, 0.33, 0.44]),  # 指定用cosine配置
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key='id',
                range=models.Range(gte=2) # id必须 >=2
           )
        ]
    ),
    limit=3,
     score_threshold = 0.9 # Cosine 要> 0.9, 才视为相关
)
print(result)

# 可以在业务层对上面的返回,基于Manhattan距离精排 (省略代码, 仅仅需要读取vector.manhattan并根据需求排序).

上面的例子中, 可以利用Filter等在第一次筛选就提高精度, 缩小精排范围。 提升性能。

总而言之,遇到曼哈顿距离和余弦距离分数不同但返回文本相同的情况不用慌。两种距离的计算方式不同,所以导致分数的数值范围不同,只要你用的是同一套向量数据库和同一种距离计算方式,就不影响最终结果的 相对顺序。 重点是要选择一个最适合当前业务需求的距离算法。