Почему люди говорят, что при использовании генератора случайных чисел существует модульное смещение?

Я видел, что этот вопрос задал много, но никогда не видел конкретного конкретного ответа на него. Поэтому я собираюсь опубликовать здесь один, который, надеюсь, поможет людям понять, почему именно существует "модульное смещение" при использовании генератора случайных чисел, например rand() в С++.

Ответ 1

So rand() - генератор псевдослучайных чисел, который выбирает натуральное число между 0 и RAND_MAX, которое является константой, определенной в cstdlib (см. этот статью для общего обзора rand()).

Теперь, что произойдет, если вы хотите создать случайное число между словами 0 и 2? Для объяснения, скажем, RAND_MAX равно 10, и я решил создать случайное число между 0 и 2, вызвав rand()%3. Однако rand()%3 не дает чисел от 0 до 2 с равной вероятностью!

Когда rand() возвращает 0, 3, 6 или 9, rand()%3 == 0. Поэтому P (0) = 4/11

Когда rand() возвращает 1, 4, 7 или 10, rand()%3 == 1. Поэтому P (1) = 4/11

Когда rand() возвращает 2, 5 или 8, rand()%3 == 2. Следовательно, P (2) = 3/11

Это не порождает числа от 0 до 2 с равной вероятностью. Конечно, для небольших диапазонов это может быть не самая большая проблема, но для большего диапазона это может исказить распределение, смещая меньшие числа.

Итак, когда rand()%n с равной вероятностью возвращает диапазон чисел от 0 до n-1? Когда RAND_MAX%n == n - 1. В этом случае наряду с нашим более ранним предположением rand() с равной вероятностью возвращает число от 0 до RAND_MAX, также будут равномерно распределены по модулю классы n.

Итак, как мы решаем эту проблему? Грубый способ состоит в том, чтобы генерировать случайные числа, пока вы не получите число в нужном диапазоне:

int x; 
do {
    x = rand();
} while (x >= n);

но это неэффективно при низких значениях n, так как у вас есть только шанс n/RAND_MAX получить значение в вашем диапазоне, и поэтому вам нужно будет выполнять вызовы RAND_MAX/n в rand() в среднем.

Более эффективный подход к формуле заключался бы в том, чтобы взять некоторый большой диапазон с длиной, делящейся на n, например, RAND_MAX - RAND_MAX % n, продолжать генерировать случайные числа, пока вы не получите тот, который находится в диапазоне, а затем возьмите модуль:

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

При малых значениях n для этого редко требуется более одного вызова rand().


Работы цитируются и читаются далее:


Ответ 2

Держите выбор случайным - это хороший способ устранить смещение.

Обновить

Мы могли бы быстро сделать код, если бы искали x в диапазоне, делящемся на n.

// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n)) 

x %= n;

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

Ответ 3

@user1413793 правильно о проблеме. Я не буду обсуждать это далее, за исключением одного замечания: да, для малых значений n и больших значений RAND_MAX смещение по модулю может быть очень маленьким. Но использование шаблона смещения означает, что вы должны учитывать смещение каждый раз, когда вычисляете случайное число и выбираете разные шаблоны для разных случаев. И если вы сделаете неправильный выбор, ошибки, которые он вносит, неуловимы и почти невозможны для модульного тестирования. По сравнению с использованием только подходящего инструмента (такого как arc4random_uniform) эта дополнительная работа, а не меньшая. Выполнение большей работы и получение худшего решения - это ужасная разработка, особенно если делать это правильно каждый раз на большинстве платформ легко.

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

Опять же, лучшее решение - это просто использовать arc4random_uniform на платформах, которые его предоставляют, или аналогичное решение для вашей платформы (например, Random.nextInt на Java). Он будет делать правильные вещи без затрат на код. Это почти всегда правильный вызов.

Если у вас нет arc4random_uniform, то вы можете использовать возможности open source, чтобы точно увидеть, как он реализован поверх более широкого диапазона ГСЧ (в данном случае ar4random, но аналогичный подход может также работать сверху). других ГСЧ).

Вот реализация OpenBSD:

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

Стоит отметить последний комментарий коммита по этому коду для тех, кому необходимо реализовать похожие вещи:

Измените arc4random_uniform(), чтобы вычислить 2**32 % upper_bound как -upper_bound % upper_bound. Упрощает код и делает его то же самое на архитектурах ILP32 и LP64, а также немного быстрее на Архитектура LP64 с использованием 32-разрядного остатка вместо 64-разрядного остальное.

Указано Джорденом Вервером о технологиях @ хорошо, deraadt; нет возражений от диджея или отто

Реализация Java также легко доступна (см. предыдущую ссылку):

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }

Ответ 4

Определение

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

Это смещение особенно сложно избежать при вычислении, где числа представлены как строки бит: 0s и 1s. Найти действительно случайные источники случайности также чрезвычайно сложно, но выходит за рамки этого обсуждения. В оставшейся части этого ответа предположим, что существует неограниченный источник действительно случайных бит.

Пример проблемы

Давайте рассмотрим моделирование рулона штампа (от 0 до 5) с использованием этих случайных бит. Есть 6 возможностей, поэтому нам нужно достаточно битов, чтобы представить число 6, которое составляет 3 бита. К сожалению, 3 случайных бита дают 8 возможных результатов:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

Мы можем уменьшить размер результата, установленного ровно до 6, взяв значение по модулю 6, однако это представляет проблему смещения по модулю: 110 дает 0, а 111 дает 1. Эта матрица загружается.

Потенциальные решения

Подход 0:

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

Подход 1:

Вместо использования модуля наивное, но математически корректное решение состоит в том, чтобы отбросить результаты, которые дают 110 и 111, и просто попробуйте снова с 3 новыми битами. К сожалению, это означает, что в каждом броске есть 25% шанс, что потребуется повторный ролл, включая каждый из повторных рулонов. Это явно непрактично для всех, кроме самых простых в использовании.

Подход 2:

Используйте больше бит: вместо 3 бит используйте 4. Это даст 16 возможных результатов. Конечно, повторная прокатка в любое время, когда результат больше 5, ухудшает ситуацию (10/16 = 62,5%), поэтому сам по себе не поможет.

Обратите внимание, что 2 * 6 = 12 < 16, поэтому мы можем безопасно принимать любые результаты менее 12 и уменьшать этот модуль 6 для равномерного распределения результатов. Остальные 4 результата должны быть отброшены, а затем повторно развернуты, как в предыдущем подходе.

Сначала звучит хорошо, но пусть проверяет математику:

4 discarded results / 16 possibilities = 25%

В этом случае 1 дополнительный бит не помог!

Этот результат неудачен, но попробуйте еще раз с 5 бит:

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

Определенное улучшение, но не достаточно хорошее во многих практических случаях. Хорошая новость заключается в том, что добавление большего количества бит никогда не увеличит шансы отбрасывания и повторного катания. Это выполняется не только для костей, но и во всех случаях.

Как показано , добавление 1 дополнительного бита может ничего не изменить. На самом деле, если мы увеличим наш бросок до 6 бит, вероятность останется 6,25%.

Это вызывает еще два вопроса:

  • Если мы добавим достаточно бит, существует ли гарантия того, что вероятность сброса уменьшится?
  • Сколько бит в общем случае достаточно?

Общее решение

К счастью, ответ на первый вопрос - да. Проблема с 6 состоит в том, что 2 ^ x mod 6 переворачивается между 2 и 4, которые по совпадению кратно 2 друг от друга, так что для четного x > 1,

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

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

Не предлагая дополнительных доказательств, в общем случае используя double число необходимых бит, обеспечит меньший, обычно незначительный, шанс сброса.

Доказательство концепции

Вот примерная программа, которая использует OpenSSL libcrypo для предоставления случайных байтов. При компиляции обязательно связывайтесь с библиотекой с -lcrypto, которая должна быть доступна всем пользователям.

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(RAND_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

Я рекомендую играть со значениями MODULUS и ROLLS, чтобы увидеть, сколько фактических повторов действительно происходит в большинстве условий. Скептический человек может также пожелать сохранить вычисленные значения для файла и проверить, что распределение выглядит нормальным.

Ответ 5

Есть два обычных жалобы с использованием modulo.

  • один действует для всех генераторов. Это легче увидеть в предельном случае. Если у вашего генератора есть RAND_MAX, который равен 2 (что не соответствует стандарту C), и вы хотите использовать только 0 или 1 в качестве значения, использование modulo будет генерировать 0 в два раза чаще (когда генератор генерирует 0 и 2), поскольку он будет сгенерировать 1 (когда генератор генерирует 1). Обратите внимание, что это верно, как только вы не отбрасываете значения, независимо от того, какое сопоставление вы используете от значений генератора до желаемого, оно будет происходить в два раза чаще, чем другое.

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

Используя что-то вроде

int alea(int n){ 
 assert (0 < n && n <= RAND_MAX); 
 int partSize = 
      n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

для генерации случайного числа между 0 и n избежит обеих проблем (и он избегает переполнения с помощью RAND_MAX == INT_MAX)

BTW, С++ 11 представил стандартные способы восстановления и другого генератора, чем rand().

Ответ 6

Решение Mark (принятое решение) почти идеально.

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

edited Mar 25 '16 at 23:16

Марк Эмери 39k21170211

Тем не менее, он имеет оговорку, которая отбрасывает 1 действительный набор результатов в любом сценарии, где RAND_MAX (RM) на 1 меньше, чем кратное N (где N = количество возможных действительных результатов).

то есть, когда "количество отброшенных значений" (D) равно N, тогда они фактически являются действительным набором (V), а не недопустимым набором (I).

Причина в том, что в какой-то момент Марк теряет из виду разницу между N и Rand_Max.

N - это набор, допустимые члены которого состоят только из положительных целых чисел, поскольку он содержит количество ответов, которые были бы действительными. (например, установите N = {1, 2, 3, ... n })

Rand_max Однако это набор, который (как определено для наших целей) включает любое количество неотрицательных целых чисел.

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

Поэтому Rand_Max лучше определить как набор "возможных ответов".

Однако N работает против количества значений в наборе действительных ответов, поэтому даже как определено в нашем конкретном случае, Rand_Max будет на одно значение меньше общего числа, которое он содержит.

Используя Mark Solution, значения отбрасываются, когда: X => RM - RM% N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

Как вы можете видеть в приведенном выше примере, когда значение X (случайное число, которое мы получаем из начальной функции) равно 252, 253, 254 или 255, мы отбрасываем его, даже если эти четыре значения составляют действительный набор возвращаемых значений.

IE: когда число значений Discarded (I) = N (Количество действительных результатов), то Действительный набор возвращаемых значений будет отброшен исходной функцией.

Если мы опишем разницу между значениями N и RM как D, то есть:

D = (RM - N)

Затем, когда значение D становится меньше, Процент ненужных повторных бросков из-за этого метода увеличивается при каждом естественном мультипликативе. (Когда RAND_MAX НЕ равен простому числу, это имеет значение)

EG:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

Поскольку процент необходимых Rerolls увеличивается по мере приближения N к RM, это может иметь значение для многих различных значений в зависимости от ограничений системы, в которой выполняется код, и значений, которые ищутся.

Чтобы отрицать это, мы можем внести простую поправку, как показано здесь:

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

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

Примеры использования небольшого значения для RAND_MAX, которое является мультипликативным для N.

Оригинальная версия Mark'а:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

Обобщенная версия 1:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.

Дополнительно, в случае, когда N должно быть числом значений в RAND_MAX; в этом случае вы можете установить N = RAND_MAX +1, если только RAND_MAX = INT_MAX.

По циклу вы можете просто использовать N = 1, и любое значение X будет принято, однако, и добавьте оператор IF для вашего окончательного множителя. Но, возможно, у вас есть код, который может иметь вескую причину для возврата 1, когда функция вызывается с n = 1...

Поэтому может быть лучше использовать 0, что обычно дает ошибку Div 0, когда вы хотите иметь n = RAND_MAX +1

Обобщенная версия 2:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

Оба эти решения решают проблему с ненужными недействительными действительными результатами, которые возникнут, когда RM +1 является продуктом n.

Вторая версия также охватывает сценарий крайнего случая, когда необходимо, чтобы n равнялся общему возможному набору значений, содержащихся в RAND_MAX.

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

Чтобы повторить:

Базовое общее решение, которое расширяет пример марки:

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

Расширенное общее решение, которое допускает один дополнительный сценарий RAND_MAX +1 = n:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

Ответ 7

При значении RAND_MAX 3 (на самом деле оно должно быть намного выше, но смещение все равно будет существовать), из этих расчетов имеет смысл, что существует смещение:

1 % 2 = 1 2 % 2 = 0 3 % 2 = 1 random_between(1, 3) % 2 = more likely a 1

В этом случае % 2 - это то, что вы не должны делать, если хотите случайное число между 0 и 1. Вы можете получить случайное число между 0 и 2, выполнив % 3, хотя в этом случае: RAND_MAX является кратным 3.

Другой метод

Намного проще, но чтобы добавить к другим ответам, вот мое решение получить случайное число между 0 и n - 1, поэтому n разные возможности без смещения.

  • количество бит (не байтов), необходимое для кодирования количества возможностей, - это количество бит случайных данных, которые вам понадобятся.
  • кодировать число из случайных бит
  • если это число >= n, перезапустите (нет по модулю).

Действительно, случайные данные получить нелегко, поэтому зачем использовать больше бит, чем нужно.

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

next: n

    | bitSize r from to |
    n < 0 ifTrue: [^0 - (self next: 0 - n)].
    n = 0 ifTrue: [^nil].
    n = 1 ifTrue: [^0].
    cache isNil ifTrue: [cache := OrderedCollection new].
    cache size < (self randmax highBit) ifTrue: [
        Security.DSSRandom default next asByteArray do: [ :byte |
            (1 to: 8) do: [ :i |    cache add: (byte bitAt: i)]
        ]
    ].
    r := 0.
    bitSize := n highBit.
    to := cache size.
    from := to - bitSize + 1.
    (from to: to) do: [ :i |
        r := r bitAt: i - from + 1 put: (cache at: i)
    ].
    cache removeFrom: from to: to.
    r >= n ifTrue: [^self next: n].
    ^r

Ответ 8

В качестве принятого ответа указывает, что "modulo bias" имеет свои корни в низком значении RAND_MAX. Он использует чрезвычайно маленькое значение RAND_MAX (10), чтобы показать, что если RAND_MAX равно 10, вы пытались сгенерировать число от 0 до 2, используя%, следующие результаты:

rand() % 3   // if RAND_MAX were only 10, gives
output of rand()   |   rand()%3
0                  |   0
1                  |   1
2                  |   2
3                  |   0
4                  |   1
5                  |   2
6                  |   0
7                  |   1
8                  |   2
9                  |   0

Таким образом, есть 4 выхода из 0 (вероятность 4/10) и только 3 выхода 1 и 2 (по 3/10 шанса каждый).

Так что это смело. Более низкие цифры имеют больше шансов выйти.

Но это проявляется так явно, когда RAND_MAX мало. Или, более конкретно, когда число, на котором вы изменяете, велико по сравнению с RAND_MAX.

Гораздо лучшее решение, чем цикл (который безумно неэффективен и даже не рекомендуется), должен использовать PRNG с гораздо большим диапазоном производительности. Алгоритм Mersenne Twister имеет максимальный выход 4 294 967 295. Таким образом, выполнение MersenneTwister::genrand_int32() % 10 для всех целей и целей будет равномерно распределено, и эффект модульного смещения почти исчезнет.

Ответ 9

Я только что написал код для неопроверенного метода монетного флага фон Неймана, который теоретически должен устранить любое смещение в процессе генерации случайных чисел. Более подробную информацию можно найти по адресу (http://en.wikipedia.org/wiki/Fair_coin)

int unbiased_random_bit() {    
    int x1, x2, prev;
    prev = 2;
    x1 = rand() % 2;
    x2 = rand() % 2;

    for (;; x1 = rand() % 2, x2 = rand() % 2)
    {
        if (x1 ^ x2)      // 01 -> 1, or 10 -> 0.
        {
            return x2;        
        }
        else if (x1 & x2)
        {
            if (!prev)    // 0011
                return 1;
            else
                prev = 1; // 1111 -> continue, bias unresolved
        }
        else
        {
            if (prev == 1)// 1100
                return 0;
            else          // 0000 -> continue, bias unresolved
                prev = 0;
        }
    }
}