计算一维数组和二维数组中所有行的余弦相似度的高效方法

4
我有一个形状为(300, )的一维数组和一个形状为(400, 300)的二维数组。现在,我想计算这个二维数组中每一行与一维数组之间的余弦相似度。因此,我的结果应该是形状为(400, )的数组,表示这些向量的相似程度。
我的初始想法是使用for循环遍历2D数组中的行,并计算向量之间的余弦相似度。是否有更快速的广播方法来代替循环?
下面是一个人造例子:
In [29]: vec = np.random.randn(300,)
In [30]: arr = np.random.randn(400, 300)

以下是我想要计算1D数组相似度的方法:
inn = (vec * arr[0]).sum()  
vecnorm = numpy.sqrt((vec * vec).sum())  
rownorm = numpy.sqrt((arr[0] * arr[0]).sum())  
similarity_score = inn / vecnorm / rownorm  

我该如何将这个方法推广到将arr[0]替换为二维数组的情况?

如果你有400个向量要“测试”,那么你的输出将是(400,),一个简单的点积就可以完成...那么如果你有300个向量呢?你需要提供更多信息。 - Julien
@ Julien 感谢您发现了这个笔误。已经更正。 - kmario23
你的余弦相似度计算是什么?你可以给我们一个完整的工作示例,使用数组形状为(4,3)和(3,)。 - hpaulj
@hpaulj已更新问题并提供了这些细节。请检查! - kmario23
有关两个2D数组的通用解决方案,请参阅我的其他帖子https://dev59.com/uLDla4cB1Zd3GeqP1weh#61643023 - milan.vancl
请查看此处的讨论 https://codereview.stackexchange.com/questions/55717/efficient-numpy-cosine-distance-calculation - Manuel Alves
3个回答

4
余弦相似度的分子可以表示为矩阵乘法,而分母则应该很容易理解 :)。
a_norm = np.linalg.norm(a, axis=1)
b_norm = np.linalg.norm(b)
(a @ b) / (a_norm * b_norm)

其中a是一个二维数组,b是一个一维数组(即向量)。


2
这种方法比使用scipy的cdist方法快10倍 - kmario23

3
您可以使用 cdist 函数来实现:
import numpy as np
from scipy.spatial.distance import cdist


x = np.random.rand(1, 300)
Y = np.random.rand(400, 300)

similarities = 1 - cdist(x, Y, metric='cosine')
print(similarities.shape)

输出

(1, 400)

注意,cdist返回的是cosine_distance(更多信息请参见这里),也就是说1 - cosine_similarity,因此需要转换结果。

1

以下是按照与 @Bi Rico's post 相同的方法进行的一个示例,但使用 einsum 进行 norm 计算 -

den = np.sqrt(np.einsum('ij,ij->i',arr,arr)*np.einsum('j,j',vec,vec))
out = arr.dot(vec) / den

此外,我们可以使用vec.dot(vec)来替换np.einsum('j,j',vec,vec)以进行一些边缘改进。
时间 -
In [45]: vec = np.random.randn(300,)
    ...: arr = np.random.randn(400, 300)

# @Bi Rico's soln with norm
In [46]: %timeit (np.linalg.norm(arr, axis=1) * np.linalg.norm(vec))
10000 loops, best of 3: 100 µs per loop

In [47]: %timeit np.sqrt(np.einsum('ij,ij->i',arr,arr)*np.einsum('j,j',vec,vec))
10000 loops, best of 3: 77.4 µs per loop

在更大的数组上 -
In [48]: vec = np.random.randn(3000,)
    ...: arr = np.random.randn(4000, 3000)

In [49]: %timeit (np.linalg.norm(arr, axis=1) * np.linalg.norm(vec))
10 loops, best of 3: 22.2 ms per loop

In [50]: %timeit np.sqrt(np.einsum('ij,ij->i',arr,arr)*np.einsum('j,j',vec,vec))
100 loops, best of 3: 8.18 ms per loop

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