1,0 є дійсним висновок від std :: generator_canonical?


124

Я завжди думав, що випадкові числа будуть лежати між нулем і одиницею, без1 , тобто це числа з напіввідкритого інтервалу [0,1). Довідки про на cppreference.com з std::generate_canonicalпідтверджує це.

Однак, коли я запускаю таку програму:

#include <iostream>
#include <limits>
#include <random>

int main()
{
    std::mt19937 rng;

    std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    rng.seed(sequence);
    rng.discard(12 * 629143 + 6);

    float random = std::generate_canonical<float,
                   std::numeric_limits<float>::digits>(rng);

    if (random == 1.0f)
    {
        std::cout << "Bug!\n";
    }

    return 0;
}

Це дає мені такий вихід:

Bug!

тобто це генерує мене ідеально 1, що спричиняє проблеми в моїй інтеграції MC. Це правильна поведінка чи є помилка з мого боку? Це дає такий же вихід з G ++ 4.7.3

g++ -std=c++11 test.c && ./a.out

та клац 3.3

clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out

Якщо це правильна поведінка, як я можу уникнути 1?

Редагувати 1 : Здається, що G ++ від git страждає від тієї ж проблеми. Я на

commit baf369d7a57fb4d0d5897b02549c3517bb8800fd
Date:   Mon Sep 1 08:26:51 2014 +0000

і компіляція з ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.outдає такий же вихід, lddдає урожай

linux-vdso.so.1 (0x00007fff39d0d000)
libstdc++.so.6 => /home/cschwan/temp/prefix/lib64/libstdc++.so.6 (0x00007f123d785000)
libm.so.6 => /lib64/libm.so.6 (0x000000317ea00000)
libgcc_s.so.1 => /home/cschwan/temp/prefix/lib64/libgcc_s.so.1 (0x00007f123d54e000)
libc.so.6 => /lib64/libc.so.6 (0x000000317e600000)
/lib64/ld-linux-x86-64.so.2 (0x000000317e200000)

Редагувати 2 : Я повідомив про поведінку тут: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176

Редагування 3 : команда Clang, здається, знає про проблему: http://llvm.org/bugs/show_bug.cgi?id=18767


21
@David Lively 1.f == 1.fу всіх випадках (які всі випадки є? Я навіть не бачив змінних у 1.f == 1.f; тут є лише один випадок: 1.f == 1.fі це незмінно true). Будь ласка, не поширюйте цей міф далі. Порівняння з плаваючою точкою завжди точні.
Р. Мартіньо Фернандес

15
@DavidLively: Ні, це не так. Порівняння завжди точне. Це ваші операнди, можливо, не точні, якщо вони обчислені, а не буквальні.
Гонки легкості на орбіті

2
@Galik будь-яке додатне число нижче 1,0 - це дійсний результат. 1,0 - ні. Це так просто. Округлення не має значення: код отримує випадкове число і не виконує жодного округлення на ньому.
Р. Мартіньо Фернандес

7
@DavidLively він говорить, що існує лише одне значення, яке порівнюється з рівним 1,0. Це значення 1,0. Значення, близькі до 1,0, не порівнюються з рівними 1,0. Не має значення, що робить функція генерації: якщо вона повертає 1.0, вона порівнюється з рівним 1.0. Якщо він не повертає 1,0, він не порівнюється з рівним 1,0. Ваш приклад з використанням abs(random - 1.f) < numeric_limits<float>::epsilonперевірок, якщо результат близький до 1,0 , що абсолютно неправильно в цьому контексті: тут є числа, близькі до 1,0, які є дійсними результатами, а саме всі ті, які менші ніж 1,0.
Р. Мартіньо Фернандес

4
@Galik Так, буде впроваджено проблеми. Але ця проблема полягає в тому, щоб реалізатор впорався. Користувач ніколи не повинен бачити 1,0, а користувач повинен завжди бачити рівний розподіл всіх результатів.
Р. Мартіньо Фернандес

Відповіді:


121

Проблема полягає у відображенні від кодоменету std::mt19937( std::uint_fast32_t) до float; алгоритм, описаний стандартом, дає невірні результати (несумісні з його описом виходу алгоритму), коли відбувається втрата точності, якщо поточний режим округлення IEEE754 є не що інше, як нескінченність без круглості до негативу (зауважте, що за замовчуванням круглий -до найближчого).

Вихід 7549723-го mt19937 з вашим насінням становить 4294967257 ( 0xffffffd9u), який при округленні до 32-бітового поплавця дає 0x1p+32, що дорівнює максимальному значенню mt19937, 4294967295 ( 0xffffffffu), коли це також округляється до 32-бітного поплавця.

Стандарт міг би забезпечити правильну поведінку, якби було вказано, що при перетворенні з виходу URNG у RealTypeз generate_canonical, округлення слід проводити до негативної нескінченності; це дало б правильний результат у цьому випадку. Як QOI, було б добре для libstdc ++ внести цю зміну.

З цією зміною 1.0більше не буде генеруватися; натомість граничні значення 0x1.fffffep-Nдля 0 < N <= 8генеруватимуться частіше (приблизно 2^(8 - N - 32)за N, залежно від фактичного розподілу MT19937).

Я б рекомендував не використовувати floatз std::generate_canonicalбезпосередньо; скоріше генеруйте число у doubleта потім округляйте до негативної безмежності:

    double rd = std::generate_canonical<double,
        std::numeric_limits<float>::digits>(rng);
    float rf = rd;
    if (rf > rd) {
      rf = std::nextafter(rf, -std::numeric_limits<float>::infinity());
    }

Ця проблема може виникнути і при std::uniform_real_distribution<float>; рішення те саме, щоб спеціалізувати розподіл на результат doubleі округлювати його до негативної нескінченності в float.


2
Якість впровадження @user - все, що робить одну відповідну реалізацію кращою за іншу, наприклад, продуктивність, поведінка у кращих випадках, корисність повідомлень про помилки.
екатмур

2
@supercat: Щоб трохи відступити, насправді є вагомі причини, щоб спробувати зробити синусоїди функції максимально точними для малих кутів, наприклад, тому що невеликі помилки sin (x) можуть перетворитись на великі помилки sin (x) / x (які трапляється досить часто в реальних розрахунках), коли х близький до нуля. "Додаткова точність" біля кратних π, як правило, є лише побічним ефектом цього.
Ільмарі Каронен

1
@IlmariKaronen: Для досить малих кутів, sin (x) - просто x. Мій вигук при синусовій функції Java стосується кутів, близьких до кратних pi. Я б сказав, що 99% часу, коли код запитує sin(x), те, що він дійсно хоче, - це синус (π / Math.PI) разів x. Люди, що підтримують Java, наполягають на тому, що краще мати повільний математичний звичайний звіт про те, що синус Math.PI є різницею між π і Math.PI, ніж мати його повідомлення про значення, яке трохи менше, незважаючи на те, що в 99% застосувань це було б краще ...
supercat

3
@ecatmur Пропозиція; оновіть цю публікацію, щоб згадати, яка std::uniform_real_distribution<float>страждає від тієї ж проблеми, що є наслідком цього. (Щоб люди, які шукають uniform_real_distribution, отримали цю запитання).
ММ

1
@ecatmur, я не впевнений, чому ти хочеш обійти негативну нескінченність. Оскільки generate_canonicalслід генерувати число в діапазоні [0,1), а ми говоримо про помилку, коли воно час від часу генерує 1,0, чи не буде округлення до нуля таким же ефективним?
Маршалл Клоу

39

Згідно стандарту, 1.0це не дійсно.

C ++ 11 §26.5.7.2 Шаблон функції create_canonical

Кожна функція, створена з шаблону, описаного в цьому розділі 26.5.7.2, відображає результат однієї або декількох викликів поставленого рівномірного генератора випадкових чисел gна один член зазначеного RealType, таким чином, якщо значення g i, отримані за g, розподіляються рівномірно, Результати інстанцій t j , 0 ≤ t j <1 розподіляються максимально рівномірно, як зазначено нижче.


25
+1 Я не бачу жодних недоліків у програмі ОП, тому я називаю це помилкою libstdc ++ та libc ++ ... що саме по собі здається малоймовірним, але ми йдемо.
Гонки легкості на орбіті

-2

Я просто зіткнувся з подібним питанням із uniform_real_distribution , і ось, як я інтерпретую парсимонічне формулювання стандарту щодо цієї теми:

Стандарт завжди визначає математичні функції з точки зору математики , ніколи з точки зору IEEE з плаваючою точкою (тому що Стандарт все ще робить вигляд, що плаваюча точка не може означати плаваючої точки IEEE). Отже, щоразу, коли ви бачите математичні формулювання в Стандарті, це говорить про реальну математику , а не про IEEE.

Стандарт говорить, що обидва uniform_real_distribution<T>(0,1)(g)і generate_canonical<T,1000>(g)повинні повертати значення в напіввідкритому діапазоні [0,1). Але це математичні значення. Якщо ви візьмете реальне число в напіввідкритому діапазоні [0,1) і представляєте його як плаваючу крапку IEEE, ну значна частина часу вона буде округляти доT(1.0) .

Коли Tце float(24 біти мантіси), ми очікуємо побачити uniform_real_distribution<float>(0,1)(g) == 1.0fприблизно 1 в 2 ^ 25 разів. Мій експеримент з грубою силою з libc ++ підтверджує це очікування.

template<class F>
void test(long long N, const F& get_a_float) {
    int count = 0;
    for (long long i = 0; i < N; ++i) {
        float f = get_a_float();
        if (f == 1.0f) {
            ++count;
        }
    }
    printf("Expected %d '1.0' results; got %d in practice\n", (int)(N >> 25), count);
}

int main() {
    std::mt19937 g(std::random_device{}());
    auto N = (1uLL << 29);
    test(N, [&g]() { return std::uniform_real_distribution<float>(0,1)(g); });
    test(N, [&g]() { return std::generate_canonical<float, 32>(g); });
}

Приклад виводу:

Expected 16 '1.0' results; got 19 in practice
Expected 16 '1.0' results; got 11 in practice

Коли Tце double(53 біти мантіси), ми очікуємо побачитиuniform_real_distribution<double>(0,1)(g) == 1.0 приблизно 1 в 2 ^ 54 рази. У мене немає терпіння перевірити це очікування. :)

Я розумію, що така поведінка прекрасна. Це може образити наше почуття «напіввідчиненого-rangeness» , що розподіл вимагаючи повернути номера «менше , ніж 1,0» може в цифрах зворотних фактів, які дорівнюють до 1.0; але це два різні значення "1,0", бачите? Перший - математичний 1,0; друге - одноточне число з плаваючою комою IEEE 1.0. І нас вчать десятиліттями не порівнювати числа з плаваючою комою для точної рівності.

Який би алгоритм ви не вводили довільних чисел, не буде байдуже, якщо іноді виходить точно 1.0. З номером з плаваючою комою ви нічого не можете зробити, крім математичних операцій, і як тільки ви зробите якусь математичну операцію, ваш код повинен буде мати справу з округленням. Навіть якби ви могли законно припустити це generate_canonical<float,1000>(g) != 1.0f, ви все одно не зможете припустити цього generate_canonical<float,1000>(g) + 1.0f != 2.0f- через округлення. Ви просто не можете від нього піти; то чому б ми робили вигляд у цьому єдиному екземплярі, що ви можете?


2
Я категорично не згоден з цим поглядом. Якщо стандарт диктує значення з напіввідкритого інтервалу, а реалізація порушує це правило, реалізація неправильна. На жаль, як правильно вказав у своїй відповіді екатмур, стандарт також диктує алгоритм, який має помилку. Це також офіційно визнано тут: open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan: Моя інтерпретація полягає в тому, що реалізація не порушує правила. Стандарт диктує значення з [0,1); реалізація повертає значення з [0,1); деякі з цих значень трапляються до IEEE, 1.0fале це просто неминуче, коли ви передаваєте їх на IEEE floats. Якщо ви хочете отримати чисті математичні результати, використовуйте символічну обчислювальну систему; якщо ви намагаєтесь використовувати IEEE з плаваючою точкою для представлення чисел, що знаходяться в межах eps1, ви перебуваєте в гріху.
Квоксплузон

Гіпотетичний приклад, який би зламав ця помилка: розділіть щось по canonical - 1.0f. Для кожного репрезентативного поплавця в [0, 1.0), x-1.0fне дорівнює нулю. Точно з 1,0f ви можете отримати ділення на нуль замість просто крихітного дільника.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.