2016년 11월 23일 수요일

정확한 확률 조정 방법

흔히들 확률에 대해 불만을 가지는 사람들이 많습니다. 0.1% 확률이 정확하게 0.1%로 일어나느냐는 것이죠.
물론 내부에서 random()같은 확률함수를 사용하지만, 이 확률함수를 믿을수 있느냐는 문제도 있습니다.
만약 그렇다면 다음과 같은 방식으로 0.1%를 정확하게 만들 수 있습니다.


public class OneByThousent
{
    private bool[] array = new bool[1000];
    private int point = 0;

    public OneByThousent()
    {
        for(int k = 0; k < 1000; ++k)
            array[k] = false;
        array[0] = true;
    }
}

여기까지 하면 1000개의 배열에 하나의 true가 들어갑니다. 0.1% 확률이죠.
물론 2000개 배열에 true를 2개 넣든가 5000개 배열에 5개를 넣던가 해도 상관 없습니다.
그 다음 이 배열을 잘 섞어줍니다. 이것은 시스템의 random함수를 사용해도 됩니다.
컴파일러마다 조금씩 다르지만, 보통 정수난수를 발생하는 함수라면,

public class OneByThousent
{
    private bool[] array = new bool[1000];
    private int point = 0;

    public OneByThousent()
    {
        for(int k = 0; k < 1000; ++k)
            array[k] = false;
        array[0];

        Shuffle();
    }

    private void Shuffle()
    {
        for(int from = 0; from < 1000; ++from)
        {
            int to = random() % 1000;    // 1)

            // from과 to를 맞바꿈
            bool tmp = array[from];
            array[from] = array[to];
            array[to] = tmp;
        }

        point = 0;
    }
}

다음에 이 array에서 하나씩 꺼내옵니다.

public class OneByThousent
{
    private bool[] array = new bool[1000];
    private int point = 0;

    public OneByThousent()
    {
        for(int k = 0; k < 1000; ++k)
            array[k] = false;
        array[0];

        Shuffle();
    }

    private void Shuffle()
    {
        for(int from = 0; from < 1000; ++from)
        {
            int to = (int)(random() * 1000);

            // from과 to를 맞바꿈
            bool tmp = array[from];
            array[from] = array[to];
            array[to] = tmp;
        }

        point = 0;
    }

    public bool Get()
    {
        bool rtn = array[point];
        ++point;
        if(point > 1000)
            Shuffle();
        return rtn;
    }
}

이렇게 하면 Get()함수는 정확하게 0.1% 확률로 true를 리턴하게 될 것입니다. 비록 random()함수가 정확한 랜덤이 아니라고 해도 말입니다.




마찬가지로, 만약

public class OneByThousent
{
    private float[] array = new float[1000];
    private int point = 0;

    public OneByThousent()
    {
        for(int k = 0; k < 1000; ++k)
            array[k] = k / 1000F;
        array[0];

        Shuffle();
    }

    private void Shuffle()
    {
        for(int from = 0; from < 1000; ++from)
        {
            int to = (int)(random() * 1000);

            // from과 to를 맞바꿈
            float tmp = array[from];
            array[from] = array[to];
            array[to] = tmp;
        }

        point = 0;
    }

    public float Get()
    {
        float rtn = array[point];
        ++point;
        if(point > 1000)
            Shuffle();
        return rtn;
    }
}

이렇게 하면 0~1 사이에서 1/1000 단위로 균일하게 분포된 난수가 만들어집니다.


다만, 이런 정확한 확률은 shuffle이 여러번 일어날 때 - 컴퓨터가 오랜 시간 돌아갈 때 정확하게 나타납니다.

만약 1/(1백만)의 확률을 만들기 위해 저런 식으로 1백만개의 어레이를 만들었다고 합시다.
그런데 저 함수로 10만개의 확률을 계산한 이후 (점검 등으로) 컴퓨터가 내려가는 일이 반복된다면, 그것은 그냥 random()함수를 사용하는 것이나 다름이 없습니다. 만약 이런 정확한 확률이 필요하다면 이 어레이를 파일이나 DB에라도 저장해 놓고 컴퓨터를 재시작한 이후에도 다시 연결될 수 있도록 해야겠죠.

1) 이런 식으로 나머지연산을 사용하면 일정 범위의 난수를 쉽게 얻을 수 있습니다.
하지만 이럴 경우 주의를 해야 합니다.
언젠가 난수를 출력해 봤는데, 난수가 다음과 같이 나오더군요(물론 이것도 컴파일러에 따라 다릅니다).

random() % 2 : 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,.......
random() % 4 : 1,3,2,0,1,3,2,0,1,3,2,0,1,3,2,0,1,3,2,0,1,3,2,0,........
random() % 8 : 6,1,7,4,3,0,5,2,6,1,7,4,3,0,5,2,6,1,7,4,3,0,5,2,..........

다른 수일 경우는 괜찮았는데, 저렇게 2의 제곱수의 나머지를 구하면 순서대로 나옵니다.

그러므로 가장 좋은 방법은 다음과 같습니다.

    int rnd = (int)((random() * 1000.0) / MAXRANDOM);

댓글 1개: