代码之家  ›  专栏  ›  技术社区  ›  mccandar

巨蟒与赛通的快速余弦距离

  •  2
  • mccandar  · 技术社区  · 7 年前

    我想 加快 余弦距离计算 scipy.spatial.distance.cosine 尽可能多地使用numpy

    def alt_cosine(x,y):
        return 1 - np.inner(x,y)/np.sqrt(np.dot(x,x)*np.dot(y,y))
    

    我试过了 赛隆

    from libc.math cimport sqrt
    def alt_cosine_2(x,y):
        return 1 - np.inner(x,y)/sqrt(np.dot(x,x)*np.dot(y,y))
    

    逐渐得到改进(对长度为50的numpy阵列进行测试)

    >>> cosine() # ... make some timings
    5.27526156300155e-05 # mean calculation time for one loop
    
    >>> alt_cosine() 
    9.913400815003115e-06
    
    >>> alt_cosine_2()
    7.0269494536660205e-06
    

    最快的方法是什么? 不幸的是,我无法将变量类型指定为 alt_cosine_2 ,我将对numpy数组使用此函数 np.float32

    2 回复  |  直到 7 年前
        1
  •  6
  •   ead    7 年前

    有一种观点认为,在赛通或numba的帮助下,numpy的功能无法加快。但这并非完全正确:Numpy的目标是为各种场景提供出色的性能,但这也意味着对于特殊场景的性能不太理想。

    有了特定的场景,您就有机会改进numpy的性能,即使这意味着要重写numpy的一些功能。例如,在这种情况下,我们可以使用cython通过因子4加速函数,使用numba通过因子8加速函数。

    让我们从您的版本作为基线开始(请参见答案末尾的列表):

    >>>%timeit cosine(x,y)   # scipy's
    31.9 µs ± 1.81 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    
    >>>%timeit np_cosine(x,y)  # your numpy-version
    4.05 µs ± 19.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    %timeit np_cosine_fhtmitchell(x,y)  # @FHTmitchell's version
    4 µs ± 53.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    
    >>>%timeit np_cy_cosine(x,y)
    2.56 µs ± 123 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    

    所以我看不到@fhtmitchell版本的改进,但与您的时间安排没有不同。

    您的向量只有50个元素,所以实际计算需要大约200-300纳秒:其他的都是调用函数的开销。减少开销的一种可能是在Cython的帮助下,将这些功能手动“内联”起来:

    %%cython 
    from libc.math cimport sqrt
    import numpy as np
    cimport numpy as np
    
    def cy_cosine(np.ndarray[np.float64_t] x, np.ndarray[np.float64_t] y):
        cdef double xx=0.0
        cdef double yy=0.0
        cdef double xy=0.0
        cdef Py_ssize_t i
        for i in range(len(x)):
            xx+=x[i]*x[i]
            yy+=y[i]*y[i]
            xy+=x[i]*y[i]
        return 1.0-xy/sqrt(xx*yy)
    

    从而导致:

    >>> %timeit cy_cosine(x,y)
    921 ns ± 19.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    

    不错!我们可以尝试通过以下更改来释放一些安全性(运行时检查+IEEE-754标准),从而挤出更多的性能:

    %%cython  -c=-ffast-math
    ...
    
    cimport cython
    @cython.boundscheck(False)
    @cython.wraparound(False)
    def cy_cosine_perf(np.ndarray[np.float64_t] x, np.ndarray[np.float64_t] y):
        ...
    

    从而导致:

    >>> %timeit cy_cosine_perf(x,y)
    828 ns ± 17.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    

    也就是说,另一个10%,这意味着因子5比numpy版本快。

    还有另一个提供类似功能/性能的工具-numba:

    import numba as nb
    import numpy as np
    @nb.jit(nopython=True, fastmath=True)
    def nb_cosine(x, y):
        xx,yy,xy=0.0,0.0,0.0
        for i in range(len(x)):
            xx+=x[i]*x[i]
            yy+=y[i]*y[i]
            xy+=x[i]*y[i]
        return 1.0-xy/np.sqrt(xx*yy)
    

    从而导致:

    >>> %timeit nb_cosine(x,y)
    495 ns ± 5.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    

    一个加速8比原来的麻木版本。

    有一些原因可以使numba更快:Cython在运行时处理数据的步幅 prevents some optimization (例如矢量化)。努巴似乎处理得更好。

    但是这里的差异完全是由于numba的开销更少:

    %%cython  -c=-ffast-math
    import numpy as np
    cimport numpy as np
    
    def cy_empty(np.ndarray[np.float64_t] x, np.ndarray[np.float64_t] y):
        return x[0]*y[0]
    
    import numba as nb
    import numpy as np
    @nb.jit(nopython=True, fastmath=True)
    def nb_empty(x, y):
        return x[0]*y[0]
    
    %timeit cy_empty(x,y)
    753 ns ± 6.81 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    %timeit nb_empty(x,y)
    456 ns ± 2.47 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    

    纽巴的开销几乎减少了2倍!

    正如@max9111所指出的,numpy内联其他的jitted函数,但它也可以调用一些开销很小的numpy函数,因此下面的版本(替换 inner 具有 dot ):

    @nb.jit(nopython=True, fastmath=True)
    def np_nb_cosine(x,y):
        return 1 - np.dot(x,y)/sqrt(np.dot(x,x)*np.dot(y,y))
    
    >>> %timeit np_nb_cosine(x,y)
    605 ns ± 5.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 
    

    只慢了大约10%。


    请注意,上述比较仅对包含50个元素的向量有效。对于更多的元素,情况是完全不同的:numpy版本使用点产品的并行mkl(或类似的)实现,并将轻松击败我们的简单尝试。

    这就引出了一个问题:是否真的值得为输入的特殊大小优化代码?有时答案是“是”,有时答案是“否”。

    如果可能的话,我会和 numba + 解决方案,对于小的输入速度非常快,但对于大的输入也具有mkl实现的全部能力。


    还有一点不同:第一个版本返回 np.float64 -对象以及cython和numba版本的python float。


    清单:

    from scipy.spatial.distance import cosine
    import numpy as np
    x=np.arange(50, dtype=np.float64)
    y=np.arange(50,100, dtype=np.float64)
    
    def np_cosine(x,y):
        return 1 - inner(x,y)/sqrt(np.dot(x,x)*dot(y,y))
    
    from numpy import inner, sqrt, dot
    def np_cosine_fhtmitchell(x,y):
        return 1 - inner(x,y)/sqrt(np.dot(x,x)*dot(y,y))
    
    %%cython
    from libc.math cimport sqrt
    import numpy as np
    def np_cy_cosine(x,y):
        return 1 - np.inner(x,y)/sqrt(np.dot(x,x)*np.dot(y,y))
    
        2
  •  2
  •   norok2    7 年前

    对于加快这种代码的懒惰方法:

    1. 使用 numexpr python模块
    2. 使用 numba Python模块
    3. 使用 SciPy numpy函数的等价物

    不幸的是,这些技巧都不适用于您,因为:

    1. dot inner 未在中实现 数字表达式
    2. 努巴 (像cython)不会加速调用numpy的函数
    3. 内部的 scipy (它们甚至在命名空间中都不可用)。

    也许你最好的选择是尝试编译 numpy 在不同的基础下 LA libs(例如lapack、blas、openblas等)和编译选项(例如多线程等)查看哪种组合对您的用例最有效。

    祝你好运!