[java] 이것이“충분한”랜덤 알고리즘입니까? 왜 더 빠르면 사용되지 않습니까?

나는이라는 클래스를 만들었고 QuickRandom그 임무는 임의의 숫자를 빠르게 생성하는 것입니다. 정말 간단합니다. 이전 값에 a를 곱하고 double소수 부분을 취하십시오.

여기 내 QuickRandom수업 전체가 있습니다 :

public class QuickRandom {
    private double prevNum;
    private double magicNumber;

    public QuickRandom(double seed1, double seed2) {
        if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
        prevNum = seed1;
        if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
        magicNumber = seed2;
    }

    public QuickRandom() {
        this(Math.random(), Math.random() * 10);
    }

    public double random() {
        return prevNum = (prevNum*magicNumber)%1;
    }

}

그리고 그것을 테스트하기 위해 작성한 코드는 다음과 같습니다.

public static void main(String[] args) {
        QuickRandom qr = new QuickRandom();

        /*for (int i = 0; i < 20; i ++) {
            System.out.println(qr.random());
        }*/

        //Warm up
        for (int i = 0; i < 10000000; i ++) {
            Math.random();
            qr.random();
            System.nanoTime();
        }

        long oldTime;

        oldTime = System.nanoTime();
        for (int i = 0; i < 100000000; i ++) {
            Math.random();
        }
        System.out.println(System.nanoTime() - oldTime);

        oldTime = System.nanoTime();
        for (int i = 0; i < 100000000; i ++) {
            qr.random();
        }
        System.out.println(System.nanoTime() - oldTime);
}

이전의 double에 “magic number”double을 곱하는 매우 간단한 알고리즘입니다. 나는 그것을 함께 빨리 던 졌으므로 아마 더 나아질 수는 있지만 이상하게도 잘 작동하는 것 같습니다.

다음은 main메소드 에서 주석 처리 된 행의 샘플 출력입니다 .

0.612201846732229
0.5823974655091941
0.31062451498865684
0.8324473610354004
0.5907187526770246
0.38650264675748947
0.5243464344127049
0.7812828761272188
0.12417247811074805
0.1322738256858378
0.20614642573072284
0.8797579436677381
0.022122999476108518
0.2017298328387873
0.8394849894162446
0.6548917685640614
0.971667953190428
0.8602096647696964
0.8438709031160894
0.694884972852229

흠. 꽤 무작위입니다. 사실, 그것은 게임에서 난수 생성기에 효과적입니다.

주석 처리되지 않은 부분의 샘플 출력은 다음과 같습니다.

5456313909
1427223941

와! 보다 거의 4 배 빠른 성능을 발휘합니다 Math.random.

나는 어딘가에 미친 모듈러스와 나눗셈 을 Math.random사용한 것을 읽은 것을 기억 System.nanoTime()합니다. 정말 필요한가요? 내 알고리즘은 훨씬 빠르게 수행되며 꽤 무작위로 보입니다.

두 가지 질문이 있습니다.

  • 내 알고리즘이 “충분히”(예를 들어, 실제로 임의의 숫자가 그렇게 중요하지 않은 게임 )입니까?
  • Math.random단순한 곱셈과 십진수를 잘라내는 것으로 충분할 때 왜 그렇게 많은 일을합니까?


답변

귀하의 QuickRandom구현은 정말 균일 한 분포를 가지고있다. 주파수는 일반적으로 낮은 값 일수록 더 높고 Math.random()분포는 더 균일합니다. 다음 을 보여주는 SSCCE 가 있습니다.

package com.stackoverflow.q14491966;

import java.util.Arrays;

public class Test {

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        int[] frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (qr.random() * 10)]++;
        }
        printDistribution("QR", frequencies);

        frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (Math.random() * 10)]++;
        }
        printDistribution("MR", frequencies);
    }

    public static void printDistribution(String name, int[] frequencies) {
        System.out.printf("%n%s distribution |8000     |9000     |10000    |11000    |12000%n", name);
        for (int i = 0; i < 10; i++) {
            char[] bar = "                                                  ".toCharArray(); // 50 chars.
            Arrays.fill(bar, 0, Math.max(0, Math.min(50, frequencies[i] / 100 - 80)), '#');
            System.out.printf("0.%dxxx: %6d  :%s%n", i, frequencies[i], new String(bar));
        }
    }

}

평균 결과는 다음과 같습니다.

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  11376  :#################################
0.1xxx:  11178  :###############################
0.2xxx:  11312  :#################################
0.3xxx:  10809  :############################
0.4xxx:  10242  :######################
0.5xxx:   8860  :########
0.6xxx:   9004  :##########
0.7xxx:   8987  :#########
0.8xxx:   9075  :##########
0.9xxx:   9157  :###########

MR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  10097  :####################
0.1xxx:   9901  :###################
0.2xxx:  10018  :####################
0.3xxx:   9956  :###################
0.4xxx:   9974  :###################
0.5xxx:  10007  :####################
0.6xxx:  10136  :#####################
0.7xxx:   9937  :###################
0.8xxx:  10029  :####################
0.9xxx:   9945  :###################    

테스트를 반복하면 초기 시드에 따라 QR 분포가 크게 변하는 반면 MR 분포는 안정적임을 알 수 있습니다. 때로는 원하는 균일 분포에 도달하지만 그렇지 않은 경우가 많습니다. 다음은 가장 극단적 인 예 중 하나입니다. 그래프의 경계를 넘어서도 있습니다.

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  41788  :##################################################
0.1xxx:  17495  :##################################################
0.2xxx:  10285  :######################
0.3xxx:   7273  :
0.4xxx:   5643  :
0.5xxx:   4608  :
0.6xxx:   3907  :
0.7xxx:   3350  :
0.8xxx:   2999  :
0.9xxx:   2652  :                                                  


답변

당신이 설명하는 것은 선형 합동 발생기 라고 불리는 임의의 생성기 입니다. 발전기는 다음과 같이 작동합니다.

  • 시드 값과 승수로 시작하십시오.
  • 난수를 생성하려면
    • 시드에 승수를 곱하십시오.
    • 시드를이 값과 동일하게 설정하십시오.
    • 이 값을 돌려줍니다.

이 생성기는 많은 훌륭한 속성을 가지고 있지만 좋은 임의 소스로 심각한 문제가 있습니다. 위에 링크 된 Wikipedia 기사는 몇 가지 장단점을 설명합니다. 간단히 말해서, 임의의 좋은 값이 필요한 경우에는이 방법이 적합하지 않을 수 있습니다.

도움이 되었기를 바랍니다!


답변

내부 숫자가 너무 적기 때문에 난수 함수는 좋지 않습니다. 주어진 단계에서 함수가 출력하는 숫자는 이전 숫자에 전적으로 의존합니다. 예를 들어, 그것이 magicNumber2 라고 가정하면 (예를 들어) 시퀀스는 다음과 같습니다.

0.10 -> 0.20

비슷한 시퀀스로 강력하게 미러링됩니다.

0.09 -> 0.18
0.11 -> 0.22

많은 경우, 이것은 게임에서 눈에 띄는 상관 관계를 생성합니다. 예를 들어 객체에 대한 X 및 Y 좌표를 생성하기 위해 함수를 연속적으로 호출하면 객체가 명확한 대각선 패턴을 형성합니다.

난수 생성기가 응용 프로그램 속도를 늦추고 있다고 믿을만한 충분한 이유가 없다면 (아마도 그럴 가능성은 낮음), 직접 시도해보아야 할 이유는 없습니다.


답변

이것의 실제 문제는 출력 히스토그램이 초기 시드에 크게 좌우된다는 것입니다. 많은 시간이 거의 균일 한 출력으로 끝나지만 많은 시간이 분명히 불균일 한 출력을 갖게됩니다.

PHP의 rand()기능이 얼마나 나쁜지에 대한이 기사에서 영감을 얻어 QuickRandomand를 사용하여 임의의 매트릭스 이미지를 만들었습니다 System.Random. 이 실행은 씨앗이 때때로 System.Random꽤 균일 한 경우에 나쁜 영향을 줄 수있는 방법 (이 경우 낮은 숫자를 선호)을 보여줍니다 .

QuickRandom

System.Random

심지어 더 나쁘다

우리가 초기화 경우 QuickRandomnew QuickRandom(0.01, 1.03)우리는이 이미지를 얻을 :

코드

using System;
using System.Drawing;
using System.Drawing.Imaging;

namespace QuickRandomTest
{
    public class QuickRandom
    {
        private double prevNum;
        private readonly double magicNumber;

        private static readonly Random rand = new Random();

        public QuickRandom(double seed1, double seed2)
        {
            if (seed1 >= 1 || seed1 < 0) throw new ArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
            prevNum = seed1;
            if (seed2 <= 1 || seed2 > 10) throw new ArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
            magicNumber = seed2;
        }

        public QuickRandom()
            : this(rand.NextDouble(), rand.NextDouble() * 10)
        {
        }

        public double Random()
        {
            return prevNum = (prevNum * magicNumber) % 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var rand = new Random();
            var qrand = new QuickRandom();
            int w = 600;
            int h = 600;
            CreateMatrix(w, h, rand.NextDouble).Save("System.Random.png", ImageFormat.Png);
            CreateMatrix(w, h, qrand.Random).Save("QuickRandom.png", ImageFormat.Png);
        }

        private static Image CreateMatrix(int width, int height, Func<double> f)
        {
            var bitmap = new Bitmap(width, height);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    var c = (int) (f()*255);
                    bitmap.SetPixel(x, y, Color.FromArgb(c,c,c));
                }
            }

            return bitmap;
        }
    }
}


답변

난수 생성기의 한 가지 문제는 ‘숨겨진 상태’가 없다는 것입니다. 마지막 호출에서 어떤 난수를 반환했는지 알면 시간이 끝날 때까지 보낼 수있는 모든 난수는 하나만 알고 있습니다. 가능한 다음 결과 등.

고려해야 할 또 다른 사항은 난수 생성기의 ‘기간’입니다. 분명히 유한 상태 크기에서 double의 가수 부분과 동일하게 반복하기 전에 최대 2 ^ 52 값만 반환 할 수 있습니다. 그러나 그것은 가장 좋은 경우입니다-1, 2, 3, 4주기의 루프가 없음을 증명할 수 있습니까? 있다면, RNG는 그러한 경우에 끔찍하고 퇴보적인 행동을 할 것입니다.

또한 난수 생성에 모든 시작점에 대해 균일 한 분포가 있습니까? 그렇지 않은 경우 RNG가 시작 시드에 따라 다른 방식으로 바이어스됩니다.

이 모든 질문에 대답 할 수 있다면 굉장합니다. 당신이 할 수 없다면, 당신은 왜 대부분의 사람들이 바퀴를 다시 발명하지 않고 입증 된 난수 생성기를 사용하는지 알고 있습니다.)

(어쨌든 좋은 속담은 다음과 같습니다. 가장 빠른 코드는 실행되지 않는 코드입니다. 세계에서 가장 빠른 random ()을 만들 수는 있지만 매우 무작위가 아닌 경우 좋지 않습니다)


답변

PRNG를 개발할 때 항상 한 가지 일반적인 테스트는 다음과 같습니다.

  1. 출력을 char 값으로 변환
  2. 문자 값을 파일에 쓰기
  3. 압축 파일

이를 통해 약 1 ~ 20 메가 바이트의 시퀀스에 대해 “충분한”PRNG 아이디어를 신속하게 반복 할 수있었습니다. 또한 절반 단어의 상태를 가진 “충분히 충분한”PRNG는 눈의 사이클 포인트를 보는 눈 능력을 빠르게 초과 할 수 있기 때문에 눈으로 검사하는 것보다 더 나은 하향식 사진을 제공했습니다.

내가 정말 까다 롭다면 좋은 알고리즘을 사용하고 DIEHARD / NIST 테스트를 실행하여 더 많은 통찰력을 얻은 다음 다시 돌아가서 더 조정할 수 있습니다.

빈도 분석과 달리 압축 테스트의 장점은 사소하게 좋은 분포를 구성하는 것이 쉽다는 것입니다. 값이 0-255 인 모든 문자를 포함하는 256 길이 블록을 출력하고이 작업을 10 만 번 수행하면됩니다. 그러나이 시퀀스의 길이는 256입니다.

작은 마진으로도 왜곡 된 분포는 압축 알고리즘에 의해 선택되어야합니다. 특히 작업에 충분한 시퀀스 (예 : 1MB)를 제공하는 경우 압축 알고리즘을 사용해야합니다. 일부 문자 또는 bigram 또는 n-gram이 더 자주 발생하는 경우 압축 알고리즘은이 분포 왜곡을 더 짧은 코드 단어로 자주 발생하는 코드로 인코딩하여 압축 델타를 얻을 수 있습니다.

대부분의 압축 알고리즘은 빠르며 구현이 필요하지 않기 때문에 (OS는 그냥 누워 있기 때문에) 압축 테스트는 개발중인 PRNG에 대한 합격 / 불합격을 빠르게 평가하는 데 매우 유용합니다.

실험에 행운을 빕니다!

오, 나는 다음과 같은 작은 코드를 사용하여 위의 rng 에서이 테스트를 수행했습니다.

import java.io.*;

public class QuickRandom {
    private double prevNum;
    private double magicNumber;

    public QuickRandom(double seed1, double seed2) {
        if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
        prevNum = seed1;
        if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
        magicNumber = seed2;
    }

    public QuickRandom() {
        this(Math.random(), Math.random() * 10);
    }

    public double random() {
        return prevNum = (prevNum*magicNumber)%1;
    }

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        FileOutputStream fout = new FileOutputStream("qr20M.bin");

        for (int i = 0; i < 20000000; i ++) {
            fout.write((char)(qr.random()*256));
        }
    }
}

결과는 다음과 같습니다.

Cris-Mac-Book-2:rt cris$ zip -9 qr20M.zip qr20M.bin2
adding: qr20M.bin2 (deflated 16%)
Cris-Mac-Book-2:rt cris$ ls -al
total 104400
drwxr-xr-x   8 cris  staff       272 Jan 25 05:09 .
drwxr-xr-x+ 48 cris  staff      1632 Jan 25 05:04 ..
-rw-r--r--   1 cris  staff      1243 Jan 25 04:54 QuickRandom.class
-rw-r--r--   1 cris  staff       883 Jan 25 05:04 QuickRandom.java
-rw-r--r--   1 cris  staff  16717260 Jan 25 04:55 qr20M.bin.gz
-rw-r--r--   1 cris  staff  20000000 Jan 25 05:07 qr20M.bin2
-rw-r--r--   1 cris  staff  16717402 Jan 25 05:09 qr20M.zip

출력 파일을 전혀 압축 할 수 없으면 PRNG가 양호하다고 생각합니다. 솔직히 말해서, PRNG가 잘하지 않을 것이라고 생각했습니다. ~ 20 Megs의 16 %만이 간단한 구조로 매우 인상적입니다. 그러나 나는 여전히 그것이 실패라고 생각합니다.


답변

가장 빠른 랜덤 생성기는 다음과 같습니다.

여기에 이미지 설명을 입력하십시오

XD, 농담, 여기에 언급 된 모든 것 외에도, 난수 시퀀스 테스트는 “어려운 작업”[1]이라는 인용에 기여하고 싶습니다. 의사 난수의 특정 속성을 확인하는 여러 테스트가 있습니다. 여기에 많은 것들이 있습니다 : http://www.random.org/analysis/#2005

랜덤 발생기 “품질”을 평가하는 간단한 방법 중 하나는 기존 Chi Square 테스트입니다.

static double chisquare(int numberCount, int maxRandomNumber) {
    long[] f = new long[maxRandomNumber];
    for (long i = 0; i < numberCount; i++) {
        f[randomint(maxRandomNumber)]++;
    }

    long t = 0;
    for (int i = 0; i < maxRandomNumber; i++) {
        t += f[i] * f[i];
    }
    return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
}

인용 [1]

χ² 테스트의 아이디어는 생성 된 숫자가 합리적으로 퍼져 있는지 여부를 확인하는 것입니다. r 보다 작은 N 개의 양수 를 생성하면 각 값의 N / r 개의 숫자 를 얻을 것으로 예상됩니다 . 그러나 이것은 문제의 본질입니다. 모든 값의 발생 빈도는 정확히 동일해서는 안됩니다. 그것은 무작위가 아닙니다!

각 값의 발생 빈도의 제곱의 합을 예상 빈도로 스케일링 한 다음 시퀀스 크기를 뺍니다. 이 숫자 “χ² 통계”는 수학적으로 다음과 같이 표현 될 수 있습니다.

치 제곱 공식

χ² 통계량이 r에 가까우 면 숫자는 임의입니다. 너무 멀면 그렇지 않습니다. “close”와 “far away”의 개념을보다 정확하게 정의 할 수 있습니다. 통계가 랜덤 시퀀스의 속성과 어떻게 관련되어 있는지 정확히 알려주는 테이블이 존재합니다. 우리가 수행하는 간단한 테스트의 경우 통계량은 2√r 이내 여야합니다

이 이론과 다음 코드를 사용합니다.

abstract class RandomFunction {
    public abstract int randomint(int range);
}

public class test {
    static QuickRandom qr = new QuickRandom();

    static double chisquare(int numberCount, int maxRandomNumber, RandomFunction function) {
        long[] f = new long[maxRandomNumber];
        for (long i = 0; i < numberCount; i++) {
            f[function.randomint(maxRandomNumber)]++;
        }

        long t = 0;
        for (int i = 0; i < maxRandomNumber; i++) {
            t += f[i] * f[i];
        }
        return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
    }

    public static void main(String[] args) {
        final int ITERATION_COUNT = 1000;
        final int N = 5000000;
        final int R = 100000;

        double total = 0.0;
        RandomFunction qrRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (qr.random() * range);
            }
        };
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, qrRandomInt);
        }
        System.out.printf("Ave Chi2 for QR: %f \n", total / ITERATION_COUNT);

        total = 0.0;
        RandomFunction mathRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (Math.random() * range);
            }
        };
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, mathRandomInt);
        }
        System.out.printf("Ave Chi2 for Math.random: %f \n", total / ITERATION_COUNT);
    }
}

나는 다음과 같은 결과를 얻었다 :

Ave Chi2 for QR: 108965,078640
Ave Chi2 for Math.random: 99988,629040

어느, QuickRandom를 들어, 멀리로부터 R (외부 r ± 2 * sqrt(r))

즉, QuickRandom은 빠를 수 있지만 (다른 답변에 명시된 바와 같이) 난수 생성기로 좋지 않습니다.


[1] SEDGEWICK ROBERT, C의 알고리즘 , Addinson Wesley Publishing Company, 1990, 페이지 516 ~ 518