Оптимизированный способ вычисления косинусного расстояния в Python

Я написал метод вычисления косинусного расстояния между двумя массивами:

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

запуск его может быть очень медленным на большом массиве. Есть ли оптимизированная версия этого метода, которая будет работать быстрее?

Update: я пробовал все предложения на сегодняшний день, включая scipy. Вот версия бить, включая предложения от Майка и Стива:

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

8 ответов


если вы можете использовать SciPy, вы можете использовать cosine С spatial.distance:

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

если вы не можете использовать SciPy, вы можете попытаться получить небольшое ускорение, переписав свой Python (EDIT: но это не сработало, как я думал, см. ниже).

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() вы можете рассчитать свои значения с большей частью работы, выполняемой кодом C внутри Python. Это должно быть быстрее, чем использование for петли.

Я не рассчитал это, поэтому я не могу догадаться, насколько быстрее это может быть. Но код SciPy почти наверняка написан на C или C++ , и он должен быть примерно таким же быстрым, как вы можете получить.

если вы делаете биоинформатику в Python, вы действительно должны использовать SciPy в любом случае.

EDIT: Дариус Бэкон приурочил мой код и нашел его медленнее. Поэтому я приурочил свой код и... да, медленнее. Урок для всех: когда вы пытаетесь ускорить события, не думаю, мера.

я озадачен тем, почему моя попытка поставить больше работы на внутренние части C Python медленнее. Я попробовал его для списков длины 1000, и он был еще медленнее.

Я не могу тратить больше времени на попытки взломать Python умно. Если вам нужно больше скорости, я предлагаю вы попробуйте составляющей.

EDIT: я только что проверил вручную, без timeit. Я нахожу, что для коротких a и b старый код быстрее; для длинных a и b новый код быстрее; в обоих случаях разница невелика. (Теперь мне интересно, могу ли я доверять timeit на моем компьютере с Windows; я хочу снова попробовать этот тест на Linux.) Я бы не стал менять рабочий код, чтобы попытаться получить его быстрее. И еще раз я призываю вас попробовать SciPy. :-)


(Я изначально думал) вы не собираетесь сильно ускорять его, не выходя на 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))))

это примерно в два раза быстрее в Python 2.6 с массивами 500k-элементов. (После смены карты на imap, следуя за Джарретом Харди.)

вот измененная версия пересмотренного кода оригинального плаката:

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)

это уродливо, но это выходит быстрее. . .

Edit: и Psyco! Он ускоряет окончательную версию еще в 4 раза. Как я мог забыть?


нет необходимости принимать abs() of a[i] и b[i] Если тебе надо.

магазине a[i] и b[i] во временных переменных, чтобы избежать индексирования более одного раза. Возможно, компилятор может оптимизировать это, но, возможно, нет.

регистрация в **2 оператора. Это упрощает его в умножении, или он использует общую функцию питания (log - multiply by 2 - antilog).

Не делайте sqrt дважды (хотя стоимость этого небольшая). Делать sqrt(denoma * denomb).


подобно ответу Дариуса Бэкона, я играл с оператором и itertools, чтобы получить более быстрый ответ. Следующее кажется на 1/3 быстрее на массиве 500 элементов в соответствии с timeit:

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)

это быстрее для массивов около 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

использование кода C внутри SciPy выигрывает большой для длинных входных массивов. Использование простых и прямых выигрышей Python для коротких входных массивов; Darius Bacon's 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)

Я сделал тестовый жгут, который тестировал функции с различными входами длины и обнаружил, что около длины 200 функция SciPy начала выигрывать. Чем больше входные массивы, тем больше он выигрывает. Для очень коротких массивов длины, скажем length 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)

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

ваше обновленное решение по-прежнему имеет два квадратных корня. Вы можете уменьшить это до одного, заменив строчку корень с:

результат = 1 - числитель / (sqrt (denoma * denomb))

умножение, как правило, немного быстрее, чем sqrt. Это может показаться не так много, как это вызывается только один раз в функции, но похоже, что вы вычисляете много косинусных расстояний, поэтому улучшение будет складываться.

ваш код выглядит так, как должен быть созрели для векторной оптимизации. Поэтому, если поддержка cross-platofrm не является проблемой, и вы хотите ускорить ее еще больше, вы можете закодировать код косинусного расстояния в C и убедиться, что ваш компилятор агрессивно векторизирует полученный код (даже Pentium II способен к некоторой векторизации с плавающей запятой)