使用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
我错过了一种更快的方法吗?
答案 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}}