当我们采用具有 N 个观测值和 M 个特征的
N x M
矩阵时,常见的任务是计算 N 个观测值之间的成对距离,从而得到 N x N
距离矩阵。流行的 Python 库 scipy
和 scikit-learn
都提供了执行此任务的方法,我们希望它们对于两者已实现的指标产生相同的结果。以下函数测试给定矩阵 arr
的等价性:
import numpy as np
from sklearn.metrics import pairwise_distances
from scipy.spatial.distance import pdist, squareform
def test_equivalence(arr: np.array, metric="cosine") -> bool:
scipy_result = squareform(pdist(arr, metric=metric))
sklearn_result = pairwise_distances(arr, metric=metric)
return np.isclose(scipy_result, sklearn_result).all()
现在我碰巧有这个
1219 x 37652
数组 arr
,其中每行总和为 1(标准化)并且 test_equivalence(arr)
产生 True,如预期的那样。也就是说,两个库返回的 N x N
余弦距离矩阵可以互换使用。然而,当我剔除最后 i
列时,test_equivalence(arr[:, -i])
产生 True 仅达到某个值(恰好是 i = 25676
)。从这个值开始,等价性不再成立。
我完全不知道为什么会这样,有什么指导吗?如果有人可以建议如何,我可以将数组共享为
.npz
文件进行调试,但也许有人已经有预感。当然,最终的问题是我应该使用哪种实现?
我还用这些其他指标测试了失败的
arr[:, -25675]
:["braycurtis", "canberra", "chebyshev", "cityblock", "correlation", "euclidean", "hamming", "matching", "minkowski", "rogerstanimoto", "russellrao", "seuclidean", "sokalmichener", "sokalsneath", "sqeuclidean", "yule"]
,其中除了“相关性”之外的所有指标都是等效的。
编辑: 未通过等效性测试的简化
(1219 x 96)
数组可以从 https://drive.switch.ch/index.php/s/B19JbTL5aZ4pY3f/download 下载并通过 np.load("tf_matrix.npz")["arr_0"]
加载。
在这种情况下,您可以尝试一些诊断方法。您可以尝试的一种诊断方法是绘制两种计算距离的方法不一致的地方。
# Modified version of test_equivalence() that returns boolean matrix of disagreements
def test_equivalence(arr: np.array, metric="cosine"):
scipy_result = squareform(pdist(arr, metric=metric))
sklearn_result = pairwise_distances(arr, metric=metric)
return np.isclose(scipy_result, sklearn_result)
plt.imshow(test_equivalence(arr))
这给出了以下情节:
请注意,除了 1200 附近的水平线和 1200 附近的垂直线之外,它在任何地方都是 True。因此,这并不是说这两种方法对所有向量都存在分歧 - 它们对特定向量存在分歧。
让我们找出哪一列包含他们不同意的向量:
>>> row, col = np.where(~test_equivalence(arr))
>>> print(col[0])
1168
向量1168有什么奇怪的吗?
>>> print(arr[1168])
[1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23 1.20000016e-23 1.20000016e-23
1.20000016e-23 1.20000016e-23]
这个向量非常非常小。但它是不是异常小?您可以通过按数组中的位置绘制每个向量的欧几里德长度来测试该理论。
plt.scatter(np.arange(len(arr)), np.linalg.norm(arr, axis=1))
plt.yscale('log')
该图显示,大多数向量的欧氏长度约为 0.1,但有一个向量除外,该向量小了 20 个数量级。又是矢量1168。
为了检查有关小向量导致问题的理论,这里有另一种显示问题的方法。我拿了你的数组,并反复简化它,直到我有一个尽可能简单的测试用例,但仍然显示了问题。
arr_small = np.array([[1, 0], [1e-15, 1e-15]])
print(test_equivalence(arr_small))
print(squareform(pdist(arr_small, metric="cosine")))
print(pairwise_distances(arr_small, metric="cosine"))
输出:
[[ True False]
[False True]]
[[0. 0.29289322]
[0.29289322 0. ]]
[[0. 1.]
[1. 0.]]
我声明两个向量,一个坐标为 (1, 0),另一个坐标为 (1e-15, 1e-15)。它们之间应该有 45 度角。用余弦距离术语来说,应该是 1 - cos(45 度) = 0.292。 pdist() 函数与此计算一致。
然而,pairwise_distances()表示距离为1。换句话说,它表示两个向量是正交的。为什么要这样做?让我们看看余弦距离的定义来了解原因。
图片来源:SciPy 文档
在此等式中,如果 u 或 v 包含全零,则分母将为零,并且您将除以零,这是未定义的。在这种情况下,pairwise_distances() 的作用是,在任何情况下,如果向量的欧几里德长度“太小”,则将向量的长度替换为 1,以避免被零除。这导致分子远小于分母,因此分数为0,距离变为1。
更准确地说,当向量的长度小于相关类型的 machine epsilon 的 10 倍(对于 64 位浮点数来说大约为 2.22e-15)时,向量“太小”。 (来源。)
相比之下,pdist() 不包含任何代码来避免除以零。
>>> print(squareform(pdist(np.array([[1, 0], [0, 0]]), metric="cosine")))
[[ 0. nan]
[nan 0.]]