NumPy纯函数用于提高性能和缓存

13

我正在使用numpy编写一些性能较为关键的代码。 这段代码将位于计算过程的最内层循环中,其运行时间以小时为单位。 一个快速的计算表明,在某些计算变体中,该代码将被执行大约10^12次。

因此,这个函数的作用是计算sigmoid(X),另一个函数则计算其导数(梯度)。 Sigmoid具有以下属性:对于
y=sigmoid(x), dy/dx= y(1-y)
在Python的numpy中,它看起来像:

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

可以看出,这两个函数都是纯函数(没有副作用),因此它们是记忆化的理想候选项, 至少在短期内,我对缓存每次调用sigmoid的担忧:存储10 ^ 12个浮点数需要数千兆字节的RAM。

有没有好的方法来优化这个问题?
Python会自动识别这些是纯函数并适当地为其缓存吗?
我是不是在无谓地担心?


7
请注意,1/(1+np.exp(-x)) 已经能够接受 numpy 数组作为输入,而且非常快(我猜测记忆化不会有任何帮助)。使用 vectorize 会使它变慢很多,因为它是使用缓慢的 for 循环实现的。 - Gustav Larsson
x的大小是多少? - M4rtini
@M4rtini,变量不确定,但是我一直假设在我的经验法则计算中,x通常是长度为100的向量。 - Frames Catherine White
4个回答

34

这些函数已经存在于scipy中。Sigmoid函数可以使用scipy.special.expit进行调用。

In [36]: from scipy.special import expit

expit 与矢量化的 sigmoid 函数进行比较:

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比自己实现公式更快:

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

逻辑分布的累积分布函数是S形函数。该函数可通过scipy.stats.logistic库的cdf方法获得,但cdf实际上会调用expit方法,因此使用该方法没有意义。您可以使用pdf方法计算S形函数的导数,或使用开销较小的_pdf方法,但自己编写方法速度更快:

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

时间 (x 的长度为 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

如果你要使用在分布尾部值较大的数据,请小心实现。指数函数很容易出现溢出情况。相比我快速实现的sigmoid_gradlogistic._cdf更加健壮。

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

使用 sech**2 (1/cosh**2) 实现的速度比上述的 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

但它更好地处理了尾巴:

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

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

当你说“你可以使用pdf方法来计算Sigmoid函数的导数,或者_pdf方法,它具有更少的开销”时,_overhead_确切地是什么意思?你是在暗示受保护的方法更快,因为它经过的代码较少吗? - Nicholas
1
@Nicholas 是的,._pdf() 对参数的错误检查较少。它也不使用分布的 loc(位置)和 scale(比例)参数——这些在 .pdf() 方法中处理。.pdf() 最终调用 ._pdf() 来进行实际计算。 - Warren Weckesser

5

我继续评论一下,这里是您通过vectorize使用sigmoid函数和直接使用numpy的比较:

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

正如您所看到的,不仅是vectorize使速度变慢了,事实上,您可以在250微秒内计算10000个sigmoid(即每个sigmoid只需25纳秒)。在Python中,单个字典查找的速度都比这慢,更不用说所有其他代码来实现备忘录了。
我能想到的唯一优化方法是为numpy编写一个sigmoidufunc,这基本上将使用C实现操作。这样,您就不必对整个数组执行每个sigmoid操作,尽管numpy确实非常快。

我已经用现有的训练RBM的代码进行了测试: 向量化:1个循环,3次中最佳时间为5.44秒 非向量化:1个循环,3次中最佳时间为4.75秒 使用def而不是lambda的非向量化:1个循环,3次中最佳时间为4.53秒 虽然时间只进行了一次循环,但这些数字并不是非常具体,但我认为它们是有指示性的。 因此,对于如此微小的更改,速度得到了显著提升。 - Frames Catherine White

1
如果您想进行备忘录优化,我建议将该代码放入一个函数中,并使用functools.lru_cache(maxsize=n)进行修饰。 通过尝试不同的maxsize值来找到适合您应用程序的大小。 为了获得最佳结果,请使用2的幂作为maxsize参数。
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

如果您正在使用2.7版本的Python(我认为你是因为你正在使用numpy),您可以查看https://pypi.python.org/pypi/repoze.lru/,这是一个具有相同语法的记忆库。
您可以通过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

我想补充一下,functools.lru_cache 是在 Python 3.2 中引入的 :) - Andrey Sobolev
好的,我已经在我的答案中添加了一个2.7版本的替代方案。 - Madison May

0

大多数情况下,我同意Warren Weckesser和他的答案above。 但是对于sigmoid的导数,可以使用以下方法:

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

时间:

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

唯一的问题是准确性:

In [005]: sg(37)
Out[005]: 0.0

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接