优化变换点测试的循环

时间:2014-03-31 13:47:32

标签: python numpy statistics

我试图在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

2 个答案:

答案 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