矢量化代码来计算(平方)Mahalanobis Distiance

时间:2015-08-04 03:22:45

标签: r normal-distribution python matrix numpy

编辑2:这篇文章似乎已经从CrossValidated转移到了StackOverflow,因为它主要是关于编程,但这意味着花哨的MathJax不再起作用了。希望这仍然可读。

假设我想用协方差矩阵x计算两个向量yS之间的马哈拉诺比斯平方距离。这是一个由

定义的相当简单的函数
M2(x, y; S) = (x - y)^T * S^-1 * (x - y)

使用python的numpy包我可以这样做

# x, y = numpy.ndarray of shape (n,)
# s_inv = numpy.ndarray of shape (n, n)
diff = x - y
d2 = diff.T.dot(s_inv).dot(diff)

或在R中

diff <- x - y
d2 <- t(diff) %*% s_inv %*% diff

在我的情况下,我得到了

    {li> m n矩阵X
  • n - 维度向量mu
  • n n协方差矩阵S

并希望找到m维向量d,以便

d_i = M2(x_i, mu; S)  ( i = 1 .. m )

其中x_ii的第X行。

使用python中的简单循环来实现这一点并不难:

d = numpy.zeros((m,))
for i in range(m):
    diff = x[i,:] - mu
    d[i] = diff.T.dot(s_inv).dot(diff)

当然,鉴于外部循环发生在python而不是numpy库中的本机代码中,意味着它没有尽可能快。 $ n $和$ m $分别约为3-4和几十万,我在交互式程序中经常这样做,所以加速会非常有用。

在数学上,我能够使用基本矩阵运算来表达这一点的唯一方法是

d = diag( X' * S^-1 * X'^T )

,其中

 x'_i = x_i - mu

编写矢量化版本很简单,但遗憾的是计算一个100亿以上的元素矩阵并且只采用对角线的效率低得多...我相信这个操作应该可以用爱因斯坦符号很容易地表达出来,因此可以通过numpy的{​​{1}}函数快速评估,但我甚至没有开始弄清楚黑魔法是如何运作的。

所以,我想知道:是否有更好的方法以数学方式(在简单的矩阵运算方面)制定这个操作,或者有人可以提出一些很好的矢量化(python或R)代码来有效地做到这一点? / p>

奖金问题,勇敢的

我实际上并不想这样做一次,我想这样做einsum ~100次。给出:

    {li>

    k m矩阵n

    {li>

    X k矩阵n

  • Un个协方差矩阵的集合,每个表示nS_j

j = 1..k矩阵m查找k,以便

D

其中D_i,j = M(x_i, u_j; S_j) i = 1..mj = 1..kx_i的{​​{1}}行,而iX一行u_j

即,矢量化以下代码:

j

2 个答案:

答案 0 :(得分:6)

首先,似乎你可能正在获得S然后反转它。你不应该这样做;它的速度慢,数值不准确。相反,你应该得到S的Cholesky因子L,使得S = L L ^ T;然后

U

由于L是三角形,因此可以有效地计算L ^ -1(x-y)。

事实证明,如果你正确地重塑它,# s_inv is (k x n x n) array containing "stacked" inverses # of covariance matrices d = numpy.zeros( (m, k) ) for j in range(k): for i in range(m): diff = x[i, :] - u[j, :] d[i, j] = diff.T.dot(s_inv[j, :, :]).dot(diff) 会很高兴地同时做一堆这些:

M^2(x, y; L L^T)
  = (x - y)^T (L L^T)^-1 (x - y)
  = (x - y)^T L^-T L^-1 (x - y)
  = || L^-1 (x - y) ||^2,

稍微断开一点,scipy.linalg.solve_triangular是L ^ -1(X_j - \ mu)的第i个分量。然后L = np.linalg.cholesky(S) y = scipy.linalg.solve_triangular(L, (X - mu[np.newaxis]).T, lower=True) d = np.einsum('ij,ij->j', y, y) 调用

y[i, j]

就像我们需要的那样。

不幸的是,einsum不会在其第一个参数上进行矢量化,因此您应该只是在那里循环。如果k只有大约100,那就不会是一个重大问题。

如果你实际上给了S ^ -1而不是S,那么你确实可以直接使用d_j = \sum_i y_{ij} y_{ij} = \sum_i y_{ij}^2 = || y_j ||^2, 来做到这一点。由于S在您的情况下非常小,因此实际上反转矩阵也可能会更快。但是,只要n是一个非常重要的大小,你就可以通过这种方式丢掉很多数值精度。

要弄清楚如何处理einsum,请根据组件编写所有内容。我将直接进入奖金案例,为了方便起见,写下S_j ^ -1 = T_j:

solve_triangular

因此,如果我们创建形状为einsum的数组D_{ij} = M^2(x_i, u_j; S_j) = (x_i - u_j)^T T_j (x_i - u_j) = \sum_k (x_i - u_j)_k ( T_j (x_i - u_j) )_k = \sum_k (x_i - u_j)_k \sum_l (T_j)_{k l} (x_i - u_j)_l = \sum_{k l} (X_{i k} - U_{j k}) (T_j)_{k l} (X_{i l} - U_{j l}) ,形状X的{​​{1}}和形状(m, n)的{​​{1}},那么我们可以把它写成

U

其中(k, n)

答案 1 :(得分:1)

Dougal用一个优秀而详细的答案来确定这一点,但我想我会分享一个小修改,我发现如果其他人试图实现这一点,我会提高效率。直截了当:

Dougal的方法如下:

def mahalanobis2(X, mu, sigma):
    L = np.linalg.cholesky(sigma)
    y = scipy.linalg.solve_triangular(L, (X - mu[np.newaxis,:]).T, lower=True)
    return np.einsum('ij,ij->j', y, y)

我尝试的数学上等效的变体是

def mahalanobis2_2(X, mu, sigma):

    # Cholesky decomposition of inverse of covariance matrix
    # (Doing this in either order should be equivalent)
    linv = np.linalg.cholesky(np.linalg.inv(sigma))

    # Just do regular matrix multiplication with this matrix
    y = (X - mu[np.newaxis,:]).dot(linv)

    # Same as above, but note different index at end because the matrix
    # y is transposed here compared to above
    return np.einsum('ij,ij->i', y, y)

使用相同的随机输入将两个版本对齐20x并记录时间(以毫秒为单位)。对于X作为1,000,000 x 3矩阵(mu和sigma 3和3x3),我得到:

Method 1 (min/max/avg): 30/62/49
Method 2 (min/max/avg): 30/47/37

第二个版本的速度增加了30%。我主要是在3或4维度上运行它,但看看它是如何缩放我试过X为1,000,000 x 100并获得:

Method 1 (min/max/avg): 970/1134/1043
Method 2 (min/max/avg): 776/907/837

这是一个大致相同的改进。

我在对Dougal的答案的评论中提到了这一点,但在此处添加了更多的可见性:

上面的第一对方法采用单个中心点mu和协方差矩阵sigma并计算到每行X的马哈拉诺比斯平方距离。我的红利问题是多次执行此操作musigma的集合并输出二维矩阵。上面的一组方法可以通过一个简单的for循环来实现,但是Dougal也使用einsum发布了一个更聪明的例子。

我决定将这些方法相互比较,使用它们来解决以下问题:给定k d - 维度正态分布(中心存储在k行{ {1}}矩阵dUkd数组d的最后两个维度中的协方差矩阵),找到密度Sn的{​​{1}}行存储n点。{/ 1}

多元正态分布的密度是该点与平均值的平方马哈拉诺比斯距离的函数。 Scipy将其实现为d以用作参考。我使用X每次使用相同的随机参数,相互运行所有三种方法10x。以下是结果,以点/秒为单位:

scipy.stats.multivariate_normal.pdf

其中d=3, k=96, n=5e5是上述两种方法中较好的一种,[Method]: (min/max/avg) Scipy: 1.18e5/1.29e5/1.22e5 Fancy 1: 1.41e5/1.53e5/1.48e5 Fancy 2: 8.69e4/9.73e4/9.03e4 Fancy 2 (cheating version): 8.61e4/9.88e4/9.04e4 是Dougal的第二种解决方案。由于Fancy 1需要计算所有协方差矩阵的逆,我还尝试了一个&#34;作弊版本&#34;它作为一个参数传递给它们,但看起来并没有什么区别。我曾计划将非矢量化实现包括在内,但这样做的速度非常慢,可能需要一整天。

我们可以从中得到的结论是,使用Dougal的第一种方法比Scipy快了大约20%。不幸的是,尽管其聪明,但第二种方法只有约60%和第一个一样快。可能还有其他一些优化措施可以完成,但对我来说已经足够快了。

我还测试了它如何通过更高的维度进行缩放。使用Fancy2

Fancy 2

d=100, k=96, n=1e4这次似乎有更大的优势。还值得注意的是,Scipy投了Scipy: 7.81e3/7.91e3/7.86e3 Fancy 1: 1.03e4/1.15e4/1.08e4 Fancy 2: 3.75e3/4.10e3/3.95e3 Fancy 2 (cheating version): 3.58e3/4.09e3/3.85e3 8/10次,可能是因为我的一些随机生成的100x100协方差矩阵接近于单数(这可能意味着其他两种方法不是数值稳定的,我没有实际上检查结果。)