使用SciKit-learn和SciPy进行K-Nearest-Neighbor构建/搜索的速度

时间:2015-05-25 23:40:06

标签: python scipy scikit-learn nearest-neighbor kdtree

我有一大堆二维点,希望能够快速查询二维空间中任意点的k-最近邻居的集合。由于它是低维的,因此KD树似乎是一种很好的方法。我的初始数据集只会很少更新,所以查询点的时间对我来说比构建时更重要。但是,每次运行程序时,我都需要重新加载对象,所以我还需要一个可以快速保存和重新加载的结构。

现成的两个选择是SciPy和SciKit-learn中的KDTree结构。下面我分析了这两个中的两个,用于在大量列表长度上构建速度和查询速度。我还挑选了SciKit-learn结构并显示了从pickle重新加载对象的时间。这些在图表中进行比较,用于生成时间的代码包含在下面。

正如我在图中所示,从酸洗中加载比从头开始加载大N的半个数量级更快,表明KDTree适合我的用例(即频繁重载但不常见)重新构建)。

Comparing build-, reload- and query-time of two KD-Tree structures

比较构建时间的代码:

# Profiling the building time for the two KD-tree structures and re-loading from a pickle
import math, timeit, pickle, sklearn.neighbors

the_lengths = [100, 1000, 10000, 100000, 1000000]

theSciPyBuildTime = []
theSklBuildTime = []
theRebuildTime = []

for length in the_lengths:
    dim = 5*int(math.sqrt(length))
    nTimes = 50
    from random import randint
    listOfRandom2DPoints = [ [randint(0,dim),randint(0,dim)] for x in range(length)]

    setup = """import scipy.spatial
import sklearn.neighbors
length = """ + str(length) + """
dim = """ + str(dim) + """
from random import randint
listOfRandom2DPoints = [ [randint(0,dim),randint(0,dim)] for x in range(length)]"""

    theSciPyBuildTime.append( timeit.timeit('scipy.spatial.KDTree(listOfRandom2DPoints, leafsize=20)', setup=setup, number=nTimes)/nTimes )
    theSklBuildTime.append( timeit.timeit('sklearn.neighbors.KDTree(listOfRandom2DPoints, leaf_size=20)', setup=setup, number=nTimes)/nTimes )

    theTreeSkl = sklearn.neighbors.KDTree(listOfRandom2DPoints, leaf_size=20, metric='euclidean')
    f = open('temp.pkl','w')
    temp = pickle.dumps(theTreeSkl)

    theRebuildTime.append( timeit.timeit('pickle.loads(temp)', 'from __main__ import pickle,temp', number=nTimes)/nTimes )

比较查询时间的代码:

# Profiling the query time for the two KD-tree structures
import scipy.spatial, sklearn.neighbors

the_lengths = [100, 1000, 10000, 100000, 1000000, 10000000]

theSciPyQueryTime = []
theSklQueryTime = []

for length in the_lengths:
    dim = 5*int(math.sqrt(length))
    nTimes = 50
    listOfRandom2DPoints = [ [randint(0,dim),randint(0,dim)] for x in range(length)]

    setup = """from __main__ import sciPiTree,sklTree
from random import randint
length = """ + str(length) + """
randPoint = [randint(0,""" + str(dim) + """),randint(0,""" + str(dim) + """)]""" 

    sciPiTree = scipy.spatial.KDTree(listOfRandom2DPoints, leafsize=20)
    sklTree = sklearn.neighbors.KDTree(listOfRandom2DPoints, leaf_size=20)

    theSciPyQueryTime.append( timeit.timeit('sciPiTree.query(randPoint,10)', setup=setup, number=nTimes)/nTimes )
    theSklQueryTime.append( timeit.timeit('sklTree.query(randPoint,10)', setup=setup, number=nTimes)/nTimes )

问题:

  1. 结果:虽然他们越来越接近非常大的N, SciKit-learn似乎在构建时间和查询时间方面都击败了SciPy。 有其他人发现了吗?

  2. 数学:有没有更好的结构? 我只是在2D空间工作(虽然数据会很完整 密集如此蛮力(out)),是否有更好的结构 低维kNN搜索?

  3. 速度:看起来这两种方法的构建时间是 越来越接近N,但我的电脑放弃了我 - 任何人都可以 为我验证这个更大的N ?!谢谢!!重建时间 继续大致线性增加?

  4. 实用性:SciPy KDTree不会腌制。据报道 this post,我收到以下错误“PicklingError:不能 泡菜:它没有找到 scipy.spatial.kdtree.innernode“ - 我认为这是因为它是一个 嵌套结构。根据{{​​3}}报道的答案, 嵌套结构可以用莳萝腌制。但是,莳萝给了我 同样的错误 - 这是为什么?

2 个答案:

答案 0 :(得分:6)

在回答问题之前,我想指出,当您的程序使用大量数字时,应始终使用numpy library中的numpy.array来存储此类数据。我不知道您使用的是哪个版本的Python,scikit-learnSciPy,但是我正在使用Python 3.7.3,scikit-learn 0.21.3和SciPy 1.3.0。当我运行您的代码以比较构建时间时,我得到了AttributeError: 'list' object has no attribute 'size'。此错误表示列表listOfRandom2DPoints没有属性size。问题是sklearn.neighbors.KDTree期望numpy.array具有属性size。类scipy.spatial.KDTree可用于Python列表,但正如您在source code of __init__ method of class scipy.spatial.KDTree中看到的那样,第一行是self.data = np.asarray(data),这意味着数据将转换为numpy.array

因此,我改变了你的说法:

from random import randint
listOfRandom2DPoints = [ [randint(0,dim),randint(0,dim)] for x in range(length)]

收件人:

import numpy as np
ListOfRandom2DPoints = np.random.randint(0, dim, size=(length, 2))

(此更改不会影响速度比较,因为更改是在设置代码中进行的。)

现在回答您的问题:

  1. 就像您说的那样,scikit-learn似乎在构建时间上甜菜了SciPy。发生这种情况的原因并不是scikit-learn具有更快的算法,而是sklearn.neighbors.KDTreeCythonlink to source code)中实现,而scipy.spatial.KDTree是用纯Python编写的代码(link to source code)。

    (如果您不知道什么是Cython,则过度简化的解释会 Cython使得用Python和main编写C代码成为可能 这样做的原因是C比Python快得多)

    SciPy库在Cython scipy.spatial.cKDTreelink to source code)中也有实现,它的作用与scipy.spatial.KDTree相同,并且如果您比较sklearn.neighbors.KDTree和{{1}的构建时间}:

    scipy.spatial.cKDTree

    构建时间非常相似,当我运行代码时,timeit.timeit('scipy.spatial.cKDTree(npListOfRandom2DPoints, leafsize=20)', setup=setup, number=nTimes) timeit.timeit('sklearn.neighbors.KDTree(npListOfRandom2DPoints, leaf_size=20)', setup=setup, number=nTimes) 快了一点(大约20%)。

    查询时间的情况非常相似,scipy.spatial.cKDTree(纯Python实现)的速度比scipy.spatial.KDTree(Cython实现)慢十倍,而sklearn.neighbors.KDTree(Cython实现)的速度也差不多为scipy.spatial.cKDTree。我测试了查询时间,最多N = 10000000,并得到与您相同的结果。无论N,查询时间都保持不变(这意味着sklearn.neighbors.KDTree的查询时间对于N = 1000和N = 1000000是相同的,而对于scipy.spatial.KDTreesklearn.neighbors.KDTree的查询时间则是相同的)。这是因为查询(搜索)时间复杂度为O(logN),即使对于N = 1000000,logN也非常小,因此差异太小而无法测量。

  2. scipy.spatial.cKDTree(类的sklearn.neighbors.KDTree方法)的构建算法的时间复杂度为O(KNlogN)(about scikit-learn Nearest Neighbor Algorithms),因此在您的情况下为O(2NlogN)实际上是O(NlogN)。基于__init__sklearn.neighbors.KDTree的构建时间非常相似,我假设scipy.spatial.cKDTree的构建算法的时间复杂度也为O(NlogN)。我不是最邻近搜索算法的专家,但是基于一些在线搜索,我会说对于低维的最邻近搜索算法,这要尽可能快。如果转到nearest neighbor search Wikipedia page,将看到有exact methodsapproximation methodsk-d tree是精确方法,它是space partitioning methods的子类型。在所有空间分区方法(仅基于Wikipedia页面的用于最接近邻居搜索的快速精确方法)中,在低维欧几里德空间用于静态上下文中最接近邻居搜索的情况下,kd树是最好的方法(插入和删除)。同样,如果您查看greedy search in proximity neighborhood graphs下的近似方法,则会看到“近似图方法被认为是近似最近邻居搜索的最新技术。”当您查看为此方法引用的研究文章(Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs)时,您会发现该方法的时间复杂度为O(NlogN)。这意味着对于低维空间,k-d树(精确方法)与逼近方法一样快。现在,我们比较了用于最近邻居搜索的结构的构建(构造)时间复杂度。所有这些算法的搜索(查询)时间复杂度为O(logN)。因此,我们可以得到的最好结果就是k(d-k)树方法的O(NlogN)的构建复杂度和O(logN)的查询复杂度。因此,根据我的研究,我会说k-d树是低维最近邻居搜索的最佳结构。

    (我认为,如果有比最近的scikit-learn和SciPy更好(更快)的方法来执行最近邻居搜索,那么从理论上讲,知道最快的排序算法的时间复杂度为O( NlogN),具有时间复杂度小于O(NlogN)的最近邻居搜索构建算法将非常令人惊讶。)

  3. 就像我说过的,您正在将scipy.spatial.cKDTree与Cython实现和sklearn.neighbors.KDTree与纯Python实现进行比较。理论上scipy.spatial.KDTree应该比sklearn.neighbors.KDTree快,我比较了它们,直到1000000,它们似乎在大N时更接近。对于N = 100,scipy.spatial.KDTree比{{ {1}},对于N = 1000000,scipy.spatial.KDTree的速度大约是sklearn.neighbors.KDTree的两倍。我不确定为什么会这样,但是我怀疑对于大N来说,内存成为比操作数更大的问题。

    我检查了重新构建时间,该时间也达到了1000000,并且确实线性增加,这是因为函数scipy.spatial.KDTree的持续时间与加载对象的大小成线性比例。

  4. 对我来说,sklearn.neighbors.KDTreepickle.loadssklearn.neighbors.KDTree的酸洗有效,所以我无法重现您的错误。我猜测问题是您的SciPy版本较旧,因此将SciPy更新到最新版本应该可以解决此问题。

    (如果您需要更多有关此问题的帮助,则应在问题中添加更多信息。您的Python和SciPy版本是什么,重现此错误的确切代码以及完整的错误消息?)

答案 1 :(得分:0)

我建议您从SciKit尝试Gaussian Mixture Models - 了解此类问题。由于您的数据是二维的,因此它应该正常工作。