Est-ce un algorithme aléatoire «assez bon»; pourquoi n'est-il pas utilisé s'il est plus rapide?

171

J'ai créé une classe appelée QuickRandom, et son travail est de produire rapidement des nombres aléatoires. C'est vraiment simple: il suffit de prendre l'ancienne valeur, de multiplier par a doubleet de prendre la partie décimale.

Voici ma QuickRandomclasse dans son intégralité:

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

}

Et voici le code que j'ai écrit pour le tester:

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

C'est un algorithme très simple qui multiplie simplement le double précédent par un double "nombre magique". Je l'ai jeté ensemble assez rapidement, donc je pourrais probablement l'améliorer, mais étrangement, cela semble fonctionner correctement.

Voici un exemple de sortie des lignes commentées dans la mainméthode:

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. Assez aléatoire. En fait, cela fonctionnerait pour un générateur de nombres aléatoires dans un jeu.

Voici un exemple de sortie de la partie non commentée:

5456313909
1427223941

Hou la la! Il fonctionne presque 4 fois plus vite queMath.random .

Je me souviens avoir lu quelque part qui Math.randomutilisait System.nanoTime()et des tonnes de modules et de divisions folles. Est-ce vraiment nécessaire? Mon algorithme fonctionne beaucoup plus rapidement et cela semble assez aléatoire.

J'ai deux questions:

  • Mon algorithme est-il "assez bon" (pour, par exemple, un jeu, où les nombres vraiment aléatoires ne sont pas trop importants)?
  • Pourquoi en Math.randomfaire autant alors qu'il semble qu'une simple multiplication et découper la décimale suffiront?
tckmn
la source
154
"semble assez aléatoire"; vous devriez générer un histogramme et exécuter une autocorrélation sur votre séquence ...
Oliver Charlesworth
63
Il signifie «semble assez aléatoire» n'est pas vraiment une mesure objective du hasard et vous devriez obtenir des statistiques réelles.
Matt H
23
@Doorknob: En termes simples, vous devriez vérifier si vos nombres ont une distribution "plate" entre 0 et 1, et voir s'il existe des modèles périodiques / répétitifs au fil du temps.
Oliver Charlesworth
22
Essayez new QuickRandom(0,5)ou new QuickRandom(.5, 2). Ceux-ci afficheront tous les deux à plusieurs reprises 0 pour votre numéro.
FrankieTheKneeMan
119
Écrire votre propre algorithme de génération de nombres aléatoires revient à écrire votre propre algorithme de chiffrement. Il y a tellement d'art antérieur, par des gens hyper-qualifiés, qu'il est insensé de passer son temps à essayer de faire les choses correctement. Il n'y a aucune raison de ne pas utiliser les fonctions de la bibliothèque Java, et si vous voulez vraiment écrire les vôtres pour une raison quelconque, visitez Wikipedia et recherchez des algorithmes comme Mersenne Twister.
steveha

Réponses:

351

Votre QuickRandomimplémentation n'a pas vraiment une distribution uniforme. Les fréquences sont généralement plus élevées aux valeurs inférieures tout en Math.random()ayant une distribution plus uniforme. Voici un SSCCE qui montre que:

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

}

Le résultat moyen ressemble à ceci:

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

Si vous répétez le test, vous verrez que la distribution QR varie fortement, en fonction des graines initiales, tandis que la distribution MR est stable. Parfois, il atteint la distribution uniforme souhaitée, mais le plus souvent, ce n'est pas le cas. Voici l'un des exemples les plus extrêmes, il dépasse même les limites du graphique:

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  :                                                  
BalusC
la source
17
+1 pour les données numériques - bien que la recherche de nombres bruts puisse être trompeuse car cela ne signifie pas qu'ils présentent une différence statistiquement significative.
Maciej Piechotka
16
Ces résultats varient fortement avec les graines initiales transmises QuickRandom. Parfois, c'est presque l'uniforme, parfois c'est bien pire que ça.
Petr Janeček
68
@ BlueRaja-DannyPflughoeft Tout PRNG où la qualité de la sortie dépend fortement de la (des) valeur (s) initiale (s) (par opposition aux constantes internes) me semble cassé.
un CVn
22
Première règle des statistiques: tracez les données . Votre analyse est précise, mais le traçage d'un histogramme le montre beaucoup plus rapidement. ;-) (Et c'est deux lignes dans R.)
Konrad Rudolph
37
Citations obligatoires: "Quiconque envisage des méthodes arithmétiques de production de chiffres aléatoires est, bien sûr, dans un état de péché." - John von Neumann (1951) «Quiconque n'a pas vu la citation ci-dessus dans au moins 100 endroits n'est probablement pas très vieux.» - DV Pryor (1993) «Les générateurs de nombres aléatoires ne doivent pas être choisis au hasard.» - Donald Knuth (1986)
Happy Green Kid Naps
133

Ce que vous décrivez est un type de générateur aléatoire appelé générateur congruentiel linéaire . Le générateur fonctionne comme suit:

  • Commencez par une valeur de départ et un multiplicateur.
  • Pour générer un nombre aléatoire:
    • Multipliez la graine par le multiplicateur.
    • Définissez la valeur de départ égale à cette valeur.
    • Renvoyez cette valeur.

Ce générateur a de nombreuses propriétés intéressantes, mais présente des problèmes importants en tant que bonne source aléatoire. L'article de Wikipedia lié ci-dessus décrit certaines des forces et des faiblesses. En bref, si vous avez besoin de bonnes valeurs aléatoires, ce n'est probablement pas une très bonne approche.

J'espère que cela t'aides!

templatetypedef
la source
@ louism- Ce n'est pas vraiment "aléatoire" en soi. Les résultats seront déterministes. Cela dit, je n'y ai pas pensé en écrivant ma réponse; peut-être que quelqu'un peut clarifier ce détail?
templatetypedef
2
Les erreurs arithmétiques à virgule flottante sont conçues pour l'implémentation. Autant que je sache, ils sont cohérents pour une certaine plate-forme mais peuvent différer par exemple entre différents téléphones mobiles et entre les architectures de PC. Bien qu'il y ait parfois des «bits de garde» supplémentaires ajoutés lors de l'exécution d'une série de calculs en virgule flottante dans une ligne, et la présence ou l'absence de ces bits de garde peut rendre un calcul subtilement différent dans le résultat. (les bits de garde étant, par exemple, l'expansion d'un double de 64 bits à 80 bits)
Patashu
2
De plus, gardez à l'esprit que la théorie derrière les LCRNG suppose que vous travaillez avec des entiers! Lancer des nombres à virgule flottante ne donnera pas la même qualité de résultats.
duskwuff -inactif-
1
@duskwuff, vous avez raison. Mais si le matériel en virgule flottante suit des règles saines, cela revient à le faire modulo la taille de la mantisse, et la théorie s'applique. Juste besoin de soins supplémentaires dans ce que vous faites.
vonbrand
113

Votre fonction de nombre aléatoire est médiocre, car elle a trop peu d'état interne - le nombre généré par la fonction à une étape donnée dépend entièrement du nombre précédent. Par exemple, si nous supposons que magicNumberc'est 2 (à titre d'exemple), alors la séquence:

0.10 -> 0.20

est fortement reflété par des séquences similaires:

0.09 -> 0.18
0.11 -> 0.22

Dans de nombreux cas, cela générera des corrélations notables dans votre jeu - par exemple, si vous effectuez des appels successifs à votre fonction pour générer les coordonnées X et Y des objets, les objets formeront des motifs diagonaux clairs.

Sauf si vous avez de bonnes raisons de croire que le générateur de nombres aléatoires ralentit votre application (et c'est TRÈS improbable), il n'y a aucune bonne raison d'essayer d'écrire le vôtre.

duskwuff -inactif-
la source
36
+1 pour une réponse pratique ... utiliser ceci dans un shoot em up et engendrer des ennemis le long des diagonales pour des tirs à la tête multiples épiques? : D
wim
@wim: vous n'avez pas besoin d'un PRNG si vous voulez de tels modèles.
Lie Ryan
109

Le vrai problème avec ceci est que son histogramme de sortie dépend beaucoup trop de la graine initiale - la plupart du temps, il se terminera avec une sortie presque uniforme, mais la plupart du temps aura une sortie nettement non uniforme.

Inspiré par cet article sur la mauvaise rand()fonction de php , j'ai créé des images matricielles aléatoires en utilisant QuickRandomet System.Random. Cette course montre comment parfois la graine peut avoir un mauvais effet (dans ce cas en favorisant les nombres inférieurs) alors que System.Randomc'est assez uniforme.

QuickRandom

System.Random

Encore pire

Si nous initialisons au QuickRandomfur new QuickRandom(0.01, 1.03)et à mesure que nous obtenons cette image:

Le code

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;
        }
    }
}
Callum Rogers
la source
2
Beau code. Oui, c'est cool. J'avais l'habitude de faire ça aussi parfois, c'est difficile d'en tirer une mesure quantifiable, mais c'est une autre bonne façon de regarder la séquence. Et si vous voulez voir des séquences plus longues que la largeur * hauteur, vous pouvez x ou l'image suivante avec ce pixel par pixel. Je pense que l'image QuickRandom est beaucoup plus esthétique, car elle est texturée comme un tapis d'algues.
Cris Stringfellow
La partie esthétique est la façon dont la séquence a tendance à augmenter au fur et à mesure que vous avancez le long de chaque ligne (puis revenez au début), car la magicNumbermultiplication produit un nombre similaire à prevNum, ce qui montre le manque d'aléatoire. Si nous utilisons les graines, new QuickRandom(0.01, 1.03)nous obtenons ce i.imgur.com/Q1Yunbe.png !
Callum Rogers
Oui, excellente analyse. Puisqu'il ne fait que multiplier le mod 1 par une constante clairement avant le wrapping, il y aura l'augmentation que vous décrivez. On dirait que cela pourrait être évité si nous prenions les placages décimaux les moins significatifs en multipliant par 1 milliard, puis en réduisant mod une palette de 256 couleurs.
Cris Stringfellow
Pouvez-vous me dire ce que vous avez utilisé pour générer ces images de sortie? Matlab?
2013
@uDaY: Jetez un œil au code, C # et System.Drawing.Bitmap.
Callum Rogers
37

Un problème avec votre générateur de nombres aléatoires est qu'il n'y a pas `` d'état caché '' - si je sais quel numéro aléatoire vous avez renvoyé lors du dernier appel, je connais chaque numéro aléatoire que vous enverrez jusqu'à la fin des temps, car il n'y en a qu'un résultat suivant possible, et ainsi de suite.

Une autre chose à considérer est la «période» de votre générateur de nombres aléatoires. Evidemment avec une taille d'état finie, égale à la partie mantisse d'un double, il ne pourra renvoyer au plus que 2 ^ 52 valeurs avant bouclage. Mais c'est dans le meilleur des cas - pouvez-vous prouver qu'il n'y a pas de boucles de période 1, 2, 3, 4 ...? S'il y en a, votre RNG aura un comportement horrible et dégénéré dans ces cas.

De plus, votre génération de nombres aléatoires aura-t-elle une distribution uniforme pour tous les points de départ? Si ce n'est pas le cas, votre RNG sera biaisé - ou pire, biaisé de différentes manières en fonction de la graine de départ.

Si vous pouvez répondre à toutes ces questions, c'est génial. Si vous ne pouvez pas, alors vous savez pourquoi la plupart des gens ne réinventent pas la roue et n'utilisent pas un générateur de nombres aléatoires éprouvé;)

(Au fait, un bon adage est le suivant: le code le plus rapide est celui qui ne s'exécute pas. Vous pouvez créer l'aléa () le plus rapide au monde, mais ce n'est pas bon s'il n'est pas très aléatoire)

Patashu
la source
8
Il y a au moins une boucle triviale sur ce générateur pour toutes les semences 0 -> 0. Selon la graine, il peut y en avoir beaucoup d'autres. (Par exemple, avec une graine de 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 courant que j'ai toujours fait lors du développement de PRNG était de:

  1. Convertir la sortie en valeurs char
  2. Ecrire la valeur des caractères dans un fichier
  3. Compresser le fichier

Cela m'a permis d'itérer rapidement des idées qui étaient des PRNG "assez bons" pour des séquences d'environ 1 à 20 mégaoctets. Cela a également donné une meilleure image descendante que de simplement l'inspecter à l'œil nu, car tout PRNG "assez bon" avec un demi-mot d'état pourrait rapidement dépasser la capacité de vos yeux à voir le point du cycle.

Si j'étais vraiment pointilleux, je pourrais prendre les bons algorithmes et exécuter les tests DIEHARD / NIST sur eux, pour obtenir plus d'informations, puis revenir en arrière et en modifier davantage.

L'avantage du test de compression, par rapport à une analyse de fréquence est que, trivialement, il est facile de construire une bonne distribution: il suffit de sortir un bloc de 256 longueurs contenant tous les caractères de valeurs 0 à 255, et de le faire 100 000 fois. Mais cette séquence a un cycle de longueur 256.

Une distribution asymétrique, même avec une petite marge, devrait être détectée par un algorithme de compression, en particulier si vous lui donnez suffisamment (disons 1 mégaoctet) de la séquence pour travailler avec. Si certains caractères, ou bigrammes ou n-grammes se produisent plus fréquemment, un algorithme de compression peut coder cette distribution asymétrique en codes qui favorisent les occurrences fréquentes avec des mots de code plus courts, et vous obtenez un delta de compression.

Étant donné que la plupart des algorithmes de compression sont rapides et ne nécessitent aucune implémentation (car les systèmes d'exploitation les ont juste traîner), le test de compression est très utile pour évaluer rapidement réussite / échec pour un PRNG que vous pourriez développer.

Bonne chance avec vos expériences!

Oh, j'ai effectué ce test sur le rng que vous avez ci-dessus, en utilisant le petit mod suivant de votre code:

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

Les résultats ont été:

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

Je considérerais un PRNG bon si le fichier de sortie ne pouvait pas être compressé du tout. Pour être honnête, je ne pensais pas que votre PRNG ferait si bien, seulement 16% sur ~ 20 Megs est assez impressionnant pour une construction aussi simple. Mais je considère toujours que c'est un échec.

Cris Stringfellow
la source
2
Imagerie ou pas, j'ai la même idée avec le zip il y a des années quand je testais mes générateurs aléatoires.
Aristos
1
Merci @Alexandre C. et Aristos et aidan. Je te crois.
Cris Stringfellow
33

Le générateur aléatoire le plus rapide que vous puissiez implémenter est le suivant:

entrez la description de l'image ici

XD, blagues à part, en plus de tout ce qui est dit ici, j'aimerais contribuer en citant que tester des séquences aléatoires "est une tâche difficile" [1], et qu'il existe plusieurs tests qui vérifient certaines propriétés des nombres pseudo-aléatoires, vous pouvez trouver un beaucoup d'entre eux ici: http://www.random.org/analysis/#2005

Un moyen simple d'évaluer la «qualité» du générateur aléatoire est l'ancien test du 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);
}

Citant [1]

L'idée du test χ² est de vérifier si les nombres produits sont raisonnablement répartis. Si nous générons N nombres positifs inférieurs à r , alors nous nous attendons à obtenir environ N / r nombres de chaque valeur. Mais - et c'est l'essence même du problème - les fréquences d'occurrence de toutes les valeurs ne devraient pas être exactement les mêmes: ce ne serait pas aléatoire!

Nous calculons simplement la somme des carrés des fréquences d'occurrence de chaque valeur, mise à l'échelle par la fréquence attendue, puis soustrayons la taille de la séquence. Ce nombre, la "statistique χ²", peut être exprimé mathématiquement par

formule chi carré

Si la statistique χ² est proche de r , alors les nombres sont aléatoires; si c'est trop loin, alors ils ne le sont pas. Les notions de «proche» et de «loin» peuvent être définies plus précisément: il existe des tableaux qui indiquent exactement comment relier la statistique aux propriétés de séquences aléatoires. Pour le test simple que nous effectuons, la statistique doit être comprise entre 2√r

En utilisant cette théorie et le code suivant:

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

J'ai obtenu le résultat suivant:

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

Qui, pour QuickRandom, est loin de r (en dehors de r ± 2 * sqrt(r) )

Cela dit, QuickRandom pourrait être rapide mais (comme indiqué dans une autre réponse) n'est pas bon comme générateur de nombres aléatoires


[1] SEDGEWICK ROBERT, Algorithms in C , Addinson Wesley Publishing Company, 1990, pages 516 à 518

Higuaro
la source
9
+1 pour xkcd qui est un wobsite incroyable (oh, et la bonne réponse): P
tckmn
1
Merci, et oui des racks xkcd! XD
higuaro
La théorie est bonne mais l'exécution est médiocre: le code est susceptible de débordement d'entier. En java, tous int[]sont initialisés à zéro, donc pas besoin de cette partie. Lancer pour flotter est inutile lorsque vous travaillez avec des doubles. Dernier: appeler les noms de méthodes random1 et random2 est assez drôle.
bestsss
@bestsss Merci pour les observations! J'ai fait une traduction directe du code C et je n'y ai pas prêté beaucoup d'attention = (. J'ai apporté quelques modifications et mis à jour la réponse. J'apprécierais toute suggestion supplémentaire
higuaro
14

J'ai mis en place une maquette rapide de votre algorithme en JavaScript pour évaluer les résultats. Il génère 100 000 entiers aléatoires de 0 à 99 et suit l'instance de chaque entier.

La première chose que je remarque, c'est que vous êtes plus susceptible d'obtenir un nombre faible qu'un nombre élevé. Vous voyez cela le plus quand seed1c'est haut et seed2bas. Dans quelques cas, je n'ai obtenu que 3 numéros.

Au mieux, votre algorithme a besoin d'être affiné.

gilly3
la source
8

Si la Math.Random() fonction appelle le système d'exploitation pour obtenir l'heure de la journée, vous ne pouvez pas la comparer à votre fonction. Votre fonction est un PRNG, alors que cette fonction recherche des nombres aléatoires réels. Pommes et oranges.

Votre PRNG peut être rapide, mais il n'a pas suffisamment d'informations d'état pour atteindre une longue période avant de se répéter (et sa logique n'est pas assez sophistiquée pour même atteindre les périodes qui sont possibles avec autant d'informations d'état).

La période est la durée de la séquence avant que votre PRNG ne commence à se répéter. Cela se produit dès que la machine PRNG effectue une transition d'état vers un état qui est identique à un état passé. À partir de là, il répétera les transitions qui ont commencé dans cet état. Un autre problème avec les PRNG peut être un faible nombre de séquences uniques, ainsi qu'une convergence dégénérée sur une séquence particulière qui se répète. Il peut également y avoir des modèles indésirables. Par exemple, supposons qu'un PRNG semble assez aléatoire lorsque les nombres sont imprimés en décimal, mais une inspection des valeurs en binaire montre que le bit 4 bascule simplement entre 0 et 1 à chaque appel. Oups!

Jetez un œil au Mersenne Twister et à d'autres algorithmes. Il existe des moyens de trouver un équilibre entre la durée de la période et les cycles du processeur. Une approche de base (utilisée dans le Mersenne Twister) consiste à faire un cycle dans le vecteur d'état. C'est-à-dire que lorsqu'un nombre est généré, il n'est pas basé sur l'état entier, juste sur quelques mots du tableau d'états soumis à quelques opérations sur quelques bits. Mais à chaque étape, l'algorithme se déplace également dans le tableau, brouillant le contenu petit à petit.

Kaz
la source
5
Je suis plutôt d'accord, sauf avec votre premier paragraphe. Les appels aléatoires intégrés (et / dev / random sur les systèmes de type Unix) sont également des PRNG. J'appellerais tout ce qui produit des nombres aléatoires algorithmiquement un PRNG, même si la graine est quelque chose de difficile à prédire. Il existe quelques "vrais" générateurs de nombres aléatoires qui utilisent la désintégration radioactive, le bruit atmosphérique, etc., mais ceux-ci génèrent souvent relativement peu de bits / seconde.
Matt Krause
Sur les boîtiers Linux, /dev/randomc'est une source d'aléa réel obtenue à partir des pilotes de périphériques, et non d'un PRNG. Il se bloque lorsqu'il n'y a pas assez de bits disponibles. Le périphérique sœur /dev/urandomne se bloque pas non plus, mais ce n'est toujours pas exactement un PRNG car il est mis à jour avec des bits aléatoires lorsqu'ils sont disponibles.
Kaz
Si la fonction Math.Random () appelle le système d'exploitation pour obtenir l'heure de la journée , c'est absolument faux. (dans l'une des versions / versions Java que je connais)
bestsss
@bestsss Cela vient de la question originale: je me souviens avoir lu quelque part que Math.random utilisait System.nanoTime () . Vos connaissances peuvent valoir la peine d'être ajoutées ici ou dans votre réponse. Je l'ai utilisé conditionnellement avec un if . :)
Kaz
Kaz, both nanoTime()+ counter / hash est utilisé pour la graine par défaut java.util.Randomd'oracle / OpenJDK. C'est pour la graine seulement alors c'est un LCG standard. En effet, le générateur OP prend 2 nombres aléatoires pour la graine, ce qui est ok - donc pas de différence que java.util.Random. System.currentTimeMillis()était la tête de série par défaut dans JDK1.4-
bestsss
7

Il existe de très nombreux générateurs de nombres pseudo aléatoires. Par exemple, le ranarray de Knuth , le twister de Mersenne , ou recherchez des générateurs LFSR. Les monumental "algorithmes seminumériques" de Knuth analysent la zone et proposent des générateurs linéaires congruentiels (simples à mettre en œuvre, rapides).

Mais je vous suggère de vous en tenir à java.util.Randomou Math.random, ils sont rapides et au moins OK pour une utilisation occasionnelle (c'est-à-dire des jeux et autres). Si vous êtes juste paranoïaque sur la distribution (un programme de Monte Carlo ou un algorithme génétique), vérifiez leur implémentation (la source est disponible quelque part) et semez-les avec un nombre vraiment aléatoire, soit à partir de votre système d'exploitation, soit à partir de random.org . Si cela est nécessaire pour certaines applications où la sécurité est essentielle, vous devrez creuser vous-même. Et comme dans ce cas vous ne devriez pas croire ce qu'un carré coloré avec des morceaux manquants jaillit ici, je vais me taire maintenant.

vonbrand
la source
7

Il est très peu probable que les performances de génération de nombres aléatoires soient un problème pour tout cas d'utilisation que vous avez proposé à moins d'accéder à une seule Randominstance à partir de plusieurs threads (parce que Randomc'est le cas synchronized).

Cependant, si tel est vraiment le cas et que vous avez besoin de beaucoup de nombres aléatoires rapidement, votre solution est beaucoup trop peu fiable. Parfois cela donne de bons résultats, parfois cela donne des résultats horribles (basés sur les paramètres initiaux).

Si vous voulez les mêmes nombres que la Randomclasse vous donne, mais plus rapidement, vous pouvez vous débarrasser de la synchronisation là-dedans:

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

}

J'ai simplement pris le java.util.Randomcode et supprimé la synchronisation qui se traduit par deux fois plus de performances que l'original sur mon Oracle HotSpot JVM 7u9. Il est toujours plus lent que le vôtre QuickRandom, mais il donne des résultats beaucoup plus cohérents. Pour être précis, pour les mêmes seedvaleurs et les applications à thread unique, il donne les mêmes nombres pseudo-aléatoires que la Randomclasse d' origine .


Ce code est basé sur le courant java.util.Randomd'OpenJDK 7u qui est sous licence GNU GPL v2 .


MODIFIER 10 mois plus tard:

Je viens de découvrir que vous n'avez même pas besoin d'utiliser mon code ci-dessus pour obtenir une Randominstance non synchronisée . Il y en a aussi un dans le JDK!

Regardez la ThreadLocalRandomclasse de Java 7 . Le code à l'intérieur est presque identique à mon code ci-dessus. La classe est simplement une Randomversion isolée des threads locaux, adaptée pour générer rapidement des nombres aléatoires. Le seul inconvénient auquel je peux penser est que vous ne pouvez pas le définir seedmanuellement.

Exemple d'utilisation:

Random random = ThreadLocalRandom.current();
Petr Janeček
la source
2
@Edit Hmm, je peux comparer QR, Math.random et ThreadLocalRandom parfois quand je ne suis pas trop paresseux :)C'est intéressant, merci!
tckmn
1. Vous pouvez gagner plus de vitesse en supprimant le masque car les 16 bits les plus élevés n'influencent pas les bits utilisés. 2. Vous pouvez utiliser ces bits, enregistrer une soustraction et obtenir un meilleur générateur (état plus grand; les bits les plus significatifs d'un produit sont les mieux distribués, mais une évaluation serait nécessaire). 3. Les gars de Sun ont simplement implémenté un RNG archaïque de Knuth et ajouté la synchronisation. :(
maaartinus
3

`` Aléatoire '' ne consiste pas seulement à obtenir des nombres ... ce que vous avez est pseudo-aléatoire

Si le pseudo-aléatoire est assez bon pour vos besoins, alors bien sûr, c'est beaucoup plus rapide (et XOR + Bitshift sera plus rapide que ce que vous avez)

Rolf

Éditer:

OK, après avoir été trop rapide dans cette réponse, permettez-moi de répondre à la vraie raison pour laquelle votre code est plus rapide:

À partir de JavaDoc for Math.Random ()

Cette méthode est correctement synchronisée pour permettre une utilisation correcte par plus d'un thread. Cependant, si de nombreux threads ont besoin de générer des nombres pseudo-aléatoires à une vitesse élevée, il peut réduire la contention pour que chaque thread ait son propre générateur de nombres pseudo-aléatoires.

C'est probablement pourquoi votre code est plus rapide.

rolfl
la source
3
Presque tout ce qui n'implique pas un générateur de bruit matériel ou une ligne directe dans les E / S du système d'exploitation, va être pseudo-aléatoire. Le vrai hasard ne peut pas être généré par un algorithme seul; vous avez besoin de bruit de quelque part. (Les RNG de certains systèmes d'exploitation obtiennent leur contribution en mesurant des éléments tels que comment / quand vous déplacez la souris, tapez des éléments, etc. Mesuré sur une échelle de microsecondes à nanosecondes, cela peut être très imprévisible.)
cHao
@OliCharlesworth: en effet, pour autant que je sache, les seules vraies valeurs aléatoires se trouvent en utilisant le bruit atmosphérique.
Jeroen Vannevel
@me ... stupide de répondre à la hâte. Le Math.random est pseudo-aléatoire, et aussi, il est synchronisé .
rolfl
@rolfl: La synchronisation pourrait très bien expliquer pourquoi Math.random()est plus lente. Il faudrait soit synchroniser ou créer un nouveau à Randomchaque fois, et ni l'un ni l'autre n'est très attrayant en termes de performances. Si je tenais à la performance, je créerais la mienne new Randomet je l'utiliserais simplement. : P
cHao
La désintégration radioactive de @JeroenVannevel est également aléatoire.
RxS
3

java.util.Random n'est pas très différent, un LCG basique décrit par Knuth. Cependant, il présente 2 principaux avantages / différences:

  • thread safe - chaque mise à jour est un CAS qui coûte plus cher qu'une simple écriture et nécessite une branche (même si parfaitement prédite à un seul thread). Selon le processeur, cela peut être une différence significative.
  • état interne non divulgué - c'est très important pour tout ce qui n'est pas trivial. Vous souhaitez que les nombres aléatoires ne soient pas prévisibles.

Ci-dessous se trouve la routine principale générant des entiers «aléatoires» dans 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));
    }

Si vous supprimez AtomicLong et l'état non divulgué (c'est-à-dire en utilisant tous les bits du long), vous obtiendrez plus de performances que la double multiplication / modulo.

Dernière note: Math.randomne doit être utilisé que pour des tests simples, il est sujet à des conflits et si vous avez même quelques threads qui l'appellent simultanément, les performances se dégradent. Une caractéristique historique peu connue de celui-ci est l'introduction de CAS en Java - pour battre un benchmark tristement célèbre (d'abord par IBM via intrinsèques, puis Sun a fait "CAS from Java")

bestsss
la source
0

C'est la fonction aléatoire que j'utilise pour mes jeux. C'est assez rapide et a une bonne (assez) distribution.

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));
       }
}
Terje
la source
1
Cela ne répond pas à la question. Pour critiquer ou demander des éclaircissements à un auteur, laissez un commentaire sous sa publication.
John Willemse
Je pense qu'il a déjà été établi que l'algorithme d'origine n'est pas assez bon? Peut-être qu'un exemple de ce qui est assez bon peut vous inspirer sur la façon de l'améliorer?
Terje
Oui, peut-être, mais cela ne répond pas du tout à la question et il n'y a aucune donnée supportant votre algorithme qui est en fait "assez bon". En général, les algorithmes de nombres aléatoires et les algorithmes de chiffrement étroitement liés ne sont jamais aussi bons que ceux des experts qui les ont implémentés dans un langage de programmation. Donc, si vous pouviez soutenir votre affirmation et expliquer pourquoi il est meilleur que l'algorithme de la question, vous répondriez au moins à une question posée.
John Willemse
Eh bien ... Les experts qui les ont implémentés dans un langage de programmation visent une distribution «parfaite», alors que dans un jeu, vous n'en avez jamais besoin. Vous voulez de la vitesse et une distribution "assez bonne". Ce code offre cela. Si c'est inapproprié ici, je supprimerai la réponse, pas de problème.
Terje
Concernant le multithreading, votre utilisation de la variable locale est un no-op, car sans volatile, le compilateur est libre d'éliminer (ou d'introduire) des variables locales à volonté.
maaartinus