Почему 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

Что не так с выражением справа? Пробовал это, и он отлично работает.

Ответ 1

Есть две проблемы с 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

Здесь есть скрытые глубины:

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

  2. Использование % 6 может (но при каждой реализации std::rand я не видел) вводит любые дополнительные статистические отклонения выше и выше представленной альтернативы. Такими случаями, когда % 6 является опасным, являются случаи, когда генератор чисел имеет корреляционные равнины в младших битах, например, довольно известную реализацию IBM (в C) rand in, я думаю, 1970-е годы, которые перевернули верхние и нижние разряды как "окончательный расцвет". Еще одно соображение состоит в том, что 6 очень мало ср. RAND_MAX, поэтому будет минимальный эффект, если RAND_MAX не будет кратным 6, что, вероятно, не так.

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

Ответ 3

Этот примерный код иллюстрирует, что std::rand - это случай унаследованного культа, который должен поднимать брови каждый раз, когда вы его видите.

Здесь есть несколько вопросов:

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

Первая проблема заключается в том, что предполагаемый контракт, независимые равномерные случайные выборки в каждом вызове, на самом деле не соответствует действительности документации, и на практике реализации исторически не смогли обеспечить даже самый простой симулякр независимости. Например, C99 §7.20.2.1 'Функция rand говорит, не уточняя:

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

Это бессмысленное предложение, потому что псевдослучайность - это свойство функции (или семейства функций), а не целого числа, но это не мешает даже бюрократам ИСО злоупотреблять языком. В конце концов, единственные читатели, которые были бы расстроены им, знают лучше, чем читать документацию для 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.

Упражнение для читателя. Почему вы можете предпочесть отклонение одного или другого подмножества? Какие вычисления необходимы для каждого испытания в двух случаях?

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

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

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

Ответ 4

Я не являюсь опытным пользователем 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-square, чтобы увидеть, значительно ли отличаются результаты, чем ожидалось. Этот вопрос об использовании стека более подробно объясняется использованием теста хи-квадрат для проверки справедливости: как проверить, честна ли матрица? , Вот несколько результатов:

> ?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 пробега.

ОБНОВЛЕНИЕ: Когда я писал свой ответ, Конрад Рудольф отправил ответ, который использует тот же подход, но получает совсем другой результат. У меня нет репутации, чтобы прокомментировать его ответ, поэтому я собираюсь обратиться к нему здесь. Во-первых, главное, что используемый им код использует одно и то же семя для генератора случайных чисел каждый раз, когда он запускается. Если вы измените семена, вы получите множество результатов. Во-вторых, если вы не измените семена, но измените количество испытаний, вы также получите множество результатов. Попробуйте увеличить или уменьшить на порядок, чтобы понять, что я имею в виду. В-третьих, происходит целая усечка или округление, где ожидаемые значения не совсем точны. Этого, вероятно, недостаточно, чтобы иметь значение, но оно есть.

В принципе, в итоге, он просто получил правильное семя и количество испытаний, что он может получить ложный результат.

Ответ 5

Можно думать, что генератор случайных чисел работает над потоком двоичных цифр. Генератор превращает поток в числа, разрезая его на куски. Если функция 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", независимо от того, где он находится в потоке или значениях других бит. Это исключительно сложно на практике. Множество различных реализаций программного обеспечения PRNG предлагают разные компромиссы между скоростью и качеством. Линейный конгруэнтный генератор, такой как std::rand обеспечивает быструю скорость для самого низкого качества. Криптографический генератор обеспечивает наивысшее качество при минимальной скорости.