Оптимизированный метод вычисления косинусного расстояния в 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

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

Обновление: я пробовал все предложения на сегодняшний день, включая 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

person Dan    schedule 01.12.2009    source источник
comment
Являются ли a и b массивами комплексных чисел?   -  person John La Rooy    schedule 01.12.2009
comment
До сих пор я пробовал все предложения, и в настоящее время предложения Майка Данлави по оптимизации существующего кода дали наилучшие результаты. Думаю, я оставлю вопрос открытым на случай, если есть другие стратегии решения проблемы, но большинство предложений в конечном итоге на самом деле работали медленнее, чем исходный код, поэтому Python должен выполнять довольно хорошую работу по оптимизации на лету. И @gnibbler, я не использую комплексные числа.   -  person Dan    schedule 01.12.2009
comment
Тогда я не понимаю, почему вы берете пресс перед квадратом.   -  person John La Rooy    schedule 01.12.2009
comment
Для больших массивов numpy должен стать намного быстрее, чем цикл в Python. Можете ли вы опубликовать данные, которые вы тестируете?   -  person John La Rooy    schedule 01.12.2009
comment
Вот что я использую для проверки: для i в диапазоне (1000000): a = cosine_distance([2,3,1],[2,3,6])   -  person Dan    schedule 01.12.2009
comment
Я только что провел быстрый тест, использование numpy было быстрее, когда списки составляли около 1000 элементов.   -  person John La Rooy    schedule 01.12.2009
comment
Причина, по которой numpy работает медленнее для небольших массивов, заключается в накладных расходах на преобразование в массивы numpy.   -  person John La Rooy    schedule 01.12.2009
comment
Полезно знать — спасибо за разъяснения, гнибблер.   -  person Dan    schedule 01.12.2009
comment
Нет проблем, вероятно, есть точка пересечения, когда чистые версии Python, использующие сумму, будут быстрее, чем цикл for, поэтому вам действительно нужно протестировать ваши типичные данные (большой означает разные вещи для разных людей)   -  person John La Rooy    schedule 01.12.2009
comment
Пока вы пытаетесь сократить код, вы можете попробовать использовать xrange() вместо range(), если вы все еще используете Python 2.x. Если вы используете Python 3, то есть только range(), и он возвращает итератор.   -  person steveha    schedule 01.12.2009
comment
Если бы весь ваш проект был в SciPy, и вам не нужно было преобразовывать в массив NumPy, потому что a и b уже были массивами NumPy, то я ожидаю, что функция SciPy будет явным победителем.   -  person steveha    schedule 01.12.2009
comment
Я только что замерил вашу текущую версию по сравнению с вашей версией, модифицированной для использования xrange(). Как я и ожидал, xrange() на мельчайший ус быстрее, потому что не нужно выделять и освобождать память для списка значений int; в моем тесте это было примерно на 0,2% быстрее для списков длиной 5000.   -  person steveha    schedule 01.12.2009
comment
С изменением на xrange() у меня закончились идеи; Я думаю, вы нашли самую быструю версию на простом Python. Если вам действительно нужно больше скорости, переключите весь проект на SciPy или напишите собственный модуль C, возможно, через Cython: docs.cython.org/src/quickstart/cythonize.html   -  person steveha    schedule 01.12.2009
comment
Упс, я говорил слишком рано. Я только что провел несколько тестов. Явным победителем стала функция Дариуса Бэкона, использующая izip(). Он извинился, сказав, что это уродливо, но получается быстрее... Ну, я не думаю, что это настолько уродливо, и это явный победитель по скорости. Это имеет смысл: решение range() включает в себя индексирование каждого списка дважды для поиска значений, а также выделение и освобождение списка; решение xrange() включает в себя индексирование каждого списка дважды для поиска и запуск итератора; решение izip() использует итератор для выборки каждого элемента один раз, что просто должно быть быстрее!   -  person steveha    schedule 01.12.2009
comment
Я просто повторил свои тесты, включая ответ gnibbler, и просто использовал from scipy.spatial.distance import cosine. Для списков длиной 500 000 функция cosine из SciPy является явным победителем, с ответом gnibbler почти так же быстро. Для списков длины 3 функция izip() Дариуса Бэкона является явным победителем, а решения, основанные на SciPy и NumPy, настолько плохи, что выбиваются из общего ряда. Который предлагает окончательное решение! Смотрите мой новый ответ.   -  person steveha    schedule 01.12.2009


Ответы (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.

РЕДАКТИРОВАТЬ: Дариус Бэкон рассчитал время моего кода и обнаружил, что он медленнее. Поэтому я рассчитал свой код и... да, он стал медленнее. Урок для всех: когда вы пытаетесь ускорить процесс, не гадайте, а измеряйте.

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

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

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

person steveha    schedule 01.12.2009
comment
Строка числителя неверна: она выполняет вложенный цикл вместо параллельного. - person Darius Bacon; 01.12.2009
comment
Кроме того, когда я исправил эту строку, чтобы получить правильный ответ, она все еще медленнее, чем исходный код. В любом случае, согласен с SciPy! (числитель = сумма (значение * значение b для значения, значение b в zip (a, b))) - person Darius Bacon; 01.12.2009
comment
Хороший звонок с SciPy. К сожалению, ваша перезапись, отличная от SciPy, возвращает неправильные значения. Замена строки числителя результатами gnibbler приводит к правильному ответу, но на самом деле это намного медленнее, чем мой исходный код. - person Dan; 01.12.2009
comment
Интересно, что scipy на самом деле был намного медленнее. Для тестирования я прогнал пару небольших массивов через 100 000 итераций. Исходный код работал ~ 1,3 секунды, а scipy - ~ 7,5 секунды. Интересно, будут ли таблицы включать массивы большего размера? - person Dan; 01.12.2009
comment
Из любопытства, поскольку у меня не установлен SciPy, но я постоянно заинтригован проектом, есть ли у вас время для этого конкретного случая с использованием косинуса из пространственного.расстояния? - person Jarret Hardie; 01.12.2009
comment
Спасибо, Стив, я попробую производственный код, в котором будут гораздо большие массивы. - person Dan; 01.12.2009
comment
scipy.spatial.distance невероятно медленный. Это неудивительно: это просто обычная функция Python< /а> :( - person nh2; 21.02.2014

(Сначала я думал), что вы не сможете сильно ускорить его, не переходя на 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 с массивами из 500 тыс. Элементов. (После смены карты на 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)

Некрасиво, но выходит быстрее. . .

Изменить: Попробуйте Psyco! Это ускоряет финальную версию еще в 4 раза. Как я мог забыть?

person Darius Bacon    schedule 01.12.2009
comment
приятное дополнение - рад слышать, что использование imap дает преимущество перед mul перед **2 - person Jarret Hardie; 01.12.2009
comment
Я не думаю, что это так уж уродливо :p - person John La Rooy; 01.12.2009
comment
Я был просто немного огорчен, увидев, что императивный код превосходит чисто функциональный код, который более прямо выражает проблему. - person Darius Bacon; 01.12.2009

Нет необходимости брать abs() из a[i] и b[i], если вы возводите его в квадрат.

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

Зарегистрируйтесь у оператора **2. Это упрощает его до умножения или использует общую степенную функцию (логарифм - умножить на 2 - антилогарифм).

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

person Mike Dunlavey    schedule 01.12.2009
comment
Хороший звонок... каждый из них сэкономил немного времени. - person Dan; 01.12.2009
comment
@Дэн: Добро пожаловать. Затем я бы посмотрел, поможет ли какое-то развертывание, если итератор будет стоить вам (они, как правило, так делают). Затем я бы сделал некоторую выборку стека, чтобы увидеть, вызывается ли функция больше, чем необходимо (или есть ли какая-либо другая незамеченная опухоль времени). - person Mike Dunlavey; 01.12.2009

Подобно ответу Дариуса Бэкона, я играл с оператором и 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)
person Jarret Hardie    schedule 01.12.2009

Это быстрее для массивов из более чем 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
person John La Rooy    schedule 01.12.2009

Использование кода C внутри SciPy выигрывает для длинных входных массивов. Использование простого и прямого Python выигрывает для коротких входных массивов; Лучше всего показал себя код Дариуса Бэкона, основанный на 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 начала выигрывать. Чем больше входные массивы, тем больше выигрыш. Для массивов очень короткой длины, скажем, длины 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)
person steveha    schedule 01.12.2009

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

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

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

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

person winwaed    schedule 10.01.2011

person    schedule
comment
Вы используете понимание списка внутри вызовов sum(). Это создаст список, затем sum() будет использовать список один раз, а затем список будет удален сборщиком мусора. Python имеет отличную функцию, называемую выражениями генератора, где вы можете использовать тот же синтаксис, что и понимание списка, но он создаст итератор. Если вы просто удалите [ и ] внутри ваших вызовов sum(), теперь вы будете использовать выражения генератора. Подробнее об этом читайте здесь: docs.python.org/ как/ - person steveha; 01.12.2009
comment
@steveha: зависит от длины ввода и функции. Я не знаю здесь, но str.join(..) быстрее с пониманием списка, чем genexps для короткого ввода (длина ~ 100). - person u0b34a0f6ae; 02.12.2009
comment
@kaizer.se: str.join - это особый случай, поскольку, когда у него есть список, он сначала суммирует линзы, затем создает строку общего размера и заполняет ее частями; в противном случае он строит строку очевидным образом (для части в iterable: result+= part) - person tzot; 23.12.2009