为什么人们说使用随机数发生器时存在模偏差?

时间:2012-06-11 17:44:03

标签: c++ random language-agnostic modulo

我已经看到这个问题很多,但从未见过真正的具体答案。所以我将在这里发布一个,希望能帮助人们理解为什么在使用随机数生成器时会出现“模偏差”,比如C ++中的rand()

10 个答案:

答案 0 :(得分:365)

因此rand()是一个伪随机数生成器,它选择0到RAND_MAX之间的自然数,这是cstdlib中定义的常量(请参阅此article关于rand())的一般概述。

如果你想在0到2之间生成一个随机数,会发生什么?为了便于解释,假设RAND_MAX为10,我决定通过调用rand()%3生成0到2之间的随机数。但是,rand()%3不会以相同的概率产生0到2之间的数字!

rand()返回0,3,6或9时, rand()%3 == 0。因此,P(0)= 4/11

rand()返回1,4,7或10时, rand()%3 == 1。因此,P(1)= 4/11

rand()返回2,5或8时, rand()%3 == 2。因此,P(2)= 3/11

这不会以相等的概率生成0到2之间的数字。当然对于小范围,这可能不是最大的问题,但是对于更大的范围,这可能会扭曲分布,从而偏向较小的数字。

那么rand()%n何时以相同的概率返回从0到n-1的数字范围?当RAND_MAX%n == n - 1时。在这种情况下,与我们之前的假设rand()一起确实以相等的概率返回介于0和RAND_MAX之间的数字,n的模数类也将是均匀分布的。

那么我们如何解决这个问题呢?粗略的方法是保持生成随机数,直到得到所需范围内的数字:

int x; 
do {
    x = rand();
} while (x >= n);

但这对n的低值来说效率很低,因为您只有n/RAND_MAX的机会获得您的范围内的值,因此您需要执行RAND_MAX/n次调用平均rand()

一种更有效的公式方法是采用一些可被n整除的长度的大范围,如RAND_MAX - RAND_MAX % n,继续生成随机数,直到得到一个位于该范围内的长度,然后取模数:

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

对于n的小值,这很少需要多次调用rand()


作品引用和进一步阅读:


答案 1 :(得分:36)

继续选择随机是消除偏见的好方法。

<强>更新

如果我们搜索可被n整除的范围内的x,我们就可以快速生成代码。

// 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次迭代。

答案 2 :(得分:17)

@ user1413793关于此问题是正确的。我不打算进一步讨论,除了提出一点:是的,对于n的小值和RAND_MAX的大值,模偏差可能非常小。但是使用偏置诱导模式意味着每次计算随机数时都必须考虑偏差,并为不同情况选择不同的模式。如果你做出错误的选择,它引入的错误是微妙的,几乎不可能进行单元测试。与仅使用适当的工具(例如arc4random_uniform)相比,这是额外的工作,而不是更少的工作。做更多工作并获得更糟糕的解决方案是糟糕的工程,尤其是在大多数平台上每次都很容易做到这一点。

不幸的是,解决方案的实现都是错误的或效率低于应有的。 (每个解决方案都有各种解释问题的注释,但没有一个解决方案已经修复以解决它们。)这可能会使偶然的答案者感到困惑,所以我在这里提供了一个已知良好的实现。

同样,最好的解决方案就是在提供它的平台上使用arc4random_uniform,或者为您的平台使用类似的远程解决方案(例如Java上的Random.nextInt)。它会做正确的事情,无需代码成本。这几乎总是正确的召唤。

如果您没有arc4random_uniform,那么您可以使用opensource的强大功能来查看在更广泛范围的RNG(ar4random的基础上如何实现它,但是类似的方法也可以在其他RNG之上工作)。

以下是OpenBSD implementation

/*
 * 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;
}

值得注意的是,对于那些需要实现类似内容的人来说,这段代码的最新提交评论是:

  

更改arc4random_uniform()以计算2**32 % upper_bound'' as - upper_bound%upper_bound''。简化代码并使其成为   在ILP32和LP64架构上也是如此,并且稍快一点   LP64架构使用32位余数而不是64位   其余部分。

     

Jorden Verwer指出技术@   好的deraadt;没有来自djm或otto的反对意见

Java实现也很容易找到(参见上一个链接):

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;
 }

答案 3 :(得分:12)

定义

Modulo Bias 是使用模运算将输出集减少到输入集子集的固有偏差。通常,只要输入和输出集之间的映射不是均匀分布就存在偏差,就像在输出集的大小不是输入集大小的除数时使用模运算的情况一样。

这种偏差在计算中特别难以避免,其中数字表示为位串:0和1。找到真正随机的随机源也非常困难,但超出了本讨论的范围。 对于本答案的其余部分,假设存在无限的真正随机位源。

问题示例

让我们考虑使用这些随机位来模拟掷骰子(0到5)。有6种可能性,因此我们需要足够的位来表示数字6,即3位。不幸的是,3个随机位产生8种可能的结果:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

我们可以通过取模6的值将结果集的大小减小到6,但这会出现模偏差问题:110产生0,{{1产生一个1. 加载这个骰子。

潜在解决方案

方法0:

理论上,人们可以雇佣一支小军队整天掷骰子并将结果记录在数据库中,然后只使用一次结果,而不是依赖随机比特。这听起来和听起来一样实用,而且很可能不会产生真正随机的结果(双关语)。

方法1:

而不是使用模数,一个天真但数学上正确的解决方案是丢弃产生111110的结果,并简单地再尝试3个新位。不幸的是,这意味着每次滚动都有<25%的概率需要重新滚动,包括每次重新滚动。除了最微不足道的用途之外,这显然是不切实际的。

方法2:

使用更多位:而不是3位,使用4.这会产生16种可能的结果。当然,任何时候结果大于5的重新滚动都会使事情变得更糟(10/16 = 62.5%),这样单独就无济于事。

注意2 * 6 = 12&lt; 16,所以我们可以安全地取任何小于12的结果并减少模6以均匀分布结果。必须丢弃其他4个结果,然后按照前一种方法重新滚动。

一开始听起来不错,但让我们检查数学:

111
  

在这种情况下, 1个额外位根本没有帮助

这个结果很不幸,但让我们再试一下5位:

4 discarded results / 16 possibilities = 25%

有一定的改进,但在许多实际情况下还不够好。好消息是,添加更多位将永远不会增加需要丢弃和重新滚动的机会。这不仅适用于骰子,也适用于所有情况。

正如所示,添加1个额外位可能不会改变任何内容。事实上,如果我们将滚动增加到6位,概率仍为6.25%。

这引出了另外两个问题:

  1. 如果我们添加足够的比特,是否可以保证丢弃的概率会减少?
  2. 在一般情况下,多少位?
  3. 一般解决方案

    谢天谢地,第一个问题的答案是肯定的。 6的问题在于2 ^ x mod 6在2和4之间翻转,其巧合地是彼此2的倍数,因此对于偶数x> 1。 1,

    32 % 6 = 2 discarded results; and
    2 discarded results / 32 possibilities = 6.25%
    

    因此,6是一个例外,而不是规则。有可能找到更大的模量,以相同的方式产生2的连续幂,但最终必须包围,并且丢弃的可能性将降低。

      

    在没有提供进一步证明的情况下,通常使用加倍数字   所需的位数将提供更小的,通常无关紧要的,   丢弃的可能性。

    概念证明

    这是一个使用OpenSSL的libcrypo提供随机字节的示例程序。编译时,请务必使用[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1) 链接到库,大多数人都应该可以使用该库。

    -lcrypto

    我鼓励玩#include <iostream> #include <assert.h> #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; } MODULUS值来查看在大多数情况下实际发生了多少次重拍。持怀疑态度的人也可能希望将计算值保存到文件中,并验证分布是否正常。

答案 4 :(得分:9)

使用模数有两种常见的抱怨。

  • 一个对所有发电机都有效。在极限情况下更容易看到。如果您的生成器的RAND_MAX为2(不符合C标准)并且您只需要0或1作为值,则使用modulo将生成0两次(当生成器生成0和2时),因为它将生成1(当生成器生成1时)。请注意,只要不删除值,就会发生这种情况,无论您使用从生成器值到所需的映射的映射,其中一个的发生频率是另一个的两倍。

  • 某种类型的发生器比其他发生器的随机性更低,至少对于它们的一些参数而言,但遗憾的是这些参数具有其他有趣的特性(例如,RAND_MAX能够比功率小一个) 2)。这个问题是众所周知的,并且很长一段时间库实现可能会避免这个问题(例如C标准中的示例rand()实现使用这种生成器,但是丢弃了16个不太重要的位),但有些人喜欢抱怨你可能运气不好

使用类似

的内容
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()。

答案 5 :(得分:7)

马克的解决方案(已接受的解决方案)几近完美。

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;
     

于2016年3月25日23:16编辑

     

Mark Amery 39k21170211

但是,它有一个警告,在RAND_MAX(RM)小于N的倍数(其中N =可能的有效结果的数量)的任何情况下,丢弃1个有效的结果集。

即,当'丢弃的值的数量'(D)等于N时,它们实际上是有效集(V),而不是无效集(I)。

使用Mark的解决方案,在以下情况下丢弃值:X =&gt; RM - RM%N

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时,即使这四个值包含有效集,我们也会丢弃它返回值。

IE:当值的计数Discarded(I)= N(有效结果的数量)时,原始函数将丢弃一组有效的返回值。

如果我们将N和RM之间的差异描述为D,即:

D = (RM - N)

然后随着D的值变小,由于这种方法导致的不需要的重新滚动的百分比在每个自然乘法处增加。 (当RAND_MAX不等于素数时,这是有效关注的)

EG:

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%

由于Rerolls所需的百分比增加,N越接近RM,这可能是许多不同值的有效关注点,这取决于运行代码的系统的约束和所寻找的值。

要否定这一点,我们可以做一个简单的修改如下所示:

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

这提供了一个更通用的公式版本,它考虑了使用模数来定义最大值的其他特性。

RAND_MAX使用较小的值的示例,它是N的乘法。

Mark'original Version:

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:

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可能会更好,这通常会提供Div 0错误

广义版本2:

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中包含的总可能值集时,第二个版本还涵盖了边缘情况。

两者中的修改方法是相同的,并且允许提供更通用的解决方案,以满足提供有效随机数和最小化丢弃值的需要。

重申:

扩展标记示例的基本通用解决方案:

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

扩展通用解决方案,允许一个额外的RAND_MAX + 1 = n场景:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

答案 6 :(得分:0)

RAND_MAX值为3(实际上它应该远高于此值,但偏差仍然存在)从这些计算中可以看出存在偏差:

1 % 2 = 1 2 % 2 = 0 3 % 2 = 1 random_between(1, 3) % 2 = more likely a 1

在这种情况下,% 2是您想要01之间的随机数时不应该做的事情。您可以通过0获取2% 3之间的随机数,因为在这种情况下:RAND_MAX3的倍数。

另一种方法

要简单得多,但要添加到其他答案,我的解决方案是在0n - 1之间获取一个随机数,以便n不同的可能性,没有偏见。

  • 编码可能性数量所需的位数(不是字节数)是您需要的随机数据的位数
  • 编码随机位数
  • 如果此号码为>= n,则重新启动(无模数)。

真正随机的数据并不容易获得,所以为什么要使用比需要更多的比特。

以下是Smalltalk中的一个示例,它使用来自伪随机数生成器的位缓存。我不是安全专家,所以请自担风险。

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

答案 7 :(得分:0)

模数减少是使随机整数生成器避免永远运行的最坏情况的一种常见方法。

但是,在不引入偏见的情况下,无法“修复”最坏的情况。不仅仅是模减少(rand() % n,在公认的答案中进行了讨论)会以这种方式引入偏差,而且还会丹尼尔·莱米尔的“乘移”减少,或者如果您在设定之后不再拒绝结果迭代次数。

这就是原因,这里我们假设我们有一个“真实的”随机发生器,可以产生无偏且独立的随​​机位。*

1976年,DE Knuth和AC Yao表明,任何仅使用随机位以给定概率生成随机整数的算法都可以表示为二叉树,其中随机位指示遍历树和每片叶子的方式(端点)对应于结果。在这种情况下,我们要处理的算法是在[0,n)中生成随机整数,其中每个整数的选择概率为1 / n。但是,如果1 / n具有不间断的二进制扩展(如果n不是2的幂,就会是这种情况),那么该二进制树必然是其中一个-

  • 具有“无限”的深度,或者
  • 在树的末端
  • 包括“拒绝”叶子,

在任何一种情况下,该算法都不会在恒定时间内运行,并且在最坏的情况下将永远运行。 (另一方面,当n为2的幂时,最佳二叉树的深度将是有限的,并且没有拒绝节点。)

二叉树概念还表明,任何“修复”这种最坏情况下的时间复杂性的方法通常都会导致偏差。例如,模减少等于二叉树,其中拒绝叶子用标记的结果替换-但是由于拒绝叶子比可能的结果更多,因此只有部分结果可以代替拒绝叶子,从而产生偏差。如果您在设置一定数量的迭代后停止拒绝,则会产生相同类型的二叉树和相同类型的偏差。 (但是,根据应用的不同,这种偏见可以忽略不计。随机整数生成还存在安全方面的问题,这些问题太复杂了,无法在此答案中讨论。)

为说明起见,以下JavaScript代码实现了J. Lumbroso(2013)称为Fast Dice Roller的随机整数算法。请注意,它包括拒绝事件和循环,这是使算法在一般情况下无偏见所必需的。

function randomInt(minInclusive, maxExclusive) {
 var maxInclusive = (maxExclusive - minInclusive) - 1
 var x = 1
 var y = 0
 while(true) {
    x = x * 2
    var randomBit = (Math.random() < 0.5 ? 0 : 1)
    y = y * 2 + randomBit
    if(x > maxInclusive) {
      if (y <= maxInclusive) { return y + minInclusive }
      // Rejection
      x = x - maxInclusive - 1
      y = y - maxInclusive - 1
    }
 }
}

注意

*此答案不涉及C中的rand()函数,因为它has many issues。也许最严重的事实是C标准没有为rand()返回的数字明确指定特定的分布,甚至没有统一的分布。

答案 8 :(得分:-1)

正如accepted answer所示,“模偏差”的根源是RAND_MAX的低值。他使用极小值RAND_MAX(10)来表示如果RAND_MAX为10,那么您尝试使用%生成0到2之间的数字,将产生以下结果:

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机会)。

所以它有偏见。较低的数字有更好的机会出来。

但只有当RAND_MAX很小 时才会显示出来。或者更具体地说,当您修改的数字与RAND_MAX相比较大时。

循环(这是非常低效且甚至不应该被建议)更好的解决方案是使用具有更大输出范围的PRNG。 Mersenne Twister算法的最大输出为4,294,967,295。因此,对于所有意图和目的而言,MersenneTwister::genrand_int32() % 10将会平均分配,并且模数偏差效应将会消失。

答案 9 :(得分:-3)

我刚刚为Von Neumann的无偏硬币翻转方法编写了一个代码,理论上应该消除随机数生成过程中的任何偏差。更多信息可在(http://en.wikipedia.org/wiki/Fair_coin

找到
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;
        }
    }
}