web-dev-qa-db-fra.com

Différences aléatoires dans les moteurs

La norme C++ 11 spécifie un nombre de moteurs différents pour la génération de nombres aléatoires: linear_congruential_engine, mersenne_twister_engine, subtract_with_carry_engine et ainsi de suite. De toute évidence, cela constitue un changement important par rapport à l’ancien usage de std::Rand

De toute évidence, l'un des principaux avantages de (au moins certains) de ces moteurs est la longueur de la période considérablement augmentée (elle est intégrée dans le nom de std::mt19937). 

Cependant, les différences entre les moteurs sont moins claires. Quels sont les points forts et les points faibles des différents moteurs? Quand faut-il utiliser l'un sur l'autre? Y at-il un défaut raisonnable qui devrait généralement être préféré?

47
Yuushi

D'après les explications ci-dessous, le moteur linéaire semble être plus rapide mais moins aléatoire, tandis que Marsenne Twister est plus complexe et aléatoire. Le moteur de nombre aléatoire soustraire avec retenue est une amélioration du moteur linéaire et il est définitivement plus aléatoire. Dans la dernière référence, il est indiqué que Mersenne Twister est plus complexe que le moteur de nombre aléatoire soustraction avec report.

Moteur de nombres aléatoires congruentiels linéaire

Un moteur générateur de nombres pseudo-aléatoires qui produit des nombres entiers non signés.

C'est le moteur de générateur le plus simple de la bibliothèque standard. Son état est une valeur entière unique, avec l'algorithme de transition suivant:

x = (ax + c) mod m

Où x est la valeur d'état en cours, a et c sont leurs paramètres de modèle respectifs et m, leur paramètre de modèle respectif s'il est supérieur à 0, ou numerics_limits :: max () plus 1, dans le cas contraire.

Son algorithme de génération est une copie directe de la valeur d'état.

Cela en fait un générateur extrêmement efficace en termes de traitement et de consommation de mémoire, mais en produisant des nombres avec différents degrés de corrélation en série, en fonction des paramètres spécifiques utilisés.

Les nombres aléatoires générés par linear_congruential_engine ont une période de m . http://www.cplusplus.com/reference/random/linear_congruential_engine/

Moteur de numérotation aléatoire Twister de Mersenne

Un moteur générateur de nombres pseudo-aléatoires qui produit des nombres entiers non signés dans l'intervalle fermé [0,2 ^ w-1].

L'algorithme utilisé par ce moteur est optimisé pour calculer de grandes séries de nombres (comme dans les expériences de Monte Carlo) avec une distribution presque uniforme dans la plage.

Le moteur a une séquence d'état interne de n éléments entiers, qui est remplie d'une série pseudo-aléatoire générée lors de la construction ou en appelant la valeur de départ de la fonction du membre.

La séquence d'état interne devient la source de n éléments: lorsque l'état est avancé (par exemple, afin de produire un nouveau nombre aléatoire), le moteur modifie la séquence d'états en modifiant la valeur actuelle à l'aide de xor mask a sur un mélange de bits déterminés par le paramètre r qui proviennent de cette valeur et d’une valeur m éléments (voir operator () pour plus de détails).

Les nombres aléatoires produits sont des versions tempérées de ces valeurs torsadées. Le revenu est une séquence d'opérations de décalage et xor définies par les paramètres u, d, s, b, t, c et l appliqués à la valeur d'état sélectionnée (voir operator ()).

Les nombres aléatoires générés par mersenne_twister_engine ont une période équivalente au nombre mersenne 2 ^ ((n-1) * w) -1 . http://www.cplusplus.com/reference/random/mersenne_twister_engine/

Moteur de nombre aléatoire soustraire avec report

Un moteur générateur de nombres pseudo-aléatoires qui produit des nombres entiers non signés.

L'algorithme utilisé par ce moteur est un générateur de fibonacci en retard, avec une séquence d'états de r éléments entiers, plus une valeur de retenue. http://www.cplusplus.com/reference/random/subtract_with_carry_engine/

Les générateurs de Fibonacci retardés ont une période maximale de (2k - 1) * ^ (2M-1) si l’addition ou la soustraction est utilisée. L'initialisation des LFG est un problème très complexe. La sortie des LFG est très sensible aux conditions initiales, et des défauts statistiques peuvent apparaître initialement mais aussi périodiquement dans la séquence de sortie, à moins de faire très attention. Un autre problème potentiel des LFG est que la théorie mathématique qui les sous-tend est incomplète, ce qui nécessite de s’appuyer sur des tests statistiques plutôt que sur les performances théoriques. http://en.wikipedia.org/wiki/Lagged_Fibonacci_generator

Et enfin: Le choix du moteur à utiliser implique un certain nombre de compromis: le moteur congruentiel linéaire est modérément rapide et nécessite très peu d’état de stockage. Les générateurs de Fibonacci retardés sont très rapides, même sur des processeurs dépourvus de jeux d'instructions arithmétiques avancés, aux dépens d'un stockage d'état plus important et de caractéristiques spectrales parfois moins souhaitables. Le twister de Mersenne est plus lent et requiert davantage de stockage d'état, mais avec les paramètres appropriés, la séquence non répétée la plus longue présente les caractéristiques spectrales les plus souhaitables (pour une définition donnée de souhaitable). dans http://fr.cppreference.com/w/cpp/numeric/random

27
fatihk

Je pense que le fait est que les générateurs aléatoires ont des propriétés différentes, ce qui peut les rendre plus adaptés ou non à un problème donné.

  • La longueur de period est l'une des propriétés.
  • La qualité des nombres aléatoires peut également être importante.
  • Le performance du générateur peut également être un problème.

Selon vos besoins, vous pouvez prendre un générateur ou un autre. Par exemple, si vous avez besoin de nombres aléatoires rapides mais que vous ne vous souciez pas vraiment de la qualité, un GLC pourrait être une bonne option. Si vous voulez des nombres aléatoires de meilleure qualité, le Twister Mersenne est probablement une meilleure option.

Pour vous aider à faire votre choix, il existe des tests et résultats standard (j'aime beaucoup le tableau p.29 de le présent document ).


EDIT: Du papier, 

  1. Les familles LCG (LCG(***) dans le papier) sont les génératrices les plus rapides, mais avec la qualité la plus médiocre.
  2. Le Twister de Mersenne (MT19937) est un peu plus lent, mais donne de meilleurs nombres aléatoires.
  3. Les soustractions avec carry (SWB(***), je pense) sont beaucoup plus lentes, mais peuvent donner de meilleures propriétés aléatoires si elles sont bien ajustées.
11
Dr_Sam

Alors que les autres réponses oublient ranlux, voici une petite note d'un développeur AMD qui l'a récemment porté à OpenCL:

https://community.AMD.com/thread/139236

RANLUX est également l’un des très rares PRNG (le seul que je connaisse à ce jour) à s’appuyer sur une théorie qui explique pourquoi il génère des nombres "aléatoires" et pourquoi ils sont bons. En effet, si la théorie est correcte (et je ne connais personne qui l'ait contestée), RANLUX au niveau de luxe le plus élevé produit des chiffres complètement décorrélés jusqu'au dernier bit, sans corrélation à longue distance tant que nous restons bien en dessous de la période (10 ^ 171). La plupart des autres générateurs peuvent en dire très peu sur leur qualité (comme Mersenne Twister, KISS, etc.). Ils doivent compter sur la réussite des tests statistiques.

Les physiciens du CERN sont fans de ce PRNG. 'Nuff a dit.

5
rubenvb

Certaines des informations contenues dans ces autres réponses sont en contradiction avec mes conclusions. J'ai effectué des tests sur Windows 8.1 avec Visual Studio 2013 et, de façon constante, j'ai trouvé que mersenne_twister_engine était d'une qualité supérieure et bien plus rapide que linear_congruential_engine ou subtract_with_carry_engine. Ceci me porte à croire, lorsque les informations des autres réponses sont prises en compte, que la mise en œuvre spécifique d'un moteur a un impact significatif sur les performances. 

C’est une grande surprise pour personne, j'en suis sûr, mais cela n’est pas mentionné dans les autres réponses où mersenne_twister_engine est dit plus lent. Je n'ai pas de résultats de test pour les autres plates-formes et compilateurs, mais avec ma configuration, mersenne_twister_engine est clairement le meilleur choix pour les performances de période, de qualité et de rapidité. Je n'ai pas défini l'utilisation de la mémoire, je ne peux donc pas parler de la propriété d'espace requis.

Voici le code que j'utilise pour tester (pour rendre portable, vous devriez seulement remplacer les appels d'API windows.h QueryPerformanceXxx() par un mécanisme de minutage approprié):

// compile with: cl.exe /EHsc
#include <random> 
#include <iostream>
#include <windows.h>

using namespace std;

void test_lc(const int a, const int b, const int s) {
    /*
    typedef linear_congruential_engine<unsigned int, 48271, 0, 2147483647> minstd_Rand;
    */
    minstd_Rand gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_mt(const int a, const int b, const int s) {
    /*
    typedef mersenne_twister_engine<unsigned int, 32, 624, 397,
    31, 0x9908b0df,
    11, 0xffffffff,
    7, 0x9d2c5680,
    15, 0xefc60000,
    18, 1812433253> mt19937;
    */
    mt19937 gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_swc(const int a, const int b, const int s) {
    /*
    typedef subtract_with_carry_engine<unsigned int, 24, 10, 24> ranlux24_base;
    */
    ranlux24_base gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

int main()
{
    int a_dist = 0;
    int b_dist = 1000;

    int samples = 100000000;

    cout << "Testing with " << samples << " samples." << endl;

    LARGE_INTEGER ElapsedTime;
    double        ElapsedSeconds = 0;

    LARGE_INTEGER Frequency;
    QueryPerformanceFrequency(&Frequency);
    double TickInterval = 1.0 / ((double) Frequency.QuadPart);

    LARGE_INTEGER StartingTime;
    LARGE_INTEGER EndingTime;
    QueryPerformanceCounter(&StartingTime);
    test_lc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "linear_congruential_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_mt(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "   mersenne_twister_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_swc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "subtract_with_carry_engine time: " << ElapsedSeconds << endl;
}

Sortie:

 Test avec 100000000 échantillons .
 Temps_engine_congruentiel_linéaire: 10.0821 
 Temps mersenne_twister_engine: 6.11615 
 soustract_with_carry_engine temps: 9.26676 
1
markamos

En général, Mersenne Twister est le meilleur (et le plus rapide) RNG, mais il nécessite un peu d’espace (environ 2,5 kilo-octets). Lequel convient le mieux à votre besoin dépend du nombre de fois que vous devez instancier l'objet générateur. (Si vous ne devez l'instancier qu'une ou plusieurs fois, utilisez MT. Si vous avez besoin de l'instancier des millions de fois, il est peut-être plus petit.)

Certaines personnes signalent que MT est plus lent que d'autres. Selon mes expériences, cela dépend beaucoup des paramètres d'optimisation de votre compilateur. Plus important encore, le paramètre -march = native peut faire une énorme différence, en fonction de l'architecture de votre hôte.

J'ai lancé un petit programme pour tester la vitesse de différents générateurs, ainsi que leurs tailles, et j'ai obtenu ceci:

std::mt19937 (2504 bytes): 1.4714 s
std::mt19937_64 (2504 bytes): 1.50923 s
std::ranlux24 (120 bytes): 16.4865 s
std::ranlux48 (120 bytes): 57.7741 s
std::minstd_Rand (4 bytes): 1.04819 s
std::minstd_Rand0 (4 bytes): 1.33398 s
std::knuth_b (1032 bytes): 1.42746 s
1
Warp

Je viens de voir cette réponse de Marnos et j'ai décidé de le tester moi-même. J'ai utilisé std::chono::high_resolution_clock pour time 100000 samples 100 times pour produire une moyenne. J'ai tout mesuré en std::chrono::nanoseconds et j'ai obtenu des résultats différents:

std::minstd_Rand avait une moyenne de 28991658 nanosecondes 

std::mt19937 avait une moyenne de 29871710 nanosecondes

ranlux48_base avait une moyenne de 29281677 nanosecondes

Ceci est sur une machine Windows 7. Le compilateur est Mingw-Builds 4.8.1 64bit. Ceci utilise évidemment le drapeau C++ 11 et aucun drapeau d'optimisation.

Lorsque j'active les optimisations -O3, les std::minstd_Rand et ranlux48_base sont en réalité plus rapides que ce que l'implémentation de high_precision_clock peut mesurer; Cependant, std::mt19937 prend toujours 730045 nanosecondes, soit 3/4 de seconde.

Ainsi, comme il l'a dit, la mise en œuvre est spécifique, mais au moins dans GCC, le temps moyen semble s'en tenir à ce que disent les descriptions de la réponse acceptée. Mersenne Twister semble profiter le moins des optimisations, alors que les deux autres ne font que jeter les nombres aléatoires incroyablement vite une fois que vous avez pris en compte les optimisations du compilateur. 

D'un autre côté, j'avais utilisé le moteur Mersenne Twister dans ma bibliothèque de génération de bruit (il ne précalculait pas les gradients). Je pense donc que je vais passer à l'un des autres pour vraiment améliorer la vitesse. Dans mon cas, le "vrai" hasard n'a pas d'importance.

Code:

#include <iostream>
#include <chrono>
#include <random>

using namespace std;
using namespace std::chrono;

int main()
{
    minstd_Rand linearCongruentialEngine;
    mt19937 mersenneTwister;
    ranlux48_base subtractWithCarry;
    uniform_real_distribution<float> distro;

    int numSamples = 100000;
    int repeats = 100;

    long long int avgL = 0;
    long long int avgM = 0;
    long long int avgS = 0;

    cout << "results:" << endl;

    for(int j = 0; j < repeats; ++j)
    {
        cout << "start of sequence: " << j << endl;

        auto start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(linearCongruentialEngine);
        auto stop = high_resolution_clock::now();
        auto L = duration_cast<nanoseconds>(stop-start).count();
        avgL += L;
        cout << "Linear Congruential:\t" << L << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(mersenneTwister);
        stop = high_resolution_clock::now();
        auto M = duration_cast<nanoseconds>(stop-start).count();
        avgM += M;
        cout << "Mersenne Twister:\t" << M << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(subtractWithCarry);
        stop = high_resolution_clock::now();
        auto S = duration_cast<nanoseconds>(stop-start).count();
        avgS += S;
        cout << "Subtract With Carry:\t" << S << endl;
    }

    cout << setprecision(10) << "\naverage:\nLinear Congruential: " << (long double)(avgL/repeats)
    << "\nMersenne Twister: " << (long double)(avgM/repeats)
    << "\nSubtract with Carry: " << (long double)(avgS/repeats) << endl;
}
0
NeomerArcana

C'est un compromis vraiment. Un PRNG comme Mersenne Twister est préférable, car il a une période extrêmement longue et d’autres propriétés statistiques intéressantes. 

Mais une longue période PRNG utilise plus de mémoire (pour le maintien de l'état interne) et prend également plus de temps pour générer un nombre aléatoire (en raison de transitions complexes et du post-traitement).

Choisissez un PNRG en fonction des besoins de votre application. En cas de doute, utilisez Mersenne Twister, c'est la valeur par défaut dans de nombreux outils.

0
Nishanth