【问题标题】:Python's scipy spatial KD-tree is slower than brute force euclidean distances?Python scipy空间KDtree比蛮力欧几里得距离慢?
【发布时间】:2020-01-20 10:38:59
【问题描述】:

我快速检查了构建树并查询它与仅计算所有欧几里德距离的性能。如果我在这棵树中查询半径内的所有其他点,它是否应该大大优于蛮力方法?

有人知道为什么我的测试代码会产生这些不同的结果吗?我用错了吗?测试用例不适合 kd-trees 吗?

PS:这是我使用的代码的简化概念验证版本。可以在here 找到我还存储和转换结果的完整代码,但它会产生相同的结果。

进口

import numpy as np
from time import time
from scipy.spatial import KDTree as kd
from functools import reduce
import matplotlib.pyplot as plt

实现

def euclid(c, cs, r):
    return ((cs[:,0] - c[0]) ** 2 + (cs[:,1] - c[1]) ** 2 + (cs[:,2] - c[2]) ** 2) < r ** 2

def find_nn_naive(cells, radius):
    for i in range(len(cells)):
        cell = cells[i]
        cands = euclid(cell, cells, radius)

def find_nn_kd_seminaive(cells, radius):
    tree = kd(cells)
    for i in range(len(cells)):
        res = tree.query_ball_point(cells[i], radius)

def find_nn_kd_by_tree(cells, radius):
    tree = kd(cells)
    return tree.query_ball_tree(tree, radius)

测试设置

min_iter = 5000
max_iter = 10000
step_iter = 1000

rng = range(min_iter, max_iter, step_iter)
elapsed_naive = np.zeros(len(rng))
elapsed_kd_sn = np.zeros(len(rng))
elapsed_kd_tr = np.zeros(len(rng))

ei = 0
for i in rng:
    random_cells = np.random.rand(i, 3) * 400.
    t = time()
    r1 = find_nn_naive(random_cells, 50.)
    elapsed_naive[ei] = time() - t
    t = time()
    r2 = find_nn_kd_seminaive(random_cells, 50.)
    elapsed_kd_sn[ei] = time() - t
    t = time()
    r3 = find_nn_kd_by_tree(random_cells, 50.)
    elapsed_kd_tr[ei] = time() - t
    ei += 1

情节

plt.plot(rng, elapsed_naive, label='naive')
plt.plot(rng, elapsed_kd_sn, label='semi kd')
plt.plot(rng, elapsed_kd_tr, label='full kd')
plt.legend()
plt.show(block=True)

【问题讨论】:

  • 剧透警告:您的幼稚实现不会返回任何内容
  • 我知道,检查完整的代码,它确实返回了东西。我也没有在这个 PoC 中使用返回值。我只是让它计算
  • 根据我的经验,scipy.spatial.cKDTree 比纯 python 实现要快得多。 cKDTree 具有完全相同的方法等,因此您只需要更改导入语句即可。你的计时结果在那种情况下也成立吗?此外,您能否分析您的函数以检查大部分时间是否用于构建树或查询它?通常可以通过指定更合适的leafsize 参数来改进查询时间。
  • 哇...与 cKDTree 的区别是惊人的!现在的情节是这样的:imgur.com/m5JXyGT
  • 它很复杂,并且依赖于数据集,因此找到合适的叶子大小的唯一方法是对真实数据运行测试。当您在真实数据上运行这些测试时,请确保分别对树的构建和查询进行计时,至少在您将树用于多个查询的情况下。来自 sklearn 实现的作者之一的This article 触及了要点,但没有太技术化。可能也值得对 sklearn 实现进行基准测试。

标签: python-3.x scipy kdtree scipy-spatial


【解决方案1】:

scipy.spatial.KDTree() 中所述:

对于大尺寸(20 已经很大),不要指望这会比蛮力运行得更快。高维最近邻查询是计算机科学中一个重要的开放性问题。

(此注释也出现在 scipy.spatial.cKDTree() 中,尽管这可能是一个复制粘贴文档错误)。

我冒昧地用适当的函数重写了您的代码,以便我可以运行一些自动基准测试(基于this template)。我还包含了一个蛮力 Numba 实现:

import numpy as np
import scipy as sp
import numba as nb

import scipy.spatial

SCALE = 400.0
RADIUS = 50.0 


def find_nn_np(points, radius=RADIUS, p=2):
    n_points, n_dim = points.shape
    result = np.empty(n_points, dtype=object)
    for i in range(n_points):
        result[i] = np.where(np.sum(np.abs(points - points[i:i + 1, :]) ** p, axis=1) < radius ** p)[0].tolist()
    return result


def find_nn_kd_tree(points, radius=RADIUS):
    tree = sp.spatial.KDTree(points)
    return tree.query_ball_point(points, radius)


def find_nn_kd_tree_cy(points, radius=RADIUS):
    tree = sp.spatial.cKDTree(points)
    return tree.query_ball_point(points, radius)


@nb.jit
def neighbors_indexes_jit(radius, center, points, p=2):
    n_points, n_dim = points.shape
    k = 0
    res_arr = np.empty(n_points, dtype=nb.int64)
    for i in range(n_points):
        dist = 0.0
        for j in range(n_dim):
            dist += abs(points[i, j] - center[j]) ** p
        if dist < radius ** p:
            res_arr[k] = i
            k += 1
    return res_arr[:k]


@nb.jit(forceobj=True, parallel=True)
def find_nn_jit(points, radius=RADIUS):
    n_points, n_dim = points.shape
    result = np.empty(n_points, dtype=object)
    for i in nb.prange(n_points):
        result[i] = neighbors_indexes_jit(radius, points[i], points, 2)
    return result

这些是我得到的基准(我省略了scipy.spatial.KDTree(),因为它与图表相差甚远,与您的发现一致):


(为完整起见,以下是适配模板所需的代码)

def gen_input(n, dim=2, scale=SCALE):
    return scale * np.random.rand(n, dim)


def equal_output(a, b):
    return all(sorted(a_i) == sorted(b_i) for a_i, b_i in zip(a, b))


funcs = find_nn_np, find_nn_jit, find_nn_kd_tree_cy


input_sizes = tuple(int(2 ** (2 + (1 * i) / 4)) for i in range(32, 32 + 16 + 1))
print('Input Sizes:\n', input_sizes, '\n')


runtimes, input_sizes, labels, results = benchmark(
    funcs, gen_input=gen_input, equal_output=equal_output,
    input_sizes=input_sizes)


plot_benchmarks(runtimes, input_sizes, labels, units='s')

【讨论】:

  • 虽然 3 维不被认为是“高维”,但它的性能比预期运行时间为 O(n²) 的东西差,而它应该执行 O(nlogn)。
  • 请注意,您对渐近复杂性的观察仅意味着对于 足够大的 n,任何给定的 KD-tree 实现都将比蛮力更快。对于scipy.spatial.KDtree()n 可能超出你的记忆力所能承受的范围。
  • 我仍然认为“yo bois,waddup”对于如此高调的软件包的如此糟糕的实施是有保证的。
  • 也许你应该在他们的存储库上提出这个问题。我同意这些数字scipy.spatial.KDtree() 几乎没用。
  • @RobinDeSchepper 请注意,这里有一些分析/计时:ibm.com/developerworks/community/blogs/jfp/entry/…
【解决方案2】:

TL;DR:

切换到 scipy.spatial.cKDTreesklearn.neighbors.KDTree 以获得 kd-tree 算法的预期性能。

【讨论】:

    猜你喜欢
    • 2015-07-15
    • 1970-01-01
    • 2013-03-02
    • 2019-09-24
    • 2010-12-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多