9

我写了一个方法来计算两个数组之间的余弦距离:

def cosine_distance(a, b):
    if len(a) != len(b):
        return False
    numerator = 0
    denoma = 0
    denomb = 0
    for i in range(len(a)):
        numerator += a[i]*b[i]
        denoma += abs(a[i])**2
        denomb += abs(b[i])**2
    result = 1 - numerator / (sqrt(denoma)*sqrt(denomb))
    return result

在大型阵列上运行它可能会非常慢。这种方法是否有运行速度更快的优化版本?

更新:我已经尝试了迄今为止的所有建议,包括 scipy。这是要击败的版本,结合了 Mike 和 Steve 的建议:

def cosine_distance(a, b):
    if len(a) != len(b):
        raise ValueError, "a and b must be same length" #Steve
    numerator = 0
    denoma = 0
    denomb = 0
    for i in range(len(a)):       #Mike's optimizations:
        ai = a[i]             #only calculate once
        bi = b[i]
        numerator += ai*bi    #faster than exponent (barely)
        denoma += ai*ai       #strip abs() since it's squaring
        denomb += bi*bi
    result = 1 - numerator / (sqrt(denoma)*sqrt(denomb))
    return result
4

8 回答 8

8

如果你可以使用 SciPy,你可以使用cosinefrom spatial.distance

http://docs.scipy.org/doc/scipy/reference/spatial.distance.html

如果你不能使用 SciPy,你可以尝试通过重写你的 Python 来获得一个小的加速(编辑:但它并没有像我想象的那样工作,见下文)。

from itertools import izip
from math import sqrt

def cosine_distance(a, b):
    if len(a) != len(b):
        raise ValueError, "a and b must be same length"
    numerator = sum(tup[0] * tup[1] for tup in izip(a,b))
    denoma = sum(avalue ** 2 for avalue in a)
    denomb = sum(bvalue ** 2 for bvalue in b)
    result = 1 - numerator / (sqrt(denoma)*sqrt(denomb))
    return result

当 a 和 b 的长度不匹配时,最好引发异常。

通过在调用中使用生成器表达式,sum()您可以计算您的值,而大部分工作由 Python 内部的 C 代码完成。这应该比使用for循环更快。

我没有计时,所以我无法猜测它可能会快多少。但几乎可以肯定 SciPy 代码是用 C 或 C++ 编写的,它应该尽可能快。

如果你在 Python 中做生物信息学,那么无论如何你真的应该使用 SciPy。

编辑:Darius Bacon 为我的代码计时,发现它变慢了。所以我给我的代码计时了......是的,它更慢。所有人的教训:当你试图加快速度时,不要猜测,要衡量。

我很困惑为什么我尝试在 Python 的 C 内部进行更多工作的速度较慢。我尝试了长度为 1000 的列表,但它仍然较慢。

我不能再花时间尝试巧妙地破解 Python。如果您需要更快的速度,我建议您尝试 SciPy。

编辑:我只是手工测试,没有时间。我发现简而言之 a 和 b,旧代码更快;对于long a和b,新代码更快;在这两种情况下,差异都不大。(我现在想知道我是否可以在我的 Windows 计算机上信任 timeit;我想在 Linux 上再次尝试这个测试。)我不会更改工作代码来尝试让它更快。还有一次,我敦促您尝试 SciPy。:-)

于 2009-12-01T00:45:10.337 回答
8

(我最初认为)如果不使用 C(如 numpy 或 scipy)或更改计算内容,您将不会加快速度。但无论如何,这就是我要尝试的方法:

from itertools import imap
from math import sqrt
from operator import mul

def cosine_distance(a, b):
    assert len(a) == len(b)
    return 1 - (sum(imap(mul, a, b))
                / sqrt(sum(imap(mul, a, a))
                       * sum(imap(mul, b, b))))

在具有 500k 元素数组的 Python 2.6 中,它的速度大约是后者的两倍。(将地图更改为 imap 后,跟随 Jarret Hardie。)

这是原始海报修订代码的调整版本:

from itertools import izip

def cosine_distance(a, b):
    assert len(a) == len(b)
    ab_sum, a_sum, b_sum = 0, 0, 0
    for ai, bi in izip(a, b):
        ab_sum += ai * bi
        a_sum += ai * ai
        b_sum += bi * bi
    return 1 - ab_sum / sqrt(a_sum * b_sum)

它很丑陋,但它确实出来得更快。. .

编辑:并尝试Psyco!它将最终版本的速度提高了 4 倍。我怎么会忘记呢?

于 2009-12-01T01:13:06.287 回答
2

没有必要采取abs(),如果你正在平方它a[i]b[i]

存储a[i]b[i]在临时变量中,以避免多次进行索引。也许编译器可以对此进行优化,但也许不能。

检查到**2运营商。是将其简化为乘法,还是使用通用幂函数(对数 - 乘以 2 - 反对数)。

不要做 sqrt 两次(尽管这样做的成本很小)。做sqrt(denoma * denomb)

于 2009-12-01T01:26:52.453 回答
1

这对于大约 1000 多个元素的数组来说更快。

from numpy import array
def cosine_distance(a, b):
    a=array(a)
    b=array(b)
    numerator=(a*b).sum()
    denoma=(a*a).sum()
    denomb=(b*b).sum()
    result = 1 - numerator / sqrt(denoma*denomb)
    return result
于 2009-12-01T00:58:29.140 回答
1

与 Darius Bacon 的答案类似,我一直在玩弄 operator 和 itertools 以产生更快的答案。根据 timeit,以下似乎在 500 项数组上快 1/3:

from math import sqrt
from itertools import imap
from operator import mul

def op_cosine(a, b):
    dot_prod = sum(imap(mul, a, b))
    a_veclen = sqrt(sum(i ** 2 for i in a))
    b_veclen = sqrt(sum(i ** 2 for i in b))

    return 1 - dot_prod / (a_veclen * b_veclen)
于 2009-12-01T01:21:32.947 回答
1

使用 SciPy 内部的 C 代码对于长输入数组来说非常重要。对短输入数组使用简单直接的 Python 获胜;Darius Bacon 的izip()基于代码的基准测试表现最佳。因此,最终的解决方案是根据输入数组的长度来决定在运行时使用哪一个:

from scipy.spatial.distance import cosine as scipy_cos_dist

from itertools import izip
from math import sqrt

def cosine_distance(a, b):
    len_a = len(a)
    assert len_a == len(b)
    if len_a > 200:  # 200 is a magic value found by benchmark
        return scipy_cos_dist(a, b)
    # function below is basically just Darius Bacon's code
    ab_sum = a_sum = b_sum = 0
    for ai, bi in izip(a, b):
        ab_sum += ai * bi
        a_sum += ai * ai
        b_sum += bi * bi
    return 1 - ab_sum / sqrt(a_sum * b_sum)

我制作了一个测试工具来测试不同长度输入的函数,发现 SciPy 函数在长度 200 左右开始获胜。输入数组越大,它就越大。对于长度很短的数组,比如长度为 3,更简单的代码会胜出。此功能会增加少量开销来决定采用哪种方式,然后采用最佳方式。

如果您有兴趣,这里是测试工具:

from darius2 import cosine_distance as fn_darius2
fn_darius2.__name__ = "fn_darius2"

from ult import cosine_distance as fn_ult
fn_ult.__name__ = "fn_ult"

from scipy.spatial.distance import cosine as fn_scipy
fn_scipy.__name__ = "fn_scipy"

import random
import time

lst_fn = [fn_darius2, fn_scipy, fn_ult]

def run_test(fn, lst0, lst1, test_len):
    start = time.time()
    for _ in xrange(test_len):
        fn(lst0, lst1)
    end = time.time()
    return end - start

for data_len in range(50, 500, 10):
    a = [random.random() for _ in xrange(data_len)]
    b = [random.random() for _ in xrange(data_len)]
    print "len(a) ==", len(a)
    test_len = 10**3
    for fn in lst_fn:
        n = fn.__name__
        r = fn(a, b)
        t = run_test(fn, a, b, test_len)
        print "%s:\t%f seconds, result %f" % (n, t, r)
于 2009-12-01T05:56:22.597 回答
0
def cd(a,b):
    if(len(a)!=len(b)):
        raise ValueError, "a and b must be the same length"
    rn = range(len(a))
    adb = sum([a[k]*b[k] for k in rn])
    nma = sqrt(sum([a[k]*a[k] for k in rn]))
    nmb = sqrt(sum([b[k]*b[k] for k in rn]))

    result = 1 - adb / (nma*nmb)
    return result
于 2009-12-01T03:05:16.157 回答
0

您更新的解决方案仍然有两个平方根。您可以通过将 sqrt 行替换为:

结果 = 1 - 分子 / (sqrt(denoma*denomb))

乘法通常比 sqrt 快很多。它可能看起来不多,因为它只在函数中调用一次,但听起来你正在计算很多余弦距离,所以改进会加起来。

您的代码看起来应该适合向量优化。因此,如果跨平台支持不是问题并且您想进一步加快速度,您可以在 C 中编写余弦距离代码,并确保您的编译器积极地矢量化生成的代码(即使 Pentium II 也能够进行一些浮点矢量化)

于 2011-01-10T21:30:56.977 回答