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


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

531   5  

5 ответов:

есть две проблемы с rand() % 6 (the 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();

это другое реализация рассматриваемого кода призвана более четко показать, что происходит.

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

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

  2. использование % 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 twister std::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

    Ничего не найдено.