我试图在Python中编写一个简单的更改点查找程序。下面,函数loglike(xs)返回iid正常样本xs的最大对数似然。函数most_probable_cp(xs)循环遍历x = 75%的中间点,并使用似然比找到xs中最可能的变化点。
我使用二进制分段,并且我自举以获得似然比的关键值,因此我需要数千次调用most_probable_cp()。有没有办法加快速度? Cython会有所帮助吗?我从未使用它。
import numpy as np
def loglike(xs):
n = len(xs)
mean = np.sum(xs)/n
sigSq = np.sum((xs - mean)**2)/n
return -0.5*n*np.log(2*np.pi*sigSq) - 0.5*n
def most_probable_cp(xs, left=None, right=None):
"""
Finds the most probable changepoint location and corresponding likelihood for xs[left:right]
"""
if left is None:
left = 0
if right is None:
right = len(xs)
OFFSETPCT = 0.125
MINNOBS = 12
ys = xs[left:right]
offset = min(int(len(ys)*OFFSETPCT), MINNOBS)
tLeft, tRight = left + offset, right - offset
if tRight <= tLeft:
raise ValueError("left and right are too close together.")
maxLike = -1e9
cp = None
dataLike = loglike(ys)
# Bottleneck is below.
for t in xrange(tLeft, tRight):
profLike = loglike(xs[left:t]) + loglike(xs[t:right])
lr = 2*(profLike - dataLike)
if lr > maxLike:
cp = t
maxLike = lr
return cp, maxLike
答案 0 :(得分:2)
首先,使用Numpy的标准偏差实现。这不仅会更快,而且更稳定。
def loglike(xs):
n = len(xs)
return -0.5 * n * np.log(2 * np.pi * np.std(xs)) - 0.5 * n
如果你真的想挤压毫秒,你可以使用瓶颈的nanstd
功能,因为它更快。如果您要废弃微秒,则可以将np.log
替换为math.log
,因为您只对一个数字进行操作,如果xs
是一个数组,则可以使用{{1相反。但在走这条路之前,我建议你使用这个版本,然后对结果进行分析,看看时间花在哪里。
修改强>
如果您对loglike xs.std()
进行了分析,您会发现大多数(大约80%)的时间用于计算python -m cProfile -o output yourprogram.py; runsnake output
。这是我们的第一个目标。正如我之前所说,最好的方法是使用np.std
。
bottleneck.nanstd
在我的基准测试中,它的加速比为8倍,而且只有30%的时间。 import bottleneck as bn
def loglike(xs):
n = len(xs)
return -0.5 * n * np.log(2 * np.pi * bn.nanstd(xs)) - 0.5 * n
是5%,所以没有必要进一步研究它。用他们的数学对象替换np.log和np.pi,并采用公因子我可以将时间再缩短一半。
len
我可以稍微提高10%的可读性:
return -0.5 * n * (math.log(2 * math.pi * bn.nanstd(xs)) - 1)
修改2
如果您想真正推送它,可以将bn.nanstd替换为专用功能。在循环之前,定义factor = math.log(2*math.pi)
def loglike(xs):
n = len(xs)
return -0.5 * n * (factor + math.log(bn.nanstd(xs)) - 1)
并使用它代替bn.nanstd,或者如果您不打算更改dtype,则使用std, _ = bn.func.nansum_selector(xs, axis=0)
。
我认为这和Python一样快。尽管如此,有一半的时间花在数字操作上,也许Cython可以对此进行优化,但是随后调用Python会增加开销,从而弥补这一点。
答案 1 :(得分:1)
我将做出一些基本的改变:目前,你在每次迭代时重新计算每个分区的平方和,平均值和计数,这使得这种“交叉验证风格”算法是二次的。
你可以做的是利用半群结构,并在更改分区时以在线方式计算每个元素的平方值,计数和均值 - 基本上融合了np.sum中的隐式循环。然后你取-0.5*n*np.log(2*np.pi*sigSq) - 0.5*n
并根据n,mean和sigSq的更新值来计算(你需要从平方值之和计算stddev)。
与np.std相比,这将渐进地加速您的代码,并以数字稳定性的潜在成本节省一些函数调用。你可能需要kahan求和。
如果你不需要实际的对数似然,你可以只关注最大化proflike
,然后在循环外计算lr
,节省一些倍数 - (你可以折叠2 *如果你做一些基本的代数,那么函数中的0.5 *就可以了。
这项技术是HLearn背后的技术及其在交叉验证方面的高性能。
编辑:一些代码可能比其他代码快得多。这里最大的成本是迭代开销和np.log调用。可能存在一些fencepost错误,但这里有要点:
rcnt = n
rsum = np.sum(arr)
rssq = np.sum(arr**2)
lcnt = 0
lsum = 0
lssq = 0
maxlike = -1e9 # or -Inf ideally
cp = -1
for i in arr: # arr is length n
lcnt += 1
lsum += i
lssq += i*i
lmean = lsum/lcnt
lvar = ((lssq - lmean**2)/(lcnt))
loglike_l = lcnt*np.log(2*np.pi*lvar) - lcnt
rcnt -= 1
rsum -= i
rssq -= i*i
rmean = rsum/rcnt
rvar = ((rssq - rmean**2)/(rcnt))
loglike_r = rcnt*np.log(2*np.pi*rvar) - rcnt
loglike_total = loglike_l + loglike_r
if maxlike < loglike_total:
cp = lcnt
maxlike = loglike_total