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


277

我看到这个问题问了很多,但从未见过真正的具体答案。因此,我将在此处发布一篇文章,希望可以帮助人们理解为什么在使用随机数生成器(例如rand()在C ++中)时确实存在“模偏差” 。

Answers:


394

所以rand()是选择0之间的自然数和伪随机数发生器RAND_MAX,它是在定义的常数cstdlib(见本文章有关的一般概述rand())。

现在,如果您想生成一个介于0和2之间的随机数,会发生什么?为了便于说明,假设RAND_MAX是10,我决定通过调用生成一个介于0和2之间的随机数rand()%3。但是,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()


作品引用和进一步阅读:



6
关于RAND_MAX%n == n - 1_的另一种思考方式是(RAND_MAX + 1) % n == 0。在阅读代码时,我倾向于% something == 0比其他计算方式更容易理解为“均匀可分”。 当然,如果您的C ++ stdlib具有RAND_MAX与相同的值INT_MAX,那(RAND_MAX + 1)肯定是行不通的。因此Mark的计算仍然是最安全的实施方式。
斯利普D.汤普森

很好的答案!
Sayali Sonawane

我可能会挑剔,但是如果目标是减少浪费的比特,那么对于RAND_MAX(RM)仅比被N整除的整数少1的边缘条件,我们可以稍微改善这一点。在这种情况下,不需要浪费比特进行X> =(RM-RM%N)),这对于较小的N值没有太大的价值,但是对于较大的N值却有较大的价值。正如Slipp D. Thompson所提到的,有一种解决方案仅适用当INT_MAX(IM)> RAND_MAX时,但在相等时中断。但是,有一个简单的解决方案,我们可以将计算X> =(RM-RM%N)修改如下:
Ben Personick

X> = RM-((((RM%N)+ 1)%N)
Ben Personick

我发布了一个附加答案,详细解释了该问题并给出了示例代码解决方案。
Ben Personick

36

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

更新资料

如果搜索范围可被整除的x,我们可以使代码变快n

// 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
运气:-P转换为双精度,然后乘以MAX_UPPER_LIMIT / RAND_MAX会更整洁并且性能更好。
boycy 2012年

22
@boycy:你错过了重点。如果rand()可以返回的值的数量不是的倍数n,那么无论您做什么,都将不可避免地获得“模偏差”,除非您丢弃其中一些值。user1413793很好地说明了这一点(尽管该答案中提出的解决方案确实令人讨厌)。
TonyK,2012年

4
@TonyK,我很抱歉,我的观点很错。没有足够认真地思考,并且认为偏差仅适用于使用显式模运算的方法。感谢您修复我的问题:-)
boycy 2012年

运算符优先级可以使RAND_MAX+1 - (RAND_MAX+1) % n工作正常进行,但是RAND_MAX+1 - ((RAND_MAX+1) % n)为了清晰起见,我仍然认为应该这样写。
Linus Arver,2012年

4
如果RAND_MAX == INT_MAX (与大多数系统一样)则无法使用。请参阅上面对@ user1413793的第二条评论。
BlueRaja-Danny Pflughoeft 2012年

19

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

不幸的是,该解决方案的实现都是不正确的或效率不高。(每个解决方案都有各种注释来解释问题,但是没有一种解决方案可以解决这些问题。)这很可能会使临时寻求答案的人感到困惑,因此我在这里提供了一个已知的良好实现。

同样,最好的解决方案是仅arc4random_uniform在提供该解决方案的平台上使用,或者针对您的平台使用类似的远程解决方案(例如Random.nextInt在Java上)。它将为您做正确的事情,而无需花费任何代码。这几乎总是正确的选择。

如果没有arc4random_uniform,则可以使用开源的功能来确切地了解如何在更大范围的RNG之上实现它(ar4random在这种情况下,但是类似的方法也可以在其他RNG之上工作)。

这是OpenBSD的实现

/*
 * 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-upper_bound % upper_bound。通过使用32位余数而不是64位余数,可以简化代码并使之在ILP32和LP64体系结构上相同,并且在LP64体系结构上也稍快一些。

Jorden Verwer在tech @ ok 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;
 }

请注意,如果arcfour_random() 实际在实现中使用真正的RC4算法,则输出肯定会有一些偏差。希望您的图书馆作者已改在同一界面后使用更好的CSPRNG。我记得现在有一个BSD实际上使用ChaCha20算法来实现arcfour_random()。有关RC4输出偏差的更多信息,这些偏差使其对于安全性或其他重要应用(例如视频扑克)无用:blog.cryptographyengineering.com/2013/03/…– rmalayter
2016年

2
@rmalayter在iOS和OS X上,arc4random从/ dev / random中读取,这是系统中质量最高的熵。(名称中的“ arc4”是具有历史意义的,为了兼容而保留。)
Rob Napier

@Rob_Napier是个很好的了解,但/dev/random过去也曾在某些平台上使用RC4(Linux在计数器模式下使用SHA-1)。不幸的是,我通过搜索发现的手册页表明RC4在提供的各种平台上仍在使用arc4random(尽管实际代码可能有所不同)。
rmalayter '16

1
我很困惑。是不是-upper_bound % upper_bound == 0??
乔恩·麦格隆

1
-upper_bound % upper_bound如果int大于32位,则@JonMcClung 确实为0 。应该是(u_int32_t)-upper_bound % upper_bound)(假设u_int32_t是BSD主义uint32_t)。
伊恩·阿伯特

14

定义

模偏差是使用模算法将输出集减少为输入集的子集时的固有偏差。通常,每当输入集和输出集之间的映射不均等时,就会存在偏差,例如在输出集的大小不等于输入集的大小的除数时使用模运算的情况。

在数字表示为比特串(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,111产生1

潜在解决方案

方法0:

从理论上讲,可以依靠一支小兵整天掷骰子并将结果记录在数据库中,然后仅使用每个结果一次,而不是依赖随机位。这听起来很实用,反正很可能不会产生真正的随机结果(双关语)。

方法1:

除了使用模量,天真但数学正确的办法是丢弃结果产量110111和简单的3个新位再试一次。不幸的是,这意味着每卷都有25%的机会需要重新卷制,包括每个重新卷制本身。对于最琐碎的用途,这显然是不切实际的。

方法二:

使用更多位:使用4,而不是3位。这将产生16种可能的结果。当然,在结果大于5的任何时候重新滚动都会使情况变得更糟(10/16 = 62.5%),因此仅靠这一点是无济于事的。

请注意,2 * 6 = 12 <16,因此我们可以安全地获取小于12的任何结果,并减少该模6以平均分配结果。必须丢弃其他4个结果,然后像以前的方法一样重新滚动。

首先听起来不错,但让我们检查一下数学:

4 discarded results / 16 possibilities = 25%

在这种情况下,多加一点一点都没有帮助

这个结果很不幸,但是让我们用5位重试:

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

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

如所示 但是,,增加1位可能不会改变任何内容。实际上,如果将滚动增加到6位,则概率仍然为6.25%。

这提出了另外两个问题:

  1. 如果我们添加足够的位,是否可以保证丢弃的可能性会减小?
  2. 一般情况下多少位就足够了?

通用解决方案

幸运的是,第一个问题的答案是肯定的。6的问题是2 ^ x mod 6在2和4之间翻转,这恰好是彼此的2的倍数,因此对于x> 1的偶数

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

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

在不提供进一步证据的情况下,通常使用所需位数的两倍将提供较小的(通常不重要的)丢弃机会。

概念验证

这是一个使用OpenSSL的libcrypo提供随机字节的示例程序。编译时,请确保链接到-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;
}

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


我真的希望没有人盲目地复制您的统一随机实现。由于断言,该randomPool = RAND_bytes(...)行将始终存在randomPool == 1。这总是会导致丢弃并重新滚动。我认为您想在另一行中声明。因此,这导致RNG 1每次迭代都返回。
Qix-蒙尼卡(Monica)

需要明确的是,由于断言它将始终成功,因此将始终根据OpenSSL 文档进行randomPool评估。1RAND_bytes()RAND_status()
Qix-蒙尼卡(Monica)

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的溢出)

顺便说一句,C ++ 11为还原和除rand()之外的其他生成器引入了标准方法。


n == RAND_MAX吗?1:(RAND_MAX-1)/(n + 1):我知道这里的想法是先将RAND_MAX分成相等的页面大小N,然后返回N之内的偏差,但是我无法将代码精确地映射到此。
zinking 2012年

1
天真版本应为(RAND_MAX + 1)/(n + 1),因为有RAND_MAX + 1个值可分为n + 1个存储桶。如果在计算RAND_MAX + 1时为了避免溢出,可以将其转换为1+(RAND_MAX-n)/(n + 1)。为了避免在计算n + 1时发生溢出,首先检查n == RAND_MAX的情况。
AProgrammer 2012年

+加上,与重新生成数字相比,进行除法似乎要花费更多。
zinking 2012年

4
取模和除法的代价相同。某些ISA甚至只提供一条指令,而总是同时提供这两种指令。重新生成数字的成本将取决于n和RAND_MAX。如果n相对于RAND_MAX小,则可能会花费很多。显然,您可能会决定这些偏见对于您的应用程序并不重要;我只是提供一种避免它们的方法。
AProgrammer 2012年

9

马克的解决方案(公认的解决方案)几乎完美。

int x;

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

x %= n;

16年3月25日在23:16编辑

马克·阿默里39k21170211

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

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

是什么原因导致这在某些时候马克失去视力之间的差异NRand_Max

N是一个集合,其有效成员仅包含正整数,因为它包含有效的响应计数。(例如:Set N= {1, 2, 3, ... n }

Rand_max 但是,这是一组(按我们的定义定义)包括任意数量的非负整数。

以最通用的形式,这里定义为 Rand Max是所有有效结果的集合,理论上可以包括负数或非数字值。

因此Rand_Max,最好将其定义为“可能的响应”集。

但是N,它会根据有效响应集中的值计数进行运算,因此即使按照我们的特定情况定义,Rand_Max该值也将比其包含的总数小一个。

使用马克的解决方案,在以下情况下会舍弃值:X => 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不等于素数时,这是有效的关注点)

例如:

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的乘积。

标记的原始版本:

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中的值数的情况下;在这种情况下,除非RAND_MAX = INT_MAX,否则您可以设置N = RAND_MAX +1。

从循环角度来看,您可以仅使用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中包含的可能的总值集时,第二个版本还讨论了极端情况。

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

重申:

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

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

 int x;

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

 x %= n;

扩展通用解决方案,它允许RAND_MAX + 1 = n的另一种情况:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x;

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

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

在某些语言(特别是解释语言)中,在while条件之外进行比较运算的计算可能会导致更快的结果,因为这是一次性计算,无论需要重试多少次。YMMV!

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x; // Resulting random number
int y; // One-time calculation of the compare value for x

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

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

难道不能说马克解决方案的问题在于,当马克·兰德和马克和n分别代表两个不同的意思时,他将兰德·马克和n视为相同的“度量单位”吗?虽然n代表结果“可能性数”,但RAND_MAX仅代表原始可能性的最大值,其中RAND_MAX + 1将是原始可能性数。我很惊讶他没有得出您的结论,因为他似乎已经承认n和RAND_MAX在以下等式中是不一样的:RAND_MAX%n = n - 1
Danilo SouzaMorães19年

@DaniloSouzaMorães谢谢达尼洛,你把事情讲得很简洁。我去展示他在做什么以及为什么以及如何做的事情,但是我认为我从来没有能够雄辩地表态他做错了什么,因为我被关于如何和如何做的逻辑的细节所笼罩。为什么有问题,我没有明确说明问题所在。您是否介意我将“答案”修改为将您在此处写的部分内容用作我自己的摘要,以解决需要在顶部解决的问题以及在哪里接受的解决方案在做什么?
Ben Personick

那将是真棒。
加油

1

随着RAND_MAX价值3(实际上它应该是高于很多,但偏置仍存在),它是有道理的,从这些计算是有偏差:

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

在这种情况下,% 2当您想要0和之间的随机数时,您不应该这样做1。通过这样做,您可能会在0和之间获得一个随机数,因为在这种情况下:是的倍数。2% 3RAND_MAX3

另一种方法

有一个简单得多的方法,但是要添加到其他答案中,这是我的解决方案,用于在0和之间获得一个随机数n - 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

-1

正如公认的答案所表明的,“模偏差”源于的低值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。在梅森倍捻机算法的4,294,967,295最大输出。MersenneTwister::genrand_int32() % 10出于所有意图和目的,这样做将平均分配,并且模偏置效应将几乎消失。


3
您的效率更高,如果RAND_MAX大大大于您要修改的数字,则可能是正确的,但是您的数字仍会存在偏差。当然,这些都是伪随机数生成器,而它们本身就是一个不同的话题,但是如果您假设一个完全随机数生成器,您的方式仍然会偏向较低的值。
user1413793

因为最大值是奇数,所以MT::genrand_int32()%2选择0(50 + 2.3e-8)%的时间和1(50-2.3e-8)%的时间。除非您要建立赌场的RGN(您可能会使用更大范围的RGN),否则任何用户都不会注意到额外的2.3e-8%的时间。您所说的数字太小,在这里无关紧要。
bobobobo

7
循环是最好的解决方案。它不是“效率极低的”;在最差的平均情况下,所需的迭代次数少于两倍。使用较高的RAND_MAX值将减少模偏置,但不会消除它。循环会。
贾里德·尼尔森

5
如果RAND_MAX它大于要修改的数字,则重新生成随机数所需的次数将逐渐减少,并且不会影响效率。我说保持循环,只要您针对最大倍数进行测试,n而不是n按照公认的答案所建议的那样进行测试。
Mark Ransom

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

这没有解决模偏差。此过程可用于消除位流中的偏差。但是,要从比特流到0到n的均匀分布(其中n不小于2的幂),需要解决模偏置问题。因此,该解决方案不能消除随机数生成过程中的任何偏差。
里克

2
@Rick嗯。冯·诺依曼方法在产生例如1到100之间的随机数时消除模偏差的逻辑扩展是:A)调用rand() % 100100次。B)如果所有结果都不相同,则取第一个。C)否则,转到A。这将起作用,但是预期的迭代次数约为10 ^ 42,因此您必须非常耐心。和不朽。
Mark Amery

@MarkAmery确实应该起作用。查看此算法,尽管它没有正确实现。第一个应该是:else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
Rick
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.