使用Python快速索引和翻转布尔数据

时间:2017-04-21 11:30:46

标签: python performance numpy bit-manipulation

使用Python,我正在运行一个模拟,在这个模拟中,物种群体经历了一系列连续的时间步骤('场景'),其中每一个都发生了灭绝。从最初的N种物种中,每次灭绝都需要选择一些幸存者,然后形成将在下一次灭绝时进行二次抽样的池。考虑到社区规模和每个物种的生存概率,每个步骤中幸存者的数量是从二项分布中随机抽取的。

以下示例显示了一系列步骤,但实际上解决方案需要能够应对分支,其中社区在一个时间步骤中存活分裂为两个单独的轨迹,每个轨道都经历自己独立的灭绝。

作为过程的草图:

1111111111111111  (Initial 16 species, choose 11 survivors)
0110110101101111  (11 species, choose 9 survivors)
0110110101100011  (9 species, choose 5 survivors)
0100100000100011  (End of simulation)

这个过程得到了很多使用,社区可能变得非常庞大,所以我试图尽可能加快速度并保持内存使用率下降。目前我有三个竞争实施

A)使用布尔numpy矩阵来存储每个时间步的活动物种。最初的动机是通过仅存储物种的存在/不存在来获得较低的内存配置文件,但numpy使用完整的字节来存储布尔值,因此这比我想象的内存效率低8倍!

import numpy as np

def using_2D_matrix(nspp=1000, nscene=250):

    # define a matrix to hold the communities and 
    # set the initial community 
    m = np.zeros((nscene, nspp), dtype='bool_')
    m[0, ] = 1

    # loop over each extinction scene, looking up the indices
    # of live species and then selecting survivors
    for i in range(0, nscene - 1):
        candidates = np.where(m[i,])[0]
        n_surv = np.random.binomial(len(candidates), 0.99)
        surv = np.random.choice(candidates, size=n_surv, replace=False)
        m[i + 1, surv] = 1

    return m

B)因此,存储一个包含幸存物种唯一指数的一维数组字典,就不再需要使用np.where。它可能具有更高的内存使用率,因为它可能需要使用uint32来存储id,但是在灭绝很高的情况下,你只需要存储一个简短的索引列表而不是整行的布尔数组,所以这将是具体案例。

def using_dict_of_arrays(nspp=1000, nscene=250):

    # initialise a dictionary holding an array giving a 
    # unique integer to each species
    m = {0: np.arange(nspp)}

    # loop over the scenes, selecting survivors
    for i in range(0, nscene - 1):
        n_surv = np.random.binomial(len(m[i]), 0.99)
        surv = np.random.choice(m[i], size=n_surv, replace=False)
        m[i + 1] = surv

    return m

其中,B比较快约10-15%。

import timeit
A = timeit.Timer(using_2D_matrix)
A.timeit(100)
# 1.6549
B = timeit.Timer(using_dictionary_of_arrays)
B.timeit(100)
# 1.3580

C)然后我考虑使用bitarray来实现这一点,将社区中物种的存在与否紧密地存储为实际位。这也可以通过使用bitops来比较社区中的重叠来提高效率。所以:

def using_bitarray(nspp=1000, nscene=250):
    # initialise the starting community
    m = {0: bitarray('1' * nspp)}

    for i in range(0, nscene):
        # pick how many die and which they are (fewer bits to swap)
        n_die = np.random.binomial(m[i].count(True), 0.01)
        unlucky = np.random.choice(m[i].search(bitarray('1')), size=n_die, replace=False)
        # clone the source community and kill some off
        m[i + 1] = bitarray(m[i])
        for s in unlucky:
            m[i + 1][s] = False

    return m

所有这些都很好,但速度要慢得多。

C = timeit.Timer(using_bitarray)
C.timeit(100)
# 2.54035

我错过了一种更快的方法吗?

3 个答案:

答案 0 :(得分:1)

这是一个非常快的替代方案:

def using_shuffled_array(nspp=1000, nscene=250):
    a = np.arange(nspp)
    np.random.shuffle(a)

    m = np.zeros(nscene, dtype=int)
    m[0] = nspp

    # loop over the scenes, selecting survivors
    for i in range(0, nscene - 1):
        m[i + 1] = np.random.binomial(m[i], 0.99)

    return a, m

不是为每一代生成一个单独的数组,而是将物种数的初始序列混洗一次,然后对于每一代,它确定有多少生存。在通话a, m = using_shuffled_array()之后,a[:m[k]]为幸存者提供k代。

这是一个时间比较:

In [487]: %timeit using_dict_of_arrays()
100 loops, best of 3: 7.93 ms per loop

In [488]: %timeit using_shuffled_array()
1000 loops, best of 3: 607 µs per loop

答案 1 :(得分:1)

您可以通过不在每一步定位和计算幸存者来加快速度。

p 成为幸存者在这一步中幸存的概率。我们不是搜索每个幸存者并用概率 p 将它们标记为灭绝,而是以概率 p 杀死所有物种,无论它们目前是否为幸存者。这是一个简短的概念证明。

import numpy as np

np.random.seed(42)

def test(nspp, nscene):
    m = np.zeros((nscene, nspp), dtype=np.uint8)
    m[0,] = 1
    for i in range(1, nscene):
        m[i] = m[i - 1] & (np.random.ranf(nspp) < 0.9)
    return m

m = test(10000, 10)
print(np.sum(m, axis=1))

<强>输出

[10000  9039  8112  7298  6558  5912  5339  4829  4388  3939]

当然,这种方法意味着你无法在每一步指定确切数量的幸存者,但希望你的模拟不需要这样做。

答案 2 :(得分:1)

前瞻性方法

循环版本以概率作为参数并处理candidates可能是空数组时的情况,我们需要退出/中断,看起来像这样 -

def using_2D_matrix(nspp=1000, nscene=250, prob=0.99):
    m = np.zeros((nscene, nspp), dtype='bool_')
    m[0, ] = 1
    for i in range(0, nscene - 1):
        candidates = np.where(m[i,])[0]
        if len(candidates)==0:
            break    
        n_surv = np.random.binomial(len(candidates), prob)
        surv = np.random.choice(candidates, size=n_surv, replace=False)
        m[i + 1, surv] = 1
    return m

现在,仔细观察,我们会发现代码基本上每行选择随机唯一元素,而后续代码则继续选择唯一元素,但只有已经为前一行选择的元素。要选择的唯一元素的数量基于概率参数prob。因此,在0.99之类的高概率下,它将为第二行选择0.99%,因为对于第一行,我们已经选择m[0, ] = 1全部。然后对于第三行,它将从第二行中选择0.99%0.99*0.99%=0.9801%,后来变为0.99^([0,1,2,3...]),依此类推。因此,模式是我们从第1行开始每行选择0.99%个元素。

我们可以利用的想法是,如果我们可以生成一个2D数组,其中每行可能的索引是随机分散的,并选择第一行的前100%元素,第一行0.9801行,第三行的第一个def vectorized_app(nspp=1000, nscene=250, prob=0.99): r = np.arange(nscene) lims = np.rint(nspp*(prob**(r))).astype(int) rands = np.random.rand(nscene, nspp).argpartition(0,axis=1) mask = lims[:,None] > np.arange(nspp) row_idx = np.repeat(r,lims) col_idx = rands[mask] out = np.zeros((nscene, nspp), dtype='bool') out[row_idx, col_idx] = 1 return out 元素,依此类推,那些将是在输出掩码数组中设置的列索引。

这就是为了给我们提供矢量化解决方案的全部想法!

实施 -

In [159]: out = vectorized_app(nspp=1000, nscene=250, prob=0.99)

In [160]: s = out.sum(1)

In [161]: s
Out[161]: 
array([1000,  990,  980,  970,  961,  951,  941,  932,  923,  914,  904,
        895,  886,  878,  869,  860,  851,  843,  835,  826,  818,  810,
        ...........................................
         88,   87,   86,   85,   84,   84,   83,   82])

示例运行 -

In [119]: %timeit using_2D_matrix(nspp=1000, nscene=250, prob=0.99)
100 loops, best of 3: 8 ms per loop

In [120]: %timeit vectorized_app(nspp=1000, nscene=250, prob=0.99)
100 loops, best of 3: 3.76 ms per loop

In [121]: 8/3.76
Out[121]: 2.127659574468085

时间和相关讨论

让我们测试一下表现 -

nspp

现在,提出的方法的瓶颈是生成随机数,特别是所需的随机数。因此,如果你使用更多的nscene和相对较小的In [143]: %timeit using_2D_matrix(nspp=10000, nscene=2500, prob=0.99) 10 loops, best of 3: 53.8 ms per loop In [144]: %timeit vectorized_app(nspp=10000, nscene=2500, prob=0.99) 1 loops, best of 3: 309 ms per loop ,循环版本正在迭代,那么矢量化方法将处于劣势 -

nscene

In [145]: %timeit using_2D_matrix(nspp=100, nscene=2500, prob=0.99) 100 loops, best of 3: 10.6 ms per loop In [146]: %timeit vectorized_app(nspp=100, nscene=2500, prob=0.99) 100 loops, best of 3: 3.24 ms per loop In [147]: %timeit using_2D_matrix(nspp=10, nscene=2500, prob=0.99) 100 loops, best of 3: 5.72 ms per loop In [148]: %timeit vectorized_app(nspp=10, nscene=2500, prob=0.99) 1000 loops, best of 3: 589 µs per loop 是一个更大的数字,结果将有利于矢量化 -

np.argpartition

吸取的经验教训

通过所经历的想法,在尝试提出建议的解决方案时,我在过程中学到的技巧是我们可以使用随机numnber生成然后使用In [149]: np.random.rand(3, 4).argpartition(0,axis=1) Out[149]: array([[3, 1, 2, 0], [0, 1, 2, 3], [1, 0, 2, 3]]) 每行创建唯一的随机数。 。以下是每行具有唯一元素的示例 -

{{1}}