首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >Python与cython的快速余弦距离

Python与cython的快速余弦距离
EN

Stack Overflow用户
提问于 2018-07-19 22:30:45
回答 2查看 2.4K关注 0票数 3

我想尽可能地加速的余弦距离计算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))

我试过cython

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指定变量类型,我将对类型为np.float32的numpy数组使用此函数

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2018-07-20 05:03:32

有一种观点认为,numpy的功能不能在cython或numba的帮助下加速。但这并不完全正确: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 ns :其他一切都是调用函数的开销。减少开销的一种可能是在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%,这意味着几乎比numpy版本快5倍。

还有另一个工具可以提供类似的功能/性能- 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)

与最初的numpy版本相比,速度提高了8。

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

但这里的差异完全是由于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)

numba的开销几乎减少了2倍!

正如@max9111所指出的,numpy内联了其他out函数,但它也能够以非常小的开销调用一些numpy函数,所以下面的版本(用dot替换inner ):

@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 + dot解决方案,它对于小输入非常快,但对于更大的输入也具有mkl实现的全部功能。

还有一个细微的区别:第一个版本返回一个np.float64浮点,而cython和numba版本返回一个Python--object。

列表:

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))
票数 8
EN

Stack Overflow用户

发布于 2018-07-19 23:39:51

使用懒惰的方式加速这类代码:

  1. 使用numexpr Python模块
  2. 使用numba Python模块
  3. 使用SciPy等效的NumPy函数<代码>H29<代码>G210

不幸的是,这些技巧对你都不起作用,因为:

  1. dotinner不是在numexpr
  2. numba中实现的(像Cython)不会加速对NumPy的functions
  3. dotinner的调用在scipy中没有不同的实现方式(它们甚至在命名空间中都不可用)。

也许你最好的办法是尝试在不同的底层LA库(如LAPACK、BLAS、OpenBLAS等)下编译numpy。和编译选项(例如,多线程等),以了解哪种组合对您的用例最有效。

祝好运!

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/51425300

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档