Why do people say there is modulo bias when using a random number generator?
我看到这个问题问了很多,但从来没有看到一个真正具体的答案。因此,我将在这里发表一篇文章,希望能帮助人们理解为什么使用随机数生成器(如EDCOX1,0)在C++中有"模数偏倚"。
因此,
如果你想生成一个介于0和2之间的随机数,会发生什么?为了解释,假设
当
当
当
这不会以相等的概率生成介于0和2之间的数字。当然,对于较小的范围,这可能不是最大的问题,但对于较大的范围,这可能会扭曲分布,使较小的数字产生偏差。
那么,当
那么我们如何解决这个问题呢?一种粗略的方法是不断生成随机数,直到您得到所需范围内的数字:
1 2 3 4 | int x; do { x = rand(); } while (x >= n); |
但是对于低值的
一个更有效的公式方法是采用一些长度可被
1 2 3 4 5 6 7 | int x; do { x = rand(); } while (x >= (RAND_MAX - RAND_MAX % n)); x %= n; |
号
对于小值的
引用和进一步阅读的作品:
cplusplus参考
永远困惑
保持随机选择是消除偏差的好方法。
更新
如果我们搜索范围内可被
1 2 3 4 5 6 7 8 9 10 11 12 | // Assumptions // rand() in [0, RAND_MAX] // n in (0, RAND_MAX] int x; // Keep searching for an x in a range divisible by n do { x = rand(); } while (x >= RAND_MAX - (RAND_MAX % n)) x %= n; |
上面的循环应该非常快,比如平均1次迭代。
@用户1413793对于该问题是正确的。我不打算再进一步讨论这个问题,只想指出一点:是的,对于小值的
不幸的是,解决方案的实现都是不正确的,或者效率比它们应该的低。(每个解决方案都有不同的注释来解释这些问题,但没有解决这些问题。)这很可能会混淆随意的答案寻求者,因此我在这里提供了一个已知良好的实现。
同样,最好的解决方案是在提供它的平台上使用EDCOX1 7,或者为平台提供类似的远程解决方案(如Java上的EDCOX1×9)。它会做正确的事情,而不会对您造成代码成本。这几乎总是正确的呼叫。
如果您没有
以下是OpenBSD实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /* * Calculate a uniformly distributed random number less than upper_bound * avoiding"modulo bias". * * Uniformity is achieved by generating new random numbers until the one * returned is outside the range [0, 2**32 % upper_bound). This * guarantees the selected random number will be inside * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound) * after reduction modulo upper_bound. */ u_int32_t arc4random_uniform(u_int32_t upper_bound) { u_int32_t r, min; if (upper_bound < 2) return 0; /* 2**32 % x == (2**32 - x) % x */ min = -upper_bound % upper_bound; /* * This could theoretically loop forever but each retry has * p > 0.5 (worst case, usually far better) of selecting a * number inside the range we need, so it should rarely need * to re-roll. */ for (;;) { r = arc4random(); if (r >= min) break; } return r % upper_bound; } |
号
值得注意的是,对于那些需要实现类似功能的人,关于此代码的最新提交注释是:
Change arc4random_uniform() to calculate
2**32 % upper_bound'' as -upper_bound % upper_bound''. Simplifies the code and makes it the
same on both ILP32 and LP64 architectures, and also slightly faster on
LP64 architectures by using a 32-bit remainder instead of a 64-bit
remainder.Pointed out by Jorden Verwer on tech@
ok deraadt; no objections from djm or otto
号
Java实现也很容易找到(见前面的链接):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public int nextInt(int n) { if (n <= 0) throw new IllegalArgumentException("n must be positive"); if ((n & -n) == n) // i.e., n is a power of 2 return (int)((n * (long)next(31)) >> 31); int bits, val; do { bits = next(31); val = bits % n; } while (bits - val + (n-1) < 0); return val; } |
定义
模偏差是使用模算法将输出集减少到输入集的一个子集时的固有偏差。一般来说,当输入和输出集之间的映射不均匀分布时,就存在偏差,例如在输出集的大小不是输入集大小的除数时使用模运算。
这种偏差在计算中尤其难以避免,在计算中将数字表示为0和1位的字符串。找到真正随机的随机源也是非常困难的,但这超出了讨论的范围。对于这个答案的其余部分,假设存在一个真正随机位的无限源。
问题示例让我们考虑使用这些随机位模拟一个压模辊(0到5)。有6种可能,所以我们需要足够的位来表示数字6,即3位。不幸的是,3个随机位产生8个可能的结果:
1 2 | 000 = 0, 001 = 1, 010 = 2, 011 = 3 100 = 4, 101 = 5, 110 = 6, 111 = 7 |
我们可以通过取值模6来将结果集的大小精确地减小到6,但是这提出了模偏差问题:
理论上,一个人可以雇佣一支小部队整天掷骰子,并将结果记录在数据库中,然后只使用一次每个结果,而不是依靠随机的比特。这几乎和听起来一样实际,而且很可能不会产生真正的随机结果(双关语的意图)。
方法1:一个简单但数学上正确的解决方案不是使用模,而是丢弃产生
使用更多的位:用4代替3位。这将产生16种可能的结果。当然,当结果大于5时,重新滚动会使情况变得更糟(10/16=62.5%),因此单靠这一点不会有帮助。
注意2*6=12<16,所以我们可以安全地取小于12的任何结果,并减少该模6来平均分配结果。其他4个结果必须丢弃,然后像前面的方法一样重新滚动。
一开始听起来不错,但让我们检查一下数学:
1 | 4 discarded results / 16 possibilities = 25% |
号
In this case, 1 extra bit didn't help at all!
号
这个结果很不幸,但是让我们用5位再试一次:
1 2 | 32 % 6 = 2 discarded results; and 2 discarded results / 32 possibilities = 6.25% |
一定的改进,但在许多实际情况下还不够好。好消息是,添加更多的位永远不会增加需要丢弃和重新滚动的机会。这不仅适用于骰子,而且适用于所有情况。
然而,如所示,添加1个额外的位可能不会改变任何东西。事实上,如果我们将滚动增加到6位,概率仍然是6.25%。
这需要另外两个问题:
一般解决方案
谢天谢地,第一个问题的答案是肯定的。6的问题是2^x mod 6在2和4之间翻转,恰好是2的倍数,因此对于偶数x>1,
1 | [2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1) |
。
因此,6是一个例外而不是规则。有可能找到更大的模,以同样的方式产生2的连续幂,但最终这必须环绕,并且丢弃的概率将降低。
Without offering further proof, in general using double the number
of bits required will provide a smaller, usually insignificant,
chance of a discard.
号概念证明
下面是一个使用openssl的libcrypo提供随机字节的示例程序。在编译时,请确保使用大多数人都可以使用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #include <iostream> #include #include <limits> #include <openssl/rand.h> volatile uint32_t dummy; uint64_t discardCount; uint32_t uniformRandomUint32(uint32_t upperBound) { assert(RAND_status() == 1); uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound; uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool)); while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) { RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool)); ++discardCount; } return randomPool % upperBound; } int main() { discardCount = 0; const uint32_t MODULUS = (1ul << 31)-1; const uint32_t ROLLS = 10000000; for(uint32_t i = 0; i < ROLLS; ++i) { dummy = uniformRandomUint32(MODULUS); } std::cout <<"Discard count =" << discardCount << std::endl; } |
我鼓励使用
关于模的使用有两种常见的抱怨。
一个对所有发电机都有效。在极限情况下更容易看到。如果您的生成器的rand_max为2(不符合C标准),并且您只希望0或1作为值,则使用modulo生成0的频率(当生成器生成0和2时)将是生成1的频率(当生成器生成1时)的两倍。请注意,只要不删除值,这是正确的,无论您使用的是从生成器值到所需值的映射,其中一个映射的频率将是另一个映射的两倍。
某些类型的生成器的有效位比其他生成器的随机位要少,至少对于某些参数来说是这样,但不幸的是,这些参数还有其他有趣的特性(这样可以使rand_max one小于2的幂)。这个问题是众所周知的,长期以来,库的实现可能会避免这个问题(例如,C标准中的示例rand()实现使用这种生成器,但去掉了16个不那么重要的位),但有些人喜欢抱怨这一点,你可能会运气不好。
使用类似
1 2 3 4 5 6 7 8 9 10 11 | int alea(int n){ assert (0 < n && n <= RAND_MAX); int partSize = n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1); int maxUsefull = partSize * n + (partSize-1); int draw; do { draw = rand(); } while (draw > maxUsefull); return draw/partSize; } |
。
生成介于0和n之间的随机数将避免这两个问题(并避免使用rand_max==int_max溢出)
BTW,C++ 11引入了还原和其他生成器的标准方法,而不是RAND()。
马克的解决方案(公认的解决方案)几乎是完美的。
1
2
3
4
5
6
7 int x;
do {
x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));
x %= n;edited Mar 25 '16 at 23:16
Mark Amery 39k21170211
号
但是,它有一个警告,即在rand_max(rm)小于n的倍数1(其中n=可能有效结果的数量)的任何情况下,丢弃1个有效结果集。
也就是说,当"丢弃的值计数"(d)等于n时,它们实际上是一个有效集(v),而不是一个无效集(i)。
使用Mark的解决方案,当:x=>rm-rm%n时,值被丢弃
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | EG: Ran Max Value (RM) = 255 Valid Outcome (N) = 4 When X => 252, Discarded values for X are: 252, 253, 254, 255 So, if Random Value Selected (X) = {252, 253, 254, 255} Number of discarded Values (I) = RM % N + 1 == N IE: I = RM % N + 1 I = 255 % 4 + 1 I = 3 + 1 I = 4 X => ( RM - RM % N ) 255 => (255 - 255 % 4) 255 => (255 - 3) 255 => (252) Discard Returns $True |
正如您在上面的示例中看到的,当x(从初始函数得到的随机数)的值为252、253、254或255时,即使这四个值包含一组有效的返回值,我们也会丢弃它。
即:当丢弃的值的计数(i)=n(有效结果的数目)时,原始函数将丢弃一组有效的返回值。
如果我们将n和rm的值之差描述为d,即:
1 | D = (RM - N) |
号
然后,当d值变小时,由于该方法而不需要重新滚动的百分比在每个自然乘法处都会增加。(当rand_max不等于质数时,这是有效的考虑)
如:
1 2 3 4 5 6 7 8 | RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125% RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625% RM=255 , N=8 Then: D = 247, Lost percentage = 3.125% RM=255 , N=16 Then: D = 239, Lost percentage = 6.25% RM=255 , N=32 Then: D = 223, Lost percentage = 12.5% RM=255 , N=64 Then: D = 191, Lost percentage = 25% RM=255 , N= 128 Then D = 127, Lost percentage = 50% |
由于所需重滚的百分比越接近rm,根据运行代码的系统的约束和正在查找的值,对于许多不同的值来说,这可能是一个值得关注的问题。
为了否定这一点,我们可以做一个简单的修正,如下所示:
1 2 3 4 5 6 7 | int x; do { x = rand(); } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ); x %= n; |
。
这提供了一个更通用的公式版本,说明了使用模数定义最大值的其他特性。
对rand_max使用小值的示例,该值是n的乘法。
Mark'原始版本:
1 2 3 | RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3. When X >= (RAND_MAX - ( RAND_MAX % n ) ) When X >= 2 the value will be discarded, even though the set is valid. |
通用版本1:
1 2 3 | RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3. When X > (RAND_MAX - ( ( RAND_MAX % n ) + 1 ) % n ) When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard. |
。
此外,在n应该是rand_max中的值数的情况下;在这种情况下,可以设置n=rand_max+1,除非rand_max=int_max。
循环方面,您可以只使用n=1,而x的任何值都将被接受,并将if语句放入最后一个乘数中。但是,也许您的代码可能有一个正当的理由,当函数以n=1调用时返回1…
因此,当您希望n=rand_max+1时,最好使用0,这通常会提供一个0级错误。
通用版本2:
1 2 3 4 5 6 7 8 9 10 11 | int x; if n != 0 { do { x = rand(); } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ); x %= n; } else { x = rand(); } |
。
这两种解决方案都解决了这个问题,不必要地丢弃了有效结果,当rm+1是n的产品时,会出现这种情况。
第二个版本还涵盖了当需要n等于rand_max中包含的总可能值集时的边缘情况场景。
两者的改进方法是相同的,并允许提供更通用的解决方案,以满足提供有效随机数和最小化丢弃值的需要。
重申:
扩展Mark示例的基本通用解决方案:
1 2 3 4 5 6 7 | int x; do { x = rand(); } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ); x %= n; |
。
扩展的通用解决方案,允许一个附加的rand_max+1=n方案:
1 2 3 4 5 6 7 8 9 10 11 | int x; if n != 0 { do { x = rand(); } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ); x %= n; } else { x = rand(); } |
。
与
在这个案例,
另一种方法
有多其他的答案,而是simpler添加到解决方案,这是我得到的随机数之间的操作系统和不同的
- 数量(不需要的位编码的字节数)的数量可能是一位你需要随机的日期
- 从随机的比特数编码
- 如果这个数是
>= n ,重新启动(上模)。
真正的随机数据是不容易获得,使用比需要更多的比特,为什么。
在下面的例子是一位缓存(使用Smalltalk,从伪随机数发生器。我使用的操作系统的安全专家在您自己的风险。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | next: n | bitSize r from to | n < 0 ifTrue: [^0 - (self next: 0 - n)]. n = 0 ifTrue: [^nil]. n = 1 ifTrue: [^0]. cache isNil ifTrue: [cache := OrderedCollection new]. cache size < (self randmax highBit) ifTrue: [ Security.DSSRandom default next asByteArray do: [ :byte | (1 to: 8) do: [ :i | cache add: (byte bitAt: i)] ] ]. r := 0. bitSize := n highBit. to := cache size. from := to - bitSize + 1. (from to: to) do: [ :i | r := r bitAt: i - from + 1 put: (cache at: i) ]. cache removeFrom: from to: to. r >= n ifTrue: [^self next: n]. ^r |
正如被接受的答案所表明的那样,"模偏差"的根源在于
1 2 3 4 5 6 7 8 9 10 11 12 | rand() % 3 // if RAND_MAX were only 10, gives output of rand() | rand()%3 0 | 0 1 | 1 2 | 2 3 | 0 4 | 1 5 | 2 6 | 0 7 | 1 8 | 2 9 | 0 |
所以有4个0的输出(4/10机会),只有3个1和2的输出(每个3/10机会)。
所以这是有偏见的。数字越低,出局的机会就越大。
但只有当
一个比循环更好的解决方案是使用输出范围大得多的PRNG(这是一种效率极低的方法,甚至不应该被建议)。Mersenne Twister算法的最大输出为4294967295。这样一来,无论出于何种目的或目的,
我刚刚为冯·诺依曼的无偏硬币翻转方法编写了一个代码,理论上应该消除随机数生成过程中的任何偏差。更多信息请访问(http://en.wikipedia.org/wiki/fair_coin)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | int unbiased_random_bit() { int x1, x2, prev; prev = 2; x1 = rand() % 2; x2 = rand() % 2; for (;; x1 = rand() % 2, x2 = rand() % 2) { if (x1 ^ x2) // 01 -> 1, or 10 -> 0. { return x2; } else if (x1 & x2) { if (!prev) // 0011 return 1; else prev = 1; // 1111 -> continue, bias unresolved } else { if (prev == 1)// 1100 return 0; else // 0000 -> continue, bias unresolved prev = 0; } } } |
。