【问题标题】:Random.nextInt(int) is [slightly] biasedRandom.nextInt(int) [稍微] 有偏见
【发布时间】:2013-07-24 10:03:22
【问题描述】:

也就是说,它永远不会使用某些特定的upperBound 参数连续生成超过 16 个偶数:

Random random = new Random();

int c = 0;
int max = 17;
int upperBound = 18;

while (c <= max) {
    int nextInt = random.nextInt(upperBound);
    boolean even = nextInt % 2 == 0;
    if (even) {
        c++;
    } else {
        c = 0;
    }
}

在此示例中,代码将永远循环,而当upperBound 为例如 16 时,它会快速终止。

这种行为的原因可能是什么?该方法的javadoc中有一些注释,但我没有理解它们。


UPD1:代码似乎以奇数上限终止,但可能会卡在偶数上限


UPD2: 我修改了代码以按照 cmets 中的建议捕获 c 的统计信息:

Random random = new Random();

int c = 0;
long trials = 1 << 58;
int max = 20;
int[] stat = new int[max + 1];

while (trials > 0) {
    while (c <= max && trials > 0) {
        int nextInt = random.nextInt(18);
        boolean even = nextInt % 2 == 0;
        if (even) {
            c++;
        } else {
            stat[c] = stat[c] + 1;
            c = 0;
        }
        trials--;
    }
}

System.out.println(Arrays.toString(stat));

现在它尝试在行中达到 20 偶数 - 以获得更好的统计数据,而 upperBound 仍然是 18

结果令人惊讶:

[16776448, 8386560, 4195328, 2104576, 1044736, 
 518144, 264704, 132096, 68864, 29952, 15104, 
 12032, 1792, 3072, 256, 512, 0, 256, 0, 0]

起初它按预期减少了 2 倍,但请注意最后一行!在这里它变得疯狂,捕获的统计数据似乎完全奇怪。

这是一个对数刻度的条形图:

c 如何获得 17 256 次的值是另一个谜

【问题讨论】:

  • 您能否在将c 设置为0 之前打印出来,看看您可以连续获得的偶数数量是否有任何规律?另外,这如何表明它有偏见?奇数的行为是否不同?所有这些都表明它并不是真正随机的。但我们已经知道了。
  • 不是答案,但如果您改用SecureRandom,代码会按预期终止。
  • LCG 是出了名的糟糕,尤其是在它们的低位。对于任何严重使用随机数,如模拟或蒙特卡洛方法,LCG 是完全不可接受的。使用良好的外部 PRNG 库。
  • 为什么它应该连续提供> 16个偶数,范围为18?你肯定不能合理地期望超过 18 个中的 9 个?
  • @EJP 如果它真的是随机的,17 个连续的偶数应该平均每 2^17 个数字或 130k 发生一次。这是使用更好的生成器时观察到的结果。

标签: java random


【解决方案1】:

http://docs.oracle.com/javase/6/docs/api/java/util/Random.html:

这个类的一个实例用于生成一个流 伪随机数。该类使用 48 位种子,已修改 使用线性同余公式。 (参见 Donald Knuth, 计算机编程,第 3 卷,第 3.2.1 节。)

如果 Random 的两个实例使用相同的种子创建,并且 为每个方法调用相同的序列,它们将生成和 返回相同的数字序列。 [...]

它是一个-随机数生成器。这意味着您实际上并没有掷骰子,而是使用公式根据当前随机值计算下一个“随机”值。为了创造随机化的错觉,使用了seed。种子是公式中用于生成随机值的第一个值。

显然java的随机实现(“公式”),不会连续生成超过16个偶数。

这种行为是seed 通常随时间初始化的原因。当你开始你的程序时,你会得到不同的结果。

这种方法的好处是您可以生成可重复的结果。例如,如果您有一个生成“随机”地图的游戏,您可以记住种子以重新生成相同的地图,如果您想再次玩它。

对于真正的随机数,某些操作系统提供特殊设备,这些设备会根据鼠标移动或网络流量等外部事件生成“随机性”。但是我不知道如何利用java。

来自 secureRandom 的 Java 文档:

许多 SecureRandom 实现都是伪随机的形式 数字生成器 (PRNG),这意味着它们使用确定性 算法从真正的随机种子产生伪随机序列。 其他实现可能会产生真正的随机数,还有其他的 可以结合使用这两种技术。

请注意,secureRandom 确实保证真实随机数。

为什么改变种子没有帮助

让我们假设随机数的范围只有 0-7。 现在我们使用以下公式生成下一个“随机”数:

 next = (current + 3) % 8

序列变为0 3 6 1 4 7 2 5

如果你现在拿到种子3,你所做的就是改变起点。

在这个只使用前一个值的简单实现中,每个值在序列环绕并重新开始之前可能只出现一次。否则会有一个无法到达的部分。

例如想象一下序列0 3 6 1 3 4 7 2 5。数字0,4,7,2 and 5 永远不会生成超过一次(根据种子的深度,它们可能永远不会生成),因为一旦序列循环 3,6,1,3,6,1,...。

简化的伪随机数生成器可以被认为是范围内所有数字的排列,您可以使用种子作为起点。如果它们更高级,则必须将排列替换为可能多次出现相同数字的列表。

更复杂的生成器可以有一个内部状态,允许相同的数字在序列中多次出现,因为状态让生成器知道从哪里继续。

【讨论】:

  • 是的,显然在实现中存在偏差 - 所以我想了解它为什么在哪里。例如,如果我使用 Math.abs(random.nextInt()) % upperBound 或者,上帝保佑,(int) (random.nextDouble() * upperBound) 它会按预期终止
  • 再一次,无论我使用什么seed(尝试了一堆),它都不会以upperBound=18结束
  • @AlexeyGrigorev:我不知道您是否在发表评论之前阅读了最后一节。如果您问为什么以这种方式实现它:不使用外部事件就无法生成真正的随机性。这就是为什么 random.org 存在的原因
【解决方案2】:

Random 的实现使用了一个简单的线性同余公式。这样的公式具有自然的周期性,并且在它们生成的序列中具有各种非随机模式。

您所看到的是这些模式之一的人工制品......没有故意的。这不是偏见的例子。而是auto-correlation 的一个例子。

如果您需要更好(更“随机”)的数字,那么您需要使用SecureRandom 而不是Random

“为什么以这种方式实现”的答案是“ ...性能。对Random.nextInt 的调用可以在数十或数百个时钟周期内完成。对 SecureRandom 的调用可能会慢至少 2 个数量级,甚至可能更多。

【讨论】:

    【解决方案3】:

    为了可移植性,Java 指定实现必须使用 java.util.Random 的劣质 LCG 方法。这种方法对于任何严重使用随机数(如复杂模拟或蒙特卡罗方法)是完全不可接受的。使用具有更好 PRNG 算法的附加库,例如 Marsaglia 的 MWC 或 KISS。 Mersenne Twister 和滞后斐波那契生成器通常也可以。

    我确信有这些算法的 Java 库。如果对您有用,我有一个带有 Java 绑定的 C 库:ojrandlib

    【讨论】: