Подтвердить что ты не робот

Первое случайное число после setSeed в Java всегда похоже

Чтобы дать некоторый контекст, я написал базовую реализацию шума Perlin в Java, и когда дело дошло до внедрения посева, я столкнулся с ошибкой, которую я не мог объяснить.

Чтобы каждый раз генерировать одни и те же случайные весовые векторы для одного и того же семени независимо от того, какой уровень шума будет задан, и в каком порядке я создал новое семя (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() (я не проверял другие типы случайности) и/или предупреждать людей об этой проблеме.

Конечно, это может быть просто ошибка реализации от моего имени, и я был бы умен, если бы это было указано мне.

4b9b3361

Ответ 1

Класс Random предназначен для низкого источника служебных данных псевдослучайных чисел. Но следствием реализации "низкой накладной" является то, что числовой поток обладает свойствами, которые далеко не идеальны... со статистической точки зрения. Вы столкнулись с одним из недостатков. Random документируется как линейный конгруэнтный генератор, и свойства таких генераторов хорошо известны.

Существует множество способов борьбы с этим. Например, если вы будете осторожны, вы можете скрыть некоторые из наиболее очевидных "бедных" характеристик. (Но вам советуют запускать статистические тесты. Вы не можете видеть неслучайность в шуме, добавленном к вашему второму изображению, но он все еще может быть там.)

В качестве альтернативы, если вы хотите, чтобы псевдослучайные числа гарантировали хорошие статистические свойства, вы должны использовать SecureRandom вместо Random. Он имеет значительно более высокие накладные расходы, но вы можете быть уверены, что многие "умные люди" потратили бы много времени на разработку, тестирование и анализ алгоритмов.

Наконец, относительно просто создать подкласс Random, который использует альтернативный алгоритм для генерации чисел; см. ссылка. Проблема в том, что вам нужно выбрать (или разработать) и реализовать соответствующий алгоритм.


Вызов этой проблемы "" является спорным. Это хорошо известное и понятное свойство LCG, и использование LCG было разумным техническим выбором. Людям нужны низкие накладные PRNG, но низкие накладные PRNG имеют плохие свойства. TANSTAAFL.

Разумеется, это не то, что Oracle созерцало бы изменение в Random. Действительно, причины не меняются четко изложены в javadoc для класса Random.

"Чтобы гарантировать это свойство, для класса Random указаны конкретные алгоритмы. Реализации Java должны использовать все алгоритмы, показанные здесь для класса Random, для абсолютной переносимости кода Java."

Ответ 2

Это известная проблема. Подобное семя будет генерировать аналогичные несколько первых значений. Случайный не был предназначен для использования таким образом. Вы должны создать экземпляр с хорошим семенем и затем генерировать умеренно размерную последовательность "случайных" чисел.

Ваше текущее решение в порядке - пока оно выглядит хорошо и достаточно быстро. Вы также можете рассмотреть возможность использования функций хэширования/микширования, которые были разработаны для решения вашей проблемы (а затем, при необходимости, с использованием вывода в качестве семени). Например, см. Параметрическая случайная функция для генерации двумерного шума

Ответ 3

Извлеките setSeed из цикла. Java PRNG представляет собой линейный конгруэнтный генератор, поэтому гарантированное получение его последовательными значениями дает результаты, которые коррелируют между итерациями цикла.

ДОПОЛНЕНИЕ

Я отбросил это, прежде чем выбежать из двери на встречу, и теперь успею проиллюстрировать то, что я говорил выше.

Я написал небольшой Ruby script, который реализует мультипликативный линейный конгруэнтный генератор с множественным модулем 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 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, когда разрешено действовать самостоятельно, а правый набор показывает, что происходит, когда вы повторяетесь на каждой итерации.