Método optimizado para calcular la distancia del coseno en Python
-
22-07-2019 - |
Pregunta
Escribí un método para calcular la distancia del coseno entre dos matrices:
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
Ejecutarlo puede ser muy lento en una gran matriz. ¿Existe una versión optimizada de este método que se ejecute más rápido?
Actualización: He probado todas las sugerencias hasta la fecha, incluso scipy. Aquí está la versión para vencer, incorporando sugerencias de Mike y 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
Solución
Si puede usar SciPy, puede usar coseno
de spatial.distance
:
http://docs.scipy.org/doc/scipy/reference/spatial.distance.html
Si no puede usar SciPy, puede intentar obtener una pequeña aceleración reescribiendo su Python (EDITAR: pero no funcionó como pensaba, ver más abajo).
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
Es mejor generar una excepción cuando las longitudes de ayb no coinciden.
Al usar expresiones generadoras dentro de las llamadas a sum ()
puede calcular sus valores con la mayor parte del trabajo realizado por el código C dentro de Python. Esto debería ser más rápido que usar un bucle for
.
No he cronometrado esto, así que no puedo adivinar qué tan rápido podría ser. Pero es casi seguro que el código SciPy está escrito en C o C ++ y debería ser lo más rápido posible.
Si está haciendo bioinformática en Python, debería utilizar SciPy de todos modos.
EDITAR: Darius Bacon cronometró mi código y lo encontró más lento. Así que cronometré mi código y ... sí, es más lento. La lección para todos: cuando intente acelerar las cosas, no adivine, mida.
Estoy desconcertado sobre por qué mi intento de poner más trabajo en los componentes internos de Python es más lento. Lo probé para listas de longitud 1000 y todavía era más lento.
No puedo pasar más tiempo intentando piratear Python de manera inteligente. Si necesita más velocidad, le sugiero que pruebe SciPy.
EDITAR: acabo de probar a mano, sin tiempo. Encuentro que para abreviar ayb, el código antiguo es más rápido; para largo ayb, el nuevo código es más rápido; en ambos casos la diferencia no es grande. (Ahora me pregunto si puedo confiar en timeit en mi computadora con Windows; quiero probar esta prueba nuevamente en Linux). No cambiaría el código de trabajo para intentar hacerlo más rápido. Y una vez más te insto a que pruebes SciPy. :-)
Otros consejos
(originalmente pensé) no vas a acelerarlo mucho sin romper a C (como numpy o scipy) o cambiar lo que calculas. Pero así es como lo intentaría, de todos modos:
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))))
Es aproximadamente el doble de rápido en Python 2.6 con matrices de 500k elementos. (Después de cambiar el mapa a imap, siguiendo a Jarret Hardie).
Aquí hay una versión modificada del código revisado del póster original:
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)
Es feo, pero sale más rápido. . .
Editar: ¡Y pruebe Psyco ! Acelera la versión final por otro factor de 4. ¿Cómo podría olvidarlo?
No es necesario tomar abs ()
de a [i]
y b [i]
si lo cuadra.
Almacene a [i]
y b [i]
en variables temporales, para evitar la indexación más de una vez.
Tal vez el compilador pueda optimizar esto, pero tal vez no.
Regístrese en el operador ** 2
. ¿Lo está simplificando en una multiplicación, o está utilizando una función de potencia general (log - multiplicar por 2 - antilog)?
No hagas sqrt dos veces (aunque el costo es pequeño). Haga sqrt (denoma * denomb)
.
Similar a la respuesta de Darius Bacon, he estado jugando con el operador y las herramientas de iterto para producir una respuesta más rápida. Lo siguiente parece ser 1/3 más rápido en una matriz de 500 elementos según 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)
Esto es más rápido para conjuntos de más de 1000 elementos.
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
El uso del código C dentro de SciPy gana mucho para las matrices de entrada largas. Usando Python simple y directo gana para arreglos de entrada cortos; El código basado en izip ()
de Darius Bacon se comparó mejor. Por lo tanto, la solución final es decidir cuál usar en tiempo de ejecución, en función de la longitud de las matrices de entrada:
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)
Hice un arnés de prueba que probó las funciones con diferentes entradas de longitud, y descubrí que alrededor de la longitud 200 la función SciPy comenzó a ganar. Cuanto más grandes son las matrices de entrada, más grande gana. Para matrices de longitud muy corta, digamos longitud 3, gana el código más simple. Esta función agrega una pequeña cantidad de sobrecarga para decidir de qué manera hacerlo, luego lo hace de la mejor manera.
En caso de estar interesado, aquí está el arnés de prueba:
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
Su solución actualizada todavía tiene dos raíces cuadradas. Puede reducir esto a uno reemplazando la línea sqrt con:
resultado = 1 - numerador / (sqrt (denoma * denomb))
Una multiplicación suele ser bastante más rápida que un sqrt. Puede que no parezca mucho, ya que solo se llama una vez en la función, pero parece que está calculando muchas distancias de coseno, por lo que la mejora se sumará.
Parece que su código debería estar maduro para optimizaciones vectoriales. Entonces, si el soporte multiplataforma no es un problema y desea acelerarlo aún más, puede codificar el código de distancia cosenoidal en C y asegurarse de que su compilador vectorice agresivamente el código resultante (incluso Pentium II es capaz de alguna vectorización de punto flotante )