【问题标题】:Efficient random generator for very large range (in python)非常大范围的高效随机生成器(在python中)
【发布时间】:2018-10-02 01:41:51
【问题描述】:

我正在尝试创建一个生成器,它返回给定范围内的数字,这些数字通过函数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 &lt;= num 不是 True 的次数就会很小)。

我的问题

你能想到以下一种吗:

  • mix_function_factory 的潜在解决方案,甚至是 mix_function 的一些其他潜在功能,我可以尝试将其推广到 num 的不同值?
  • 解决原始问题的更好方法?

提前非常感谢....

【问题讨论】:

  • 也许您可以根据num 的大小执行方法 1 或 2:如果较小,请在预先计算的列表上使用 shuffle,如果较大则使用 set 方法
  • 其他需要考虑的事情:如果生成器重复一个数字,真的有多糟糕?如果您可以摆脱偶尔重复的数字(可能在您的代码的另一部分进行一些更改),这会带来更多的可能性,如果num 真的很大,那么发生的机会可能会非常小。

标签: python performance generator shuffle


【解决方案1】:

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

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


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

  • 加法:我们可以将任意整数c 加到x
  • 乘法:我们可以将x 与任何与n 没有质因数的数字m 相乘。

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

【讨论】:

  • @Tim:这个答案有效地使用了Linear Congruential Generator。 IIRC,在mn 相对质数或某些类似条件下,你确实得到了范围内的所有整数一次,但按照现代标准,它不是一个非常强大的 PRNG。如果它足以满足您的目的,那就太好了,但请注意它可能很弱,尤其是在mn 以及c 的某些选择中。
  • @PeterCordes:不幸的是,它甚至不是 LCG。它甚至比这更弱。我犯了和你一样的错误,但是这个答案实际上只是m的倍数,因为它增加了x,而不是使用以前的输出作为下一个x。这真的弱。
  • 谢谢你们。阅读了链接的文章后,这很容易被改编成真正的 LCG,并且由于我们已经有了质因数,我们可以选择 m 的值来确保它是真正的排列。我原以为这对我的目的来说已经足够了,但是对于我的实际用例(也许我应该最初说明)这些数字是有限 n 维空间中点的参数化,我可以看到点如何在某些我不想要的飞机上收集。有什么建议吗?
【解决方案2】:

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

您可以将shuffleset 解决方案与num 值的阈值混合使用。这基本上是在一个生成器中组装您的 2 个第一个解决方案:

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 的大值)效果很好,因为随机生成器中没有那么多重复。

【讨论】:

  • 哈哈,刚发帖就想到了这个。不过谢谢你 :) 这就是我认为我会暂时使用的方法,但我似乎仍然不太理想。如果没有人能找出一个可行的“双射工厂”,我会接受答案。我认为这也可能是我过早过度优化的情况......
  • 另外,我已将原始帖子从 &lt;= num 更改为 &lt;= num - 1(否则它将继续循环)。我也尝试编辑您的答案,但无法编辑,因为它不超过 6 个字符,也许您不会有这个限制?
  • 好的,已编辑。您的两种方法都可以,我怀疑是否有通用解决方案。好吧,我们拭目以待。无论如何,好问题。
  • 干杯 :) @aran-fey 的解决方案似乎很好地改变了数字。尽管我怀疑您根据 num 的值使用不同算法的想法会提高他回答的效率。我需要用一些实际值进行测试以找出答案
【解决方案3】:

在 Python 中获得最佳性能比在低级语言中要困难得多。例如,在 C 语言中,您通常可以通过将乘法替换为移位来节省一些热内循环。 python 字节码定向的开销消除了这一点。当然,当您考虑要针对哪个“python”变体(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 变体,但目前只能实现各种字长,因为您会需要显式屏蔽。我不确定如何为新的位大小生成参数(也许它在论文中?),但可以通过执行 period/2 跳转并验证值是否不同来简单地测试它们。

当然,如果您只对 RNG 进行 200 次调用,您实际上可能不需要在数学方面避免重复。


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

请注意,LFSR 确实与基于随机起点按顺序生成数字具有相同的明显偏差 - 例如,如果 rng_outputs[a:b] 都使您的 foo 函数失败, 那么rng_outputs[b] 将更有可能作为第一个输出,无论起点如何。 PCG 的“stream”参数通过不按相同顺序生成数字来避免这种情况。

Edit2:我已经完成了我认为是实现LFSRs in python 的“简短项目”,包括跳跃,经过全面测试。

【讨论】:

    猜你喜欢
    • 2011-07-23
    • 2017-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-01
    相关资源
    最近更新 更多