È un algoritmo casuale "abbastanza buono"? perché non viene utilizzato se è più veloce?


171

Ho chiamato una classe QuickRandome il suo compito è produrre rapidamente numeri casuali. È davvero semplice: prendi il vecchio valore, moltiplica per a doublee prendi la parte decimale.

Ecco la mia QuickRandomclasse nella sua interezza:

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

}

Ed ecco il codice che ho scritto per testarlo:

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

È un algoritmo molto semplice che moltiplica semplicemente il doppio precedente per un doppio "numero magico". L'ho messo insieme abbastanza rapidamente, quindi probabilmente potrei renderlo migliore, ma stranamente, sembra funzionare bene.

Questo è l'output di esempio delle righe commentate nel mainmetodo:

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

Hm. Abbastanza casuale. In effetti, ciò funzionerebbe per un generatore di numeri casuali in un gioco.

Ecco un esempio di output della parte non commentata:

5456313909
1427223941

Wow! Esegue quasi 4 volte più velocemente di Math.random.

Ricordo di aver letto da qualche parte ciò che Math.randomusava System.nanoTime()e tonnellate di moduli folli e roba da divisione. È davvero necessario? Il mio algoritmo si comporta molto più velocemente e sembra abbastanza casuale.

Ho due domande:

  • Il mio algoritmo è "abbastanza buono" (per esempio, un gioco in cui i numeri davvero casuali non sono troppo importanti)?
  • Perché fa Math.randomcosì tanto quando sembra solo una semplice moltiplicazione e il taglio del decimale sarà sufficiente?

154
"sembra piuttosto casuale"; dovresti generare un istogramma ed eseguire un po 'di autocorrelazione sulla sequenza ...
Oliver Charlesworth

63
Vuol dire che "sembra piuttosto casuale" non è in realtà una misura oggettiva della casualità e dovresti ottenere alcune statistiche reali.
Matt H

23
@Doorknob: In parole povere, dovresti indagare se i tuoi numeri hanno una distribuzione "piatta" tra 0 e 1 e vedere se ci sono schemi periodici / ripetitivi nel tempo.
Oliver Charlesworth,

22
Prova new QuickRandom(0,5)o new QuickRandom(.5, 2). Entrambi genereranno ripetutamente 0 per il tuo numero.
FrankieTheKneeMan

119
Scrivere il tuo algoritmo di generazione di numeri casuali è come scrivere il tuo algoritmo di crittografia. C'è così tanta arte precedente, da parte di persone che sono iper qualificate, che è insensato passare il tempo a cercare di farlo bene. Non c'è motivo per non usare le funzioni della libreria Java e se vuoi davvero scriverne una tua per qualche ragione, visita Wikipedia e cerca gli algoritmi come Mersenne Twister.
steveha,

Risposte:


351

L' QuickRandomimplementazione non ha realmente una distribuzione uniforme. Le frequenze sono generalmente più alte ai valori più bassi mentre Math.random()ha una distribuzione più uniforme. Ecco un SSCCE che dimostra che:

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

}

Il risultato medio è simile al seguente:

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  :###################    

Se ripeti il ​​test, vedrai che la distribuzione QR varia notevolmente, a seconda dei seed iniziali, mentre la distribuzione MR è stabile. A volte raggiunge la distribuzione uniforme desiderata, ma più spesso non lo fa. Ecco uno degli esempi più estremi, è persino oltre i confini del grafico:

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  :                                                  

17
+1 per i dati numerici, sebbene la ricerca di numeri grezzi possa essere fuorviante in quanto non significa che abbiano differenze statisticamente significative.
Maciej Piechotka,

16
Questi risultati variano notevolmente con i semi iniziali passati QuickRandom. A volte, è vicino all'uniforme, a volte è molto peggio di così.
Petr Janeček,

68
@ BlueRaja-DannyPflughoeft Qualsiasi PRNG in cui la qualità dell'output dipende fortemente dai valori iniziali del seme (al contrario delle costanti interne) mi sembra rotto.
un CVn il

22
Prima regola delle statistiche: tracciare i dati . La tua analisi è precisa, ma la stampa di un istogramma mostra questo molto più rapidamente. ;-) (Ed è due righe in R.)
Konrad Rudolph,

37
Citazioni obbligatorie: "Chiunque consideri i metodi aritmetici per produrre cifre casuali è, ovviamente, in uno stato di peccato". - John von Neumann (1951) "Chiunque non abbia visto la citazione sopra in almeno 100 posti probabilmente non è molto vecchio." - DV Pryor (1993) "I generatori di numeri casuali non dovrebbero essere scelti a caso." - Donald Knuth (1986)
Happy Green Kid Naps

133

Quello che stai descrivendo è un tipo di generatore casuale chiamato generatore lineare congruenziale . Il generatore funziona come segue:

  • Inizia con un valore seed e un moltiplicatore.
  • Per generare un numero casuale:
    • Moltiplica il seme per il moltiplicatore.
    • Impostare il seme uguale a questo valore.
    • Restituisce questo valore.

Questo generatore ha molte belle proprietà, ma ha problemi significativi come una buona fonte casuale. L'articolo di Wikipedia collegato sopra descrive alcuni dei punti di forza e di debolezza. In breve, se hai bisogno di buoni valori casuali, probabilmente questo non è un ottimo approccio.

Spero che questo ti aiuti!


@ louismo- Non è davvero "casuale" di per sé. I risultati saranno deterministici. Detto questo, non ci ho pensato quando ho scritto la mia risposta; forse qualcuno può chiarire quel dettaglio?
templatetypedef

2
Gli errori aritmetici in virgola mobile sono progettati per l'implementazione. Per quanto ne so, sono coerenti per una determinata piattaforma ma possono differire ad esempio tra telefoni cellulari diversi e tra architetture di PC. Sebbene a volte vengano aggiunti ulteriori "bit di protezione" quando si eseguono una serie di calcoli in virgola mobile in una riga, la presenza o l'assenza di questi bit di protezione può rendere leggermente diverso il calcolo del risultato. (essendo i bit di guardia, ad esempio, l'espansione di un doppio da 64 bit a 80 bit)
Patashu

2
Inoltre, tieni presente che la teoria alla base degli LCRNG presuppone che tu stia lavorando con numeri interi! Lanciare numeri in virgola mobile non produrrà la stessa qualità dei risultati.
duskwuff -inattivo-

1
@duskwuff, hai ragione. Ma se l'hardware in virgola mobile segue regole sane, farlo equivale a farlo con le dimensioni della mantissa e la teoria si applica. Ho solo bisogno di cure extra in quello che stai facendo.
vonbrand,

113

La tua funzione di numero casuale è scadente, poiché ha uno stato interno troppo scarso: il numero emesso dalla funzione in un determinato passaggio dipende interamente dal numero precedente. Ad esempio, se assumiamo che magicNumbersia 2 (a titolo di esempio), la sequenza:

0.10 -> 0.20

è fortemente riflesso da sequenze simili:

0.09 -> 0.18
0.11 -> 0.22

In molti casi, questo genererà evidenti correlazioni nel tuo gioco - ad esempio, se fai chiamate successive alla tua funzione per generare coordinate X e Y per gli oggetti, gli oggetti formeranno chiari schemi diagonali.

A meno che tu non abbia buone ragioni per credere che il generatore di numeri casuali stia rallentando la tua applicazione (e questo è MOLTO improbabile), non ci sono buoni motivi per provare a scrivere la tua.


36
+1 per una risposta pratica ... usala in uno sparatutto e genera nemici lungo diagonali per epici colpi alla testa? : D
mer

@wim: non hai bisogno di un PRNG se vuoi questi schemi.
Sdraiati Ryan il

109

Il vero problema con questo è che il suo istogramma di output dipende in gran parte dal seme iniziale - per la maggior parte del tempo finirà con un output quasi uniforme, ma per la maggior parte del tempo avrà un output chiaramente non uniforme.

Ispirato da questo articolo su quanto sia cattiva la rand()funzione di php , ho realizzato alcune immagini a matrice casuale usando QuickRandome System.Random. Questa corsa mostra come a volte il seme possa avere un effetto negativo (in questo caso favorendo numeri più bassi) dove as System.Randomè piuttosto uniforme.

QuickRandom

System.Random

Persino peggio

Se inizializziamo QuickRandomquando new QuickRandom(0.01, 1.03)otteniamo questa immagine:

Il codice

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

2
Bel codice. Sì, è fantastico. Lo facevo anche a volte, è difficile ottenere una misura quantificabile da esso, ma è un altro buon modo per guardare la sequenza. E se volessi dare un'occhiata a sequenze più lunghe della larghezza * altezza potresti xor l'immagine successiva con questo pixel per pixel. Penso che l'immagine di QuickRandom sia molto più esteticamente piacevole, a causa del fatto che è strutturata come un tappeto di alghe.
Cris Stringfellow,

La parte esteticamente gradevole è come la sequenza tende ad aumentare man mano che si procede lungo ciascuna riga (e poi di nuovo all'inizio di nuovo) poiché la magicNumbermoltiplicazione produce un numero simile a prevNum, che mostra la mancanza di casualità. Se usiamo i semi new QuickRandom(0.01, 1.03), otteniamo questo i.imgur.com/Q1Yunbe.png !
Callum Rogers,

Sì, ottima analisi. Dal momento che moltiplica semplicemente la mod 1 per una costante chiaramente prima che avvenga il wrapping, ci sarà l'aumento che descrivi. Sembra che questo potrebbe essere evitato se prendessimo i decimali meno significativi dicendo moltiplicando per 1 miliardo e riducendo mod una tavolozza di 256 colori.
Cris Stringfellow,

Puoi dirmi cosa hai usato per generare quelle immagini di output? Matlab?
ud

@uDaY: dai un'occhiata al codice, C # e System.Drawing.Bitmap.
Callum Rogers,

37

Un problema con il tuo generatore di numeri casuali è che non esiste uno "stato nascosto" - se so quale numero casuale hai restituito nell'ultima chiamata, conosco ogni singolo numero casuale che invierai fino alla fine dei tempi, poiché ce n'è solo uno possibile prossimo risultato, e così via e così via.

Un'altra cosa da considerare è il "periodo" del generatore di numeri casuali. Ovviamente con una dimensione a stati finiti, uguale alla porzione di mantissa di un doppio, sarà in grado di restituire al massimo 2 ^ 52 valori prima di eseguire il loop. Ma è nel migliore dei casi: puoi provare che non ci sono anelli del periodo 1, 2, 3, 4 ...? Se ci sono, il tuo RNG avrà un comportamento terribile e degenerato in quei casi.

Inoltre, la generazione di numeri casuali avrà una distribuzione uniforme per tutti i punti di partenza? In caso contrario, il tuo RNG sarà distorto, o peggio, distorto in diversi modi a seconda del seme iniziale.

Se riesci a rispondere a tutte queste domande, fantastico. Se non puoi, allora sai perché la maggior parte delle persone non reinventa la ruota e usa un generatore di numeri casuali comprovato;)

(A proposito, un buon adagio è: il codice più veloce è il codice che non viene eseguito. Potresti rendere il più veloce casuale () al mondo, ma non va bene se non è molto casuale)


8
C'è almeno un ciclo banale su questo generatore per tutti i semi: 0 -> 0. A seconda del seme, potrebbero essercene molti altri. (Per esempio, con un seme di 3.0, 0.5 -> 0.5, 0.25 -> 0.75 -> 0.25, 0.2 -> 0.6 -> 0.8 -> 0.4 -> 0.2, etc.)
duskwuff -inactive-

36

Un test comune che ho sempre fatto durante lo sviluppo di PRNG era quello di:

  1. Converti l'output in valori char
  2. Scrivi il valore dei caratteri in un file
  3. Comprimi file

Questo mi ha permesso di scorrere rapidamente idee che erano PRNG "abbastanza buone" per sequenze da circa 1 a 20 megabyte. Ha anche fornito una migliore immagine dall'alto verso il basso rispetto al solo ispezionarlo a occhio, poiché qualsiasi PRNG "abbastanza buono" con mezza parola di stato potrebbe rapidamente superare la capacità dei tuoi occhi di vedere il punto del ciclo.

Se fossi davvero esigente, potrei prendere i buoni algoritmi ed eseguire i test DIEHARD / NIST su di essi, per avere più informazioni, quindi tornare indietro e modificare un po 'di più.

Il vantaggio del test di compressione, al contrario di un'analisi di frequenza, è che, banalmente, è facile costruire una buona distribuzione: è sufficiente emettere un blocco di lunghezza 256 contenente tutti i caratteri con valori compresi tra 0 e 255 e farlo 100.000 volte. Ma questa sequenza ha un ciclo di lunghezza 256.

Una distribuzione distorta, anche con un piccolo margine, dovrebbe essere rilevata da un algoritmo di compressione, in particolare se gli dai abbastanza (diciamo 1 megabyte) della sequenza con cui lavorare. Se alcuni caratteri, o bigrammi o n-grammi si verificano più frequentemente, un algoritmo di compressione può codificare questa inclinazione di distribuzione in codici che favoriscono le ricorrenze frequenti con parole in codice più brevi e si ottiene un delta di compressione.

Poiché la maggior parte degli algoritmi di compressione sono veloci e non richiedono alcuna implementazione (poiché i sistemi operativi li hanno solo in giro), il test di compressione è molto utile per classificare rapidamente il passaggio / fallimento per un PRNG che potresti sviluppare.

Buona fortuna con i tuoi esperimenti!

Oh, ho eseguito questo test sull'Rng che hai sopra, usando la seguente piccola mod del tuo codice:

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

I risultati furono:

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

Considererei un PRNG buono se il file di output non potesse essere compresso affatto. Ad essere sincero, non pensavo che il tuo PRNG avrebbe funzionato così bene, solo il 16% su ~ 20 Meg è impressionante per una costruzione così semplice. Ma lo considero ancora un fallimento.


2
Immaginandolo o no, ho la stessa idea con lo zip anni fa quando collaudo i miei generatori casuali.
Aristos,

1
Grazie @Alexandre C. e Aristos e aidan. Ti credo.
Cris Stringfellow,

33

Il generatore casuale più veloce che potresti implementare è questo:

inserisci qui la descrizione dell'immagine

XD, scherzi a parte, oltre a tutto quanto detto qui, vorrei contribuire citando che testare sequenze casuali "è un compito difficile" [1], e ci sono diversi test che controllano determinate proprietà di numeri pseudo-casuali, puoi trovare un molti qui: http://www.random.org/analysis/#2005

Un modo semplice per valutare la "qualità" del generatore casuale è il vecchio test 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);
}

Citando [1]

L'idea del test χ² è di verificare se i numeri prodotti sono distribuiti ragionevolmente. Se generiamo N numeri positivi inferiori a r , allora ci aspetteremmo di ottenere N / r numeri di ciascun valore. Ma --- e questa è l'essenza della questione --- le frequenze di occorrenza di tutti i valori non dovrebbero essere esattamente le stesse: non sarebbe casuale!

Calcoliamo semplicemente la somma dei quadrati delle frequenze di occorrenza di ciascun valore, ridimensionata in base alla frequenza prevista e quindi sottrarre la dimensione della sequenza. Questo numero, la "statistica χ²", può essere espresso matematicamente come

formula chi quadrata

Se la statistica χ² è vicina a r , i numeri sono casuali; se è troppo lontano, allora non lo sono. Le nozioni di "vicino" e "molto lontano" possono essere definite più precisamente: esistono tabelle che dicono esattamente come correlare la statistica alle proprietà delle sequenze casuali. Per il semplice test che stiamo eseguendo, la statistica dovrebbe essere entro 2√r

Utilizzando questa teoria e il seguente codice:

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

Ho ottenuto il seguente risultato:

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

Che, per QuickRandom, è lontano da r (al di fuori di r ± 2 * sqrt(r))

Detto questo, QuickRandom potrebbe essere veloce ma (come affermato in altre risposte) non è buono come generatore di numeri casuali


[1] SEDGEWICK ROBERT, Algorithms in C , Addinson Wesley Publishing Company, 1990, pagine da 516 a 518


9
+1 per xkcd che è una straordinaria wobsite (oh, e la grande risposta): P
tckmn

1
Grazie e sì rack xkcd! XD
higuaro,

La teoria va bene ma l'esecuzione è scarsa: il codice è suscettibile di overflow di numeri interi. In java tutti int[]sono inizializzati a zero, quindi non è necessario per questa parte. Lanciare in float è inutile quando lavori con i doppi. Ultimo: chiamare i nomi dei metodi random1 e random2 è abbastanza divertente.
bestsss

@bestsss Grazie per le osservazioni! Ho fatto una traduzione diretta dal codice C e non ho prestato molta attenzione ad esso = (. Ho apportato alcune modifiche e aggiornato la risposta. Gradirei qualsiasi suggerimento aggiuntivo
higuaro

14

Ho messo insieme un rapido modello del tuo algoritmo in JavaScript per valutare i risultati. Genera 100.000 numeri interi casuali da 0 a 99 e tiene traccia dell'istanza di ciascun numero intero.

La prima cosa che noto è che è più probabile che tu ottenga un numero basso rispetto a un numero alto. Lo vedi di più quando seed1è alto e seed2basso. In un paio di casi, ho ricevuto solo 3 numeri.

Nella migliore delle ipotesi, il tuo algoritmo necessita di alcuni perfezionamenti.


8

Se la Math.Random()funzione chiama il sistema operativo per ottenere l'ora del giorno, non è possibile confrontarla con la propria funzione. La tua funzione è un PRNG, mentre quella funzione è alla ricerca di numeri casuali reali. Mele e arance

Il tuo PRNG può essere veloce, ma non ha abbastanza informazioni sullo stato per raggiungere un lungo periodo prima che si ripeta (e la sua logica non è abbastanza sofisticata da raggiungere anche i periodi possibili con tante informazioni sullo stato).

Il periodo è la lunghezza della sequenza prima che il PRNG inizi a ripetersi. Ciò accade non appena la macchina PRNG effettua una transizione di stato verso uno stato identico a uno stato passato. Da lì, ripeterà le transizioni iniziate in quello stato. Un altro problema con i PRNG può essere un basso numero di sequenze uniche, nonché la convergenza degenerata su una sequenza particolare che si ripete. Ci possono essere anche modelli indesiderati. Ad esempio, supponiamo che un PRNG sembri abbastanza casuale quando i numeri sono stampati in decimali, ma un'ispezione dei valori in binario mostra che il bit 4 commuta semplicemente tra 0 e 1 su ogni chiamata. Oops!

Dai un'occhiata a Mersenne Twister e ad altri algoritmi. Esistono modi per trovare un equilibrio tra la durata del periodo e i cicli della CPU. Un approccio di base (utilizzato nel Twister di Mersenne) è quello di spostarsi nel vettore di stato. Vale a dire, quando viene generato un numero, non si basa sull'intero stato, ma solo su alcune parole della matrice di stato soggette ad alcune operazioni a bit. Ma ad ogni passo, l'algoritmo si sposta anche nell'array, mescolando i contenuti un po 'alla volta.


5
Sono principalmente d'accordo, tranne che con il tuo primo paragrafo. Le chiamate casuali integrate (e / dev / random su sistemi simili a Unix) sono anche PRNG. Definirei qualsiasi cosa che produca algoritmicamente numeri casuali come PRNG, anche se il seme è qualcosa che è difficile da prevedere. Esistono alcuni "veri" generatori di numeri casuali che usano decadimento radioattivo, rumore atmosferico, ecc., Ma spesso generano relativamente pochi bit / secondo.
Matt Krause,

Sui box Linux, /dev/randomè una fonte di casualità reale ottenuta dai driver di dispositivo e non un PRNG. Si blocca quando non sono disponibili abbastanza bit. Inoltre, il dispositivo /dev/urandomgemello non si blocca, ma non è ancora esattamente un PRNG poiché viene aggiornato con bit casuali quando sono disponibili.
Kaz,

Se la funzione Math.Random () chiama il sistema operativo per ottenere l'ora del giorno , questo è assolutamente falso. (in uno qualsiasi dei sapori / versioni di Java che conosco)
bestsss

@bestsss Questo è dalla domanda originale: ricordo di aver letto da qualche parte che Math.random ha usato System.nanoTime () . Potrebbe essere utile aggiungere la tua conoscenza lì o nella tua risposta. L'ho usato in modo condizionale con un if . :)
Kaz

Kaz, sia nanoTime()+ counter / hash è usato per il seed predefinito java.util.Randomdi oracle / OpenJDK. Questo è solo per il seme, quindi è un LCG standard. In effetti il ​​generatore OP prende 2 numeri casuali per il seme, il che è ok - quindi nessuna differenza java.util.Random. System.currentTimeMillis()era il seme predefinito in JDK1.4-
bestsss

7

Ci sono molti, molti generatori di numeri pseudo casuali là fuori. Ad esempio il ranarray di Knuth , il tornado di Mersenne o i generatori LFSR. I monumentali "algoritmi seminumerici" di Knuth analizzano l'area e propongono alcuni generatori congruenziali lineari (semplici da implementare, veloci).

Ma ti suggerirei di attenersi a java.util.Randomo Math.random, sono veloci e almeno OK per un uso occasionale (ad esempio, giochi e simili). Se sei solo paranoico sulla distribuzione (alcuni programmi Monte Carlo o un algoritmo genetico), controlla la loro implementazione (la fonte è disponibile da qualche parte) e seminali con un numero davvero casuale, dal tuo sistema operativo o da random.org . Se questo è necessario per alcune applicazioni in cui la sicurezza è fondamentale, dovrai scavare. E come in quel caso non dovresti credere a quello che un quadrato colorato con i pezzi mancanti spunta qui, sto zitto ora.


7

È molto improbabile che le prestazioni di generazione di numeri casuali rappresentino un problema per qualsiasi caso d'uso che si è presentato a meno che non si acceda a una singola Randomistanza da più thread (perché lo Randomè synchronized).

Tuttavia, se è davvero così e hai bisogno di molti numeri casuali in fretta, la tua soluzione è troppo inaffidabile. A volte dà buoni risultati, a volte dà risultati orribili (in base alle impostazioni iniziali).

Se vuoi gli stessi numeri che la Randomclasse ti dà, solo più velocemente, potresti sbarazzarti della sincronizzazione lì dentro:

public class QuickRandom {

    private long seed;

    private static final long MULTIPLIER = 0x5DEECE66DL;
    private static final long ADDEND = 0xBL;
    private static final long MASK = (1L << 48) - 1;

    public QuickRandom() {
        this((8682522807148012L * 181783497276652981L) ^ System.nanoTime());
    }

    public QuickRandom(long seed) {
        this.seed = (seed ^ MULTIPLIER) & MASK;
    }

    public double nextDouble() {
        return (((long)(next(26)) << 27) + next(27)) / (double)(1L << 53);
    }

    private int next(int bits) {
        seed = (seed * MULTIPLIER + ADDEND) & MASK;
        return (int)(seed >>> (48 - bits));
    }

}

Ho semplicemente preso il java.util.Randomcodice e rimosso la sincronizzazione che risulta in prestazioni doppie rispetto all'originale sul mio Oracle HotSpot JVM 7u9. È ancora più lento del tuo QuickRandom, ma dà risultati molto più coerenti. Per essere precisi, per gli stessi seedvalori e le applicazioni a thread singolo, fornisce gli stessi numeri pseudo-casuali della Randomclasse originale .


Questo codice si basa sull'attuale java.util.RandomOpenJDK 7u che è concesso in licenza sotto GNU GPL v2 .


MODIFICA 10 mesi dopo:

Ho appena scoperto che non è nemmeno necessario utilizzare il mio codice sopra per ottenere Randomun'istanza non sincronizzata . Ce n'è anche uno nel JDK!

Guarda la ThreadLocalRandomclasse di Java 7 . Il codice al suo interno è quasi identico al mio codice sopra. La classe è semplicemente una Randomversione isolata dal thread locale adatta per generare rapidamente numeri casuali. L'unico aspetto negativo che mi viene in mente è che non è possibile impostarlo seedmanualmente.

Esempio di utilizzo:

Random random = ThreadLocalRandom.current();

2
@Modifica Hmm, potrei confrontare QR, Math.random e ThreadLocalRandom qualche volta quando non sono troppo pigro :)È interessante, grazie!
martedì

1. Puoi ottenere un po 'più di velocità facendo cadere la maschera poiché i 16 bit più alti non influenzano i bit usati. 2. È possibile utilizzare quei bit, salvare una sottrazione e ottenere un generatore migliore (stato più grande; i bit più significativi di un prodotto sono i più ben distribuiti, ma sarebbe necessaria una valutazione). 3. I ragazzi di Sun hanno semplicemente implementato un RNG arcaico di Knuth e hanno aggiunto la sincronizzazione. :(
maaartinus,

3

'Casuale' è molto più che ottenere numeri .... quello che hai è pseudo-casuale

Se lo pseudo-casuale è abbastanza buono per i tuoi scopi, allora sicuramente è molto più veloce (e XOR + Bitshift sarà più veloce di quello che hai)

Rolf

Modificare:

OK, dopo essere stato troppo affrettato in questa risposta, lasciami rispondere al vero motivo per cui il tuo codice è più veloce:

Da JavaDoc per Math.Random ()

Questo metodo è correttamente sincronizzato per consentire l'uso corretto di più di un thread. Tuttavia, se molti thread devono generare numeri pseudocasuali a una velocità elevata, è possibile ridurre la contesa per ogni thread di avere il proprio generatore di numeri pseudocasuali.

Questo è probabilmente il motivo per cui il tuo codice è più veloce.


3
Praticamente qualsiasi cosa che non coinvolga un generatore di rumore hardware o una linea diretta nelle cose I / O del sistema operativo, sarà pseudo-casuale. La casualità autentica non può essere generata da un solo algoritmo; hai bisogno di rumore da qualche parte. (Alcuni RNG di alcuni sistemi operativi ottengono il loro input misurando cose come / quando muovi il mouse, scrivi cose, ecc. Misurato su una scala da microsecondi a nanosecondi, che può essere altamente imprevedibile.)
cHao

@OliCharlesworth: in effetti, per quanto ne so, gli unici valori casuali veri si trovano usando il rumore atmosferico.
Jeroen Vannevel,

@me ... stupido rispondere in fretta. Math.random è pseudocasuale e inoltre è sincronizzato .
rolfl,

@Rolfl: la sincronizzazione potrebbe benissimo spiegare perché Math.random()è più lenta. Dovrebbe o sincronizzarsi o crearne uno nuovo Randomogni volta, e nessuno dei due è molto attraente dal punto di vista delle prestazioni. Se mi preoccupassi delle prestazioni, ne creerei una mia new Randome la userei. : P
cHao,

Anche il decadimento radioattivo di @JeroenVannevel è casuale.
RxS,

3

java.util.Random non è molto diverso, un LCG di base descritto da Knuth. Tuttavia ha 2 principali vantaggi / differenze:

  • thread-safe: ogni aggiornamento è un CAS più costoso di una semplice scrittura e necessita di un ramo (anche se previsto con thread singolo perfetto). A seconda della CPU potrebbe esserci una differenza significativa.
  • stato interno non divulgato - questo è molto importante per qualsiasi cosa non banale. Desideri che i numeri casuali non siano prevedibili.

Di seguito è la routine principale che genera numeri interi "casuali" in java.util.Random.


  protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
          oldseed = seed.get();
          nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

Se si rimuove AtomicLong e lo stato non divulgato (ovvero utilizzando tutti i bit di long ), si otterrebbero maggiori prestazioni rispetto alla doppia moltiplicazione / modulo.

Ultima nota: Math.randomnon dovrebbe essere usato per altro che semplici test, è soggetto a contese e se hai anche un paio di thread che lo chiamano contemporaneamente le prestazioni diminuiscono. Una sua caratteristica storica poco nota è l'introduzione di CAS in Java - per battere un famigerato benchmark (prima da IBM via intrinsics e poi Sun fatto "CAS da Java")


0

Questa è la funzione casuale che utilizzo per i miei giochi. È abbastanza veloce e ha una buona (abbastanza) distribuzione.

public class FastRandom {

    public static int randSeed;

      public static final int random()
      {
        // this makes a 'nod' to being potentially called from multiple threads
        int seed = randSeed;

        seed    *= 1103515245;
        seed    += 12345;
        randSeed = seed;
        return seed;
      }

      public static final int random(int range)
      {
        return ((random()>>>15) * range) >>> 17;
      }

      public static final boolean randomBoolean()
      {
         return random() > 0;
      }

       public static final float randomFloat()
       {
         return (random()>>>8) * (1.f/(1<<24));
       }

       public static final double randomDouble() {
           return (random()>>>8) * (1.0/(1<<24));
       }
}

1
Questo non fornisce una risposta alla domanda. Per criticare o richiedere chiarimenti a un autore, lascia un commento sotto il suo post.
John Willemse,

Penso che sia già stato stabilito che l'algoritmo originale non è abbastanza buono? Forse un esempio di ciò che è abbastanza buono può portare all'ispirazione su come migliorarlo?
Terje,

Sì, forse, ma non risponde affatto alla domanda e non ci sono dati che supportano il tuo algoritmo in realtà "abbastanza buono". Generalmente, gli algoritmi di numeri casuali e gli algoritmi di crittografia strettamente correlati non sono mai buoni come quelli degli esperti che li hanno implementati in un linguaggio di programmazione. Quindi, se potessi supportare la tua richiesta e approfondire il motivo per cui è meglio dell'algoritmo nella Domanda, risponderesti almeno a una domanda.
John Willemse,

Bene ... Gli esperti che li hanno implementati in un linguaggio di programmazione mirano a una distribuzione "perfetta", mentre in un gioco non ne hai mai bisogno. Volete velocità e distribuzione "abbastanza buona". Questo codice offre questo. Se è inappropriato qui, eliminerò la risposta, nessun problema.
Terje,

Per quanto riguarda il multithreading, l'utilizzo della variabile locale è un no-op, poiché senza volatile, il compilatore è libero di eliminare (o introdurre) le variabili locali a piacimento.
maaartinus,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.