混合数值和分类数据的观测值之间成对距离计算的有效实现

时间:2019-05-14 09:19:57

标签: python numpy categorical-data numba pairwise-distance

我正在一个数据科学项目中,必须计算数据集中每对观测值之间的欧几里得距离。

由于我要处理非常大的数据集,因此必须使用高效的成对距离计算(在内存使用和计算时间方面)。

一种解决方案是使用Scipy中的pdist函数,该函数以一维数组的形式返回结果,而没有重复的实例。

但是,此函数无法处理分类变量。对于这些,我想在值相同的情况下将距离设置为0,在其他情况下将距离设置为1。

我尝试用Numba在Python中实现此变体。该函数将包含所有观测值的2D Numpy数组和包含变量类型(float64category的1D数组作为输入。

这是代码:

import numpy as np
from numba.decorators import autojit

def pairwise(X, types):
    m = X.shape[0]
    n = X.shape[1]

    D = np.empty((int(m * (m - 1) / 2), 1), dtype=np.float)
    ind = 0

    for i in range(m):
        for j in range(i+1, m):
            d = 0.0

            for k in range(n):
                if types[k] == 'float64':
                    tmp = X[i, k] - X[j, k]
                    d += tmp * tmp
                else:
                    if X[i, k] != X[j, k]:
                        d += 1.

            D[ind] = np.sqrt(d)
            ind += 1

    return D.reshape(1, -1)[0]

pairwise_numba = autojit(pairwise)

vectors = np.random.rand(20000, 100)
types = np.array(['float64']*100)

dists = pairwise_numba(vectors, types)

尽管使用了Numba,该实现仍然非常缓慢。是否可以改善我的代码以使其更快?

2 个答案:

答案 0 :(得分:0)

autojit已过时,recommended改为使用jit。而且几乎总是应该使用jit(nopython=True),如果无法从python中降低某些内容,这会使numba失败。

在代码上使用nopython会发现两个问题。一个简单的解决方法-该行需要引用特定的numpy类型而不是float

 - D = np.empty((int(m * (m - 1) / 2), 1), dtype=np.float)
 + D = np.empty((int(m * (m - 1) / 2), 1), dtype=np.float64)

第二个是您使用字符串保存类型信息-numba对使用字符串的支持有限。您可以将类型信息编码为数字数组,例如0代表数字,1代表分类。因此可以实现。

@jit(nopython=True)
def pairwise_nopython(X, types):
    m = X.shape[0]
    n = X.shape[1]

    D = np.empty((int(m * (m - 1) / 2), 1), dtype=np.float64)
    ind = 0

    for i in range(m):
        for j in range(i+1, m):
            d = 0.0

            for k in range(n):
                if types[k] == 0: #numeric
                    tmp = X[i, k] - X[j, k]
                    d += tmp * tmp
                else:
                    if X[i, k] != X[j, k]:
                        d += 1.

            D[ind] = np.sqrt(d)
            ind += 1

    return D.reshape(1, -1)[0]

答案 1 :(得分:0)

如果您真的希望numba快速执行,则需要在jit模式下nopython使用该功能,否则numba可能会退回到较慢的对象模式(并且可能非常慢)。 / p>

但是在nopython模式下不能编译函数(从numba版本0.43.1开始),这是因为:

  • dtype的{​​{1}}参数。 np.empty就是Python np.float,将由NumPy(但不是numba)翻译为float。如果您使用numba,则必须使用它。
  • 在numba中缺少字符串支持。因此np.float_行将无法编译。

第一个问题很简单。关于第二个问题:提供一个布尔数组,而不是试图使字符串比较起作用。与比较最多7个字符相比,使用布尔数组并评估一个布尔值的准确性也要快得多。尤其是在最里面的循环中!

所以它可能看起来像这样:

types[k] == 'float64'

但是,如果将浮点类型上的import numpy as np import numba as nb @nb.njit def pairwise_numba(X, is_float_type): m = X.shape[0] n = X.shape[1] D = np.empty((int(m * (m - 1) / 2), 1), dtype=np.float64) # corrected dtype ind = 0 for i in range(m): for j in range(i+1, m): d = 0.0 for k in range(n): if is_float_type[k]: tmp = X[i, k] - X[j, k] d += tmp * tmp else: if X[i, k] != X[j, k]: d += 1. D[ind] = np.sqrt(d) ind += 1 return D.reshape(1, -1)[0] dists = pairwise_numba(vectors, types == 'float64') # pass in the boolean array 与数字算法结合起来以计算不相等的类别,则可以简化逻辑:

scipy.spatial.distances.pdist

它不会显着更快,但是可以大大简化numba函数中的逻辑。

然后,您还可以通过将平方距离传递给numba函数来避免创建其他数组:

from scipy.spatial.distance import pdist

@nb.njit
def categorial_sum(X):
    m = X.shape[0]
    n = X.shape[1]
    D = np.zeros(int(m * (m - 1) / 2), dtype=np.float64)  # corrected dtype
    ind = 0

    for i in range(m):
        for j in range(i+1, m):
            d = 0.0
            for k in range(n):
                if X[i, k] != X[j, k]:
                    d += 1.
            D[ind] = d
            ind += 1

    return D

def pdist_with_categorial(vectors, types):
    where_float_type = types == 'float64'
    # calculate the squared distance of the float values
    distances_squared = pdist(vectors[:, where_float_type], metric='sqeuclidean')
    # sum the number of mismatched categorials and add that to the distances 
    # and then take the square root
    return np.sqrt(distances_squared + categorial_sum(vectors[:, ~where_float_type]))