Czytając, jak używać std :: rand, znalazłem ten kod na cppreference.com
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
Co jest nie tak z wyrażeniem po prawej stronie? Wypróbowałem i działa idealnie.
std::uniform_int_distribution
do gry w kościrand()
jest tak zły w typowych implementacjach, że równie dobrze możesz użyć xkcd RNG . Więc to jest złe, ponieważ używarand()
.uniform_int_distribution
.)Odpowiedzi:
Istnieją dwa problemy z
rand() % 6
(1+
nie dotyczy żadnego problemu).Po pierwsze, jak wskazało kilka odpowiedzi, jeśli niskie bity
rand()
nie są odpowiednio jednorodne, wynik operatora reszty również nie jest jednolity.Po drugie, jeśli liczba odrębnych wartości utworzonych przez
rand()
nie jest wielokrotnością 6, to reszta da więcej wartości niskich niż wysokich. To prawda, nawet jeślirand()
zwraca idealnie rozłożone wartości.Jako skrajny przykład udawaj, że
rand()
generuje równomiernie rozłożone wartości w zakresie[0..6]
. Jeśli spojrzysz na reszty dla tych wartości, gdyrand()
zwrócisz wartość z zakresu[0..5]
, reszta daje równomiernie rozłożone wyniki w zakresie[0..5]
. Kiedyrand()
zwraca 6,rand() % 6
zwraca 0, tak jakbyrand()
zwróciło 0. W ten sposób otrzymujesz rozkład z dwukrotnie większą liczbą zer niż każda inna wartość.Drugi to prawdziwy problem
rand() % 6
.Sposobem na uniknięcie tego problemu jest odrzucenie wartości, które powodowałyby niejednorodne duplikaty. Obliczasz największą wielokrotność liczby 6, która jest mniejsza lub równa
RAND_MAX
, i za każdym razem, gdyrand()
zwraca wartość, która jest większa lub równa tej wielokrotności, odrzucasz ją i ponownie wywołujesz `rand () tyle razy, ile potrzeba.Więc:
To inna implementacja omawianego kodu, mająca na celu wyraźniejsze pokazanie, co się dzieje.
źródło
Są tu ukryte głębiny:
Zastosowanie małych plików
u
wRAND_MAX + 1u
.RAND_MAX
jest zdefiniowany jakoint
typ i często jest największy z możliwychint
. ZachowanieRAND_MAX + 1
byłoby niezdefiniowane w takich przypadkach, gdy przepełnieniesigned
typu. Pisanie1u
wymusza konwersję typuRAND_MAX
naunsigned
, zapobiegając w ten sposób przepełnieniu.Użycie
% 6
can (ale w każdej implementacjistd::rand
, którą widziałem , nie wprowadza żadnych dodatkowych błędów statystycznych poza przedstawioną alternatywą). Takie sytuacje, w których% 6
jest niebezpieczny, to przypadki, w których generator liczb ma równiny korelacji w bitach niskiego rzędu, takie jak dość znana implementacja IBM (w języku C) zrand
, jak sądzę, lat siedemdziesiątych XX wieku, która odwróciła wysokie i niskie bity jako „ostateczne zakrętas". Kolejną kwestią jest to, że 6 jest bardzo małe, por.RAND_MAX
, więc efekt będzie minimalny, jeśliRAND_MAX
nie będzie wielokrotnością liczby 6, co prawdopodobnie nie jest.Podsumowując, ostatnio, ze względu na jego podatność, użyłbym
% 6
. Nie jest prawdopodobne, aby wprowadził jakiekolwiek anomalie statystyczne poza tymi, które wprowadza sam generator. Jeśli nadal masz wątpliwości, przetestuj swój generator, aby sprawdzić, czy ma on odpowiednie właściwości statystyczne dla Twojego przypadku użycia.źródło
% 6
daje wynik tendencyjny, ilekroć liczba odrębnych wartości wygenerowanych przezrand()
nie jest wielokrotnością liczby 6. Zasada gołębia. To prawda, że odchylenie jest małe, gdyRAND_MAX
jest znacznie większe niż 6, ale istnieje. A w przypadku większych zakresów efekt jest oczywiście większy.x==7
. Zasadniczo zakres jest podzielony na[0, RAND_MAX]
7 podzakresów, 6 o tej samej wielkości i jeden mniejszy na końcu. Wyniki z ostatniego podzakresu są odrzucane. Jest dość oczywiste, że w ten sposób nie można mieć na końcu dwóch mniejszych podzakresów.Ten przykładowy kod pokazuje, że
std::rand
jest to przypadek legendarnego baldaszka kultowego cargo, który powinien podnosić brwi za każdym razem, gdy go widzisz.Jest tu kilka problemów:
Ludzie kontraktowi zwykle zakładają - nawet biedne, nieszczęsne dusze, które nie wiedzą nic lepszego i nie będą myśleć o tym dokładnie w ten sposób - są takie, że
rand
próbki z jednolitego rozkładu liczb całkowitych w 0, 1, 2,…RAND_MAX
,, a każde wywołanie daje niezależną próbkę.Pierwszy problem polega na tym, że założona umowa, niezależne, jednolite losowe próbki w każdym zaproszeniu, nie jest w rzeczywistości tym, co mówi dokumentacja - aw praktyce historycznie wdrożenia nie zapewniły nawet najdrobniejszego symulakru niezależności. Na przykład C99 §7.20.2.1 „
rand
Funkcja” mówi bez rozwinięcia:To bezsensowne zdanie, ponieważ pseudolosowość jest właściwością funkcji (lub rodziny funkcji ), a nie liczby całkowitej, ale to nie powstrzymuje nawet biurokratów ISO przed nadużywaniem języka. W końcu jedyni czytelnicy, którzy byliby tym zdenerwowani, wiedzą, że lepiej nie czytać dokumentacji
rand
ze strachu przed rozpadem ich komórek mózgowych.Typowa historyczna implementacja w C działa tak:
Ma to niefortunną właściwość, że nawet jeśli pojedyncza próbka może być równomiernie rozłożona w ramach jednolitego losowego ziarna (co zależy od określonej wartości
RAND_MAX
), naprzemiennie zmienia się między parzystymi i nieparzystymi liczbami całkowitymi w kolejnych wywołaniach - powyrażenie
(a & 1) ^ (b & 1)
daje 1 ze 100% prawdopodobieństwem, co nie ma miejsca w przypadku niezależnych prób losowych w dowolnym rozkładzie obsługiwanym przez parzyste i nieparzyste liczby całkowite. W ten sposób pojawił się kult cargo, w którym należy odrzucić mniej znaczące bity, aby ścigać nieuchwytną bestię o „lepszej losowości”. (Uwaga spoilera: to nie jest termin techniczny. To znak, że ktokolwiek czytasz prozę, albo nie wie, o czym mówi, albo myśli , że nie masz pojęcia i musi być protekcjonalny.)Drugi problem polega na tym, że nawet gdyby każde wywołanie próbowało niezależnie od jednolitego losowego rozkładu na 0, 1, 2,…
RAND_MAX
,, wynikrand() % 6
nie byłby równomiernie rozłożony na 0, 1, 2, 3, 4, 5 jak kostka rzuć, chyba żeRAND_MAX
jest przystająca do -1 modulo 6. Prosty kontrprzykład: JeśliRAND_MAX
= 6, to zrand()
, wszystkie wyniki mają równe prawdopodobieństwo 1/7, ale zrand() % 6
, wynik 0 ma prawdopodobieństwo 2/7, podczas gdy wszystkie inne wyniki mają prawdopodobieństwo 1/7 .Właściwym sposobem na to jest próbkowanie odrzucenia: wielokrotnie losuj niezależną, jednolitą próbkę losową
s
z 0, 1, 2,…RAND_MAX
i odrzucaj (na przykład) wyniki 0, 1, 2,… -((RAND_MAX + 1) % 6) - 1
jeśli otrzymasz jeden z te, zacznij od nowa; w przeciwnym razie wydajnośćs % 6
.W ten sposób zbiór wyników z
rand()
, który akceptujemy, jest podzielny po równo przez 6, a każdy możliwy wynik zs % 6
jest uzyskiwany przez tę samą liczbę zaakceptowanych wyników zrand()
, więc jeślirand()
jest równomiernie rozłożony, to tak jests
. Nie ma ograniczeń co do liczby prób, ale oczekiwana liczba jest mniejsza niż 2, a prawdopodobieństwo sukcesu rośnie wykładniczo wraz z liczbą prób.Wybór , który Efekty o
rand()
odrzuceniu ma znaczenia, pod warunkiem, że mapa równą liczbę nich do każdej liczby całkowitej poniżej 6. Kod na cppreference.com sprawia, że inny wybór, bo od pierwszego problemu wyżej, że nic nie jest gwarantowane o dystrybucja lub niezależność wynikówrand()
, aw praktyce bity niskiego rzędu wykazywały wzorce, które nie „wyglądają wystarczająco losowo” (nie wspominając o tym, że następny wynik jest deterministyczną funkcją poprzedniego).Ćwiczeń dla czytelnika: udowodnić, że kod w cppreference.com uzyskuje się równomierne rozprowadzenie na matrycy rolek, jeżeli
rand()
wydajność rozkład jednolity o 0, 1, 2, ...,RAND_MAX
.Ćwiczenie dla czytelnika: Dlaczego wolałbyś odrzucić jeden lub drugi podzbiór? Jakie obliczenia są potrzebne dla każdego procesu w dwóch przypadkach?
Trzeci problem polega na tym, że przestrzeń nasienna jest tak mała, że nawet jeśli ziarno jest równomiernie rozłożone, przeciwnik uzbrojony w wiedzę o twoim programie i jednym wyniku, ale nie w ziarnie, może łatwo przewidzieć ziarno i późniejsze wyniki, co sprawia, że nie wydają się takie w końcu losowe. Więc nawet nie myśl o używaniu tego do kryptografii.
Możesz przejść fantazyjną, nadmiernie inżynierską trasę i
std::uniform_int_distribution
klasę C ++ 11 z odpowiednim losowym urządzeniem i ulubionym losowym silnikiem, takim jak zawsze popularny twister Mersenne,std::mt19937
aby grać w kości ze swoim czteroletnim kuzynem, ale nawet to nie będzie być zdolnym do generowania materiału klucza kryptograficznego - a twister Mersenne jest również strasznym świrem kosmicznym ze stanem wielokilobajtowym siejącym spustoszenie w pamięci podręcznej procesora z nieprzyzwoitym czasem konfiguracji, więc jest zły nawet dla np. równoległych symulacji Monte Carlo z odtwarzalne drzewa obliczeń podrzędnych; jego popularność prawdopodobnie wynika głównie z chwytliwej nazwy. Ale możesz go użyć do rzucania zabawkowymi kośćmi, jak na tym przykładzie!Innym podejściem jest użycie prostego kryptograficznego generatora liczb pseudolosowych z małym stanem, na przykład prostego szybkiego usuwania klucza PRNG , lub po prostu szyfrowania strumieniowego, takiego jak AES-CTR lub ChaCha20, jeśli masz pewność ( np. W symulacji Monte Carlo dla badania w naukach przyrodniczych), że nie ma żadnych negatywnych konsekwencji w przewidywaniu przeszłych skutków, jeśli państwo kiedykolwiek zostanie zagrożone.
źródło
(RAND_MAX + 1 )% 6
wartościami. Nie ma znaczenia, jak podzielisz możliwe wyniki. Możesz je odrzucić z dowolnego miejsca w zakresie[0, RAND_MAX)
, o ile akceptowany zakres jest wielokrotnością 6. Do diabła, możesz odrzucić każdy wynikx>6
i już nie będziesz go potrzebować%6
.W żadnym wypadku nie jestem doświadczonym użytkownikiem C ++, ale chciałem sprawdzić, czy inne odpowiedzi mówią,
std::rand()/((RAND_MAX + 1u)/6)
że jestem mniej stronniczy, niż1+std::rand()%6
jest w rzeczywistości. Dlatego napisałem program testowy do zestawienia wyników dla obu metod (od dawna nie pisałem C ++, sprawdź to). Link do uruchomienia kodu znajduje się tutaj . Jest również odtwarzany w następujący sposób:Następnie wziąłem wynik tego i użyłem
chisq.test
funkcji w R, aby uruchomić test Chi-kwadrat, aby sprawdzić, czy wyniki różnią się znacznie od oczekiwanych. To pytanie dotyczące wymiany stosów dotyczy bardziej szczegółowo korzystania z testu chi-kwadrat do testowania uczciwości matrycy: Jak sprawdzić, czy kość jest uczciwa? . Oto wyniki kilku przebiegów:W trzech przebiegach, które przeprowadziłem, wartość p dla obu metod była zawsze większa niż typowe wartości alfa używane do testowania istotności (0,05). Oznacza to, że nie uważalibyśmy żadnego z nich za stronnicze. Co ciekawe, rzekomo bezstronna metoda ma konsekwentnie niższe wartości p, co wskazuje, że w rzeczywistości może być bardziej stronnicza. Z zastrzeżeniem, że zrobiłem tylko 3 przebiegi.
AKTUALIZACJA: Kiedy pisałem swoją odpowiedź, Konrad Rudolph opublikował odpowiedź, która ma to samo podejście, ale daje zupełnie inny wynik. Nie mam reputacji, by komentować jego odpowiedź, więc odniosę się do tego tutaj. Po pierwsze, najważniejsze jest to, że kod, którego używa, używa tego samego ziarna dla generatora liczb losowych za każdym razem, gdy jest uruchamiany. Jeśli zmienisz ziarno, w rzeczywistości otrzymasz różnorodne wyniki. Po drugie, jeśli nie zmienisz ziarna, ale zmienisz liczbę prób, otrzymasz również różnorodne wyniki. Spróbuj zwiększyć lub zmniejszyć o rząd wielkości, aby zobaczyć, o co mi chodzi. Po trzecie, dochodzi do obcięcia lub zaokrąglenia liczb całkowitych, gdy oczekiwane wartości nie są całkiem dokładne. Prawdopodobnie to nie wystarczy, aby coś zmienić, ale jest.
Podsumowując, po prostu przypadkiem otrzymał właściwe ziarno i liczbę prób, które mogły uzyskać fałszywy wynik.
źródło
rand()%6
zrand()/(1+RAND_MAX)/6
. Jest to raczej porównanie prostego pobrania pozostałej części z próbką odrzucenia (wyjaśnienie można znaleźć w innych odpowiedziach). W konsekwencji twój drugi kod jest nieprawidłowy (while
pętla nic nie robi). Twoje testy statystyczne również mają problemy (nie możesz po prostu powtórzyć testu na solidność, nie wykonałeś korekty,…).std::srand
(i bez użycia<random>
) jest dość trudne do wykonania w sposób zgodny ze standardami i nie chciałem, aby jego złożoność umniejszała pozostały kod. Nie ma to również znaczenia dla obliczeń: powtórzenie tej samej sekwencji w symulacji jest całkowicie dopuszczalne. Oczywiście różne nasiona będą dawać różne wyniki, a niektóre nie być znaczące. Jest to całkowicie oczekiwane na podstawie sposobu zdefiniowania wartości p.std::rand
daje wyjątkowo dobre symulacje rzutu monetą dla k6 w zakresie losowych nasion.RAND_MAX
, która określa wielkość efektu odchylenia modulo. Istotność statystyczna to prawdopodobieństwo w ramach hipotezy zerowej, że fałszywie ją odrzucasz. Jaka jest moc statystyczna - prawdopodobieństwo przy alternatywnej hipotezie, że twój test poprawnie odrzuca hipotezę zerową? Czyrand() % 6
wykryłbyś w ten sposób, gdy RAND_MAX = 2 ^ 31 - 1?Można myśleć o generatorze liczb losowych jako o pracy na strumieniu cyfr binarnych. Generator zamienia strumień na liczby, dzieląc go na kawałki. Jeśli
std:rand
funkcja działa zRAND_MAX
wartością 32767, to używa 15 bitów w każdym wycinku.Kiedy weźmiemy moduły liczby od 0 do 32767 włącznie, znajdziemy 5462 '0 i' 1, ale tylko 5461 '2,' 3, '4 i' 5 '. Stąd wynik jest tendencyjny. Im większa wartość RAND_MAX, tym mniejsze będzie odchylenie, ale jest nieuniknione.
To, co nie jest odchylane, to liczba z zakresu [0 .. (2 ^ n) -1]. Możesz wygenerować (teoretycznie) lepszą liczbę z zakresu 0..5, wyodrębniając 3 bity, zamieniając je na liczbę całkowitą z zakresu 0..7 i odrzucając 6 i 7.
Można mieć nadzieję, że każdy bit w strumieniu bitów ma równe szanse na bycie „0” lub „1”, niezależnie od tego, gdzie znajduje się w strumieniu lub wartości innych bitów. W praktyce jest to wyjątkowo trudne. Wiele różnych implementacji programowych PRNG oferuje różne kompromisy między szybkością a jakością. Liniowy generator kongruencjalny, taki jak
std::rand
oferuje największą prędkość przy najniższej jakości. Generator kryptograficzny zapewnia najwyższą jakość przy najniższej prędkości.źródło