适用于非常大范围的高效随机生成器(在python中)

时间:2018-04-21 14:38:47

标签: python performance generator shuffle

我正在尝试创建一个生成器,该生成器返回给定范围内的数字,该数字通过函数foo给出的特定测试。但是我希望这些数字以随机顺序进行测试。以下代码将实现此目的:

from random import shuffle

def MyGenerator(foo, num):
    order = list(range(num))
    shuffle(order)
    for i in order:
        if foo(i):
            yield i

问题

此解决方案的问题在于,有时范围会非常大(num可能是10**8及以上的顺序)。在内存中有这么大的列表时,这个函数会变慢。我试图通过以下代码避免此问题:

from random import randint    

def MyGenerator(foo, num):
    tried = set()
    while len(tried) <= num - 1:
        i = randint(0, num-1)
        if i in tried:
            continue
        tried.add(i)
        if foo(i):
            yield i

这在大多数情况下效果很好,因为在大多数情况下num会非常大,foo会传递合理数量的数字以及__next__方法的总次数将被称为相对较小(例如,最多200通常小得多)。因此,我们可能会偶然发现通过foo测试的值并且tried的大小永远不会变大。 (即使它只通过了10%的时间,我们也不希望tried大致超过2000左右。)

但是,当num较小时(接近调用__next__方法的次数,或foo大部分时间失败,上述解决方案效率非常低 - 随机猜测数字,直到它猜到一个不在tried中的数字。

我尝试过的解决方案......

我希望使用某种函数将数字0,1,2,..., n以大致随机的方式映射到自己身上。 (这不是用于任何安全目的,因此如果它不是世界上最“随机”的功能则无关紧要)。这里的函数(Create a random bijective function which has same domain and range)将带符号的32位整数映射到自身,但我不确定如何使映射适应较小的范围。给定num我甚至不需要0,1,..num上的双关,只需要n大于num的值{并且'接近'{使用你认为合适的关闭的任何定义)。然后我可以做以下事情:

def mix_function_factory(num):
    # something here???
    def foo(index):
        # something else here??
    return foo

def MyGenerator(foo, num):
    mix_function = mix_function_factory(num):
    for i in range(num):
        index = mix_function(i)
        if index <= num:
            if foo(index):
                yield index

(只要广告不在大于num的一组数字上,index <= num不为真的次数就会很小。

我的问题

您能想到以下其中一项:

  • mix_function_factorymix_function的其他一些潜在函数的潜在解决方案,我可以尝试针对num的不同值进行推广?
  • 解决原始问题的更好方法是什么?

非常感谢...

3 个答案:

答案 0 :(得分:8)

问题基本上是在0..n-1范围内生成整数的随机排列。

幸运的是,这些数字具有非常有用的属性:它们都具有模数n的明确值。如果我们可以对这些数字应用一些数学运算,同时注意保持每个数字的模数n不同,那么很容易生成出现随机的排列。最好的部分是我们不需要任何记忆来跟踪我们已经生成的数字,因为每个数字都是用一个简单的公式计算的。

我们可以对范围中的每个数字x执行的操作示例包括:

  • 添加:我们可以将任何整数c添加到x
  • 乘法:我们可以将x乘以与m不共享素数因子的任何数字n

0..n-1范围内仅应用这两项操作已经取得了相当令人满意的结果:

>>> n = 7
>>> c = 1
>>> m = 3
>>> [((x+c) * m) % n for x in range(n)]
[3, 6, 2, 5, 1, 4, 0]

看起来是随机的,不是吗?

如果我们从随机数生成cm,它实际上随机。但请记住,无法保证此算法将生成所有可能的排列,或者每个排列具有相同的生成概率。

实施

关于实现的困难部分实际上只是生成一个合适的随机m。我使用this answer中的素数分解代码来执行此操作。

import random

# credit for prime factorization code goes
# to https://stackoverflow.com/a/17000452/1222951
def prime_factors(n):
    gaps = [1,2,2,4,2,4,2,4,6,2,6]
    length, cycle = 11, 3
    f, fs, next_ = 2, [], 0
    while f * f <= n:
        while n % f == 0:
            fs.append(f)
            n /= f
        f += gaps[next_]
        next_ += 1
        if next_ == length:
            next_ = cycle
    if n > 1: fs.append(n)
    return fs

def generate_c_and_m(n, seed=None):
    # we need to know n's prime factors to find a suitable multiplier m
    p_factors = set(prime_factors(n))

    def is_valid_multiplier(m):
        # m must not share any prime factors with n
        factors = prime_factors(m)
        return not p_factors.intersection(factors)

    # if no seed was given, generate random values for c and m
    if seed is None:
        c = random.randint(n)
        m = random.randint(1, 2*n)
    else:
        c = seed
        m = seed

    # make sure m is valid
    while not is_valid_multiplier(m):
        m += 1

    return c, m

既然我们可以为cm生成合适的值,那么创建排列是微不足道的:

def random_range(n, seed=None):
    c, m = generate_c_and_m(n, seed)

    for x in range(n):
        yield ((x + c) * m) % n

您的生成器功能可以实现为

def MyGenerator(foo, num):
    for x in random_range(num):
        if foo(x):
            yield x

答案 1 :(得分:3)

这可能是最佳算法依赖于 num的情况,那么为什么不使用包含在一个生成器中的2个可选算法呢?

您可以将shuffleset个解决方案与num的值的阈值混合使用。这基本上是在一台发电机中组装你的第一个解决方案:

from random import shuffle,randint

def MyGenerator(foo, num):
    if num < 100000 # has to be adjusted by experiments
      order = list(range(num))
      shuffle(order)
      for i in order:
          if foo(i):
              yield i
    else:   # big values, few collisions with random generator 
      tried = set()
      while len(tried) < num:
        i = randint(0, num-1)
        if i in tried:
           continue
        tried.add(i)
        if foo(i):
           yield i

randint解决方案(对于num的大值)效果很好,因为随机生成器中没有那么多重复。

答案 2 :(得分:1)

在Python中获得最佳性能比在低级语言中要复杂得多。例如,在C中,通过将乘法替换为移位,您通常可以在热内循环中保存一点。 python字节码定向的开销会消除这种情况。当然,当你考虑&#34; python&#34;的哪个变体时,这会再次改变 。你正在瞄准(pypy?numpy?cython?) - 你真的必须根据你正在使用的代码来编写你的代码。

但更重要的是安排操作以避免序列化依赖,因为所有CPU现在都超标量化。当然,真正的编译器知道这一点,但是当选择算法时,它仍然很重要。

通过使用numpy.arange()生成数字并将((x + c) * m) % n直接应用于numpy ndarray,最简单的方法之一是获得现有答案。每个可以避免的python级循环都有帮助。

如果该函数可以直接应用于numpy ndarrays,那可能会更好。当然,python中一个足够小的函数无论如何都将由函数调用开销占主导地位。

今天最好的快速随机数发生器是PCG。我写了一个纯粹的python端口here,但专注于灵活性和易于理解而不是速度。

Xoroshiro128 +质量第二,速度更快,但学习信息量不足。

Python(以及其他许多人)Mersenne Twister的默认选择是最差的。

(还有一些名为splitmix64的东西,我不知道要放置什么 - 有些人说它比xoroshiro128 +更好,但它有一个时期问题 - 当然,你可能会想要在这里)

default-PCG和xoroshiro128 +都使用2N位状态来生成N位数。这通常是可取的,但意味着数字将重复。然而,PCG具有避免这种情况的替代模式。

当然,这很大程度上取决于num是否(接近)2的幂。理论上,可以为任何位宽创建PCG变体,但目前只有各种字大小被实现,因为你& #39;需要显式屏蔽。我不确定如何为新的比特大小生成参数(可能是本文中的参数?),但只需进行一段时间/ 2跳并验证值是不同的就可以测试它们

当然,如果你只是拨打200个电话给RNG,你可能实际上并不需要在数学方面避免重复。

或者,您可以使用LFSR 存在于每个位大小(但请注意,它永远不会生成全零值(或等效地,全1值) ))。 LFSR是串行的,并且(AFAIK)不可跳转,因此不能轻易地分割成多个任务。编辑:我发现这是不真实的,只是将前进步骤表示为矩阵,并指数跳跃。

请注意,LFSR do 具有与基于随机起点按顺序生成数字相同的明显偏差 - 例如,如果rng_outputs [a:b]全部失败,则{{1}函数,然后foo更可能作为第一个输出,无论起点如何。 PCG&#34; stream&#34;参数通过不以相同的顺序生成数字来避免这种情况。

Edit2:我已经完成了我认为的简短项目&#34;实施LFSRs in python,包括跳跃,经过全面测试。