使用类似种子时,为什么初始随机数相似?

我发现了使用Java的Random类生成随机数的奇怪之处。 基本上,如果使用近似种子(例如1到1000之间)创建多个Random对象,则每个生成器生成的第一个值几乎相同,但下一个值看起来很好(我没有进一步搜索)。

以下是两个第一个生成的双打,种子从0到9:

  • 0 0.730967787376657 0.24053641567148587
  • 1 0.7308781907032909 0.41008081149220166
  • 2 0.7311469360199058 0.9014476240300544
  • 3 0.731057369148862 0.07099203475193139
  • 4 0.7306094602878371 0.9187140138555101
  • 5 0.730519863614471 0.08825840967622589
  • 6 0.7307886238322471 0.5796252073129174
  • 7 0.7306990420600421 0.7491696031336331
  • 8 0.7302511331990172 0.5968915822372118
  • 9 0.7301615514268123 0.7664359929590888

从991到1000:

  • 991 0.7142160704801332 0.9453385235522973
  • 992 0.7109015598097105 0.21848118381994108
  • 993 0.7108119780375055 0.38802559454181795
  • 994 0.7110807233541204 0.8793923921785096
  • 995 0.7109911564830766 0.048936787999225295
  • 996 0.7105432327208906 0.896658767102804
  • 997 0.7104536509486856 0.0662031629235198
  • 998 0.7107223962653005 0.5575699754613725
  • 999 0.7106328293942568 0.7271143712820883
  • 1000 0.7101849056320707 0.574836350385667

这是一个图,显示了种子从0到100,000生成的第一个值。

基于种子生成的第一个随机双精度:

图片

我搜索了有关这方面的信息,但我没有看到任何涉及这个确切问题的信息。 我知道LCG算法有很多问题,但我不知道这个问题,我想知道这是否是一个已知问题。

而且,你知道这个问题是仅针对第一个值(或前几个值),还是更常见且使用密切种子应该避免?

谢谢。

下载和阅读Random源,以及一些关于伪随机生成器的论文,你将得到最好的服务,但这里是源的一些相关部分。 首先,有三个控制算法的常量参数:

 private final static long multiplier = 0x5DEECE66DL; private final static long addend = 0xBL; private final static long mask = (1L << 48) - 1; 

乘数可以达到大约2 ^ 34并且更改,掩码2 ^ 48 - 1,并且此加法的加数非常接近0。

使用种子创建Random时,构造函数调用setSeed

 synchronized public void setSeed(long seed) { seed = (seed ^ multiplier) & mask; this.seed.set(seed); haveNextNextGaussian = false; } 

你提供的种子非常接近于零,因此当两者被“或”在一起时,设置的初始种子值由multiplier控制。 在种子接近零的所有测试案例中,内部使用的种子大约为2 ^ 34; 但很容易看出,即使您提供了非常大的种子数,类似的用户提供的种子也会产生类似的内部种子。

最后一块是next(int)方法,它实际上根据当前种子生成所请求长度的随机整数,然后更新种子:

 protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed >>> (48 - bits)); } 

这称为“线性同余”伪随机生成器,这意味着它通过将当前种子乘以常数乘数然后添加常数加数(然后屏蔽以获取低48位,在这种情况下)来生成每个连续种子。 生成器的质量取决于乘数和加数的选择,但所有这些生成器的输出可以根据当前输入轻松预测,并且在重复之前有一段设定的时间(因此建议不要在敏感时使用它们)应用程序)。

你看到nextDouble给出类似种子的类似初始输出的原因是,因为下一个整数的计算只涉及乘法和加法,所以下一个整数的幅度不会受低位差异的影响。 计算下一个双精度涉及基于种子计算大整数并将其除以另一个(常数)大整数,并且结果的大小主要受整数幅度的影响。

重复计算下一个种子将放大种子低位的差异,因为常数乘法器的重复乘法,并且因为48位掩码每次抛出最高位,直到最终你看到的是什么看起来像甚至蔓延。

我不会称之为“问题”。

而且,你知道这个问题是仅针对第一个值(或前几个值),还是更常见且使用密切种子应该避免?

连续数字之间的相关模式是非加密PRNG的常见问题,这只是一种表现forms。 相关性(严格自相关)是算法背后的数学中固有的。 如果你想了解这一点,你应该首先阅读Knuth的计算机编程艺术第3章的相关部分。

如果你需要不可预测性,你应该使用随机的(真)随机种子…或让系统为你挑选一个“非常随机”的种子; 例如,使用no-args构造函数。 或者更好的是,使用真正的随机数源或加密质量的PRNG而不是Random


作为记录:

  1. javadoc(Java 7)没有指定Random()如何种子本身。
  2. Java 7 for Linux上的Random()实现是从纳秒时钟播种的,XORed带有’uniquifier’序列。 ‘uniquifier’序列是LCG,它使用不同的乘数,其状态是静态的。 这是为了避免种子的自相关…

这是伪随机种子的一种相当典型的行为 – 它们不需要提供完全不同的随机序列,它们只能保证如果使用相同的种子,您可以再次获得相同的序列。

行为发生的原因是PRNG的数学forms–Java使用线性同余生成器,因此您只是通过一轮线性同余生成器看到运行种子的结果。 这还不足以完全混淆所有位模式,因此您会看到类似种子的类似结果。

您最好的策略可能只是使用非常不同的种子 – 一种选择是通过散列您当前使用的种子值来获取这些种子。

通过制作随机种子(例如,使用System.currentTimeMillis()或System.nanoTime()上的一些数学函数进行种子生成),您可以获得更好的随机结果。 也可以在这里查看更多信息