Случайное C ++ дает разные числа для одного и того же начального числа Mersenne Twister при использовании точности с плавающей точкой - PullRequest
0 голосов
/ 28 ноября 2018

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

При исследовании влияния числовой точности я столкнулся со следующей проблемой: Для того же семени Мерсенна Твистера std::uniform_real_distribution<float>(-1, 1) возвращает значения, отличные от std::uniform_real_distribution<double>(-1, 1) и std::uniform_real_distribution<long double>(-1, 1), так какследующий пример показывает:

#include <iomanip>
#include <iostream>
#include <random>

template < typename T >
void numbers( int seed ) {
  std::mt19937                        gen( seed );
  std::uniform_real_distribution< T > dis( -1, 1 );
  auto p = std::numeric_limits< T >::max_digits10;
  std::cout << std::setprecision( p ) << std::scientific << std::setw( p + 7 )
            << dis( gen ) << "\n"
            << std::setw( p + 7 ) << dis( gen ) << "\n"
            << std::setw( p + 7 ) << dis( gen ) << "\n"
            << "**********\n";
}

int main() {
  int seed = 123;
  numbers< float >( seed );
  numbers< double >( seed );
  numbers< long double >( seed );
}

Результат:

$ /usr/bin/clang++ -v
Apple LLVM version 10.0.0 (clang-1000.11.45.5)
Target: x86_64-apple-darwin18.2.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

$ /usr/bin/clang++ bug.cpp -std=c++17
$ ./a.out 
 3.929383755e-01
 4.259105921e-01
-4.277213216e-01
**********
 4.25910643160561708e-01
-1.43058149942132062e-01
 3.81769702875451866e-01
**********
 4.259106431605616525145e-01
-1.430581499421320209545e-01
 3.817697028754518623166e-01
**********

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

Видите ли вы такое же поведение в вашемкомпилятор?Есть ли причина для такого неожиданного (для меня) расхождения?

Подход

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

Подход, который я выберу для создания воспроизводимых прогонов, заключается в том, чтобы всегда генерировать значения с максимально возможной точностью и приводить их к более низкой точности по требованию (например, float x = y, где y равно double или long double, в зависимости от обстоятельств).

Ответы [ 3 ]

0 голосов
/ 28 ноября 2018

Инициализация генератора случайных чисел для конкретного начального числа будет определять последовательность случайных битов, которые он выдает.Однако вы не используете эти биты одинаково в каждом случае.std::uniform_real_distribution<double> имеет больший пробел, чем std::uniform_real_distribution<float> (при условии sizeof(double) > sizeof(float) на вашей платформе), поэтому ему потребуется потреблять большее количество случайных битов для генерации полностью равномерного распределения.

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

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

0 голосов
/ 29 ноября 2018

Просто добавьте к превосходному ответу @MaxLanghof с более подробной информацией:

Для двойного кода можно было бы сделать что-то вроде этого - сгенерировать целое число u64 и использовать его из 53 битов для создания числа с плавающей запятой вдоль линий

double r = (u64 >> 11) * (1.0 / (uint64_t(1) << 53));

Для длинных двойных, предполагая, что формат Intel 80бит, с 64-битной мантиссой, он будет делать примерно то же самое, получит 64 бита, вернет обратно длинный двойной.

long double r = u64 * (1.0 / (uint64_t(1) << 64)); // pseudocode

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

В случае с плавающей запятой 32 бит используются для создания одного поплавка

float r = (u32 >> 8) * (1.0f / (uint32_t(1) << 24));

32 бита случайности используются, и еще 32 бита используются для следующего числа,который вместе с порядком байтов делает второй float примерно таким же, как первый double / long double.

Ссылка: http://xoshiro.di.unimi.it/

0 голосов
/ 28 ноября 2018

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

Существует только два способа, которымиРеализация может удовлетворить ваши ожидания «того же алгоритма, следовательно, те же результаты (минус точность)»:

  1. std::uniform_real_distribution<long double>(-1, 1) только так случайна, как std::uniform_real_distribution<float>(-1, 1).Более того, первый имеет столько же возможных выходов, сколько и второй.Если последний может выдавать больше разных значений, чем первый, то ему нужно потреблять больше битов случайности из базового Twister Mersenne.Если он не может - ну, какой смысл его использовать (и как бы он все еще был «равномерным»)?

  2. std::uniform_real_distribution<float>(-1, 1) потребляет (и в основном отбрасывает) ровно столько битслучайность из лежащего в основе Mersenne Twister как std::uniform_real_distribution<long double>(-1, 1).Это было бы очень расточительно и неэффективно.

Поскольку ни одна из здравомыслящих реализаций не сделает ни одного из вышеперечисленных, std::uniform_real_distribution<long double>(-1, 1) продвинет базовый Twister Mersenne Twister на большее количество шагов, чем std::uniform_real_distribution<float>(-1, 1) для каждого сгенерированногочисло.Это, конечно, изменит ход случайных чисел.Это также объясняет, почему варианты long double и double относительно близки друг к другу: изначально они делят большую часть своих случайных битов (тогда как float, вероятно, требует намного меньше битов и, следовательно, расходится быстрее).

...