Почему RAND ()%6 предвзято?
при чтении как использовать std:: rand, я нашел этот код на cppreference.com
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
что не так с выражением справа? Попробовал, и он отлично работает.
5 ответов:
есть две проблемы с
rand() % 6(the1+не влияет ни на одну проблему).во-первых, как указывали несколько ответов, если низкие биты
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();это другое реализация рассматриваемого кода призвана более четко показать, что происходит.
здесь есть скрытые глубины:
небольшая
uнаRAND_MAX + 1u.RAND_MAXопределяется, чтобы бытьintтип, и часто самый большой возможныйint. ПоведениеRAND_MAX + 1будет undefined в таких случаях, как вы были бы переполнены, аsignedтип. Пишу1uсилы преобразования типаRAND_MAXдоunsigned, чтобы избежать переполнения.использование
% 6можете (но на каждой реализацииstd::randЯ видел не) ввести любое дополнительное статистическое смещение выше и за пределами представленной альтернативы. Такие случаи, где% 6опасны случаи, когда генератор чисел имеет корреляционные равнины в битах низкого порядка, такие как довольно известная реализация IBM (в C)randв, Я думаю, 1970-е годы, которые перевернули высокие и низкие биты как "окончательный расцвет". Еще одно соображение заключается в том, что 6 это очень маленький кф.RAND_MAX, Так что будет минимальный эффект, еслиRAND_MAXне кратно 6, что, вероятно, не так.в заключение, в эти дни, из-за его сговорчивости, я бы использовал
% 6. Это вряд ли приведет к каким-либо статистическим аномалиям, кроме тех, которые были введены самим генератором. Если вы все еще сомневаетесь, тест ваш генератор, чтобы увидеть, если он имеет соответствующие статистические свойства для вашего случая.
этот пример кода иллюстрирует, что
std::randЭто случай legacy cargo cult balderdash, который должен заставить ваши брови подниматься каждый раз, когда вы его видите.есть несколько вопросов:
контракт люди обычно предполагают-даже бедные несчастные души, которые не знают ничего лучше и не будут думать об этом именно в этих терминах-это
randпробы с равномерное распределение на целых 0, 1, 2, ...,RAND_MAX, и каждый вызов дает независимая образец.например, с C99 §7.20.2.1 ‘ функция ' говорит, Не вдаваясь в подробности:
The
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% вероятностью, что не относится к независимая случайные образцы на любом распределении поддерживается четное и нечетное целые числа. Таким образом, возник культ груза, который нужно отбросить младшие биты, чтобы преследовать неуловимого зверя "лучшей случайности". (Спойлер: это не технический термин. Это признак того, что тот, чью прозу Вы читаете, либо не знает, о чем они говорят, либо думает вы невежественны и должны быть снисходительны.)вторая проблема заключается в том, что даже если каждый вызов образец независимо от равномерного случайного распределения on 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.упражнение для читателя: Почему вы можете предпочесть один или другой подмножества отклонить? Какой расчет необходим для каждого судебного разбирательства в этих двух случаях?
третья проблема заключается в том, что семенное пространство настолько мало, что даже если семя равномерно распределено, противник, вооруженный знанием вашей программы и одним результатом, но не семенем, может легко предсказать семя и последующие результаты, что делает их не такими в конце концов, случайность. так что даже не думайте об использовании этого для тайнописи.
можно пойти галантерейных помогает вынести высокие маршрута и C++11-х
std::uniform_int_distributionкласс с соответствующим случайным устройством и вашим любимым случайным двигателем, таким как популярный Mersenne twisterstd::mt19937чтобы играть в кости с вашим четырехлетним кузеном, но даже это не будет пригодно для создания криптографического ключевого материала-и Mersenne twister тоже ужасный космический боров с многокилобайтным состоянием разрушение Кеша вашего процессора с непристойным временем установки, так что это плохо даже для,например, параллельное моделирование Монте-Карло с воспроизводимыми деревьями субкомпутаций; его популярность, вероятно, возникает главным образом из его броского названия. Но вы можете использовать его для игры в кости, как в этом примере!другой подход заключается в использовании простого криптографического генератора псевдослучайных чисел с малым состоянием, например простого быстрое стирание ключа PRNG, или просто a потоковый шифр, такой как AES-CTR или ChaCha20, если вы уверены (например, в моделировании Монте-Карло для исследований в естественных науках), что нет никаких неблагоприятных последствий для прогнозирования прошлых результатов, если государство когда-либо будет скомпрометировано.
Я ни в коем случае не опытный пользователь 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 для запуска Хи-квадрат тест, чтобы увидеть, если результаты значительно отличаются от ожидаемых. Этот вопрос stackexchange более подробно описывает использование теста хи-квадрат для проверки справедливости матрицы:как я могу проверить, справедливо ли умереть?. Вот результаты для нескольких запусков:> ?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в трех прогонах, которые я сделал, значение p для обоих методов всегда было больше, чем типичные значения Альфа, используемые для проверки значимости (0,05). Это означает, что мы не будем считать ни одного из них предвзятым. Интересно, что предположительно беспристрастный метод имеет последовательно более низкие значения p, что указывает на то, что он может быть более предвзятым. Нюанс в том, что я сделал только 3 трассы.
UPDATE: пока я писал свой ответ, Конрад Рудольф опубликовал ответ, который использует тот же подход, но получает совсем другой результат. У меня нет репутации, чтобы прокомментировать его ответ, поэтому я собираюсь обратиться к нему здесь. Во-первых, главное, что код, который он использует, использует одно и то же семя для случайного генератор чисел каждый раз, когда он запускается. Если вы измените семя, вы на самом деле получите различные результаты. Во-вторых, если вы не меняете семя, а меняете количество испытаний, вы также получаете различные результаты. Попробуйте увеличить или уменьшить на порядок, чтобы увидеть, что я имею в виду. В-третьих, происходит некоторое усечение или округление целых чисел, где ожидаемые значения не совсем точны. Вероятно, этого недостаточно, чтобы изменить ситуацию, но это есть.
в основном, в общем, он просто случилось получить правильное семя и количество испытаний, которые он мог бы получить ложный результат.
можно представить генератор случайных чисел как работающий на потоке двоичных цифр. Генератор превращает поток в числа, разрезая его на куски. Если
std:randфункция работает сRAND_MAXиз 32767, то он использует 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' независимо от того, где он находится в потоке или значения других битов. Это очень трудно практиковать. Множество различных реализаций программного обеспечения PRNGs предлагают различные компромиссы между скоростью и качеством. Линейный конгруэнтный генератор, такой как
std::randпредлагает самую быструю скорость для самого низкого качества. Криптографический генератор предлагает самое высокомарочное для самой низкой скорости.
Comments