2017-09-01 78 views
7

我的代码调用许多“差分函数”来计算“Yin algorithm”(基频提取器)。优化“差分函数”的计算

的不同功能(在本文EQ 6)被定义为:

enter image description here

这是我实现的不同功能:

def differenceFunction(x, W, tau_max): 
    df = [0] * tau_max 
    for tau in range(1, tau_max): 
     for j in range(0, W - tau): 
      tmp = long(x[j] - x[j + tau]) 
      df[tau] += tmp * tmp 
    return df 

例如有:

x = np.random.randint(0, high=32000, size=2048, dtype='int16') 
W = 2048 
tau_max = 106 
differenceFunction(x, W, tau_max) 

有没有一种方法来优化这个双循环补偿utation(只有python,最好没有其他库,而不是numpy)?

编辑:改变的代码,以避免指数误差(j循环,@Elliot回答)

EDIT2:改变的代码,以用x [0](j循环,@hynekcer评语)

+1

你想更快的方法吗?或者使用矩阵寻找矢量化解决方案。 – Dark

+1

我要找使用python – PatriceG

+3

一种方式最快的方法,如果你想保留的for循环,并获得atmost速度使用'numba'。你是否使用其他库? – Dark

回答

6

编辑:改进的速度,以220微秒 - 看到在端编辑 - 直接版本

所需计算可以通过Autocorrelation function或类似地通过卷积能够容易地评价。维纳 - 钦钦定理允许用两个快速傅立叶变换(FFT)计算自相关,其时间复杂度为O(n  log  n)。 我使用来自Scipy包的加密卷积函数fftconvolve。一个好处是,这里很容易解释为什么它的工作原理。一切都是矢量化的,在Python解释器级别没有循环。

from scipy.signal import fftconvolve 

def difference_by_convol(x, W, tau_max): 
    x = np.array(x, np.float64) 
    w = x.size 
    x_cumsum = np.concatenate((np.array([0.]), (x * x).cumsum())) 
    conv = fftconvolve(x, x[::-1]) 
    df = x_cumsum[w:0:-1] + x_cumsum[w] - x_cumsum[:w] - 2 * conv[w - 1:] 
    return df[:tau_max + 1] 
  • differenceFunction_1loop功能Elliot's answer相比:它是FFT更快:430  微秒相比原来的1170微秒。它的启动速度大约为tau_max >= 40。数值精度很高。与精确整数结果相比,最高相对误差小于1E-14。 (因此它可以很容易地四舍五入到精确的长整数解决方案。)
  • 参数tau_max对算法不重要。它最终只限制输出。索引0处的零元素被添加到输出中,因为索引应该在Python中以0开头。
  • 参数W在Python中不重要。尺寸更好地被内省。
  • 数据最初转换为np.float64以防止重复转换。它快了一半。任何小于np.int64的类型都会因为溢出而无法接受。
  • 所需的差分函数是双能量减自相关函数。这可以通过卷积来评估:correlate(x, x) = convolve(x, reversed(x)
  • “自从Scipy v0.19正常convolve自动选择这种方法或基于估计速度更快的直接方法。”这种启发式方法不适用于这种情况,因为卷积比tau_max估计得多得多,并且它必须比直接方法快得多。
  • 它也可以通过Nippy的ftp模块来计算,不需要Scipy,通过将Python的答案Calculate autocorrelation using FFT in matlab重写(最后在下面)。我认为上面的解决方案可以更容易理解。

证明:(用于Pythonistas :-)

原始幼稚的做法可以写为:

df = [sum((x[j] - x[j + t]) ** 2 for j in range(w - t)) for t in range(tau_max + 1)] 

其中tau_max < w

导出由规则(a - b)**2 == a**2 + b**2 - 2 * a * b

df = [ sum(x[j] ** 2 for j in range(w - t)) 
     + sum(x[j] ** 2 for j in range(t, w)) 
     - 2 * sum(x[j] * x[j + t] for j in range(w - t)) 
     for t in range(tau_max + 1)] 

替代用的x_cumsum = [sum(x[j] ** 2 for j in range(i)) for i in range(w + 1)]帮助,可以在线性时间可以容易地计算前两个元素。替代sum(x[j] * x[j + t] for j in range(w - t))卷积conv = convolvefft(x, reversed(x), mode='full'),其输出大小为len(x) + len(x) - 1

df = [x_cumsum[w - t] + x_cumsum[w] - x_cumsum[t] 
     - 2 * convolve(x, x[::-1])[w - 1 + t] 
     for t in range(tau_max + 1)] 

优化由向量的表达式:通过numpy的FFT实施溶液直接:

df = x_cumsum[w:0:-1] + x_cumsum[w] - x_cumsum[:w] - 2 * conv[w - 1:] 

每一步也可以测试并比较由测试数据数值


EDIT。

def difference_fft(x, W, tau_max): 
    x = np.array(x, np.float64) 
    w = x.size 
    tau_max = min(tau_max, w) 
    x_cumsum = np.concatenate((np.array([0.]), (x * x).cumsum())) 
    size = w + tau_max 
    p2 = (size // 32).bit_length() 
    nice_numbers = (16, 18, 20, 24, 25, 27, 30, 32) 
    size_pad = min(x * 2 ** p2 for x in nice_numbers if x * 2 ** p2 >= size) 
    fc = np.fft.rfft(x, size_pad) 
    conv = np.fft.irfft(fc * fc.conjugate())[:tau_max] 
    return x_cumsum[w:w - tau_max:-1] + x_cumsum[w] - x_cumsum[:tau_max] - 2 * conv 

它的两倍多比我以前的解决方案更快,因为卷积的长度W + tau_max后仅限于小素因子最近的“好”数量,而不是评价全2 * W。也不需要像使用`fftconvolve(x,reversed(x))那样对相同的数据进行两次转换。

In [211]: %timeit differenceFunction_1loop(x, W, tau_max) 
1.1 ms ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) 

In [212]: %timeit difference_by_convol(x, W, tau_max) 
431 µs ± 5.69 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) 

In [213]: %timeit difference_fft(x, W, tau_max) 
218 µs ± 685 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each) 

最新解决方案比艾略特的difference_by_convol为tau_max> = 20更快这比不依赖由于间接费用的类似比率多的数据大小。

+0

谢谢。我编辑了代码。 – PatriceG

+0

@PatriceGuyot增加了两倍更快的方法。 – hynekcer

0

我不知道如何找到替代你的嵌套循环问题,但对于算术函数,你可以使用numpy库。它比手动操作更快。

import numpy as np 
tmp = np.subtract(long(x[j] ,x[j + tau]) 
0

我会做这样的事情:

>>> x = np.random.randint(0, high=32000, size=2048, dtype='int16') 
>>> tau_max = 106 
>>> res = np.square((x[tau_max:] - x[:-tau_max])) 

但是我相信这是不是这样做的最快方式。

6

首先,你应该考虑数组的边界。你原来编写的代码将得到一个IndexError。 您可以通过矢量化内环

import numpy as np 

# original version 
def differenceFunction_2loop(x, W, tau_max): 
    df = np.zeros(tau_max, np.long) 
    for tau in range(1, tau_max): 
     for j in range(0, W - tau): # -tau eliminates the IndexError 
      tmp = np.long(x[j] -x[j + tau]) 
      df[tau] += np.square(tmp) 
    return df 

# vectorized inner loop 
def differenceFunction_1loop(x, W, tau_max): 
    df = np.zeros(tau_max, np.long) 
    for tau in range(1, tau_max): 
     tmp = (x[:-tau]) - (x[tau:]).astype(np.long) 
     df[tau] = np.dot(tmp, tmp) 
    return df 

x = np.random.randint(0, high=32000, size=2048, dtype='int16') 
W = 2048 
tau_max = 106 
twoloop = differenceFunction_2loop(x, W, tau_max) 
oneloop = differenceFunction_1loop(x, W, tau_max) 

# confirm that the result comes out the same. 
print(np.all(twoloop == oneloop)) 
# True 

现在对于一些基准获得有关显著加速。在ipython我得到以下

In [103]: %timeit twoloop = differenceFunction_2loop(x, W, tau_max) 
1 loop, best of 3: 2.35 s per loop 

In [104]: %timeit oneloop = differenceFunction_1loop(x, W, tau_max) 
100 loops, best of 3: 8.23 ms per loop 

所以,大约300倍加速。

+0

注意:矢量化是一种技术,当CPUs同时处理数据片段时。消除范围检查称为迭代分裂。 – egorlitvinenko

+1

@egorlitvinenko有更多的机制,为什么在载体numpy的操作是这样不是在解释语言中的循环速度更快。其中一小部分是在某些情况下使用的向量寄存器上的SSE指令(流式SIMD扩展(单指令多数据))。上面有用的简单术语[矢量化](https://en.wikipedia.org/wiki/Vectorization)对应于维基百科。 – hynekcer

+0

@hynekcer我会看。谢谢你的解释。 – egorlitvinenko

1

在相对优化的算法可以用numba.jit优化解释:

import timeit 

import numpy as np 
from numba import jit 


def differenceFunction(x, W, tau_max): 
    df = [0] * tau_max 
    for tau in range(1, tau_max): 
     for j in range(0, W - tau): 
      tmp = int(x[j] - x[j + tau]) 
      df[tau] += tmp * tmp 
    return df 


@jit 
def differenceFunction2(x, W, tau_max): 
    df = np.ndarray(shape=(tau_max,)) 
    for tau in range(1, tau_max): 
     for j in range(0, W - tau): 
      tmp = int(x[j] - x[j + tau]) 
      df[tau] += tmp * tmp 
    return df 


x = np.random.randint(0, high=32000, size=2048, dtype='int16') 
W = 2048 
tau_max = 106 
differenceFunction(x, W, tau_max) 


print('old', 
     timeit.timeit('differenceFunction(x, W, tau_max)', 'from __main__ import differenceFunction, x, W, tau_max', 
        number=20)/20) 
print('new', 
     timeit.timeit('differenceFunction2(x, W, tau_max)', 'from __main__ import differenceFunction2, x, W, tau_max', 
        number=20)/20) 

结果:

old 0.18265145074453273 
new 0.016223197058214667 

您可以更好的结果结合算法的优化和numba.jit

1

下面是使用列表理解另一种方法。它大约不到原始功能所用时间的十分之一,但不会超过Elliot's answer。无论如何,只要把它放在那里。

import numpy as np 
import time 

# original version 
def differenceFunction_2loop(x, W, tau_max): 
    df = np.zeros(tau_max, np.long) 
    for tau in range(1, tau_max): 
     for j in range(0, W - tau): # -tau eliminates the IndexError 
      tmp = np.long(x[j] -x[j + tau]) 
      df[tau] += np.square(tmp) 
    return df 

# vectorized inner loop 
def differenceFunction_1loop(x, W, tau_max): 
    df = np.zeros(tau_max, np.long) 
    for tau in range(1, tau_max): 
     tmp = (x[:-tau]) - (x[tau:]).astype(np.long) 
     df[tau] = np.dot(tmp, tmp) 
    return df 

# with list comprehension 
def differenceFunction_1loop_listcomp(x, W, tau_max): 
    df = [sum(((x[:-tau]) - (x[tau:]).astype(np.long))**2) for tau in range(1, tau_max)] 
    return [0] + df[:] 

x = np.random.randint(0, high=32000, size=2048, dtype='int16') 
W = 2048 
tau_max = 106 

s = time.clock() 
twoloop = differenceFunction_2loop(x, W, tau_max) 
print(time.clock() - s) 

s = time.clock() 
oneloop = differenceFunction_1loop(x, W, tau_max) 
print(time.clock() - s) 

s = time.clock() 
listcomprehension = differenceFunction_1loop_listcomp(x, W, tau_max) 
print(time.clock() - s) 

# confirm that the result comes out the same. 
print(np.all(twoloop == listcomprehension)) 
# True 

性能测试结果(大约):

differenceFunction_2loop() = 0.47s 
differenceFunction_1loop() = 0.003s 
differenceFunction_1loop_listcomp() = 0.033s