两个数字列表之间的余弦相似度

243

我想计算两个列表之间的余弦相似度,例如列表1是dataSetI,列表2是dataSetII

假设dataSetI[3, 45, 7, 2]dataSetII[2, 54, 13, 15]。列表的长度始终相等。我想将余弦相似度作为介于0和1之间的数字进行报告。

dataSetI = [3, 45, 7, 2]
dataSetII = [2, 54, 13, 15]

def cosine_similarity(list1, list2):
  # How to?
  pass

print(cosine_similarity(dataSetI, dataSetII))
18个回答

311
另一种仅基于numpy的版本
from numpy import dot
from numpy.linalg import norm

cos_sim = dot(a, b)/(norm(a)*norm(b))

37
我的系统上,使用这种方法比使用 scipy.spatial.distance.cosine 的速度要快得多。 - Ozzah
11
更简洁的写法:cos_sim = a @ b.T / norm(a) / norm(b)。其中 @ 表示矩阵乘法,norm() 表示向量的 L2 范数(也称欧几里得范数)。整个式子计算两个向量的余弦相似度。 - Union find
在我的系统上,对于微小/虚拟用例,SciPy的速度大致相同。 SciPy被优化为高效比较大量向量列表。对于大量向量列表,SciPy的速度比其他方法快几个数量级。 - jameslol

268

你应该尝试 SciPy。它有一堆有用的科学例程,例如“计算积分数值、解决微分方程、优化和稀疏矩阵的例程”。它使用超快速的优化 NumPy 进行数值计算。点击 这里 进行安装。

请注意,spatial.distance.cosine 计算的是距离,而不是相似度。因此,您必须从 1 中减去该值以获得相似度

from scipy import spatial

dataSetI = [3, 45, 7, 2]
dataSetII = [2, 54, 13, 15]
result = 1 - spatial.distance.cosine(dataSetI, dataSetII)

116
您可以使用sklearn.metrics.pairwise中的cosine_similarity函数。详情请参考文档
In [23]: from sklearn.metrics.pairwise import cosine_similarity

In [24]: cosine_similarity([[1, 0, -1]], [[-1,-1, 0]])
Out[24]: array([[-0.5]])

51

我不认为性能在这里很重要,但我无法抵制。zip()函数完全复制两个向量(实际上更像是矩阵转置),只是为了按“Pythonic”顺序获取数据。测试一下底层实现的运行时间可能会很有趣:

import math
def cosine_similarity(v1,v2):
    "compute cosine similarity of v1 to v2: (v1 dot v2)/{||v1||*||v2||)"
    sumxx, sumxy, sumyy = 0, 0, 0
    for i in range(len(v1)):
        x = v1[i]; y = v2[i]
        sumxx += x*x
        sumyy += y*y
        sumxy += x*y
    return sumxy/math.sqrt(sumxx*sumyy)

v1,v2 = [3, 45, 7, 2], [2, 54, 13, 15]
print(v1, v2, cosine_similarity(v1,v2))

Output: [3, 45, 7, 2] [2, 54, 13, 15] 0.972284251712

这种方法通过像C语言一样逐个提取元素的方式来消除噪音,但不进行批量数组复制,所有重要操作都可以在一个循环中完成,并且只使用一个平方根。

注:更新了打印调用为函数。该代码原本是Python 2.7,现在使用带有from __future__ import print_function语句的Python 2.7运行。无论哪种方式输出结果都相同。

在3.0 GHz Core 2 Duo上运行的CPython 2.7.3:

>>> timeit.timeit("cosine_similarity(v1,v2)",setup="from __main__ import cosine_similarity, v1, v2")
2.4261788514654654
>>> timeit.timeit("cosine_measure(v1,v2)",setup="from __main__ import cosine_measure, v1, v2")
8.794677709375264

因此,在这种情况下,不使用 Pythonic 的方法大约快了3.6倍。


28

不使用任何导入

math.sqrt(x)

可以被替换为

x** .5

如果不使用numpy.dot(),则必须使用列表解析式创建自己的点积函数:

def dot(A,B): 
    return (sum(a*b for a,b in zip(A,B)))

然后只需要简单地应用余弦相似度公式:

def cosine_similarity(a,b):
    return dot(a,b) / ( (dot(a,a) **.5) * (dot(b,b) ** .5) )

17

我对这个问题的几个回答进行了基准测试,以下代码片段被认为是最佳选择:

def dot_product2(v1, v2):
    return sum(map(operator.mul, v1, v2))


def vector_cos5(v1, v2):
    prod = dot_product2(v1, v2)
    len1 = math.sqrt(dot_product2(v1, v1))
    len2 = math.sqrt(dot_product2(v2, v2))
    return prod / (len1 * len2)

结果让我感到惊讶的是,基于scipy实现并不是最快的。我进行了分析,发现scipy中的cosine函数将一个向量从Python列表转换为NumPy数组需要花费很多时间。

图片描述


请告知您使用的工具,以获取每行代码消耗时间的可视化表示。 - YoungSheldon
@YoungSheldon 已经很久了。可能是 https://github.com/uber-archive/pyflame - mckelvin

17

计算余弦距离、余弦相似度、角度距离和角度相似度的Python代码:

  • 余弦距离
  • 余弦相似度
  • 角度距离
  • 角度相似度

import math

from scipy import spatial


def calculate_cosine_distance(a, b):
    cosine_distance = float(spatial.distance.cosine(a, b))
    return cosine_distance


def calculate_cosine_similarity(a, b):
    cosine_similarity = 1 - calculate_cosine_distance(a, b)
    return cosine_similarity


def calculate_angular_distance(a, b):
    cosine_similarity = calculate_cosine_similarity(a, b)
    angular_distance = math.acos(cosine_similarity) / math.pi
    return angular_distance


def calculate_angular_similarity(a, b):
    angular_similarity = 1 - calculate_angular_distance(a, b)
    return angular_similarity

相似度搜索:

如果您想在嵌入数组中查找最接近的余弦相似度,可以使用Tensorflow,如以下代码所示。

在我的测试中,使用GPU在不到一秒钟内找到了1M个嵌入(1'000'000 x 512)中形状为1x512的嵌入的最接近值。

import time

import numpy as np  # np.__version__ == '1.23.5'
import tensorflow as tf  # tf.__version__ == '2.11.0'

EMBEDDINGS_LENGTH = 512
NUMBER_OF_EMBEDDINGS = 1000 * 1000


def calculate_cosine_similarities(x, embeddings):
    cosine_similarities = -1 * tf.keras.losses.cosine_similarity(x, embeddings)
    return cosine_similarities.numpy()


def find_closest_embeddings(x, embeddings, top_k=1):
    cosine_similarities = calculate_cosine_similarities(x, embeddings)
    values, indices = tf.math.top_k(cosine_similarities, k=top_k)
    return values.numpy(), indices.numpy()


def main():
    # x shape: (512)
    # Embeddings shape: (1000000, 512)
    x = np.random.rand(EMBEDDINGS_LENGTH).astype(np.float32)
    embeddings = np.random.rand(NUMBER_OF_EMBEDDINGS, EMBEDDINGS_LENGTH).astype(np.float32)

    print('Embeddings shape: ', embeddings.shape)

    n = 100
    sum_duration = 0
    for i in range(n):
        start = time.time()
        best_values, best_indices = find_closest_embeddings(x, embeddings, top_k=1)
        end = time.time()

        duration = end - start
        sum_duration += duration

        print('Duration (seconds): {}, Best value: {}, Best index: {}'.format(duration, best_values[0], best_indices[0]))

    # Average duration (seconds): 1.707 for Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
    # Average duration (seconds): 0.961 for NVIDIA 1080 ti
    print('Average duration (seconds): ', sum_duration / n)


if __name__ == '__main__':
    main()

想要进行更高级的相似性搜索,您可以使用 Milvus, Weaviate 或者 Faiss



11
import math
from itertools import izip

def dot_product(v1, v2):
    return sum(map(lambda x: x[0] * x[1], izip(v1, v2)))

def cosine_measure(v1, v2):
    prod = dot_product(v1, v2)
    len1 = math.sqrt(dot_product(v1, v1))
    len2 = math.sqrt(dot_product(v2, v2))
    return prod / (len1 * len2)

在计算后,您可以四舍五入它:

cosine = format(round(cosine_measure(v1, v2), 3))

如果您想要非常简短的话,可以使用这个一行代码:
from math import sqrt
from itertools import izip

def cosine_measure(v1, v2):
    return (lambda (x, y, z): x / sqrt(y * z))(reduce(lambda x, y: (x[0] + y[0] * y[1], x[1] + y[0]**2, x[2] + y[1]**2), izip(v1, v2), (0, 0, 0)))

8
您可以使用这个简单的函数来计算余弦相似度:
def cosine_similarity(a, b):
  return sum([i*j for i,j in zip(a, b)])/(math.sqrt(sum([i*i for i in a]))* math.sqrt(sum([i*i for i in b])))

4

您可以使用简单的函数在Python中完成此操作:

def get_cosine(text1, text2):
  vec1 = text1
  vec2 = text2
  intersection = set(vec1.keys()) & set(vec2.keys())
  numerator = sum([vec1[x] * vec2[x] for x in intersection])
  sum1 = sum([vec1[x]**2 for x in vec1.keys()])
  sum2 = sum([vec2[x]**2 for x in vec2.keys()])
  denominator = math.sqrt(sum1) * math.sqrt(sum2)
  if not denominator:
     return 0.0
  else:
     return round(float(numerator) / denominator, 3)
dataSet1 = [3, 45, 7, 2]
dataSet2 = [2, 54, 13, 15]
get_cosine(dataSet1, dataSet2)

3
这是余弦函数的文本实现。对于数值输入,它会给出错误的输出。 - alvas
你能解释一下为什么在这行代码中使用了 set 吗?“intersection = set(vec1.keys()) & set(vec2.keys())”。 - Ghos3t
1
你的函数似乎期望接收映射(maps),但是你却传递了整数列表。 - Ghos3t

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接