我正在尝试创建一个生成器,该生成器返回给定范围内的数字,该数字通过函数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_factory
或mix_function
的其他一些潜在函数的潜在解决方案,我可以尝试针对num
的不同值进行推广?非常感谢...
答案 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]
看起来是随机的,不是吗?
如果我们从随机数生成c
和m
,它实际上也随机。但请记住,无法保证此算法将生成所有可能的排列,或者每个排列具有相同的生成概率。
关于实现的困难部分实际上只是生成一个合适的随机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
既然我们可以为c
和m
生成合适的值,那么创建排列是微不足道的:
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个可选算法呢?
您可以将shuffle
和set
个解决方案与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,包括跳跃,经过全面测试。