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

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

В настоящее время я вычисляю 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-битными. У кого-нибудь есть предложения?

30

Решение

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

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

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

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

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

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

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

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

4

Другие решения

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

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

operator() из 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. Это позволяет PRNG звонить seed_seq::generate, которая производит несмещенную последовательность в диапазоне [0 ≤ я < 232), с выходным диапазоном, достаточно большим, чтобы заполнить все его состояние.

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, Тем не менее, я не уверен, что наилучшая практика для этого, что касается длины или источника элементов в начальной последовательности.

24

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

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

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

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

Как насчет…

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

1

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

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

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

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

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

0

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

На мой взгляд, вам нужен параллелизуемый генератор случайных чисел (ГСЧ). Это означает, что вам нужно только один ГСЧ, с которым вы работаете только один seed (который вы можете сгенерировать с помощью genSeed), а затем последовательность случайных чисел, которые обычно бывают образованы в последовательной среде, разделяется на X непересекающихся последовательностей; где X — количество потоков. Существует очень хорошая библиотека, которая предоставляет RNG такого типа в C ++, соответствует стандарту C ++ для RNG и называется 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)}. Этот метод называется разбиением блоков, так как вы разделяете последовательность на два последовательных блока.

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

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

0
По вопросам рекламы [email protected]