Générer des nombres aléatoires suivant une distribution normale en C / C ++


114

Comment générer facilement des nombres aléatoires suivant une distribution normale en C ou C ++?

Je ne veux pas utiliser Boost.

Je sais que Knuth en parle longuement mais je n'ai pas ses livres sous la main pour le moment.


Réponses:


92

Il existe de nombreuses méthodes pour générer des nombres à distribution gaussienne à partir d'un RNG régulier .

La transformée Box-Muller est couramment utilisée. Il produit correctement des valeurs avec une distribution normale. Le calcul est simple. 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-en un et enregistrez l'autre pour la prochaine demande d'un nombre aléatoire.


10
Si vous avez besoin de vitesse, la méthode polaire est cependant plus rapide. Et encore plus l'algorithme Ziggurat (bien que beaucoup plus complexe à écrire).
Joey

2
trouvé une implémentation du Ziggurat ici people.sc.fsu.edu/~jburkardt/c_src/ziggurat/ziggurat.html C'est assez complet.
dwbrito

24
Notez que C ++ 11 ajoute std::normal_distributionce qui fait exactement ce que vous demandez sans entrer dans les détails mathématiques.

3
std :: normal_distribution n'est pas garanti d'être cohérent sur toutes les plates-formes. Je fais les tests maintenant, et MSVC fournit un ensemble de valeurs différent de, par exemple, Clang. Les moteurs C ++ 11 semblent générer les mêmes séquences (étant donné la même graine), mais les distributions C ++ 11 semblent être implémentées en utilisant différents algorithmes sur différentes plates-formes.
Arno Duvenhage

47

C ++ 11

Offres C ++ 11 std::normal_distribution, ce que j'irais aujourd'hui.

C ou C ++ plus ancien

Voici quelques solutions par ordre de complexité croissante:

  1. Ajouter 12 nombres aléatoires uniformes de 0 à 1 et soustraire 6. Cela correspondra à la moyenne et à l'écart type d'une variable normale. Un inconvénient évident est que la plage est limitée à ± 6 - contrairement à une vraie distribution normale.

  2. La transformation Box-Muller. Ceci est répertorié ci-dessus et est relativement simple à mettre en œuvre. Si vous avez besoin d'échantillons très précis, sachez cependant que la transformée de Box-Muller combinée à certains générateurs uniformes souffre d'une anomalie appelée Neave Effect 1 .

  3. Pour une meilleure précision, je suggère de dessiner des uniformes et d'appliquer la distribution normale cumulative inverse pour arriver à des variables normalement distribuées. Voici un très bon algorithme pour les distributions normales cumulées inverses.

1. HR Neave, «Sur l'utilisation de la transformation de Box-Muller avec des générateurs de nombres pseudo-aléatoires congruents multiplicatifs», Applied Statistics, 22, 92-97, 1973


par hasard auriez-vous un autre lien vers le pdf sur l'effet Neave? ou la référence originale de l'article de revue? merci
pyCthon

2
@stonybrooknick La référence d'origine est ajoutée. Cool remarque: en cherchant sur google "box muller neave" pour trouver la référence, cette question très stackoverflow est apparue sur la première page de résultats!
Peter G.

ouais, ce n'est pas tout à fait connu en dehors de certaines petites communautés et groupes d'intérêt
pyCthon

@Peter G. Pourquoi quelqu'un voterait-il contre votre réponse? - peut-être que la même personne a également fait mon commentaire ci-dessous, ce qui me convient, mais j'ai trouvé que votre réponse était très bonne. Ce serait bien si SO fait des downvotes forcent un vrai commentaire..Je soupçonne que la plupart des downvotes de vieux sujets sont juste frivoles et trolly.
Pete855217

"Ajouter 12 nombres uniformes de 0 à 1 et soustraire 6." - la distribution de cette variable aura une distribution normale? Pouvez-vous fournir un lien avec la dérivation, car lors de la dérivation du théorème de la limite centrale, n -> + inf est une hypothèse très nécessaire.
bruziuz

31

Une méthode simple et rapide consiste simplement à additionner un certain nombre de nombres aléatoires uniformément répartis et à prendre leur moyenne. Voir le théorème central des limites pour une explication complète de la raison pour laquelle cela fonctionne.


+1 Approche très intéressante. Est-il vérifié de donner réellement des sous-ensembles normalement distribués pour des groupes plus petits?
Morlock

4
@Morlock Plus le nombre d'échantillons que vous moyenne est grand, plus vous vous rapprochez d'une distribution gaussienne. Si votre application a des exigences strictes pour la précision de la distribution, vous feriez peut-être mieux d'utiliser quelque chose de plus rigoureux, comme Box-Muller, mais pour de nombreuses applications, par exemple la génération de bruit blanc pour les applications audio, vous pouvez vous en tirer avec un nombre assez restreint. d'échantillons moyennés (par exemple 16).
Paul R

2
De plus, comment paramétrer cela pour obtenir une certaine quantité de variance, disons que vous voulez une moyenne de 10 avec un écart type de 1?
Morlock

1
@Ben: pourriez-vous m'indiquer un algorithme efficace pour cela? Je n'ai jamais utilisé la technique de moyennage pour générer un bruit approximativement gaussien pour le traitement audio et d'image avec des contraintes en temps réel - s'il existe un moyen d'y parvenir en moins de cycles d'horloge, cela pourrait être très utile.
Paul R du

1
@Petter: vous avez probablement raison dans le cas général, pour les valeurs en virgule flottante. Cependant, il existe encore des domaines d'application comme l'audio, où vous voulez un bruit gaussien entier (ou à virgule fixe) rapide, et la précision n'est pas trop importante, où la méthode de moyenne simple est plus efficace et utile (en particulier pour les applications embarquées, où être support matériel en virgule flottante).
Paul R du

24

J'ai créé un projet open source C ++ pour un benchmark de génération de nombres aléatoires normalement distribués .

Il compare plusieurs algorithmes, dont

  • Méthode du théorème de limite centrale
  • Transformation de Box-Muller
  • Méthode polaire de Marsaglia
  • Algorithme de Ziggurat
  • Méthode d'échantillonnage par transformée inverse.
  • cpp11randomutilise C ++ 11 std::normal_distributionavec std::minstd_rand(il s'agit en fait d'une transformation Box-Muller en clang).

Les résultats de la version simple précision ( float) sur iMac Corei5-3330S@2.70GHz, clang 6.1, 64 bits:

normaldistf

Pour l'exactitude, le programme vérifie la moyenne, l'écart type, l'asymétrie et l'aplatissement des échantillons. Il a été constaté que la méthode CLT en additionnant 4, 8 ou 16 nombres uniformes n'a pas un bon kurtosis comme 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 jeu d'instructions SSE2 / AVX est beaucoup plus rapide (x1,79, x2,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, et peut être ziggurat sinon.


PS le benchmark utilise un LCG PRNG le plus simple pour générer des nombres aléatoires distribués uniformes. Cela peut donc ne pas être suffisant pour certaines applications. Mais la comparaison des performances doit être juste car toutes les implémentations utilisent le même PRNG, de sorte que le benchmark teste principalement les performances de la transformation.


2
"Mais la comparaison des performances doit être juste car toutes les implémentations utilisent le même PRNG" .. Sauf que BM utilise un RN d'entrée par sortie, alors que CLT en utilise beaucoup plus, etc ... donc le temps de générer un # aléatoire uniforme compte.
greggo

14

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 diagramme 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 vers le haut est le résultat souhaité).


1
Qu'est-ce que sampleNormalManual ()?
resolutionPuzzles

@solvingPuzzles - désolé, a corrigé le code. C'est un appel récursif.
Pete855217

1
Cela risque de planter lors d'un événement rare (présenter l'application à votre patron sonne une cloche?). Cela doit être implémenté en utilisant une boucle, sans utiliser la récursivité. La méthode ne semble pas familière. Quelle est la source / comment s'appelle-t-elle?
le porc

Box-Muller transcrit à partir d'une implémentation java. Comme je l'ai dit, c'est rapide et sale, n'hésitez pas à le réparer.
Pete855217

1
FWIW, de nombreux compilateurs pourront transformer cet appel récursif particulier en un «saut en haut de la fonction». La question est de savoir si vous voulez compter dessus :-) En outre, la probabilité que cela prenne> 10 itérations est de 1 sur 4,8 millions. p (> 20) est le carré de cela, etc.
greggo

12

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 de bibliothèque du rapport technique C ++ 1 et est disponible dans les compilateurs Microsoft et gcc à jour, indépendamment de boost.


25
Il n'a pas demandé de standard, il a demandé «pas de boost».
JoeG

12

C'est ainsi que vous générez 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;

le generatordevrait vraiment être semé.
Walter

Il est toujours semé. Il existe une graine par défaut.
Petter



4

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.


Cela a déjà été mentionné par Ben ( stackoverflow.com/a/11977979/635608 )
Mat

3

J'ai suivi la définition du PDF donnée dans http://www.mathworks.com/help/stats/normal-distribution.html et j'ai trouvé 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.


-1 Ne fonctionne pas pour par exemple RANDN2 (0.0, d + 1.0). Les macros sont connues pour cela.
Petter

La macro échouera si rand()of RANDUrenvoie un zéro, car Ln (0) n'est pas défini.
interDist

Avez-vous réellement essayé ce code? Il semble que vous ayez créé une fonction qui génère des nombres distribués par Rayleigh . Comparez à la transformation Box – Muller , où ils se multiplient avec cos(2*pi*rand/RAND_MAX), alors que vous multipliez avec (rand()%2 ? -1.0 : 1.0).
HelloGoodbye


1

Implémentation 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

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 : c'est la précision de la machine, fiable et rapide. Les goulots d'étranglement sont rarement dans la génération de nombres aléatoires gaussiens.

De plus, cela montre l'inconvénient des approches de type Ziggurat.

La principale 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. Premièrement, si l'on applique la méthode de Box – Muller aux nombres d'un mauvais générateur congruentiel linéaire, les nombres transformés fournissent une couverture extrêmement pauvre de l'espace. Des graphiques de nombres transformés avec des queues en spirale peuvent être trouvés dans de nombreux livres, notamment dans le livre classique de Ripley, qui fut probablement le premier à faire cette observation "


0

1) Une manière graphiquement 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 une boîte autour de la courbe gaussienne en utilisant votre générateur de nombres pseudo-aléatoires en C. Vous pouvez calculer si ce point est à l'intérieur ou en dessous de la distribution gaussienne en utilisant l'équation de la distribution. Si ce point est à l'intérieur de la distribution gaussienne, alors vous avez votre nombre aléatoire gaussien comme 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 une boîte qui s'approche de l'infini dans la dimension x. Mais la courbe guassienne s'approche assez rapidement de 0 dans la dimension y, donc je ne m'inquiéterais pas à ce sujet. La contrainte de la taille de vos variables en C peut être davantage un facteur limitant de votre précision.

2) Une autre façon serait d'utiliser le théorème central des limites qui stipule que lorsque des variables aléatoires indépendantes sont ajoutées, elles forment une distribution normale. En gardant ce théorème à l'esprit, 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 lorsque vous ne souhaitez pas utiliser une bibliothèque préexistante. Gardez à l'esprit que cette réponse vient de quelqu'un avec peu ou pas d'expérience en calcul ou en statistiques.


0

Méthode de Monte Carlo La manière la plus intuitive de le faire serait d'utiliser une méthode de Monte Carlo . Prenez une plage appropriée -X, + X. Des valeurs plus élevées de X donneront une distribution normale plus précise, mais prend plus de temps à converger. une. Choisissez un nombre aléatoire z entre -X et X. b. Restez avec une probabilité N(z, mean, variance)où N est la distribution gaussienne. Laissez tomber autrement et revenez à l'étape (a).



-3

L'ordinateur est un appareil déterministe. Il n'y a pas d'aléatoire dans le calcul. De plus, le dispositif arithmétique de la CPU peut évaluer la somme sur un ensemble fini de nombres entiers (effectuant une évaluation dans un corps fini) et un ensemble fini de nombres rationnels réels. Et a également effectué des opérations au niveau du bit. Les mathématiques prennent un accord avec des ensembles plus grands comme [0.0, 1.0] avec un nombre infini de points.

Vous pouvez écouter du fil à l'intérieur de l'ordinateur avec un contrôleur, mais aurait-il des distributions uniformes? Je ne sais pas. Mais si l'on suppose que son signal est le résultat d'accumuler des valeurs d'énormes quantités de variables aléatoires indépendantes, vous recevrez une variable aléatoire distribuée à peu près normale (cela a été prouvé dans la théorie des probabilités)

Il existe des algorithmes appelés - générateur pseudo-aléatoire. Comme je l'ai ressenti, le but du générateur pseudo aléatoire est d'émuler le caractère aléatoire. Et le critère de goodnes est: - la distribution empirique est convergée (dans un certain sens - point par point, uniforme, L2) vers théorique - les valeurs que vous recevez du générateur aléatoire semblent être indépendantes. Bien sûr, ce n'est pas vrai du «vrai point de vue», mais nous supposons que c'est vrai.

Une des méthodes les plus populaires - vous pouvez additionner 12 irv avec des distributions uniformes .... Mais pour être honnête lors de la dérivation Théorème de la limite centrale avec l'aide de la transformée de Fourier, série Taylor, il est nécessaire d'avoir n -> + inf hypothèses quelques fois. Donc, par exemple théorique - Personnellement, je ne comprends pas comment les gens effectuent une somme de 12 irv avec une distribution uniforme.

J'avais la théorie de la probilité à l'université. Et surtout pour moi, ce n'est qu'une question mathématique. À l'université, j'ai vu le modèle suivant:


double generateUniform(double a, double b)
{
  return uniformGen.generateReal(a, b);
}

double generateRelei(double sigma)
{
  return sigma * sqrt(-2 * log(1.0 - uniformGen.generateReal(0.0, 1.0 -kEps)));
}
double generateNorm(double m, double sigma)
{
  double y2 = generateUniform(0.0, 2 * kPi);
  double y1 = generateRelei(1.0);
  double x1 = y1 * cos(y2);
  return sigma*x1 + m;
}

Une telle façon de le faire n'était qu'un exemple, je suppose qu'il existe d'autres façons de le mettre en œuvre.

La preuve de son exactitude peut être trouvée dans ce livre "Moscou, BMSTU, 2004: XVI Théorie des probabilités, Exemple 6.12, p.246-247" de Krishchenko Alexander Petrovich ISBN 5-7038-2485-0

Malheureusement, je ne connais pas l'existence de la traduction de ce livre en anglais.


J'ai plusieurs votes négatifs. Faites-moi savoir ce qui est mauvais ici?
bruziuz

La question est de savoir comment générer des nombres pseudo aléatoires dans l'ordinateur (je sais, le langage est lâche ici), ce n'est pas une question d'existence mathématique.
user2820579

Oui tu as raison. Et la réponse est de savoir comment générer un nombre pseudo aléatoire avec une distribution normale basée sur un générateur qui a une distribution uniforme. Le code source a été fourni, vous pouvez le réécrire dans n'importe quelle langue.
bruziuz

Bien sûr, je pense que les gars recherchent par exemple "Recettes numériques en C / C ++". En passant, juste pour compléter notre discussion, les auteurs de ce dernier livre donnent des références intéressantes pour un couple de générateurs pseudo-aléatoires qui remplissent les normes pour être des générateurs "décents".
user2820579

1
J'ai fait une sauvegarde ici: sites.google.com/site/burlachenkok/download
bruziuz
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.