Лучший способ посева mt19937 64 для моделирования Монте-Карло

Я работаю над программой, которая запускает моделирование Монте-Карло; в частности, я использую алгоритм Metropolis. Программа должна генерировать, возможно, миллиарды "случайных" чисел. Я знаю, что Mersenne twister очень популярен для моделирования Монте-Карло, но я хотел бы убедиться, что я засеваю генератор наилучшим образом.

В настоящее время я вычисляю 32-разрядное семя, используя следующий метод:

mt19937_64 prng; //pseudo random number generator
unsigned long seed; //store seed so that every run can follow the same sequence
unsigned char seed_count; //to help keep seeds from repeating because of temporal proximity

unsigned long genSeed() {
    return (  static_cast<unsigned long>(time(NULL))      << 16 )
         | ( (static_cast<unsigned long>(clock()) & 0xFF) << 8  )
         | ( (static_cast<unsigned long>(seed_count++) & 0xFF) );
}

//...

seed = genSeed();
prng.seed(seed);

Я чувствую, что есть много лучшие способы гарантировать неповторяющиеся новые семена, и я уверен, что mt19937_64 можно засеять более чем 32-битными. У кого-нибудь есть предложения?

6 ответов


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

  1. программа возобновлена на той же машине позже,
  2. два потока запускаются на одной машине одновременно,
  3. программа запускается на двух разных машинах одновременно.

1 решается с использованием времени с эпохи, 2 решается с глобальным atomic counter, 3 решается с зависимым от платформы id (см. как получить (почти) уникальный системный идентификатор кросс-платформенным способом?)

теперь дело в том, что это лучший способ объединить их, чтобы получить uint_fast64_t (тип семени std::mt19937_64)? Я предполагаю, что мы не знаем априори диапазон каждого параметра или что они слишком велики, поэтому мы не можем просто играть с битовыми сдвигами, получая уникальное семя тривиальным образом.

A std::seed_seq было бы легко так держать, однако его тип возврата uint_least32_t - не лучший выбор.

хороший 64-битный хашер-гораздо лучший выбор. В STL предложит std::hash под functional заголовок, возможность объединить в три выше цифры в строку и затем передает его на домработника. Тип возврата -size_t что на 64 машинах очень правоподобно для того чтобы соответствовать нашим требованиям.

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

A std::random_device может также использоваться для генерации семян (столкновения все еще могут произойти, трудно сказать, если более или менее часто), однако, поскольку реализация зависит от библиотеки и может спуститься к псевдослучайному генератору, необходимо проверить энтропию устройства и избежать использования устройства с нулевой энтропией для этой цели, поскольку вы, вероятно, сломаете точки выше (особенно пункт 3). К сожалению, вы можете обнаружить энтропию только тогда, когда вы берете программу на конкретную машину и тестируете с установленной библиотекой.


использовать std::random_device для создания семян. Он будет предоставлять недетерминированные случайные числа при условии, что ваша реализация поддерживает его. В противном случае разрешено использовать какой-то другой движок случайных чисел.

std::mt19937_64 prng;
seed = std::random_device{}();
prng.seed(seed);

operator() of std::random_device возвращает unsigned int, поэтому, если ваша платформа имеет 32-бит ints, и вы хотите 64-битное семя, вам нужно будет вызвать его дважды.

std::mt19937_64 prng;
std::random_device device;
seed = (static_cast<uint64_t>(device()) << 32) | device();
prng.seed(seed);

другой доступный вариант использует std::seed_seq для затравки ПГСЧ. Это позволяет PRNG вызывать seed_seq::generate, который производит необъективную последовательность в диапазоне [0 ≤ i 32), С рядом выхода большим достаточно для того чтобы заполнить свое все государство.

std::mt19937_64 prng;
std::random_device device;
std::seed_seq seq{device(), device(), device(), device()};
prng.seed(seq);

я вызываю random_device 4 раза, чтобы создать начальную последовательность 4 элемента для seed_seq. Однако я не уверен, какова наилучшая практика для этого, что касается длины или источника элементов в начальной последовательности.


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

единственная существенная проблема, которую я вижу с вашим текущим подходом, - это условие гонки: если вы собираетесь начать несколько симуляций одновременно, это должно быть сделано из отдельных потоков. Если это делается из отдельных потоков, вам необходимо обновить seed_count в a потокобезопасный способ или несколько симуляций могут закончиться одним и тем же seed_count. Вы можете просто сделать это std::atomic<int> чтобы решить эту проблему.

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

  1. при запуске программы возьмите текущее системное время (используя таймер с высоким разрешением) один раз и сохраните его.
  2. назначьте каждой симуляции уникальный идентификатор (это может просто будьте целым числом, инициализированным до 0 (которое должно быть сгенерировано без каких-либо условий гонки, как упоминалось выше), которое увеличивается каждый раз, когда начинается моделирование, эффективно, как ваш seed_count.
  3. при заполнении симуляции просто используйте изначально созданную метку времени + уникальный идентификатор. Если вы сделаете это, каждая симуляция в процессе будет гарантирована уникальным семенем.

Как насчет...

есть некоторый основной код, который запускает потоки, и есть копии функции, запущенной в этих потоках, каждая копия с собственным Marsenne Twister. Я прав? Если это так, почему бы не использовать другой случайный генератор в основном коде? Он будет засеян отметкой времени и отправит последовательные псевдослучайные числа в экземпляры функций в качестве их семян.


функция POSIX gettimeofday(2) дает время с точностью до микросекунд.

функция потока POSIX gettid(2) возвращает идентификатор текущего потока.

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

Если Вам также нужно, чтобы он был уникальным на нескольких машинах, вы также можете получить имя хоста, IP-адрес или MAC-адрес.

Я бы предположил, что 32 бита, вероятно, достаточно, поскольку доступно более 4 миллиардов уникальных семян. Если вы не используете миллиарды процессов, что кажется маловероятным, вы должны быть в порядке, не переходя на 64-битные семена.


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

на мой взгляд, вам нужен параллелизируемый генератор случайных чисел (RNG). Это значит, что ты только нужно один RNG, который вы создаете только с один seed (который вы можете генерировать с помощью своего генсека), а затем последовательность случайных чисел, которые обычно были бы геренированы в последовательной среде, разбивается на X неперекрывающихся последовательностей; где X-количество потоков. Существует очень хорошая библиотека, которая предоставляет этот тип RNGs в C++, следует стандарту C++ для RNGs и называется TRNG (http://numbercrunch.de/trng).

вот немного больше информации. Существует два способа достижения неперекрывающихся последовательностей в потоке. Предположим, что последовательность случайных чисел из один RNG - это r = {r (1), r(2), r(3),...} и у вас есть только две нити. Если вы заранее знаете, сколько случайных чисел вам понадобится на поток, скажем M, вы можете дать первый M последовательности r первому потоку, т. е. {r (1), r (2),..., r(M)}, и второй M во второй поток, то есть {r(M+1), r (M+2),...r (2M)}. Этот метод называется blocksplitting, так как вы разделяете последовательность на два последовательных блока.

второй способ-создать последовательность для первого потока как {r(1), r(3), r(5), ...} и для второго потока как {r(2), r(4), r(6),...}, что имеет то преимущество, что вам не нужно заранее знать, сколько случайных чисел вам понадобится в потоке. Это называется leapfroging.

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