关于c ++:为什么人们说使用随机数生成器时存在模偏差?

Why do people say there is modulo bias when using a random number generator?

我看到这个问题问了很多,但从来没有看到一个真正具体的答案。因此,我将在这里发表一篇文章,希望能帮助人们理解为什么使用随机数生成器(如EDCOX1,0)在C++中有"模数偏倚"。


因此,rand()是一个伪随机数发生器,它选择一个介于0和RAND_MAX之间的自然数,这是cstdlib中定义的常数(有关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的模类也将均匀分布。

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

1
2
3
4
int x;
do {
    x = rand();
} while (x >= n);

但是对于低值的n,这是无效的,因为您只有一个n/RAND_MAX的机会在您的范围内获得一个值,所以您需要平均执行RAND_MAX/n调用rand()

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

1
2
3
4
5
6
7
int x;

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

x %= n;

对于小值的n,很少需要对rand()进行多个调用。

引用和进一步阅读的作品:

  • cplusplus参考

  • 永远困惑


保持随机选择是消除偏差的好方法。

更新

如果我们搜索范围内可被n除尽的x,就可以使代码更快。

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

不幸的是,解决方案的实现都是不正确的,或者效率比它们应该的低。(每个解决方案都有不同的注释来解释这些问题,但没有解决这些问题。)这很可能会混淆随意的答案寻求者,因此我在这里提供了一个已知良好的实现。

同样,最好的解决方案是在提供它的平台上使用EDCOX1 7,或者为平台提供类似的远程解决方案(如Java上的EDCOX1×9)。它会做正确的事情,而不会对您造成代码成本。这几乎总是正确的呼叫。

如果您没有arc4random_uniform,那么您可以使用OpenSource的功能来准确地了解它是如何在更广泛的RNG之上实现的(在这种情况下,ar4random),但是类似的方法也可以在其他RNG之上工作。

以下是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,但是这提出了模偏差问题:110产生0,111产生1。这个模具已经装好了。

潜在解决方案方法0:

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

方法1:

一个简单但数学上正确的解决方案不是使用模,而是丢弃产生110111的结果,然后简单地用3个新位再试一次。不幸的是,这意味着每卷纸上有25%的机会需要重新卷纸,包括每卷纸本身。这显然是不切实际的,除了最微不足道的用途。

方法2:

使用更多的位:用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提供随机字节的示例程序。在编译时,请确保使用大多数人都可以使用的-lcrypto链接到库。

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

    我鼓励使用MODULUSROLLS值,以了解在大多数情况下实际发生了多少次转鼓。持怀疑态度的人也可能希望将计算值保存到文件中,并验证分布是否正常。


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

    • 一个对所有发电机都有效。在极限情况下更容易看到。如果您的生成器的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();
    }


    RAND_MAX3(在现实的价值比它应该是多高,但仍然会存在偏差,从这些计算让义),有一个倾向:

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

    在这个案例,% 2是你当你不想要的T之间的随机数和10。你可以得到之间的随机数和20% 3虽然通过做,因为在这样的案例:一3RAND_MAX多)。

    另一种方法

    有多其他的答案,而是simpler添加到解决方案,这是我得到的随机数之间的操作系统和不同的0n - 1n的可能性,没有偏差。

    • 数量(不需要的位编码的字节数)的数量可能是一位你需要随机的日期
    • 从随机的比特数编码
    • 如果这个数是>= 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

    正如被接受的答案所表明的那样,"模偏差"的根源在于RAND_MAX的低值。他用一个非常小的值RAND_MAX(10)来表示,如果rand_max为10,那么您试图用%生成一个介于0和2之间的数字,结果如下:

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

    所以这是有偏见的。数字越低,出局的机会就越大。

    但只有当RAND_MAX很小时,这种情况才会如此明显。或者更具体地说,与RAND_MAX相比,您修改的数字较大。

    一个比循环更好的解决方案是使用输出范围大得多的PRNG(这是一种效率极低的方法,甚至不应该被建议)。Mersenne Twister算法的最大输出为4294967295。这样一来,无论出于何种目的或目的,MersenneTwister::genrand_int32() % 10都将均匀分布,模偏压效应将几乎消失。


    我刚刚为冯·诺依曼的无偏硬币翻转方法编写了一个代码,理论上应该消除随机数生成过程中的任何偏差。更多信息请访问(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;
            }
        }
    }