【问题标题】:Vectorized spatial distance in python using numpypython中使用numpy的矢量化空间距离
【发布时间】:2019-02-01 11:15:44
【问题描述】:

我在 python 中有一个 numpy 数组,其中包含许多 (10k+) 个 3D 顶点(坐标为 [x,y,z] 的向量)。我需要计算所有可能的这些点对之间的距离。

使用 scipy 很容易做到:

import scipy
D = spdist.cdist(verts, verts)

但我不能使用它,因为项目策略是引入新的依赖项。

所以我想出了这个幼稚的代码:

def vert_dist(self, A, B):
    return ((B[0]-A[0])**2+(B[1]-A[1])**2+(B[2]-A[2])**2)**(1.0/2)

# Pairwise distance between verts
#Use SciPy, otherwise use fallback
try:
    import scipy.spatial.distance as spdist
    D = spdist.cdist(verts, verts)
except ImportError:
    #FIXME: This is VERY SLOW:
    D = np.empty((len(verts), len(verts)), dtype=np.float64)
    for i,v in enumerate(verts):
        #self.app.setStatus(_("Calculating distance %d of %d (SciPy not installed => using SLOW AF fallback method)"%(i,len(verts))), True)
        for j in range(i,len(verts)):
            D[j][i] = D[i][j] = self.vert_dist(v,verts[j])

vert_dist() 计算两个顶点之间的 3D 距离,其余代码仅迭代 1D 数组中的顶点,并为每个顶点计算同一数组中彼此之间的距离并生成 2D 距离数组。

但是与 scipy 的原生 C 代码相比,这非常慢(1000 倍)。我想知道我是否可以使用纯 numpy 加速它。至少在某种程度上。

更多信息:https://github.com/scipy/scipy/issues/9172

顺便说一句,我尝试过 PyPy JIT 编译器,它甚至比纯 python 慢(10 倍)。

更新:我能够像这样加快速度:

    def vert_dist_matrix(self, verts):
            #FIXME: This is VERY SLOW:
            D = np.empty((len(verts), len(verts)), dtype=np.float64)
            for i,v in enumerate(verts):
                    D[i] = D[:,i] = np.sqrt(np.sum(np.square(verts-verts[i]), axis=1))
            return D

这通过一次计算整行来消除内部循环,这使得事情变得相当快,但仍然明显比 scipy 慢。所以我还是看@Divakar 的解决方案

【问题讨论】:

  • 您在寻找这样的东西吗? stackoverflow.com/a/51352819/4909087
  • 这个答案在讨论性能时也很有趣:stackoverflow.com/a/37903795/8069403
  • @coldspeed 我已经看过这篇文章,但我不清楚如何修改它以进行((B[0]-A[0])**2+(B[1]-A[1])**2+(B[2]-A[2])**2)**(1.0/2) 计算,而不仅仅是减法。
  • @xdze2 我试过这个,但它只替换了 vert_dist() 函数,这已经足够快了。当我将 ((B[0]-A[0])**2+(B[1]-A[1])**2+(B[2]-A[2])**2)**(1.0/2) 替换为 np.sqrt(np.sum(np.square(B-A))) 时,它的可读性更高,但它仍然没有消除 2D for 循环,这是这段代码的实际缓慢部分。

标签: python numpy multidimensional-array scipy vertex


【解决方案1】:

eucl_dist 包(免责声明:我是它的作者)基本上包含两种方法来解决计算平方欧几里得距离的问题,比SciPy's cdist 更有效,特别是对于大型数组(体面到大量列)。

我们将使用它的source code中的一些代码来适应我们这里的问题,给我们两种方法。

方法#1

wiki contents 之后,我们可以利用matrix-multiplication 和一些NumPy specific implementations 作为我们的第一种方法,就像这样 -

def pdist_squareformed_numpy(a):
    a_sumrows = np.einsum('ij,ij->i',a,a)
    dist = a_sumrows[:,None] + a_sumrows -2*np.dot(a,a.T)
    np.fill_diagonal(dist,0)
    return dist

方法 #2

另一种方法是创建输入数组的“扩展”版本,在 github 源代码链接中再次详细讨论了我们的第二种方法,这对于较小的列更好,就像这里的情况一样 -

def ext_arrs(A,B, precision="float64"):
    nA,dim = A.shape
    A_ext = np.ones((nA,dim*3),dtype=precision)
    A_ext[:,dim:2*dim] = A
    A_ext[:,2*dim:] = A**2

    nB = B.shape[0]
    B_ext = np.ones((dim*3,nB),dtype=precision)
    B_ext[:dim] = (B**2).T
    B_ext[dim:2*dim] = -2.0*B.T
    return A_ext, B_ext

def pdist_squareformed_numpy_v2(a):
    A_ext, B_ext = ext_arrs(a,a)
    dist = A_ext.dot(B_ext)
    np.fill_diagonal(dist,0)
    return dist

请注意,这些为我们提供了平方的 eucludean 距离。因此,对于实际距离,如果需要最终输出,我们希望使用 np.sqrt()

示例运行 -

In [380]: np.random.seed(0)
     ...: a = np.random.rand(5,3)

In [381]: from scipy.spatial.distance import cdist

In [382]: cdist(a,a)
Out[382]: 
array([[0.  , 0.29, 0.42, 0.2 , 0.57],
       [0.29, 0.  , 0.58, 0.42, 0.76],
       [0.42, 0.58, 0.  , 0.45, 0.9 ],
       [0.2 , 0.42, 0.45, 0.  , 0.51],
       [0.57, 0.76, 0.9 , 0.51, 0.  ]])

In [383]: np.sqrt(pdist_squareformed_numpy(a))
Out[383]: 
array([[0.  , 0.29, 0.42, 0.2 , 0.57],
       [0.29, 0.  , 0.58, 0.42, 0.76],
       [0.42, 0.58, 0.  , 0.45, 0.9 ],
       [0.2 , 0.42, 0.45, 0.  , 0.51],
       [0.57, 0.76, 0.9 , 0.51, 0.  ]])

In [384]: np.sqrt(pdist_squareformed_numpy_v2(a))
Out[384]: 
array([[0.  , 0.29, 0.42, 0.2 , 0.57],
       [0.29, 0.  , 0.58, 0.42, 0.76],
       [0.42, 0.58, 0.  , 0.45, 0.9 ],
       [0.2 , 0.42, 0.45, 0.  , 0.51],
       [0.57, 0.76, 0.9 , 0.51, 0.  ]])

10k 积分的计时 -

In [385]: a = np.random.rand(10000,3)

In [386]: %timeit cdist(a,a)
1 loop, best of 3: 309 ms per loop

# Approach #1
In [388]: %timeit pdist_squareformed_numpy(a) # squared eucl distances
1 loop, best of 3: 668 ms per loop

In [389]: %timeit np.sqrt(pdist_squareformed_numpy(a)) # actual eucl distances
1 loop, best of 3: 812 ms per loop

# Approach #2
In [390]: %timeit pdist_squareformed_numpy_v2(a) # squared eucl distances
1 loop, best of 3: 237 ms per loop

In [391]: %timeit np.sqrt(pdist_squareformed_numpy_v2(a)) # actual eucl distances
1 loop, best of 3: 395 ms per loop

第二种方法在性能上似乎接近cdist 一种!

【讨论】:

  • 谢谢!很好的编辑。我删除了我以前的 cmets,稍后将返回删除这个。
  • @Divakar 这真的很酷,而且速度极快。谢谢!但在pdist_squareformed_numpy_v2() 中,我不得不将return dist 替换为return np.abs(np.nan_to_num(dist))。它现在效果更好,但在某些情况下会给出不准确的结果。编写此代码以便可以将比 1e-5 更近的顶点视为相同的顶点。但是我必须将精度降低到 1e-1 才能获得与 scipy 相同的结果。可能有一些准确性的损失。
  • @Harvie.CZ 你为什么使用np.nan_to_num?你的输入数组中有NaNs 吗?
  • @Divakar 我在np.abs() 之前添加了它,只是发现np.abs() 不再需要它了。可能np.sqrt() 正在返回负值的复数,其余代码无法解释(例如,使用
  • @Divakar 都在工作!我会做更多的基准测试,可能会选择第一个,因为它更短且更容易理解。
猜你喜欢
  • 1970-01-01
  • 2015-03-12
  • 2018-05-24
  • 1970-01-01
  • 2014-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多