Numpy Pure Functions para rendimiento, almacenamiento en caché

Estoy escribiendo algunos códigos críticos de rendimiento moderado en números. Este código estará en el bucle más interno, de un cálculo que el tiempo de ejecución se mide en horas. Un cálculo rápido sugiere que este código se ejecutará algo así como 10 ^ 12 veces, en algunas variaciones del cálculo.

Así que la función es calcular sigmoide (X) y otra para calcular su derivada (gradiente). Sigmoide tiene la propiedad que para
y = sigmoide (x), dy / dx = y (1-y)
En python para numpy esto se ve así:

sigmoid = vectorize(lambda(x): 1.0/(1.0+exp(-x))) grad_sigmoid = vectorize(lambda (x): sigmoid(x)*(1-sigmoid(x))) 

Como se puede ver, ambas funciones son puras (sin efectos secundarios), por lo que son candidatas ideales para la memoria, al menos a corto plazo, tengo algunas preocupaciones sobre el almacenamiento en caché de cada llamada al sigmoide que se haya realizado: Almacenamiento de 10 ^ 12 flotadores que Tomaría varios terabytes de la memoria RAM.

¿Hay una buena manera de optimizar esto?
¿Python detectará que estas son funciones puras y las almacenará en caché para mí, según corresponda?
¿Me estoy preocupando por nada?

Estas funciones ya existen en scipy. La función sigmoide está disponible como scipy.special.expit .

 In [36]: from scipy.special import expit 

Compare expit con la función sigmoide vectorizada:

 In [38]: x = np.linspace(-6, 6, 1001) In [39]: %timeit y = sigmoid(x) 100 loops, best of 3: 2.4 ms per loop In [40]: %timeit y = expit(x) 10000 loops, best of 3: 20.6 µs per loop 

expit también es más rápido que implementar la fórmula usted mismo:

 In [41]: %timeit y = 1.0 / (1.0 + np.exp(-x)) 10000 loops, best of 3: 27 µs per loop 

El CDF de la distribución logística es la función sigmoide. Está disponible como el método cdf de scipy.stats.logistic , pero cdf finalmente llama expit , por lo que no tiene sentido usar ese método. Puede usar el método pdf para calcular la derivada de la función sigmoidea, o el método _pdf que tiene menos sobrecarga, pero “rodar los suyos” es más rápido:

 In [44]: def sigmoid_grad(x): ....: ex = np.exp(-x) ....: y = ex / (1 + ex)**2 ....: return y 

Tiempo (x tiene longitud 1001):

 In [45]: from scipy.stats import logistic In [46]: %timeit y = logistic._pdf(x) 10000 loops, best of 3: 73.8 µs per loop In [47]: %timeit y = sigmoid_grad(x) 10000 loops, best of 3: 29.7 µs per loop 

Tenga cuidado con su implementación si va a utilizar valores que están muy lejos de la cola. La función exponencial puede desbordarse bastante fácilmente. logistic._cdf es un poco más robusto que mi implementación rápida de sigmoid_grad :

 In [60]: sigmoid_grad(-500) /home/warren/anaconda/bin/ipython:3: RuntimeWarning: overflow encountered in double_scalars import sys Out[60]: 0.0 In [61]: logistic._pdf(-500) Out[61]: 7.1245764067412855e-218 

Una implementación que usa sech**2 ( 1/cosh**2 ) es un poco más lenta que la anterior sigmoid_grad :

 In [101]: def sigmoid_grad_sech2(x): .....: y = (0.5 / np.cosh(0.5*x))**2 .....: return y .....: In [102]: %timeit y = sigmoid_grad_sech2(x) 10000 loops, best of 3: 34 µs per loop 

Pero maneja mejor las colas:

 In [103]: sigmoid_grad_sech2(-500) Out[103]: 7.1245764067412855e-218 In [104]: sigmoid_grad_sech2(500) Out[104]: 7.1245764067412855e-218 

Solo ampliando mi comentario, aquí hay una comparación entre su sigmoide a través de vectorize y usar numpy directamente:

 In [1]: x = np.random.normal(size=10000) In [2]: sigmoid = np.vectorize(lambda x: 1.0 / (1.0 + np.exp(-x))) In [3]: %timeit sigmoid(x) 10 loops, best of 3: 63.3 ms per loop In [4]: %timeit 1.0 / (1.0 + np.exp(-x)) 1000 loops, best of 3: 250 us per loop 

Como puede ver, la vectorize no solo lo hace mucho más lento, sino que puede calcular 10000 sigmoides en 250 microsegundos (es decir, 25 nanosegundos para cada uno). Una sola búsqueda de diccionario en Python es más lenta que eso, y mucho menos el rest del código para tener la memoria en su lugar.

La única forma de optimizar esto que se me ocurra es escribir un ufunc sigmoide para numpy, que básicamente implementará la operación en C. De esa manera, no tendrá que hacer cada operación en el sigmoide a toda la matriz, aunque Numpy hace esto muy rápido.

Si desea memorizar este proceso, envolvería ese código en una función y decoraría con functools.lru_cache(maxsize=n) . Experimente con el valor de maxsize máximo para encontrar el tamaño adecuado para su aplicación. Para obtener los mejores resultados, use un argumento de maxsize que sea una potencia de dos.

 from functools import lru_cache lru_cache(maxsize=8096) def sigmoids(x): sigmoid = vectorize(lambda(x): 1.0/(1.0+exp(-x))) grad_sigmoid = vectorize(lambda (x): sigmoid(x)*(1-sigmoid(x))) return sigmoid, grad_sigmoid 

Si está en la versión 2.7 (que espero que esté ya que está usando numpy), puede consultar https://pypi.python.org/pypi/repoze.lru/ para obtener una biblioteca de memorias con una syntax idéntica.

Puede instalarlo a través de pip: pip install repoze.lru

 from repoze.lru import lru_cache lru_cache(maxsize=8096) def sigmoids(x): sigmoid = vectorize(lambda(x): 1.0/(1.0+exp(-x))) grad_sigmoid = vectorize(lambda (x): sigmoid(x)*(1-sigmoid(x))) return sigmoid, grad_sigmoid 

Sobre todo estoy de acuerdo con Warren Weckesser y su respuesta anterior . Pero para el derivado de sigmoide se puede usar lo siguiente:

 In [002]: def sg(x): ...: s = scipy.special.expit(x) ...: return s * (1.0 - s) 

Tiempos:

 In [003]: %timeit y = logistic._pdf(x) 10000 loops, best of 3: 45 µs per loop In [004]: %timeit y = sg(x) 10000 loops, best of 3: 20.4 µs per loop 

El único problema es la precisión:

 In [005]: sg(37) Out[005]: 0.0 In [006]: logistic._pdf(37) Out[006]: 8.5330476257440658e-17