80

我这样做是为了测试 randint 的随机性:

>>> from random import randint
>>>
>>> uniques = []
>>> for i in range(4500):  # You can see I was optimistic.
...     x = randint(500, 5000)
...     if x in uniques:
...         raise Exception('We duped %d at iteration number %d' % (x, i))
...     uniques.append(x)
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
Exception: We duped 887 at iteration number 7

我尝试了大约 10 次,而我得到的最好结果是在中继器之前进行了 121 次迭代。这是您可以从标准库中获得的最佳结果吗?

4

11 回答 11

292

生日悖论,或者为什么 PRNG 比你想象的更频繁地产生重复。


OP的问题有几个问题在起作用。一是上面提到的生日悖论,二是你正在生成的东西的性质,这并不能固有地保证给定的数字不会重复。

生日悖论适用于给定值在生成器期间可能多次出现的情况 - 因此重复可能在值样本中发生。生日悖论的影响是,获得此类副本的真正可能性非常大,而且它们之间的平均周期比人们原本可能想象的要小。感知概率和实际概率之间的这种不协调使生日悖论成为认知偏差的一个很好的例子,其中一个天真的直觉估计可能会大错特错。

伪随机数生成器 (PRNG) 的快速入门

问题的第一部分是您正在获取随机数生成器的公开值并将其转换为更小的数字,因此可能值的空间减少了。尽管一些伪随机数生成器在其周期内不重复值,但这种转换将域更改为更小的域。较小的域使“无重复”条件无效,因此您可以预期重复的可能性很大。

一些算法,例如线性同余 PRNG ( A'=AX|M)确实保证了整个周期的唯一性。在 LCG 中,生成的值包含累加器的整个状态,并且不保存任何其他状态。生成器是确定性的,不能在周期内重复一个数字 - 任何给定的累加器值只能暗示一个可能的连续值。因此,每个值在生成器的周期内只能出现一次。然而,这种 PRNG 的周期相对较小——对于 LCG 算法的典型实现来说约为 2^30——并且不可能大于不同值的数量。

并非所有 PRNG 算法都具有此特性;有些可以在周期内重复给定值。在 OP 的问题中,Mersenne Twister算法(在 Python 的random模块中使用)的周期很长——远大于 2^32。与线性同余 PRNG 不同,结果不纯粹是先前输出值的函数,因为累加器包含附加状态。对于 32 位整数输出和 ~2^19937 的周期,它不可能提供这样的保证。

Mersenne Twister 是一种流行的 PRNG 算法,因为它具有良好的统计和几何特性以及非常长的周期——这是用于仿真模型的 PRNG 的理想特性。

  • 良好的统计特性意味着算法生成的数字是均匀分布的,没有数字的出现概率明显高于其他数字。较差的统计特性可能会在结果中产生不必要的偏差。

  • 良好的几何特性意味着 N 个数的集合不位于 N 维空间中的超平面上。较差的几何特性会在仿真模型中产生虚假的相关性并扭曲结果。

  • 长周期意味着您可以在序列环绕到开始之前生成大量数字。如果模型需要大量迭代或必须从多个种子运行,那么典型 LCG 实现中可用的 2^30 左右的离散数字可能不够用。MT19337算法的周期很长——2^19337-1,也就是10^5821左右。相比之下,宇宙中的原子总数估计约为 10^80。

MT19337 PRNG 产生的 32 位整数不可能代表足够的离散值,以避免在如此大的周期内重复。在这种情况下,如果样本足够大,可能会出现重复值并且不可避免。

简而言之,生日悖论

这个问题最初被定义为房间里任意两个人生日相同的概率。关键是房间里的任何两个人都可以共享一个生日。人们往往天真地把这个问题误解为房间里某人与特定个人共享生日的概率,这是认知偏差的来源,往往导致人们低估概率。这是不正确的假设 - 没有要求匹配特定个人,任何两个人都可以匹配。

该图显示了随着房间中人数的增加,共享生日的概率。 对于 23 个人来说,两人生日相同的概率刚刚超过 50%。

任何两个人之间发生匹配的概率远高于与特定个人匹配的概率,因为匹配不一定是特定日期。相反,您只需找到两个生日相同的人。从这张图(可以在该主题的维基百科页面上找到),我们可以看到,我们只需要房间里的 23 人,就有 50% 的机会找到两个以这种方式匹配的人。

有关该主题的 Wikipedia 条目中,我们可以得到一个很好的总结。 在 OP 的问题中,我们有 4,500 个可能的“生日”,而不是 365 个。对于生成的给定数量的随机值(等于“人”),我们想知道任何两个相同值出现在序列中的概率。

计算生日悖论对 OP 问题的可能影响

对于 100 个数字的序列,我们有可能匹配的(100 * 99) / 2 = 4950 对(请参阅了解问题)(即第一个可以匹配第二个、第三个等,第二个可以匹配第三个、第四个等,依此类推),所以可能匹配的组合数量不仅仅是 100 个。

通过计算概率,我们得到 的表达式1 - (4500! / (4500**100 * (4500 - 100)!) 。下面的 Python 代码片段对匹配对出现的概率进行了简单的评估。

# === birthday.py ===========================================
#
from math import log10, factorial

PV=4500          # Number of possible values
SS=100           # Sample size

# These intermediate results are exceedingly large numbers;
# Python automatically starts using bignums behind the scenes.
#
numerator = factorial (PV)          
denominator = (PV ** SS) * factorial (PV - SS)

# Now we need to get from bignums to floats without intermediate
# values too large to cast into a double.  Taking the logs and 
# subtracting them is equivalent to division.
#  
log_prob_no_pair = log10 (numerator) - log10 (denominator)

# We've just calculated the log of the probability that *NO*
# two matching pairs occur in the sample.  The probability
# of at least one collision is 1.0 - the probability that no 
# matching pairs exist.
#
print 1.0 - (10 ** log_prob_no_pair)

对于从 4500 个可能值的总体中抽样的 100 个数字内发生的匹配,这会产生p=0.669的合理外观结果。(也许有人可以验证这一点并在错误时发表评论)。由此我们可以看出,OP 观察到的匹配数字之间的运行长度似乎是相当合理的。

脚注:使用洗牌获得唯一的伪随机数序列

请参阅下面来自 S. Mark 的答案,以获取获得保证的唯一随机数集的方法。张贴者所指的技术采用一组数字(您提供,因此您可以使它们独一无二)并将它们随机排列。从混洗后的数组中按顺序绘制数字将为您提供一系列保证不会重复的伪随机数。

脚注:加密安全 PRNG

MT 算法在密码学上并不安全,因为通过观察数字序列来推断生成器的内部状态相对容易。其他算法(例如Blum Blum Shub)用于加密应用,但可能不适合模拟或一般随机数应用。加密安全的 PRNG 可能很昂贵(可能需要 bignum 计算)或者可能没有良好的几何特性。在这种类型的算法的情况下,主要要求是通过观察一系列值来推断生成器的内部状态在计算上应该是不可行的。

于 2010-01-27T08:58:59.760 回答
46

在责怪 Python 之前,你真的应该复习一些概率和统计理论。从阅读生日悖论开始

顺便说一下,randomPython 中的模块使用了Mersenne twister PRNG,它被认为非常好,有一个巨大的周期,并且经过了广泛的测试。所以请放心,您会得到很好的照顾。

于 2010-01-27T08:51:38.750 回答
42

如果您不想重复,请生成顺序数组并使用random.shuffle

于 2010-01-27T08:54:45.207 回答
42

作为对 Nimbuz 答案的回答:

http://xkcd.com/221/

替代文字

于 2010-01-29T11:56:23.327 回答
15

真正的随机性肯定包括在用尽整个可能值集之前重复值。否则它不会是随机的,因为您将能够预测一个值不会重复多长时间。

如果你曾经掷过骰子,你肯定经常连续得到 3 个六点……

于 2010-01-27T09:10:23.843 回答
5

Python 的随机实现实际上是最先进的:

于 2010-01-27T08:57:51.843 回答
5

您正在4500从 range 生成随机数500 <= x <= 5000。然后,您检查每个数字是否以前生成过。生日问题告诉我们,如果n尝试超出范围,其中两个数字匹配的概率是多少d

您还可以反转公式来计算您必须生成多少个数字,直到生成重复的机会大于50%. 在这种情况下,您有>50%机会在迭代后找到重复的数字79

于 2010-01-27T09:30:04.293 回答
4

那不是中继器。重复器是当您重复相同的序列时。不只是一个数字。

于 2010-01-27T09:34:32.487 回答
0

您已经定义了一个包含 4501 个值 (500-5000) 的随机空间,并且您正在迭代 4500 次。您基本上可以保证在您编写的测试中发生冲突。

换一种方式考虑:

  • 当结果数组为空时 P(dupe) = 0
  • 数组 P(dupe) 中的 1 个值 = 1/4500
  • 数组 P(dupe) 中的 2 个值 = 2/4500
  • 等等

因此,当您达到 45/4500 时,该插入有 1% 的可能性是重复的,并且该概率随着每个后续插入而不断增加。

要创建一个真正测试随机函数能力的测试,请增加可能随机值的范围(例如:500-500000) 您可能会或可能不会上当受骗。但是平均而言,您将获得更多的迭代。

于 2010-01-27T09:24:20.333 回答
0

对于遇到此问题的其他人,我使用了 uuid.uuid4() ,它就像一个魅力。

于 2010-01-27T20:31:14.997 回答
0

有生日悖论。考虑到这一点,您会意识到您所说的是找到“764、3875、4290、4378、764”或类似的东西并不是很随机,因为该序列中的数字重复。真正的方法是相互比较序列。我写了一个python脚本来做到这一点。

from random import randint
y = 21533456
uniques = []
for i in range(y):  
    x1 = str(randint(500, 5000))
    x2 = str(randint(500, 5000))
    x3 = str(randint(500, 5000))
    x4 = str(randint(500, 5000))
    x = (x1 + ", " + x2 + ", " + x3 + ", " + x4)
if x in uniques:
    raise Exception('We duped the sequence %d at iteration number %d' % (x, i))
else:
    raise Exception('Couldn\'t find a repeating sequence in %d iterations' % (y))
uniques.append(x)
于 2014-07-31T17:03:54.513 回答