Java中setSeed之后的第一个随机数总是相似的

时间:2015-01-03 22:58:17

标签: java random noise

为了给出一些背景信息,我一直在用Java编写一个基本的Perlin噪声实现,当实现播种时,我遇到了一个我无法解释的错误。

每次为同一种子生成相同的随机权重向量,无论哪一组坐标是什么?我们根据原始种子和权重向量坐标的组合生成了一个新的种子(newSeed),并将其用作权重随机化的种子,查询噪声级别。矢量运行:

rnd.setSeed(newSeed);
weight = new NVector(2);
weight.setElement(0, rnd.nextDouble() * 2 - 1);
weight.setElement(1, rnd.nextDouble() * 2 - 1);
weight.normalize()

NVector是一个自制的矢量数学类。

然而,在运行时,程序产生了非常糟糕的噪音: Very bad noise, with vertical streaks

经过一番挖掘后,我发现每个向量的第一个元素非常相似(因此每个nextDouble()调用后的第一个setSeed()调用)导致向量中每个向量的第一个元素网格相似。

这可以通过运行来证明:

long seed = Long.valueOf(args[0]);
int loops = Integer.valueOf(args[1]);
double avgFirst = 0.0, avgSecond = 0.0, avgThird = 0.0;
double lastfirst = 0.0, lastSecond = 0.0, lastThird = 0.0;
for(int i = 0; i<loops; i++)
{
    ran.setSeed(seed + i);
    double first = ran.nextDouble();
    double second = ran.nextDouble();
    double third = ran.nextDouble();
    avgFirst += Math.abs(first - lastfirst);
    avgSecond += Math.abs(second - lastSecond);
    avgThird += Math.abs(third - lastThird);
    lastfirst = first;
    lastSecond = second;
    lastThird = third;
}
System.out.println("Average first difference.: " + avgFirst/loops);
System.out.println("Average second Difference: " + avgSecond/loops);
System.out.println("Average third Difference.: " + avgSecond/loops);

在程序参数指定的种子范围内调用setSeed()方法后,生成第一,第二和第三随机数之间的平均差异;这对我来说是这些结果:

C:\java Test 462454356345 10000
Average first difference.: 7.44638117976783E-4
Average second Difference: 0.34131692827329957
Average third Difference.: 0.34131692827329957

C:\java Test 46245445 10000
Average first difference.: 0.0017196011123287126
Average second Difference: 0.3416750057190849
Average third Difference.: 0.3416750057190849

C:\java Test 1 10000
Average first difference.: 0.0021601598225344998
Average second Difference: 0.3409914232342002
Average third Difference.: 0.3409914232342002

在这里你可以看到第一个平均差异明显小于其余的差异,并且随着种子数量的增加似乎在减少。

因此,通过在设置权重向量之前向nextDouble()添加一个简单的虚拟调用,我能够修复我的perlin噪声实现:

rnd.setSeed(newSeed);
rnd.nextDouble();
weight.setElement(0, rnd.nextDouble() * 2 - 1);
weight.setElement(1, rnd.nextDouble() * 2 - 1);

导致:

enter image description here

我想知道为什么第一次调用nextDouble()(我没有检查其他类型的随机性)的这种不良变化发生和/或提醒人们注意这个问题。

当然,这可能只是我的实施错误,如果向我指出,我会很高兴。

3 个答案:

答案 0 :(得分:29)

Random 类旨在成为伪随机数的低开销源。但是低开销的结果是#34;实现是从统计角度来看,数字流具有很长的完美特性。你遇到了一个不完美的地方。 Random被记录为线性同余生成器,这些生成器的属性是众所周知的。

有多种方法可以解决这个问题。例如,如果你小心,你可以隐藏一些最明显的&#34;穷人&#34;特点。 (但是你会被建议进行一些统计测试。你不能看到第二张图像中添加的噪声中的非随机性,但它仍然存在。)

或者,如果您想要保证良好统计属性的伪随机数,那么您应该使用 SecureRandom 而不是 Random 。它具有明显更高的管理费用,但你可以放心,许多聪明的人&#34;将花费大量时间进行算法的设计,测试和分析。

最后,创建一个使用替代算法生成数字的Random子类相对简单;见link。问题是您必须选择(或设计)并实施适当的算法。


将此称为&#34; 问题&#34;值得商榷。这是LCG的众所周知和理解的属性,并且LCG的使用是一种有意义的工程选择。人们想要低开销的PRNG,但低开销的PRNG具有较差的属性。 TANSTAAFL。

当然,这不是Oracle在Random中考虑改变的事情。实际上,Random类的javadoc清楚地说明了不改变的原因。

  

&#34;为了保证此属性,为类Random指定了特定的算法。为了Java代码的绝对可移植性,Java实现必须使用此处显示的所有算法Random。&#34;

答案 1 :(得分:11)

这是已知问题。类似的种子将产生类似的少数第一个值。随机并没有真正被设计成以这种方式使用。您应该使用良好的种子创建实例,然后生成中等大小的序列&#34;随机&#34;号。

您目前的解决方案还可以 - 只要它看起来不错并且速度足够快。您还可以考虑使用散列/混合函数来解决您的问题(然后,可选地,使用输出作为种子)。例如,请参阅:Parametric Random Function For 2D Noise Generation

答案 2 :(得分:7)

setSeed移出循环。 Java的PRNG是一个线性同余生成器,因此保证用连续值对其进行播种,可以得到在循环迭代中相关的结果。

<强>附录

在跑出会议大门之前我把它打破了,现在有时间说明我上面说的话。

我写了一个Ruby脚本,它实现了Schrage的便携式素数模乘法线性同余生成器。我实例化了两个LCG副本,两个都是值为1.但是,在输出循环的每次迭代中,我根据循环索引重新设置了第二个副本。这是代码:

# Implementation of a Linear Congruential Generator (LCG)
class LCG
  attr_reader :state
  M = (1 << 31) - 1    # Modulus = 2**31 - 1, which is prime

  # constructor requires setting a seed value to use as initial state
  def initialize(seed)
    reseed(seed)
  end

  # users can explicitly reset the seed.
  def reseed(seed)
    @state = seed.to_i
  end

  # Schrage's portable prime modulus multiplicative LCG
  def value
    @state = 16807 * @state % M
    # return the generated integer value AND its U(0,1) mapping as an array
    [@state, @state.to_f / M]
  end
end

if __FILE__ == $0
  # create two instances of LCG, both initially seeded with 1
  mylcg1 = LCG.new(1)
  mylcg2 = LCG.new(1)
  puts "   default progression     manual reseeding"
  10.times do |n|
    mylcg2.reseed(1 + n)  # explicitly reseed 2nd LCG based on loop index
    printf "%d %11d %f %11d %f\n", n, *mylcg1.value, *mylcg2.value
  end
end

以及它产生的输出:

   default progression     manual reseeding
0       16807 0.000008       16807 0.000008
1   282475249 0.131538       33614 0.000016
2  1622650073 0.755605       50421 0.000023
3   984943658 0.458650       67228 0.000031
4  1144108930 0.532767       84035 0.000039
5   470211272 0.218959      100842 0.000047
6   101027544 0.047045      117649 0.000055
7  1457850878 0.678865      134456 0.000063
8  1458777923 0.679296      151263 0.000070
9  2007237709 0.934693      168070 0.000078

列是迭代次数,后跟LCG生成的基础整数,以及缩放到范围(0,1)时的结果。左边的列显示了LCG在自己进行时的自然进展,而右边的集显示了每次迭代重新设置时会发生什么。

相关问题