Comment puis-je générer facilement des nombres aléatoires après une distribution normale en C ou C++?
Je ne veux aucune utilisation de Boost.
Je sais que Knuth en parle longuement, mais je n'ai pas ses livres sous la main pour le moment.
Il existe de nombreuses méthodes pour générer des nombres distribués gaussiens à partir d'un RNG ordinaire .
La transformation Box-Muller est couramment utilisée. Il produit correctement des valeurs avec une distribution normale. Le calcul est facile. Vous générez deux nombres aléatoires (uniformes), et en leur appliquant une formule, vous obtenez deux nombres aléatoires normalement distribués. Renvoyez un et enregistrez l'autre pour la prochaine demande d'un nombre aléatoire.
C++ 11 offres std::normal_distribution
, comme je le ferais aujourd'hui.
Voici quelques solutions par ordre croissant de complexité:
Ajoutez 12 nombres aléatoires uniformes de 0 à 1 et soustrayez 6. Cela correspondra à l'écart moyen et l'écart type d'une variable normale. Un inconvénient évident est que la plage est limitée à ± 6 - contrairement à une distribution normale.
La transformation de Box-Muller. Ceci est listé ci-dessus et est relativement simple à mettre en œuvre. Toutefois, si vous avez besoin d'échantillons très précis, sachez que la transformation Box-Muller, combinée à certains générateurs d'uniformes, présente une anomalie appelée effet Neave.1.
Pour une meilleure précision, je suggère de dessiner des uniformes et d’appliquer la distribution normale cumulative inverse pour obtenir des variables de distribution normale. Ici est un très bon algorithme pour les distributions normales cumulées inverses.
1. H. R. Neave, «Utilisation de la transformation de Box-Muller avec des générateurs de nombres pseudo-aléatoires multiplicatifs congruentiels», Applied Statistics, 22, 92-97, 1973.
J'ai créé un projet open source C++ pour la référence de génération de nombres aléatoires normalement distribuée .
Il compare plusieurs algorithmes, y compris
cpp11random
utilise C++ 11 std::normal_distribution
avec std::minstd_Rand
(il s’agit en fait d’une transformation de Box-Muller en mode Clang).Résultats de la version à simple précision (float
) sur l'iMac [email protected], clang 6.1, 64 bits:
Pour que le résultat soit correct, le programme vérifie la moyenne, l'écart type, l'asymétrie et le kurtosis des échantillons. Il a été constaté que la méthode CLT en additionnant 4, 8 ou 16 nombres uniformes n’avait pas un bon kurtosis que les autres méthodes.
L'algorithme Ziggurat a de meilleures performances que les autres. Cependant, il ne convient pas au parallélisme SIMD car il nécessite une recherche de table et des branches. Box-Muller avec le jeu d'instructions SSE2/AVX est beaucoup plus rapide (x1,79, x 2,99) que la version non SIMD de l'algorithme ziggurat.
Par conséquent, je suggérerai d’utiliser Box-Muller pour l’architecture avec des jeux d’instructions SIMD, sinon la ziggourat pourrait être utile.
P.S. Le benchmark utilise le plus simple des LCG PRNG pour générer des nombres aléatoires répartis uniformes. Cela peut donc ne pas suffire pour certaines applications. Toutefois, la comparaison des performances doit être juste car toutes les implémentations utilisent le même PRNG. Par conséquent, la référence teste principalement les performances de la transformation.
Voici un exemple C++, basé sur certaines des références. C'est rapide et sale, il vaut mieux ne pas réinventer et utiliser la bibliothèque boost.
#include "math.h" // for Rand, and Rand
double sampleNormal() {
double u = ((double) Rand() / (Rand_MAX)) * 2 - 1;
double v = ((double) Rand() / (Rand_MAX)) * 2 - 1;
double r = u * u + v * v;
if (r == 0 || r > 1) return sampleNormal();
double c = sqrt(-2 * log(r) / r);
return u * c;
}
Vous pouvez utiliser un graphique QQ pour examiner les résultats et voir dans quelle mesure il se rapproche d'une distribution normale réelle (classez vos échantillons 1..x, transformez les rangs en proportions du nombre total de x, c'est-à-dire combien d'échantillons, obtenez les valeurs z et tracez-les. Une ligne droite ascendante est le résultat souhaité).
Utilisez std::tr1::normal_distribution
.
L'espace de noms std :: tr1 ne fait pas partie de boost. C'est l'espace de noms qui contient les ajouts à la bibliothèque du rapport technique C++ 1 et est disponible dans les compilateurs Microsoft et gcc à jour, indépendamment de boost.
Voici comment générer les exemples sur un compilateur C++ moderne.
#include <random>
...
std::mt19937 generator;
double mean = 0.0;
double stddev = 1.0;
std::normal_distribution<double> normal(mean, stddev);
cerr << "Normal: " << normal(generator) << endl;
Vous pouvez utiliser le GSL . Quelques exemples complets sont donnés pour montrer comment l’utiliser.
Consultez la page: http://www.cplusplus.com/reference/random/normal_distribution/ . C'est le moyen le plus simple de produire des distributions normales.
Si vous utilisez C++ 11, vous pouvez utiliser std::normal_distribution
:
#include <random>
std::default_random_engine generator;
std::normal_distribution<double> distribution(/*mean=*/0.0, /*stddev=*/1.0);
double randomNumber = distribution(generator);
Il existe de nombreuses autres distributions que vous pouvez utiliser pour transformer la sortie du moteur de nombres aléatoires.
J'ai suivi la définition du PDF donnée dans http://www.mathworks.com/help/stats/normal-distribution.html et en est venue à ceci:
const double DBL_EPS_COMP = 1 - DBL_EPSILON; // DBL_EPSILON is defined in <limits.h>.
inline double RandU() {
return DBL_EPSILON + ((double) Rand()/Rand_MAX);
}
inline double RandN2(double mu, double sigma) {
return mu + (Rand()%2 ? -1.0 : 1.0)*sigma*pow(-log(DBL_EPS_COMP*RandU()), 0.5);
}
inline double RandN() {
return RandN2(0, 1.0);
}
Ce n'est peut-être pas la meilleure approche, mais c'est assez simple.
Implémentation de Box-Muller:
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <iostream>
using namespace std;
// return a uniformly distributed random number
double RandomGenerator()
{
return ( (double)(Rand()) + 1. )/( (double)(Rand_MAX) + 1. );
}
// return a normally distributed random number
double normalRandom()
{
double y1=RandomGenerator();
double y2=RandomGenerator();
return cos(2*3.14*y2)*sqrt(-2.*log(y1));
}
int main(){
double sigma = 82.;
double Mi = 40.;
for(int i=0;i<100;i++){
double x = normalRandom()*sigma+Mi;
cout << " x = " << x << endl;
}
return 0;
}
1) La manière graphique intuitive de générer des nombres aléatoires gaussiens consiste à utiliser quelque chose de similaire à la méthode de Monte Carlo. Vous généreriez un point aléatoire dans un cadre autour de la courbe gaussienne à l'aide de votre générateur de nombre pseudo-aléatoire en C. Vous pouvez calculer si ce point se situe à l'intérieur ou sous la distribution gaussienne à l'aide de l'équation de la distribution. Si ce point est à l'intérieur de la distribution gaussienne, votre nombre aléatoire gaussien est alors la valeur x du point.
Cette méthode n'est pas parfaite car techniquement, la courbe gaussienne continue vers l'infini et vous ne pouvez pas créer de boîte qui se rapproche de l'infini dans la dimension x. Mais la courbe de Guassian se rapproche assez vite de 0 dans la dimension y donc je ne m'inquiéterais pas pour ça. La contrainte de la taille de vos variables en C peut être un facteur plus limitant de votre précision.
2) Une autre solution consisterait à utiliser le théorème de la limite centrale, qui stipule que lorsque des variables aléatoires indépendantes sont ajoutées, elles forment une distribution normale. En gardant à l'esprit ce théorème, vous pouvez approximer un nombre aléatoire gaussien en ajoutant une grande quantité de variables aléatoires indépendantes.
Ces méthodes ne sont pas les plus pratiques, mais il faut s'y attendre si vous ne souhaitez pas utiliser une bibliothèque préexistante. N'oubliez pas que cette réponse provient de quelqu'un qui a peu ou pas d'expérience en calcul ou en statistiques.
Il existe différents algorithmes pour la distribution normale cumulative inverse. Les plus populaires en finance quantitative sont testés sur http://chasethedevil.github.io/post/monte-carlo--inverse-cumulative-normal-distribution/
À mon avis, il n'y a pas beaucoup d'incitation à utiliser autre chose que l'algorithme AS241 de Wichura : il s'agit d'une précision machine fiable et rapide. Les goulots d'étranglement sont rarement liés à la génération de nombres aléatoires gaussiens.
En outre, cela montre les inconvénients de la Ziggurat.
La meilleure réponse ici préconise Box-Müller, vous devez être conscient qu'il a des lacunes connues. Je cite https://www.sciencedirect.com/science/article/pii/S0895717710005935 :
dans la littérature, Box – Muller est parfois considéré comme légèrement inférieur, principalement pour deux raisons. Tout d'abord, si l'on applique la méthode de Box-Muller aux nombres d'un mauvais générateur de congruence linéaire, les nombres transformés fournissent une couverture extrêmement médiocre de l'espace. Des parcelles de nombres transformés avec des queues en spirale peuvent être trouvées dans de nombreux livres, notamment dans le livre classique de Ripley, qui a probablement été le premier à faire cette observation "
La liste comp.lang.c FAQ partage trois manières différentes de générer facilement des nombres aléatoires avec une distribution gaussienne.
Vous pouvez en prendre un aperçu: http://c-faq.com/lib/gaussian.html