我知道如何使用scipy.spatial.distance.cdist计算数组中点之间的欧几里得距离
类似于这个问题的答案:Calculate Distances Between One Point in Matrix From All Other Points
然而,我想做的计算假设 * 循环边界条件 *,例如。所以在这种情况下,点[0,0]与点[0,n-1]距离为1,而不是距离n-1。(然后我将为目标细胞阈值距离内的所有点制作一个遮罩,但这不是问题的核心)。
我能想到的唯一方法是重复计算9次,域索引在x,y和x&y方向上增加/减去n,然后堆叠结果并在9个切片上找到最小值。为了说明9次重复的必要性,我将一个简单的示意图放在一起,只有1个J点,用圆圈标记,并显示了一个例子,在这种情况下,三角形标记的单元格在左上角反射的域中具有最近的邻居。
这是我使用cdist开发的代码:
import numpy as np
from scipy import spatial
n=5 # size of 2D box (n X n points)
np.random.seed(1) # to make reproducible
a=np.random.uniform(size=(n,n))
i=np.argwhere(a>-1) # all points, for each loc we want distance to nearest J
j=np.argwhere(a>0.85) # set of J locations to find distance to.
# this will be used in the KDtree soln
global maxdist
maxdist=2.0
def dist_v1(i,j):
dist=[]
# 3x3 search required for periodic boundaries.
for xoff in [-n,0,n]:
for yoff in [-n,0,n]:
jo=j.copy()
jo[:,0]-=xoff
jo[:,1]-=yoff
dist.append(np.amin(spatial.distance.cdist(i,jo,metric='euclidean'),1))
dist=np.amin(np.stack(dist),0).reshape([n,n])
return(dist)
这是可行的,并产生例如。:
print(dist_v1(i,j))
[[1.41421356 1. 1.41421356 1.41421356 1. ]
[2.23606798 2. 1.41421356 1. 1.41421356]
[2. 2. 1. 0. 1. ]
[1.41421356 1. 1.41421356 1. 1. ]
[1. 0. 1. 1. 0. ]]
0显然标记了J点,距离是正确的(这个编辑纠正了我以前的尝试,这是不正确的)。
请注意,如果您更改最后两行以堆叠原始距离,然后仅使用一个最小值,如下所示:
def dist_v2(i,j):
dist=[]
# 3x3 search required for periodic boundaries.
for xoff in [-n,0,n]:
for yoff in [-n,0,n]:
jo=j.copy()
jo[:,0]-=xoff
jo[:,1]-=yoff
dist.append(spatial.distance.cdist(i,jo,metric='euclidean'))
dist=np.amin(np.dstack(dist),(1,2)).reshape([n,n])
return(dist)
对于小的n(<10),它更快,但是对于大的数组(n>10),它相当慢
...但无论哪种方式,对于我的大型数组(N=500,J点数约为70),它都很慢,这种搜索占用了大约99%的计算时间,(使用循环也有点难看)-有更好/更快的方法吗?
我想到的其他选择是:
1.scipy.spatial.KDTree.query_ball_point
通过进一步搜索,我发现有一个函数scipy.spatial.KDTree.query_ball_point可以直接计算我的J点半径内的坐标,但它似乎没有任何使用周期边界的功能,所以我认为仍然需要使用3x 3循环,堆栈,然后像我上面那样使用amin,所以我不确定这是否会更快。
我使用这个函数编码了一个解决方案,而不用担心周期性边界条件(即,这并没有回答我的问题)
def dist_v3(n,j):
x, y = np.mgrid[0:n, 0:n]
points = np.c_[x.ravel(), y.ravel()]
tree=spatial.KDTree(points)
mask=np.zeros([n,n])
for results in tree.query_ball_point((j), maxdist):
mask[points[results][:,0],points[results][:,1]]=1
return(mask)
也许我没有以最有效的方式使用它,但即使没有周期性边界,这也已经和我基于cdist的解决方案一样慢了。在两个cdist解决方案中包括掩码函数,即将这些函数中的return(dist)
替换为return(np.where(dist<=maxdist,1,0))
,然后使用timeit,我得到了以下n=100的时间:
from timeit import timeit
print("cdist v1:",timeit(lambda: dist_v1(i,j), number=3)*100)
print("cdist v2:",timeit(lambda: dist_v2(i,j), number=3)*100)
print("KDtree:", timeit(lambda: dist_v3(n,j), number=3)*100)
cdist v1: 181.80927299981704
cdist v2: 554.8205785999016
KDtree: 605.119637199823
1.在[0,0]的设定距离内为点制作一个相对坐标数组,然后手动循环J点,用这个相对点列表设置一个掩码-这有一个优点,即“相对距离”计算只执行一次(我的J点每个时间步都会改变),但我怀疑循环会非常慢。
1.为2D域中的每个点预先计算一组掩码,因此在模型集成的每个时间步中,我只需为J点选择掩码并应用。这将使用大量的内存(与n^4成比例),也许仍然很慢,因为你需要循环J点来合并组合掩码。
5条答案
按热度按时间rsaldnfx1#
我将从图像处理的Angular 展示另一种方法,您可能会感兴趣,不管它是否是最快的。为了方便起见,我只对奇数
n
实现了它。与其考虑一组
nxn
点i
,不如考虑nxn
框。我们可以将其视为二进制图像。假设j
中的每个点都是该图像中的正像素。对于n=5
,它看起来像这样:现在让我们考虑图像处理的另一个概念:膨胀。对于任何输入像素,如果其
neighborhood
中有一个正像素,则输出像素将为1。这个邻域由所谓的Structuring Element
定义:一个布尔核,其中的1将显示要考虑哪些邻居。下面是我如何定义这个问题的SE:
直观地说,H是一个掩码,表示从中心到满足方程
x*x+y*y=r
的所有点。也就是说,H中的所有点都位于距离中心sqrt(r)
的位置。再看一个图像,它会非常清晰:这是一个不断扩大的像素圈。每个掩模中的每个白色像素表示与中心像素的距离正好为
sqrt(r)
的点。你也可以看出,如果我们迭代地增加r
的值,我们实际上会稳定地覆盖特定位置周围的所有像素位置,最终覆盖整个图像。(某些r值不会给出给予响应,因为任何一对点都不存在这样的距离sqrt(r)。我们跳过那些r值-比如3。这就是主算法的作用。
r
的值从0开始递增到某个上限。所以你可以说这个算法依赖于nxn图像中唯一距离对的数量。
这也意味着,如果你在j中有越来越多的点,算法实际上会变得更快,这违背了常识!
这种膨胀算法的最坏情况是当你有最少数量的点(正好是j中的一个点)时,因为这样它就需要将bjr设置为一个非常高的值才能从很远的点获得匹配。
在执行方面:
如果你看n=5的图像,你可以知道我做了什么;我只是简单地用四个象限填充图像,以表示循环空间,然后添加一些额外的零填充来考虑最坏情况下的搜索边界。
如果我们把dout可视化,我们实际上可以得到一个很棒的距离的视觉表示。
黑点基本上是存在j点的位置。最亮的点自然意味着离任何j最远的点。请注意,我将值保持平方形式以获得整数图像。实际距离仍然是一个平方根。结果与球场算法的输出相匹配。
输出:
现在进行时间检查,n=501。
输出:
我会说它们大致相等,尽管膨胀有一个非常微小的边缘。事实上,膨胀仍然缺少一个平方根运算,让我们加上它。
输出:
平方根基本上对时间的影响可以忽略不计。
我之前说过,当j* 中有更多的点时,* 膨胀会变快。所以让我们增加j的点数。
现在查看时间:
输出:
球树实际上变慢了,而膨胀变快了!这是因为如果有许多j点,我们可以通过几次重复的膨胀快速找到所有的距离。我发现这种效果相当有趣--通常你会期望运行时随着点数的增加而变差,但这里发生了相反的情况。
相反,如果我们减小j,我们会看到膨胀变慢:
输出:
我认为我们可以安全地得出结论,基于卷积或内核的方法在这个特定的问题上会提供更好的收益,而不是基于点或树的方法。
最后,我在一开始就提到过,我会再提一次:这整个实现仅考虑了n的奇数值;我没有耐心去计算偶数的适当填充。(如果你熟悉图像处理,你可能以前遇到过这个问题:所有的事情都容易与奇数大小)。
这也可以进一步优化,因为我只是一个偶尔涉猎数字。
bxjv4tth2#
[编辑] -我发现代码跟踪工作完成点的方式中有一个错误,用
mask_kernel
修复了它。新代码的纯python版本慢1.5倍,但numba版本稍微快一些(由于其他一些优化)。【当前最佳:~ 100倍至120倍原始速度]
首先,感谢您提交这个问题,我有很多的乐趣优化它!
我目前的最佳解决方案依赖于这样的假设,即网格是规则的,并且“源”点(我们需要计算距离的点)大致均匀分布。
这里的想法是,所有的距离都将是1,
sqrt(2)
,sqrt(3)
,.这样我们就可以提前做数值计算了。然后我们简单地将这些值放在一个矩阵中,并将该矩阵复制到每个源点(并确保保持在每个点上找到的最小值)。这涵盖了绝大多数要点(>99%)。然后,我们对剩下的1%应用另一种更“经典”的方法。代码如下:
测试此版本
给
这已经是一个~ 17倍的加速!我还实现了一个numba版本的
sq_distance
和apply_kernel
:[这是新的正确版本]并测试,
得到了以下结果
由于JIT编译,第一次运行非常慢,但除此之外,它是120倍的改进!
通过调整
kernel_size
参数(或overlap
),可以从该算法中获得更多信息。当前选择的kernel_size
仅对少量源点有效。例如,这个选择在source_points=np.argwhere(a>0.85)
(13秒)中失败,而手动设置kernel_size=5
在22毫秒内给出答案。[编辑2]:
我对代码的非numba部分给予了更多的关注,并设法获得了相当显著的加速,非常接近numba可以实现的目标:下面是函数
apply_kernel
的新版本:主要的优化是
测试与
给
这是一个很好的100倍的cdist改进,一个~ 5.5倍的改进,比以前的numpy-only版本,只是~25%慢,我可以实现与numba。
hpcdzsge3#
这里有一个固定版本的代码和一个不同的方法,这是一个有点快。他们给予相同的结果,所以我有理由相信他们是正确的:
样本运行,1000点:
我们看到我的方法快了9倍,巧合的是,这是OP版本必须检查的偏移配置的数量。它在各个坐标上使用
pdist
来获得绝对差。当这些大于网格间距的一半时,我们减去一个周期。剩下的就是取欧几里德范数和解包存储。u1ehiz5o4#
对于计算多个距离,我认为很难击败一个简单的BallTree(或类似的)。
我不太理解循环边界,或者至少为什么你需要循环3x3次,因为我看到它的行为像环面,它足以制作5个副本。更新:确实你需要3x3的边缘。我更新了代码。
为了确保我的
minimum_distance
是正确的,对n = 200
做了一个np.all( minimum_distance == dist_v1(i,j) )
测试,得到了True
。对于用提供的代码生成的
n = 500
,用于冷启动的%%time
给出了所以我生成了500个数据点,
使用球树
更新:
注意这里我把数据复制到框的N,E,S,W,NE,SE,NW,SE来处理边界条件。同样,对于
n = 200
,这给出了相同的结果。你可以调整leaf_size
,但我觉得这个设置是好的。性能对
j
中的点数敏感。tpxzln5u5#
这是我计时的8种不同的解决方案,有些是我自己的,有些是针对我的问题发布的,使用4种广泛的方法:
1.空间CDIST
1.空间KD树
1.核方法
这是包含8个测试例程的代码:
在我的Linux 12核心桌面(48 GB RAM)上的输出(以秒为单位),n=350和63点:
对于n=500和npts=176:
因此,总结起来,我得出了以下结论:
1.如果你有一个相当大的问题,避免使用
cdist
,因为与其他方法相比,它非常慢(超过一个数量级!)1.如果你的问题在计算时间上没有太大的限制,我推荐“
KDtree
查询”方法,因为它只需要两行没有周期性边界的代码和几行有周期性边界的代码来建立j 9数组。如此简单,如此快速!1.为了获得最佳性能(例如一个模型的长时间积分,每个时间步都需要,就像我的情况一样),那么Kernal解决方案现在是最快的。更重的代码,但另一个CA。第2章加速