Java中setSeed之后的第一个随机数总是相似的
Posted
技术标签:
【中文标题】Java中setSeed之后的第一个随机数总是相似的【英文标题】:First random number after setSeed in Java always similar 【发布时间】:2015-03-01 20:45:03 【问题描述】:为了提供一些上下文,我一直在用 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
是矢量数学的自制类。
但是,在运行时,程序会产生非常糟糕的噪音:
经过一番挖掘,我发现每个向量的第一个元素非常相似(因此每个 setSeed()
调用之后的第一个 nextDouble()
调用)导致向量网格中每个向量的第一个元素相似。
这可以通过运行来证明:
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);
导致:
我想知道为什么在第一次调用 nextDouble()
(我没有检查其他类型的随机性)时会出现这种糟糕的变化和/或提醒人们注意这个问题。
当然,这可能只是代表我的一个实施错误,如果有人向我指出,我将非常感激。
【问题讨论】:
@geert3 是的,我很想这样做,但不幸的是,由于可以按任何顺序调用不同的坐标值,我需要一种方法来为每个排列可靠地返回相同的数字序列调用顺序,因此必须每次设置种子以选择正确的伪随机数序列。 我个人会使用 SipHash,以种子为键,坐标为输入。 @CodesInChaos 是的,我现在继续使用散列,这提高了随机性,并且由于散列表,大大提高了性能。感谢班塔尔。 作为一般说明:种子应该设置一次和一次。 请注意,.NETRandom
有 similar issues。
【参考方案1】:
Random
类被设计为伪随机数的低开销源。但是“低开销”实现的结果是数字流具有远非完美的属性......从统计角度来看。您遇到了其中一种缺陷。 Random
被记录为线性同余生成器,并且此类生成器的属性众所周知。
有多种方法可以解决此问题。例如,如果你小心,你可以隐藏一些最明显的“差”特征。 (但建议您进行一些统计测试。您看不到添加到第二张图像的噪声中的非随机性,但它可能仍然存在。)
或者,如果您想要保证良好统计特性的伪随机数,那么您应该使用 SecureRandom
而不是 Random
。它的开销显着增加,但您可以放心,许多“聪明人”会在算法的设计、测试和分析上花费大量时间。
最后,创建一个Random
的子类相对简单,它使用另一种算法来生成数字;见link。问题是您必须选择(或设计)并实施适当的算法。
将此称为“问题”值得商榷。这是 LCG 的众所周知和理解的特性,使用 LCG 是明智的工程选择。人们想要低开销的 PRNG,但低开销的 PRNG 的属性很差。坦斯塔夫。
当然,Oracle 不会考虑在Random
中更改这一点。确实,Random
类的javadoc 中明确说明了不更改的原因。
“为了保证这个属性,为类
Random
指定了特定的算法。Java实现必须使用这里为类Random
显示的所有算法,以保证Java代码的绝对可移植性。”
【讨论】:
“天下没有免费的午餐” “总有不是行聚合的数字流。” 公平地说,现在有更好的随机算法,它们不仅速度更快,而且具有更好的属性。您链接到的 xorshift 可能是一个合理的选择,尽管我会选择 128bit version,因为它更快、具有更好的属性并且仍然只需要少量内存。【参考方案2】:这是已知问题。相似的种子将产生相似的少数第一值。 Random 并不是真正设计用于这种方式的。您应该使用良好的种子创建实例,然后生成大小适中的“随机”数字序列。
您当前的解决方案是可以的 - 只要它看起来不错并且足够快。您还可以考虑使用旨在解决您的问题的散列/混合函数(然后,可以选择使用输出作为种子)。例如参见:Parametric Random Function For 2D Noise Generation
【讨论】:
【参考方案3】:将您的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 在允许其自行进行时的自然进程,而右列显示当您在每次迭代中重新设置种子时会发生什么。
【讨论】:
啊哈!虽然我将setSeed
移出循环不是我的噪声生成代码中的一个选项(请参阅我在问题的 cmets 中对 geert3 的解释),但 PRNG 是线性同余的事实确实解释了它生成的序列的这一方面。谢谢!以上是关于Java中setSeed之后的第一个随机数总是相似的的主要内容,如果未能解决你的问题,请参考以下文章