Чому rand ()% 6 упереджений?


109

Читаючи, як використовувати std :: rand, я знайшов цей код на cppreference.com

int x = 7;
while(x > 6) 
    x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

Що не так з виразом справа? Спробував це, і він працює чудово.


24
Зауважте, що ще краще використовувати std::uniform_int_distributionдля кубиків
Caleth

1
@Caleth Так, це було просто зрозуміти, чому цей код був "неправильним" ..
yO_

15
Змінено "неправильно" на "упереджено"
Cubbi

3
rand()настільки погано в типових реалізаціях, ви також можете використовувати xkcd RNG . Так що це неправильно, оскільки він використовує rand().
CodesInChaos

3
Я написав цю річ (ну не коментар - це @Cubbi), і те, що я мав на увазі, було те , що пояснив відповідь Піта Бекера . (FYI, це в основному той же алгоритм, що і libstdc ++ 's uniform_int_distribution.)
ТК

Відповіді:


136

Існує дві проблеми rand() % 6( 1+не стосується жодної проблеми).

По-перше, як було зазначено декілька відповідей, якщо низькі біти rand()не є належним чином однаковими, результат оператора, що залишився, також не є рівномірним.

По-друге, якщо кількість розрізнених значень, отриманих rand()не кратним 6, то решта дасть більше низьких значень, ніж високі. Це вірно, навіть якщо rand()повертає ідеально розподілені значення.

Як крайній приклад, зробіть вигляд, що rand()виробляє однаково розподілені значення в діапазоні [0..6]. Якщо ви подивитеся на залишки для цих значень, коли rand()повертає значення в діапазоні [0..5], залишок створює рівномірно розподілені результати в діапазоні [0..5]. Коли rand()повертає 6, rand() % 6повертає 0 так само, як якщо rand()б повернувся 0. Отже, ви отримуєте розподіл у два рази більше 0, ніж будь-яке інше значення.

Друга - справжня проблема rand() % 6.

Спосіб уникнення цієї проблеми полягає в тому, щоб відкинути значення, які давали б неоднорідні дублікати. Ви обчислюєте найбільше кратне число 6, яке менше або дорівнює RAND_MAX, і кожного разу, коли rand()повертаєте значення, яке більше або дорівнює цьому кратному, ви відхиляєте його і знову викликаєте `rand (), скільки разів потрібно.

Так:

int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
    value = rand();

Це інша реалізація коду, про який йдеться, покликаного чіткіше показати, що відбувається.


2
Я пообіцяв хоча б одного чергового на цьому сайті підготувати документ про це, але вважаю, що вибірки та відхилення можуть призвести до великих моментів; наприклад, перевищення дисперсії.
Вірсавія

30
Я зробив графік, скільки упередженості вводить ця методика, якщо rand_max 32768, що є в деяких реалізаціях. ericlippert.com/2013/12/16/…
Ерік Ліпперт

2
@Bathsheba: це правда, що деякі функції відхилення можуть викликати це, але це просте відхилення перетворить рівномірний IID в інший рівномірний розподіл IID. Жоден біт не переносить, настільки незалежний, всі зразки використовують одне і те ж ідентичне відхилення та тривіальне, щоб показати однаковість. А вищі моменти рівномірної інтегральної випадкової величини повністю визначаються її діапазоном.
MSalters

4
@MSalters: Ваше перше речення правильне для справжнього генератора, не обов'язково істинне для псевдогенератора. Коли я вийду на пенсію, я збираюся написати документ про це.
Вірсавія

2
@Anthony Подумайте з точки зору кубиків. Ви хочете, щоб випадкове число було від 1 до 3, і у вас є лише стандартна 6-сторонна плашка. Ви можете отримати це, просто віднявши 3, якщо прокрутити 4-6. Але скажімо, замість цього ви хочете, щоб число було від 1 до 5. Якщо ви віднімете 5, коли ви згорнете 6, то ви отримаєте вдвічі більше 1, ніж будь-яке інше число. Це в основному те, що робить код cppreference. Правильне, що потрібно зробити, - це перезапустити 6. Ось що тут робить Піт: розділіть штамп, щоб було однакову кількість способів прокрутити кожне число та переписати будь-які числа, які не вписалися в парні поділи
Рей

19

Тут є приховані глибини:

  1. Використання малого uв RAND_MAX + 1u. RAND_MAXвизначається як intтип і часто є найбільш можливим int. Поведінка RAND_MAX + 1не буде визначеною в таких випадках, коли ви переповнюєте signedтип. Сила запису 1uвводить тип перетворення RAND_MAXдо unsigned, тому ухиляється від переповнення.

  2. Використання % 6 балончика (але в кожному застосуванні, що std::randя бачив ) не вводить будь-яких додаткових статистичних ухилів вище та за межі представленої альтернативи. Такі випадки, коли % 6небезпечно, - це випадки, коли генератор чисел має рівнини кореляції в бітах низького порядку, наприклад, досить відома реалізація IBM (в С) randв, я думаю, 1970-х роках, які перевели високий і низький біти як "остаточний процвітати ». Подальше врахування полягає в тому, що 6 - це дуже мала пор. RAND_MAX, тож буде мінімальний ефект, якщо RAND_MAXвін не кратний 6, що, мабуть, не є.

На закінчення, ці дні, завдяки своїй простежуваності, я б користувався % 6. Внести будь-які статистичні аномалії більше, ніж ті, які вводив сам генератор. Якщо ви все ще сумніваєтеся, протестуйте свій генератор, щоб перевірити, чи він має відповідні статистичні властивості для вашого випадку використання.


12
% 6створює упереджений результат, коли кількість чітких значень, генерованих rand()не, кратне 6. Принцип «Голуб-дірка» Зрозуміло, ухил невеликий, коли RAND_MAXвін значно більший за 6, але він є. А для більших діапазонів цілей ефект, звичайно, більший.
Піт Бекер

2
@ PeteBecker: Дійсно, я повинен це зробити чітко. Але зауважте, що ви також отримуєте лущення голубів, коли вибірки наближаються до RAND_MAX, завдяки ефектам усікання на цілі поділки.
Вірсавія

2
@Bathsheba, чи не такий ефект укорочення призводить до результату, що перевищує 6, і, таким чином, до повторного виконання всієї операції?
Герхард

1
@ Герхард: Правильно. Насправді це веде саме до результату x==7. bsically, ви поділяєте діапазон [0, RAND_MAX]на 7 піддіапазонів, 6 однакового розміру і один менший піддіапазон в кінці. Результати від останнього піддіапазону відкидаються. Досить очевидно, що таким чином у вас не може бути двох менших піддіапазонів.
MSalters

@MSalters: Дійсно. Але зауважте, що інший спосіб все ще страждає через усічення. Моя гіпотеза полягає в тому, що народне багатство останнього, оскільки статистичні підводні камені важче зрозуміти!
Вірсавія

13

Цей приклад коду ілюструє, що std::randце випадок застарілого вантажного культового балдердашу, який повинен змушувати піднімати брови кожного разу, коли ви його бачите.

Тут є кілька питань:

Люди, які підписують контракт, зазвичай припускають - навіть бідні нещасні душі, які не знають нічого кращого і не думають про це саме в цих термінах, - це randвибірки з рівномірного розподілу на цілі числа в 0, 1, 2,… RAND_MAX,, і кожен виклик дає незалежний зразок.

Перша проблема полягає в тому, що передбачуваний контракт, незалежні єдині випадкові вибірки в кожному виклику, насправді не є тим, що йдеться в документації, і на практиці реалізація історично не могла забезпечити навіть найпростіший симулякр незалежності. Наприклад, C99 § 7.20.2.1 " randФункція" говорить без деталізації:

randФункція обчислює послідовність псевдовипадкових чисел в діапазоні від 0 до RAND_MAX.

Це безглузде речення, оскільки псевдовипадковість - це властивість функції (або сімейства функцій ), а не цілого числа, але це не заважає навіть чиновникам ISO не зловживати мовою. Зрештою, єдині читачі, які б це засмутили, знають краще, ніж читати документацію, randбо бояться, що їх мозкові клітини розпадуться.

Типова історична реалізація на C працює так:

static unsigned int seed = 1;

static void
srand(unsigned int s)
{
    seed = s;
}

static unsigned int
rand(void)
{
    seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
    return (int)seed;
}

Це має прикрою властивістю те, що, хоча один зразок може бути рівномірно розподілений під рівномірним випадковим насінням (що залежить від конкретного значення RAND_MAX), він чергує між парними та непарними цілими числами в послідовних викликах - після

int a = rand();
int b = rand();

вираз (a & 1) ^ (b & 1)дає 1 зі 100% -ною ймовірністю, що не стосується незалежних випадкових вибірок у будь-якому розподілі, підтримуваному на парні та непарні числа. Таким чином, виник культовий культ, що слід відкидати біти низького порядку, щоб переслідувати невловимого звіра на «кращу випадковість». (Попередження спойлера: Це не технічний термін. Це ознака того, що чиюсь прозу ви читаєте, чи не знає, про що вони говорять, або вважає, що ви незрозумілі і до неї потрібно поблажливо.)

Друга проблема полягає в тому, що навіть якби кожен виклик робив вибірку незалежно від рівномірного випадкового розподілу на 0, 1, 2, ..., RAND_MAXрезультат rand() % 6не був би розподілений рівномірно в 0, 1, 2, 3, 4, 5, як штамп рол, якщо RAND_MAXце не відповідає -1 модулю 6. Простий контрприклад: Якщо RAND_MAX= 6, то з rand(), всі результати мають рівну ймовірність 1/7, але з rand() % 6, результат 0 має ймовірність 2/7, тоді як усі інші результати мають ймовірність 1/7 .

Правильний спосіб зробити це за допомогою вибірки відхилення: кілька разів малюйте незалежну рівномірну випадкову вибірку sз 0, 1, 2,… RAND_MAX, і відхиляйте (наприклад) результати 0, 1, 2,…, ((RAND_MAX + 1) % 6) - 1- якщо ви отримаєте один із ті, почніть спочатку; інакше врожайність s % 6.

unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
    continue;
return s % 6;

Таким чином, набір результатів, rand()які ми приймаємо, рівномірно ділиться на 6, і кожен можливий результат від s % 6цього отримується однаковою кількістю прийнятих результатів rand(), тому якщо rand()рівномірно розподілений, то так і є s. Кількість випробувань не обмежена , але очікувана кількість менше 2, а ймовірність успіху зростає в експоненціальній залежності від кількості випробувань.

Вибір яких Результати rand()відхиленим несуттєво, при умови , що ви на карту рівного числа їх з кожним цілим числом нижче 6. код на cppreference.com робить інший вибір, з - за першу проблему вище , що нічого не гарантовано про розподіл або незалежність виходів rand(), і на практиці біти низького порядку демонстрували шаблони, які не виглядають "досить випадковими" (не майте на увазі, що наступний вихід є детермінованою функцією попереднього).

Вправа для читача: Доведіть , що код на cppreference.com дає рівномірний розподіл на штампованих рулонах , якщо rand()дає рівномірний розподіл на 0, 1, 2, ..., RAND_MAX.

Вправа для читача: Чому ви можете віддати перевагу одній або іншій підмножині? Яке обчислення потрібно для кожного випробування у двох випадках?

Третя проблема полягає в тому, що насіннєвий простір настільки малий, що навіть якщо насіння є рівномірно розподіленим, супротивник, озброєний знаннями вашої програми та одним результатом, але не насінням, може легко передбачити насіння та подальші результати, через що вони здаються не такими випадковий зрештою. Тому навіть не думайте використовувати це для криптографії.

Ви можете піти на химерний переобладнаний маршрут та std::uniform_int_distributionклас C ++ 11 з відповідним випадковим пристроєм та вашим улюбленим випадковим двигуном, як колись популярний твістер Mersenne, std::mt19937щоб грати у кубики зі своїм чотирирічним двоюрідним братом, але навіть це не збирається бути придатним для створення криптографічного ключового матеріалу - і твістер Mersenne - це жахливий космічний гугл із багатокілобайтним станом, що сприймає загрозу в кеші вашого процесора з нецензурним часом настройки, тому це погано навіть для, наприклад , паралельних моделювань Монте-Карло з відтворювані дерева підрахунків; її популярність, швидше за все, виникає в основному від його привабливої ​​назви. Але ви можете використовувати його для прокатки іграшкових кісток, як цей приклад!

Інший підхід полягає у використанні простого генератора криптографічних псевдовипадкових чисел з невеликим станом, наприклад простого швидкого стирання клавіш PRNG , або просто потокового шифру, такого як AES-CTR або ChaCha20, якщо ви впевнені ( наприклад , у моделюванні в Монте-Карло для дослідження в природничих науках) про те, що передбачення минулих результатів не матиме несприятливих наслідків, якщо держава буде коли-небудь порушена.


4
"непристойний час установки" У будь-якому разі ви не повинні використовувати більше одного генератора випадкових чисел (за нитку), тому час установки буде амортизовано, якщо ваша програма не працює дуже довго.
JAB

2
Downvote BTW за те, що він не розуміє, що цикл у питанні робить такий самий відбір відхилення, абсолютно однакових (RAND_MAX + 1 )% 6значень. Не має значення, як підрозділити можливі результати. Ви можете відхилити їх з будь-якої точки діапазону [0, RAND_MAX), якщо розмір прийнятого діапазону кратний 6. Пекло, ви можете вирівняти відхилення будь-якого результату x>6і більше вам не знадобиться %6.
MSalters

12
Я не зовсім задоволений цією відповіддю. Штани можуть бути хорошими, але ти ведеш це в неправильному напрямку. Наприклад, ви скаржитеся, що "краща випадковість" не є технічним терміном і що це безглуздо. Це наполовину вірно. Так, це не технічний термін, але це цілком значуща стенограма в контексті. Звернутись на те, що користувачі такого терміна є неосвіченими чи зловмисними - це сама по собі одна з цих речей. "Добру випадковість" може бути дуже важко точно визначити, але досить просто зрозуміти, коли функція дає результати з кращими або гіршими властивостями випадковості.
Конрад Рудольф

3
Мені сподобалась ця відповідь. Це трохи нерозумно, але в ньому багато хорошої довідкової інформації. Майте на увазі, що фахівці РЕАЛЬНО використовують лише апаратні випадкові генератори, проблема в тому, що важко.
Tiger4Hire

10
Для мене це навпаки. Хоча він містить добру інформацію, це занадто велика сума зухвалих, щоб зустріти що-небудь, крім думки. Корисність убік.
Містер Лістер

2

Я не є досвідченим користувачем C ++ будь-якими способами, але мені було цікаво дізнатись, чи відповідають інші відповіді щодо std::rand()/((RAND_MAX + 1u)/6)менш упередженого, ніж 1+std::rand()%6насправді. Тому я написав тестову програму для підрахунку результатів для обох методів (я не писав C ++ у віках, будь ласка, перевірте це). Посилання для запуску коду знаходиться тут . Він також відтворюється так:

// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>

int main()
{
    std::srand(std::time(nullptr)); // use current time as seed for random generator

    // Roll the die 6000000 times using the supposedly unbiased method and keep track of the results

    int results[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

        results[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results[n] << ' ';
    }

    std::cout << "\n";


    // Roll the die 6000000 times using the supposedly biased method and keep track of the results

    int results_bias[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()%6;

        results_bias[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results_bias[n] << ' ';
    }
}

Потім я взяв результат цього і використав chisq.testфункцію R, щоб запустити тест Chi-квадрата, щоб побачити, чи результати значно відрізняються від очікуваних. У цьому питанні щодо обміну ставками детальніше описується тест чи-квадрата для перевірки справедливості померлого: Як я можу перевірити, чи є матриця справедливою? . Ось результати за кілька пробіжок:

> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )

> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 8.6168, df = 5, p-value = 0.1254

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 1.6034, df = 5, p-value = 0.9008

> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075   )
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.051, df = 5, p-value = 0.2169

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 4.319, df = 5, p-value = 0.5045

> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.9592, df = 5, p-value = 0.1585

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 2.8229, df = 5, p-value = 0.7273

У трьох пробігах, які я робив, значення р для обох методів завжди було більше типових значень альфа, використовуваних для перевірки значущості (0,05). Це означає, що ми не вважатимемо жодного з них упередженим. Цікаво, що нібито неупереджений метод має стабільно нижчі значення p, що свідчить про те, що він може бути фактично більш упередженим. Застереження будучи тим, що я зробив лише 3 пробіжки.

ОНОВЛЕННЯ: Поки я писав свою відповідь, Конрад Рудольф опублікував відповідь, яка використовує той самий підхід, але отримує зовсім інший результат. Я не маю репутації коментувати його відповідь, тому я збираюся виступити з цим тут. По-перше, головне, що код, який він використовує, використовує те саме насіння для генератора випадкових чисел кожного разу, коли він запускається. Якщо поміняти насіння, ви фактично отримаєте різноманітні результати. По-друге, якщо ви не зміните насіння, але зміните кількість випробувань, ви також отримаєте різноманітні результати. Спробуйте збільшити або зменшити на порядок, щоб побачити, що я маю на увазі. По-третє, відбувається деяке ціле укорочення або округлення, де очікувані значення не зовсім точні. Мабуть, недостатньо, щоб змінити значення, але воно є.

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


Ваша реалізація містить фатальний недолік через непорозуміння з вашого боку: цитований уривок не порівнюється rand()%6з rand()/(1+RAND_MAX)/6. Скоріше, це порівняння прямого взяття залишку з відбору проб відхилення (див. Інші відповіді для пояснення). Отже, ваш другий код неправильний ( whileцикл нічого не робить). У вашому статистичному тестуванні також є проблеми (ви не можете просто запустити повторення тесту на надійність, ви не виконали корекцію,…).
Конрад Рудольф

1
@KonradRudolph У мене немає коментаря, щоб коментувати вашу відповідь, тому я додав його як оновлення до моєї. У ваших також є фатальний недолік у тому, що трапляється використовувати набір насіння та кількість випробувань за кожний цикл, що дає помилковий результат. Якби ви повторювали повтори з різними насінням, ви, можливо, це зловили. Але так, ви правильні, поки цикл нічого не робить, але це також не змінює результати цього конкретного кодового блоку
анжама

Я насправді виконував повтори. Насіннє навмисно не встановлено, оскільки встановити випадкове насіння за допомогою std::srand(і не використовувати <random>) досить складно зробити стандартними стандартами, і я не хотів, щоб її складність відштовхувалась від решти коду. Це також не має значення для розрахунку: повторення тієї ж послідовності в моделюванні цілком прийнятно. Звичайно , різні насіння будуть давати різні результати, і деякі з них будуть незначними. Це цілком очікується, виходячи з того, як визначено значення p.
Конрад Рудольф

1
Щури, я помилився у своїх повторах; і ви маєте рацію, 95-й квантил повторень пробіжок досить близький до p = 0,05 - тобто саме того, чого ми очікували б тоді нульовим. Підсумовуючи, моя стандартна бібліотечна реалізація std::randдає надзвичайно хороші імітації викидання монет для d6 у межах діапазону випадкових насінин.
Конрад Рудольф

1
Статистичне значення - це лише одна частина історії. Ви маєте нульову гіпотезу (рівномірно розподілену) та альтернативну гіпотезу (зміщення модуля) - фактично сімейство альтернативних гіпотез, індексоване вибором RAND_MAX, яке визначає розмір ефекту модульного зміщення. Статистична значущість - це ймовірність під нульовою гіпотезою, що ви помилково її відкидаєте. Яка статистична потужність - ймовірність альтернативної гіпотези, що ваш тест правильно відкидає нульову гіпотезу? Ви б виявили rand() % 6цей спосіб, коли RAND_MAX = 2 ^ 31 - 1?
Squeamish Ossifrage

2

Генератор випадкових чисел можна подумати як про роботу над потоком двійкових цифр. Генератор перетворює потік у числа, розрізаючи його на шматки. Якщо std:randфункція працює з RAND_MAX32767, то вона використовує 15 біт у кожному фрагменті.

Коли беруть модулі числа від 0 до 32767 включно, то виявляється, що 5462 '0 і' 1, але лише 5461 '2, 3,' 4 і '5. Отже результат є необ’єктивним. Чим більше значення RAND_MAX, тим меншою буде упередженість, але це неможливо.

Що не є упередженим - це число в діапазоні [0 .. (2 ^ n) -1]. Можна генерувати (теоретично) краще число в діапазоні 0..5, витягуючи 3 біти, перетворюючи їх на ціле число в діапазоні 0..7 і відкидаючи 6 і 7.

Можна сподіватися, що кожен біт у потоці бітів має рівний шанс бути "0" або "1", незалежно від того, де він знаходиться в потоці або значення інших бітів. На практиці це надзвичайно важко. Багато різних реалізацій програмних PRNG пропонують різні компроміси між швидкістю та якістю. Лінійний конгруенційний генератор, такий як std::randнайшвидша швидкість за найнижчу якість. Криптографічний генератор пропонує найвищу якість з найнижчою швидкістю.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.