有没有办法让这个 Python kNN 函数更高效?

Posted

技术标签:

【中文标题】有没有办法让这个 Python kNN 函数更高效?【英文标题】:Is there a way to make this Python kNN function more efficient? 【发布时间】:2014-12-11 12:59:15 【问题描述】:

在MATLAB 遇到问题后,我决定尝试 Python:

我编写了一个函数,当样本属于我自己的类时,我使用我自己的距离函数计算 kNN:

def closestK(sample, otherSamples, distFunc, k):
"Returns the closest k samples to sample based on distFunc"
    n = len(otherSamples)
    d = [distFunc(sample, otherSamples[i]) for i in range(0,n)]
    idx  = sorted(range(0,len(d)), key=lambda k: d[k])
    return idx[1:(k+1)]

def kNN(samples, distFunc, k):
    return [[closestK(samples[i], samples, distFunc, k)] for i in range(len(samples))]

这是距离函数:

@staticmethod    
def distanceRepr(c1, c2):
    r1 = c1.repr
    r2 = c2.repr
    # because cdist needs 2D array
    if r1.ndim == 1:
        r1 = np.vstack([r1,r1])
    if r2.ndim == 1:
        r2 = np.vstack([r2,r2])

    return scipy.spatial.distance.cdist(r1, r2, 'euclidean').min()

但与“正常”kNN 函数相比,它的运行速度仍然慢得惊人,即使使用“蛮力”算法也是如此。我做错了吗?

更新

我正在添加类的构造函数。 repr 属性包含一组向量(从 1 到任意值),并且距离被计算为两组 repr 之间的最小欧几里得距离。

class myCluster:
    def __init__(self, index = -1, P = np.array([])):
        if index ==-1 :
            self.repr = np.array([])
            self.IDs = np.array([])
            self.n = 0
            self.center = np.array([])
        else:
            self.repr = np.array(P)
            self.IDs = np.array(index)
            self.n = 1
            self.center = np.array(P)

以及其余相关代码(X 是一个矩阵,其行是样本,列是变量):

level = [myCluster(i, X[i,:]) for i in range(0,n)]
kNN(level, myCluster.distanceRepr, 3)

更新 2

我做了一些测量,花费大部分时间的线路是

d = [distFunc(sample, otherSamples[i]) for i in range(0,n)]

所以distFunc 有一些东西。当我将其更改为返回时

np.linalg.norm(c1.repr-c2.repr)

即“正常”向量计算,no排序,运行时间保持不变。所以问题出在这个函数的调用上。使用类将运行时间更改为 60 倍是否有意义?

【问题讨论】:

你指的这个“正常”kNN函数究竟是什么? @moarningsun:scikit 中的 NearestNeighbors 函数。在我测试的设置中,他们做的完全一样:为 n 个欧几里得向量计算 kNN,只是我的向量被一个类“包装”了。 使用 Numpy 函数代替 d = [dist...idx = sorted(.... 之类的列表解析是解决方案 转换为 Numpy 取决于 distFunc 是什么。如果您提供完整的来源,您可以获得更好的答案。 @mskimm:我已经添加了相关代码。谢谢。 只是好奇,当您使用sklearn.NearestNeighbors 类时,您是否也在使用algorithm='ball_tree'algorithm='kd_tree' 选项?这些将大大加快算法速度,但前提是您进行重复查询。我也不确定这是否是您问题的重点,所以想先发表评论。 【参考方案1】:

您只是遇到了 Python 的缓慢性(或者更确切地说,我应该说是 CPython 解释器)。来自wikipedia:

NumPy 针对 Python 的 CPython 参考实现,这是一个非优化的 bytecode 编译器/解释器。为这个版本的 Python 编写的数学算法的运行速度通常比compiled 等价物慢得多。 NumPy 试图通过提供多维数组以及在数组上有效操作的函数和运算符来解决这个问题。因此,任何可以主要表示为对数组和矩阵的运算的算法几乎可以与等效的 C 代码一样快地运行。

来自Scipy FAQ:

Python 的列表是高效的通用容器。它们支持(相当)有效的插入、删除、追加和连接,Python 的列表推导使它们易于构造和操作。但是,它们有一定的局限性:它们不支持像元素加法和乘法这样的“向量化”操作,而且它们可以包含不同类型的对象这一事实意味着 Python 必须存储每个元素的类型信息,并且必须执行类型调度代码在对每个元素进行操作时。这也意味着很少有列表操作可以通过高效的 C 循环执行——每次迭代都需要类型检查和其他 Python API 簿记。

请注意,这不仅仅涉及 Python;有关更多背景,请参见例如this 和 this question 在 SO 上。

由于动态类型系统和解释器的开销,如果 Python 不能利用各种已编译的 C 和 Fortran 库(例如 Numpy )。此外,还有像 Numba 和 PyPy 这样的 JIT 编译器,它们试图让 Python 代码的执行速度更接近于静态类型编译代码的速度。

底线:相对于要卸载到快速 C 代码的工作,您在纯 Python 中做了很多工作。我想您需要采用更像“面向数组”的编码风格而不是面向对象的方式来使用 Numpy 实现良好的性能(在这方面,MATLAB 是一个非常相似的故事)。另一方面,如果您使用更高效的算法(请参阅 Ara 的回答),那么 Python 的缓慢可能不是这样的问题。

【讨论】:

谢谢。 Java 会在这方面表现更好吗? 我不相信这里有很多由 python 完成的实际计算。 op可以给我们一些分析信息吗?例如通过运行 python -m cProfile -s time YOU_PROGRAM.py 并将结果发布到 pastebin ? @Roy,我对 Java 一点也不熟悉。 @Ara,没错。几乎所有时间都花在开销上:引用计数、函数调用、各种查找等。只要以 C 速度进行实际计算,所有这些都需要多达 60 次(由 OP 计时)!有关这种差异的更多演示,请参见 here 或 here. 我不一定同意大部分时间都花在开销上,它也可能在 numpy 上:我们在 numpy 中创建一个 2000*2000 矩阵只是为了找到它的最小值!无论如何,来自@Roy 的一些分析信息会很棒。【参考方案2】:

以下是我能想到的几点:

您每次调用最接近的K 时都会计算一个样本与其他样本之间的距离,因此您计算每个样本之间的距离两次(一次是距离(a,b),然后是距离(b,a)),这可以通过以下方式避免一劳永逸地计算它 您重新计算 r(这可能涉及一个代价高昂的 vstack)2 * (n - 1) 次,其中 n 是 len(sample),您也可以一劳永逸地计算它(并将其存储为 myCluster 的一个属性?) . 您只需要前 k 个元素(无需在第 k 个元素之后排序),就计算完整列表的排序 要计算集合点之间的最小距离,您需要创建一个包含每个距离的矩阵,然后取其最小值:您当然可以做得更好

我的建议是使用 insert 方法实现一个 top-k 类,该方法仅在您优于当前第 k 个元素时插入(并删除它)并修改 myCluster 以包含河。那么你的代码可能看起来像

kNN = i : TopK() for i in xrange(len(samples))
for i, sample1 in enumerate(samples):
    for j, sample2 in enumerate(samples[:i]):
        dist = distanceRepr(sample1, sample2)
        kNN[i].insert(j, -dist)
        kNN[j].insert(i, -dist)
return kNN

这是一个可能的实现,ok TopK:

import heapq

class TopK:
    def __init__(self, k):
        self.k = k
        self.content = []

    def insert (self, key, score):
        if len(self.content) < self.k:
            heapq.heappush(self.content, (score, key))
        else:
            heapq.heappushpop(self.content, (score, key))

    def get_keys(self):
        return [elem[1] for elem in self.content]

对于distanceRepr,您可以使用类似:

import scipy.spatial

def distanceRepr(set0 ,set1):
    if len(set0) < len(set1):
        min_set = set0
        max_set = set1
    else:
        min_set = set1
        max_set = set0
    if len(min_set) == 0:
        raise Exception("Empty set")

    min_dist = scipy.inf
    tree = scipy.spatial.cKDTree(max_set)

    for point in min_set:
        distance, _ = tree.query(point, 1, 0., 2, min_dist)
        if min_dist > distance:
            min_dist = min(min_dist, distance)

    return min_dist

它将比您当前的中大型条目方法更快(假设使用 size > 5k 的 sample1 和 2),它还将使用更小的内存,使其能够处理大样本(其中 @987654325 @ 只是内存不足)。

【讨论】:

谢谢。关于您的第一条评论,我知道,但另一种选择是将所有距离保存在内存中,我不希望这样做。关于第二个,请看我的第二个更新。关于你的第三个,即使我将 scikit 与 k=n 一起使用,它所花费的时间几乎与小 k 相同。似乎这里有更基本的东西。 正如您在此处看到的,我不会在内存中存储超过所需的内容并避免重新计算。

以上是关于有没有办法让这个 Python kNN 函数更高效?的主要内容,如果未能解决你的问题,请参考以下文章

高效的knn算法

python 如何画出KD数

有没有办法让这个代码更有效,更小?

Python数据挖掘—回归—KNN

更高效的 iCloud 同步?

有没有办法让这个 jquery 悬停动画更流畅?