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

Почему исходные случайные числа аналогичны при использовании похожих семян?

Я обнаружил что-то странное с генерацией случайных чисел, используя класс Java Random. В принципе, если вы создаете несколько объектов Random с использованием близких семян (например, от 1 до 1000), первое значение, генерируемое каждым генератором, будет почти таким же, но следующие значения выглядят нормально (я не искал дальше).

Вот два первых сгенерированных двойника с семенами от 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.

Первый случайный двойной сгенерированный на основе семени:

image

Я искал информацию об этом, но я ничего не видел в этой конкретной проблеме. Я знаю, что есть много проблем с алгоритмами LCG, но я не знал об этом, и мне было интересно, если это была известная проблема.

А также, знаете ли вы, следует ли избегать этой проблемы только для первого значения (или первых нескольких значений), или если оно более общее и использование закрытых семян?

Спасибо.

4b9b3361

Ответ 1

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

Ответ 2

Я бы не назвал это "проблемой".

А также, знаете ли вы, следует ли избегать этой проблемы только для первого значения (или первых нескольких значений), или если оно более общее и использование закрытых семян?

Корреляционные шаблоны между последовательными числами - общая проблема с некриптовыми PRNG, и это всего лишь одно проявление. Корреляция (строго автокорреляция) присуща математике, лежащей в основе алгоритма (ов). Если вы хотите это понять, вам, вероятно, следует начать с изучения соответствующей части книги Knuth Art of Computer Programming Chapter 3.

Если вам нужна непредсказуемость, вы должны использовать (истинное) случайное семя для Random... или позволить системе выбрать "довольно случайный" для вас; например используя конструктор no-args. Или еще лучше, используйте реальный источник случайных чисел или PRNG криптовариантного качества вместо Random.


Для записи:

  • В javadoc (Java 7) не указывается, как сами семантики Random().
  • Реализация Random() на Java 7 для Linux высевается из наносекундных часов, XORed с последовательностью "uniquifier". Последовательность "uniquifier" представляет собой LCG, который использует разный множитель и состояние которого статично. Это сделано для того, чтобы избежать автоматической корреляции семян...

Ответ 3

Это довольно типичное поведение для псевдослучайных семян - они не обязаны предоставлять совершенно разные случайные последовательности, они дают гарантию, что вы можете снова получить такую ​​же последовательность, если используете одно и то же семя.

Поведение происходит из-за математической формы PRNG - Java использует линейный конгруэнтный генератор, поэтому вы просто видите результаты, запускающие семя через один раунд линейного конгруэнтного генератора. Этого недостаточно, чтобы полностью перепутать все битовые шаблоны, поэтому вы видите похожие результаты для похожих семян.

Ваша лучшая стратегия, вероятно, состоит в том, чтобы использовать очень разные семена - одним из вариантов было бы получить их путем хэширования начальных значений, которые вы используете в настоящее время.

Ответ 4

Производя случайные семена (например, используя некоторые математические функции в System.currentTimeMillis() или System.nanoTime() для генерации семян), вы можете получить лучший случайный результат. Также можно посмотреть здесь для получения дополнительной информации