난수 생성기를 사용할 때 사람들이 모듈로 바이어스가 있다고 말하는 이유는 무엇입니까?


277

나는이 질문에 많은 질문을 보았지만 그것에 대한 확실한 대답은 보지 못했습니다. 그래서 여기 rand()에 C ++에서 와 같이 난수 생성기를 사용할 때 왜 "모듈러스 편향"이 있는지 이해하는 데 도움이 될 것 입니다.

답변:


394

그래서 rand()0 사이의 자연수를 선택하고, 의사 난수 생성기 RAND_MAX에 정의 된 상수 인 cstdlib(이 참조 문서 에 대한 일반적인 개요가 rand()).

이제 0과 2 사이의 난수를 생성하려면 어떻게됩니까? 설명을 위해 RAND_MAX10 이라고 가정 하고을 호출하여 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().


인용 및 추가 자료 :



6
에 대해 생각하는 또 다른 방법은 RAND_MAX%n == n - 1_입니다 (RAND_MAX + 1) % n == 0. 코드를 읽을 때 다른 코드 % something == 0를 계산하는 것보다 쉽게 ​​"분할 할 수없는" 것으로 이해하는 경향이 있습니다. 물론, C ++ 다음 stdlib가있는 경우 RAND_MAX와 같은 값으로 INT_MAX, (RAND_MAX + 1)확실하게 작동하지 않을 것입니다; 따라서 Mark의 계산은 가장 안전한 구현으로 유지됩니다.
슬립 D. 톰슨

아주 좋은 답변입니다!
Sayali Sonawane

나는 nitpicking 일 수 있지만, 낭비 된 비트를 줄이는 것이 목표라면 RAND_MAX (RM)이 N으로 동등하게 나눌 수있는 것보다 1 작지 않은 가장자리 조건에 대해 약간 개선 할 수 있습니다.이 시나리오에서는 비트를 낭비 할 필요가 없습니다. 작은 값 N에 대해서는 값이 적지 만 큰 값 N에 대해서는 값이 커지는 X> = (RM-RM % N)). Slipp D. Thompson이 언급했듯이 작동하는 솔루션이 있습니다. INT_MAX (IM)> RAND_MAX이지만 같으면 끊어집니다. 그러나 이에 대한 간단한 해결책이 있습니다. 다음과 같이 계산 X> = (RM-RM % N)을 수정할 수 있습니다.
Ben Personick

X> = RM-(((RM % N) + 1) % N)
Ben Personick

문제를 자세하게 설명하고 예제 코드 솔루션을 제공하는 추가 답변을 게시했습니다.
Ben Personick

36

무작위를 계속 선택하는 것은 편견을 제거하는 좋은 방법입니다.

최신 정보

범위를 x로 나눌 수있는 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 회 반복해야합니다.


2
Yuck :-P를 double로 변환 한 다음 MAX_UPPER_LIMIT / RAND_MAX를 곱하면 훨씬 깨끗하고 성능이 좋습니다.
boycy

22
@boycy : 요점을 놓쳤다. rand()반환 할 수있는 값의 수가 배수가 아닌 경우 n수행하는 모든 값을 무시하지 않으면 불가피하게 '모듈러스 바이어스'를 얻습니다. user1413793은 잘 설명합니다 (해답에서 제안 된 솔루션은 정말 유쾌하지만).
TonyK

4
@TonyK 사과하지만 요점을 놓쳤다. 충분히 열심히 생각하지 않았고, 바이어스는 명시적인 모듈러스 연산을 사용하는 방법에만 적용될 것이라고 생각했습니다. 나를
고쳐

연산자 우선 순위가 RAND_MAX+1 - (RAND_MAX+1) % n올바르게 작동하지만 RAND_MAX+1 - ((RAND_MAX+1) % n)명확성 을 위해 작성해야한다고 생각합니다 .
Linus Arver

4
RAND_MAX == INT_MAX (대부분의 시스템에서와 같이) 작동 하지 않습니다 . 위의 @ user1413793에 대한 두 번째 주석을 참조하십시오.
BlueRaja-대니 Pflughoeft

19

문제에 대해서는 @ user1413793이 맞습니다. 한 가지 점을 제외하고는 더 이상 논의하지 않을 것입니다. 예, 작은 값 n과 큰 값의 RAND_MAX경우 모듈러스 바이어스가 매우 작을 수 있습니다. 그러나 바이어스 유도 패턴을 사용한다는 것은 임의의 숫자를 계산할 때마다 바이어스를 고려해야하고 경우에 따라 다른 패턴을 선택해야한다는 것을 의미합니다. 그리고 만약 당신이 잘못된 선택을한다면, 그것이 도입 한 버그는 미묘하고 단위 테스트가 거의 불가능합니다. 적절한 도구 (예 :)를 사용하는 것과 비교할 때 arc4random_uniform, 이는 더 적은 작업이 아니라 추가 작업입니다. 더 많은 작업을하고 더 나쁜 솔루션을 얻는 것은 끔찍한 엔지니어링입니다. 특히 대부분의 플랫폼에서 매번 작업을 수행 할 때 특히 그렇습니다.

불행히도, 솔루션의 구현은 모두 잘못되었거나 덜 효율적입니다. (각 솔루션에는 문제를 설명하는 다양한 의견이 있지만 문제를 해결하기 위해 해결 된 솔루션은 없습니다.) 이는 일반적인 답을 찾는 사람을 혼란스럽게 만들 가능성이 높으므로 여기서 잘 알려진 구현을 제공하고 있습니다.

다시 말하지만 최상의 솔루션은 arc4random_uniform해당 플랫폼을 제공하는 플랫폼 또는 플랫폼과 유사한 범위의 솔루션 (예 : Random.nextIntJava)에서 사용하는 것입니다. 코드 비용없이 올바른 작업을 수행합니다. 이것은 거의 항상 올바른 전화입니다.

이없는 경우 arc4random_uniform오픈 소스의 힘을 사용하여 더 넓은 범위의 RNG 위에 어떻게 구현되는지 확인할 수 있습니다 ( ar4random이 경우 비슷한 방법이 다른 RNG에서도 작동 할 수 있음).

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 아키텍처에서 동일하게하고 64 비트 나머지 대신 32 비트 나머지를 사용하여 LP64 아키텍처에서 약간 더 빠릅니다.

Jorden Verwer가 tech @ ok deraadt; djm 또는 otto에 대한 반대 의견 없음

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;
 }

arcfour_random() 실제로 구현에 실제 RC4 알고리즘을 사용 하는 경우 출력에 약간의 편차가 있습니다. 라이브러리 작성자가 동일한 인터페이스에서 더 나은 CSPRNG를 사용하도록 전환했으면합니다. 필자는 BSD 중 하나가 실제로 ChaCha20 알고리즘을 사용하여 구현한다고 생각합니다 arcfour_random(). 더는 쓸모 보안 또는 비디오 포커와 같은 다른 중요한 애플리케이션을위한 렌더링 RC4 출력 편견에 : blog.cryptographyengineering.com/2013/03/...
rmalayter

2
@rmalayter iOS 및 OS X에서 arc4random은 시스템의 최고 품질 엔트로피 인 / dev / random에서 읽습니다. (이름에서 "arc4"는 역사적이며 호환성을 위해 보존됩니다.)
Rob Napier

@Rob_Napier는 알고 있지만 /dev/random, 과거에는 일부 플랫폼에서 RC4를 사용했습니다 (리눅스는 카운터 모드에서 SHA-1을 사용합니다). 불행히도 검색을 통해 찾은 매뉴얼 페이지는 RC4가 제공하는 다양한 플랫폼에서 여전히 사용되고 있음을 나타냅니다 arc4random(실제 코드는 다를 수 있음).
rmalayter

1
혼란 스러워요. 그렇지 않습니까 -upper_bound % upper_bound == 0??
Jon McClung

1
@JonMcClung -upper_bound % upper_boundint32 비트보다 넓은 경우 실제로 0이됩니다 . 이어야합니다 (u_int32_t)-upper_bound % upper_bound)( u_int32_t에 대한 BSD-ism 이라고 가정 uint32_t).
Ian Abbott '

14

정의

모듈로 바이어스 는 모듈로 산술을 사용하여 출력 세트를 입력 세트의 서브 세트로 줄이는 고유의 바이어스입니다. 일반적으로, 출력 세트의 크기가 입력 세트의 크기의 제수가 아닌 경우 모듈로 산술을 사용하는 경우와 같이 입력과 출력 세트 사이의 매핑이 균등하게 분포되지 않을 때마다 바이어스가 존재합니다.

이 바이어스는 숫자가 비트 열 (0과 1)로 표현되는 컴퓨팅에서 특히 피하기 어렵습니다. 무작위로 무작위로 무작위를 찾는 것도 매우 어렵지만이 논의의 범위를 벗어납니다. 이 답변의 나머지 부분에서는 실제로 임의의 비트의 무제한 소스가 있다고 가정하십시오.

문제 예

이 임의의 비트를 사용하여 다이 롤 (0-5)을 시뮬레이션하는 것을 고려해 봅시다. 6 가지 가능성이 있으므로 숫자 6을 나타내는 충분한 비트가 필요합니다. 3은 3 비트입니다. 불행하게도, 3 개의 랜덤 비트는 8 가지 가능한 결과를 산출합니다.

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

우리는 모듈로 6 값을 취함으로써 결과 세트의 크기를 정확히 6으로 줄일 수 있습니다. 그러나 이것은 모듈로 바이어스 문제를 나타냅니다 : 1100을 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 %입니다.

이것은 두 가지 추가 질문을 제기합니다.

  1. 비트를 충분히 추가하면 폐기 확률이 줄어들 것이라는 보장이 있습니까?
  2. 일반적인 경우 몇 비트로 충분 합니까?

일반적인 해결책

고맙게도 첫 번째 질문에 대한 대답은 '예'입니다. 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의 거듭 제곱을 산출하는 더 큰 모듈러스를 찾을 수 있지만, 결국 랩핑해야하며 폐기 확률이 줄어 듭니다.

추가 증거를 제공하지 않으면 일반적으로 필요한 비트 수의 두 배를 사용 하면 더 작고 일반적으로 중요하지 않은 폐기 기회가 제공됩니다.

개념의 증거

다음은 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;
}

대부분의 조건에서 실제로 얼마나 많은 재롤이 발생하는지 확인 하려면 MODULUSROLLS값을 사용하는 것이 좋습니다 . 회의론자는 계산 된 값을 파일에 저장하고 분포가 정상적으로 나타나는지 확인할 수 있습니다.


나는 아무도 당신의 균일 한 무작위 구현을 맹목적으로 복사하지 않았기를 바랍니다. randomPool = RAND_bytes(...)라인은 항상 발생합니다 randomPool == 1주장 때문. 이로 인해 항상 폐기와 재롤이 발생합니다. 별도의 줄에 선언하고 싶다고 생각합니다. 결과적으로 이로 인해 RNG가 1반복 될 때마다 리턴됩니다 .
Qix-MONICA가 MISTREATED

분명히, 주장으로 인해 항상 성공할 것이기 때문에 항상 OpenSSL 문서 에 따라 randomPool평가됩니다 . 1RAND_bytes()RAND_status()
Qix-MONICA가 MISTREATED

9

모듈로 사용에 대한 두 가지 일반적인 불만이 있습니다.

  • 하나는 모든 발전기에 유효합니다. 제한적인 경우 더보기 쉽습니다. 제너레이터에 RAND_MAX가 2이고 (C 표준과 호환되지 않음) 값으로 0 또는 1 만 원하는 경우 모듈로를 사용하면 0보다 2 배 더 자주 제너레이터가 생성됩니다 (제너레이터가 0과 2를 생성 할 때) 1을 생성하십시오 (생성기가 1을 생성 할 때). 생성기 값에서 원하는 값으로 매핑하는 것이 무엇이든 관계없이 값을 삭제하지 않는 즉시 적용됩니다.

  • 어떤 종류의 발전기는 적어도 일부 매개 변수에 대해 다른 비트보다 덜 중요한 비트를 덜 무작위로 가지고 있지만 슬프게도 이러한 매개 변수에는 다른 흥미로운 특성이 있습니다 (예 : RAND_MAX는 2의 거듭 제곱보다 작습니다). 문제는 잘 알려져 있으며 장시간 라이브러리 구현에서는 문제를 피할 수 있습니다 (예 : C 표준의 샘플 rand () 구현은 이러한 종류의 생성기를 사용하지만 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, C ++ 11은 rand () 이외의 축소 및 기타 생성기에 표준 방법을 도입했습니다.


n == RAND_MAX? 1 : (RAND_MAX-1) / (n + 1) : 여기서 아이디어는 먼저 RAND_MAX를 동일한 페이지 크기 N으로 나누고 N 내의 편차를 반환하는 것이지만 코드를 정확하게 매핑 할 수는 없습니다.
zinking (

1
n + 1 버킷으로 나눌 RAND_MAX + 1 값이 있으므로 순진 버전은 (RAND_MAX + 1) / (n + 1)이어야합니다. RAND_MAX + 1을 계산할 때 오버플로를 피하려면 1+ (RAND_MAX-n) / (n + 1)로 변환 할 수 있습니다. n + 1을 계산할 때 오버플로를 피하기 위해 먼저 n == RAND_MAX를 확인합니다.
AProgrammer

+ 더하기, 나누기 작업은 재생성 숫자에 비해 훨씬 비용이 많이 드는 것으로 보입니다.
zinking

4
모듈로를 취하고 나누는 비용은 같습니다. 일부 ISA는 항상 두 가지를 모두 제공하는 하나의 명령 만 제공합니다. 숫자 재생성 비용은 n과 RAND_MAX에 따라 다릅니다. RAND_MAX에 비해 n이 작 으면 비용이 많이들 수 있습니다. 그리고 편향이 애플리케이션에 중요하지 않다는 것을 분명히 결정할 수 있습니다. 난 그냥 그들을 피할 수있는 방법을 제공합니다.
AProgrammer

9

Mark 's Solution (허용 된 솔루션)은 거의 완벽합니다.

int x;

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

x %= n;

생성 25 mar.

마크 애 머리 39k21170211

그러나 RAND_MAX( RM)가 N(Where N= 가능한 유효한 결과 수) 의 배수보다 1이 작은 시나리오에서 유효한 결과 집합 1 개를 버리는 경고 가 있습니다.

즉, '버려진 값 수'( D) 가과 같으면 N실제로 유효 집합 ( V))이 아니라 유효하지 않은 집합 ( I)이 아닙니다 .

이것이 발생하는 것은 마크의 차이의 시력 상실 어떤 지점에 NRand_Max.

N유효한 멤버 수는 긍정적 인 정수로만 구성된 집합입니다. 여기에는 유효한 응답 수가 포함되어 있습니다. (예 : Set N= {1, 2, 3, ... n })

Rand_max 그러나 (우리의 목적을 위해 정의 된) 음수가 아닌 정수를 포함하는 세트입니다.

가장 일반적인 형태로, 여기에 정의 된 것은 Rand Max모든 유효한 결과 집합으로 이론적으로 음수 또는 숫자가 아닌 값을 포함 할 수 있습니다.

따라서 Rand_Max"가능한 응답"세트로 정의하는 것이 좋습니다.

그러나 N유효한 응답 세트 내의 값 수에 대해 작동하므로 특정 경우에 정의 된 경우 Rand_Max에도 포함 된 총 수보다 하나 적은 값이됩니다.

Mark 's 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이면이 4 개의 값이 유효한 반환 값 세트를 구성하더라도이를 버립니다. .

IE : 버리기 (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%

필요한 Reroll의 비율이 N이 RM에 가까워 질수록 증가하기 때문에 코드를 실행하는 시스템의 제약 조건과 찾는 값에 따라 여러 가지 다른 값에서 유효한 문제가 될 수 있습니다.

이를 무효화하기 위해 다음과 같이 간단한 수정을 할 수 있습니다.

 int x;

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

 x %= n;

이것은 계수를 사용하여 최대 값을 정의하는 추가 특성을 설명하는보다 일반적인 버전의 공식을 제공합니다.

N을 곱한 RAND_MAX에 작은 값을 사용하는 예

Mark'original Version :

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의 값 수 여야하는 경우; 이 경우 RAND_MAX = INT_MAX가 아니라면 N = RAND_MAX +1을 설정할 수 있습니다.

루프 방식으로 N = 1을 사용할 수 있으며 X의 모든 값이 허용되지만 최종 승수에 IF 문을 넣습니다. 그러나 아마도 함수가 n = 1로 호출 될 때 1을 반환하는 유효한 이유가있을 수있는 코드가있을 수 있습니다 ...

따라서 n = RAND_MAX + 1을 원할 때 일반적으로 Div 0 오류를 제공하는 0을 사용하는 것이 좋습니다.

일반화 된 버전 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의 곱일 때 발생하는 불필요하게 폐기 된 유효한 결과로 문제를 해결합니다.

두 번째 버전은 또한 RAND_MAX에 포함 된 총 가능한 값 집합과 같도록 n이 필요한 경우의 시나리오를 다룹니다.

수정 된 접근 방식은 동일하며 유효한 난수를 제공하고 폐기 된 값을 최소화해야하는보다 일반적인 솔루션을 제공합니다.

반복해서 :

마크의 예를 확장하는 기본 일반 솔루션 :

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

 int x;

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

 x %= n;

RAND_MAX + 1 = n의 한 가지 추가 시나리오를 허용하는 확장 일반 솔루션 :

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x;

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

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

일부 조건 (특히 해석 된 언어)에서 while 조건 이외의 비교 연산 계산을 수행하면 재시도 횟수에 관계없이 일회성 계산이므로 더 빠른 결과를 얻을 수 있습니다. YMMV!

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x; // Resulting random number
int y; // One-time calculation of the compare value for x

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

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

Mark의 솔루션의 문제점은 RAND_MAX와 n이 실제로 두 가지 다른 의미를 가질 때 동일한 "측정 단위"로 취급한다는 점이 안전하지 않습니까? n은 결과 "가능성 수"를 나타내지 만 RAND_MAX는 원래 가능성의 최대 값 만 나타내며, 여기서 RAND_MAX + 1은 원래 가능성 수입니다. 나는 그가 N 인정 것 같았다 때문에 그가 당신의 결론을하지 않았다 놀라게 RAND_MAX 방정식과 같은 일을하지 않았다 해요 :RAND_MAX%n = n - 1
다닐 수자 모라에스을

@ DaniloSouzaMorães 감사합니다 Danilo, 문제를 간결하게 작성했습니다. 나는 그가 왜, 어떻게 그 일을했는지 ​​시연하기 위해 갔다. 왜 문제가 있는지, 나는 무엇이 문제인지 명확하게 진술하지 않습니다. 여기에서 작성한 내용 중 일부를 허용 된 솔루션이 수행하는 작업의 문제에 대한 내 자신의 요약으로 사용하도록 답변을 수정해도 좋습니까?
벤 Personick

대단 할 것입니다. 그것을 위해 이동
다닐 수자 모라에스에게

1

A의 RAND_MAX3은 편견이 있음이 계산에서 의미가 있습니다 (실제로는 그보다 훨씬 더 높아야한다하지만 편견은 여전히 존재하는 것) :

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

이 경우 와 % 2사이의 임의의 숫자를 원할 때 수행하지 말아야 할 것 입니다. 당신은 사이의 임의의 숫자를 얻을 수 및 수행하여 때문에이 경우에는, 비록 : 의 배수입니다 .0102% 3RAND_MAX3

다른 방법

훨씬 간단하지만 다른 답변에 추가하려면 다음 0n - 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

-1

허용되는 답변에서 알 수 있듯이 "모듈러스 바이어스"는 낮은 값의 근본을 갖습니다 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

따라서 0의 4 개 출력 (4/10 확률)과 1과 2의 3 개의 출력 (각 3/10 기회)이 있습니다.

편향되어 있습니다. 숫자가 낮을수록 나올 확률이 높습니다.

그러나 그것은 아주 RAND_MAX작은 때에 만 분명히 나타납니다 . 또는 더 구체적으로, 당신이 모딩하는 숫자가에 비해 클 때RAND_MAX.

루핑 보다 훨씬 더 나은 해결책은 (비효율적으로 제안되어서는 안됨) 훨씬 더 큰 출력 범위를 가진 PRNG를 사용하는 것입니다. 메르 센 트위스터 알고리즘 4,294,967,295의 최대 출력을 갖는다. MersenneTwister::genrand_int32() % 10모든 의도와 목적 을 위해 그렇게함으로써 균등하게 분배되고 모듈로 바이어스 효과는 사라질 것입니다.


3
귀하가 더 효율적이며 RAND_MAX가 귀하가 모딩하는 수보다 훨씬 큰 경우에도 귀하의 편향은 여전히 ​​바이어스됩니다. 어쨌든 이들은 모두 의사 난수 생성기이며 그 자체로 다른 주제이지만, 완전 난수 생성기를 가정하면 여전히 낮은 값을 바이어스합니다.
user1413793

가장 높은 값이 홀수이므로 시간의 MT::genrand_int32()%20 (50 + 2.3e-8) %와 시간의 1 (50-2.3e-8) %를 선택합니다. 카지노의 RGN을 구축하지 않는 한 (아마도 훨씬 더 넓은 범위의 RGN을 사용할 것임) 사용자는 2.3e-8 %의 추가 시간을 느끼지 못합니다. 여기서 중요하지 않은 숫자에 대해 이야기하고 있습니다.
bobobobo

7
루핑이 가장 좋은 솔루션입니다. "비효율적으로"비효율적입니다. 최악의 평균 경우 두 번 미만의 반복이 필요합니다. 높은 RAND_MAX값을 사용 하면 모듈러스 바이어스가 감소하지만 제거하지는 않습니다. 루핑 것입니다.
러드 닐슨

5
경우 RAND_MAX당신에 의해 모딩하는 수보다 충분히 더 큰, 횟수는 임의의 숫자가 하고서과 효율성에 영향을주지 않습니다 다시 생성해야합니다. 나는 당신이 받아 들인 대답에서 제안한 n것보다 가장 큰 배수에 대해 테스트하는 한 반복문을 유지한다고 말합니다 n.
Mark Ransom

-3

방금 Von Neumann의 Unbiased Coin Flip Method에 대한 코드를 작성했습니다.이 코드는 이론적으로 난수 생성 프로세스의 바이어스를 제거해야합니다. 더 많은 정보는에서 찾을 수 있습니다 ( 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;
        }
    }
}

이것은 모듈로 바이어스를 다루지 않습니다. 이 프로세스는 비트 스트림에서 바이어스를 제거하는 데 사용될 수 있습니다. 그러나, 비트 스트림으로부터 0에서 n까지의 균일 한 분포로 가려면 n이 2의 거듭 제곱보다 작지 않은 경우 모듈로 바이어스를 처리해야합니다. 따라서이 솔루션은 난수 생성 프로세스에서 편견을
Rick

2
@ 릭 흠. 1과 100 사이의 난수를 생성 할 때 모듈로 바이어스를 제거하기위한 Von Neumann 방법의 논리적 확장은 다음과 같습니다. A) rand() % 100100 번 호출 . B) 모든 결과가 다르면 첫 번째 결과를 취하십시오. C) 그렇지 않으면 GOTO A. 이것은 효과가 있지만 약 10 ^ 42의 예상 반복 횟수로 꽤 인내심을 가져야합니다. 그리고 불멸의.
Mark Amery

@MarkAmery 실제로 작동합니다. 올바르게 구현되지 않았지만이 알고리즘을 살펴 봅니다. 첫번째 다른 것 :else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
Rick
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.