Il en rand()
va de même d'un générateur de nombres pseudo-aléatoires qui choisit un nombre naturel entre 0 et RAND_MAX
, qui est une constante définie dans cstdlib
(voir cet article pour un aperçu général rand()
).
Maintenant, que se passe-t-il si vous voulez générer un nombre aléatoire entre disons 0 et 2? Pour des raisons d'explication, disons RAND_MAX
est 10 et je décide de générer un nombre aléatoire entre 0 et 2 en appelant rand()%3
. Cependant, rand()%3
ne produit pas les nombres entre 0 et 2 avec une probabilité égale!
Lorsque rand()
renvoie 0, 3, 6 ou 9 rand()%3 == 0
,. Par conséquent, P (0) = 4/11
Lorsque rand()
renvoie 1, 4, 7 ou 10 rand()%3 == 1
,. Par conséquent, P (1) = 4/11
Lorsque rand()
renvoie 2, 5 ou 8 rand()%3 == 2
,. Par conséquent, P (2) = 3/11
Cela ne génère pas les nombres entre 0 et 2 avec une probabilité égale. Bien sûr, pour les petites plages, ce n'est peut-être pas le plus gros problème, mais pour une plus grande plage, cela pourrait fausser la distribution, biaisant les petits nombres.
Alors, quand rand()%n
renvoie une plage de nombres de 0 à n-1 avec une probabilité égale? Quand RAND_MAX%n == n - 1
. Dans ce cas, avec notre hypothèse précédente, rand()
renvoie un nombre compris entre 0 et RAND_MAX
avec une probabilité égale, les classes modulo de n seraient également réparties également.
Alors, comment pouvons-nous résoudre ce problème? Une façon grossière est de continuer à générer des nombres aléatoires jusqu'à ce que vous obteniez un nombre dans la plage souhaitée:
int x;
do {
x = rand();
} while (x >= n);
mais cela est inefficace pour les valeurs faibles de n
, car vous n'avez qu'une n/RAND_MAX
chance d'obtenir une valeur dans votre plage, et vous devrez donc effectuer des RAND_MAX/n
appels vers rand()
en moyenne.
Une approche de formule plus efficace consisterait à prendre une grande plage avec une longueur divisible par n
, comme RAND_MAX - RAND_MAX % n
, continuer à générer des nombres aléatoires jusqu'à ce que vous obteniez celui qui se trouve dans la plage, puis à prendre le module:
int x;
do {
x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));
x %= n;
Pour les petites valeurs de n
, cela nécessitera rarement plus d'un appel à rand()
.
Ouvrages cités et lectures complémentaires:
RAND_MAX%n == n - 1
_ est(RAND_MAX + 1) % n == 0
. Lors de la lecture de code, j'ai tendance à comprendre% something == 0
comme «également divisible» plus facilement que les autres façons de le calculer. Bien sûr, si votre stdlib C ++ aRAND_MAX
la même valeur queINT_MAX
,(RAND_MAX + 1)
cela ne fonctionnerait sûrement pas; le calcul de Mark reste donc la mise en œuvre la plus sûre.Continuer à sélectionner un hasard est un bon moyen de supprimer le biais.
Mettre à jour
Nous pourrions rendre le code rapide si nous recherchons un x dans la plage divisible par
n
.La boucle ci-dessus devrait être très rapide, disons 1 itération en moyenne.
la source
rand()
peuvent renvoyer n'est pas un multiple den
, quoi que vous fassiez, vous obtiendrez inévitablement un «biais modulo», à moins que vous ne supprimiez certaines de ces valeurs. user1413793 explique cela très bien (bien que la solution proposée dans cette réponse soit vraiment dégueulasse).RAND_MAX+1 - (RAND_MAX+1) % n
fonctionner correctement, mais je pense toujours qu'elle devrait être écriteRAND_MAX+1 - ((RAND_MAX+1) % n)
pour plus de clarté.RAND_MAX == INT_MAX
(comme c'est le cas sur la plupart des systèmes) . Voir mon deuxième commentaire à @ user1413793 ci-dessus.@ user1413793 a raison du problème. Je ne vais pas en parler davantage, sauf pour faire une remarque: oui, pour les petites valeurs de
n
et les grandes valeurs deRAND_MAX
, le biais modulo peut être très petit. Mais l'utilisation d'un modèle induisant un biais signifie que vous devez tenir compte du biais chaque fois que vous calculez un nombre aléatoire et choisir des modèles différents pour différents cas. Et si vous faites le mauvais choix, les bugs qu'il introduit sont subtils et presque impossibles à tester unitairement. Comparé à l'utilisation de l'outil approprié (tel quearc4random_uniform
), c'est un travail supplémentaire, pas moins de travail. Faire plus de travail et obtenir une pire solution est une ingénierie terrible, surtout quand il est facile de bien le faire à chaque fois sur la plupart des plates-formes.Malheureusement, les implémentations de la solution sont toutes incorrectes ou moins efficaces qu'elles ne devraient l'être. (Chaque solution a divers commentaires expliquant les problèmes, mais aucune des solutions n'a été corrigée pour les résoudre.) Cela est susceptible de dérouter le chercheur de réponse occasionnel, donc je fournis ici une bonne mise en œuvre connue.
Encore une fois, la meilleure solution est simplement d'utiliser
arc4random_uniform
sur les plates-formes qui le fournissent, ou une solution à distance similaire pour votre plate-forme (commeRandom.nextInt
sur Java). Il fera la bonne chose sans aucun coût pour vous. C'est presque toujours l'appel correct à faire.Si ce n'est pas le cas
arc4random_uniform
, vous pouvez utiliser la puissance de l'opensource pour voir exactement comment il est implémenté au-dessus d'un RNG à plus large éventail (ar4random
dans ce cas, mais une approche similaire pourrait également fonctionner au-dessus d'autres RNG).Voici l' implémentation d'OpenBSD :
Il convient de noter le dernier commentaire de validation sur ce code pour ceux qui ont besoin d'implémenter des choses similaires:
L'implémentation Java est également facilement repérable (voir le lien précédent):
la source
arcfour_random()
réellement utilise le véritable algorithme RC4 dans sa mise en œuvre, la sortie aura certainement un biais. J'espère que les auteurs de votre bibliothèque sont passés à l'utilisation d'un meilleur CSPRNG derrière la même interface. Je me souviens que l'un des BSD utilise actuellement l'algorithme ChaCha20 pour l'implémenterarcfour_random()
. En savoir plus sur les biais de sortie RC4 qui le rendent inutile pour la sécurité ou d'autres applications critiques telles que le poker vidéo: blog.cryptographyengineering.com/2013/03/…/dev/random
a également utilisé RC4 sur certaines plates-formes dans le passé (Linux utilise SHA-1 en mode compteur). Malheureusement, les pages de manuel que j'ai trouvées via la recherche indiquent que RC4 est toujours utilisé sur diverses plates-formes qui offrentarc4random
(bien que le code réel puisse être différent).-upper_bound % upper_bound == 0
??-upper_bound % upper_bound
sera en effet égal à 0 s'ilint
est supérieur à 32 bits. Cela devrait être(u_int32_t)-upper_bound % upper_bound)
(en supposant queu_int32_t
c'est un BSD-isme pouruint32_t
).Définition
Modulo Bias biais modulo est le biais inhérent à l'utilisation de l'arithmétique modulo pour réduire un ensemble de sortie à un sous-ensemble de l'ensemble d'entrée. En général, un biais existe chaque fois que le mappage entre l'ensemble d'entrée et l'ensemble de sortie n'est pas également distribué, comme dans le cas de l'utilisation de l'arithmétique modulo lorsque la taille de l'ensemble de sortie n'est pas un diviseur de la taille de l'ensemble d'entrée.
Ce biais est particulièrement difficile à éviter en informatique, où les nombres sont représentés sous la forme de chaînes de bits: 0 et 1. Trouver des sources de hasard vraiment aléatoires est également extrêmement difficile, mais dépasse le cadre de cette discussion.Pour le reste de cette réponse, supposons qu'il existe une source illimitée de bits vraiment aléatoires.
Exemple de problème
Considérons la simulation d'un jet de dé (0 à 5) à l'aide de ces bits aléatoires. Il y a 6 possibilités, nous avons donc besoin de suffisamment de bits pour représenter le nombre 6, qui est de 3 bits. Malheureusement, 3 bits aléatoires donnent 8 résultats possibles:
Nous pouvons réduire la taille de l'ensemble de résultats à exactement 6 en prenant la valeur modulo 6, mais cela présente le problème de biais modulo :
110
donne un 0 et111
donne un 1. Ce dé est chargé.Solutions potentielles
Approche 0:
Plutôt que de compter sur des bits aléatoires, en théorie, on pourrait embaucher une petite armée pour lancer des dés toute la journée et enregistrer les résultats dans une base de données, puis utiliser chaque résultat une seule fois. C'est à peu près aussi pratique que cela puisse paraître, et plus que probable ne donnerait de toute façon pas de résultats vraiment aléatoires (jeu de mots).
Approche 1:
Au lieu d'utiliser le module, une solution naïve mais mathématiquement correcte consiste à rejeter les résultats qui donnent
110
et à111
réessayer simplement avec 3 nouveaux bits. Malheureusement, cela signifie qu'il y a 25% de chances sur chaque lancer qu'un relancement sera nécessaire, y compris chacun des relances eux - mêmes. Ceci est clairement impraticable pour tous, mais le plus trivial des utilisations.Approche 2:
Utilisez plus de bits: au lieu de 3 bits, utilisez 4. Cela donne 16 résultats possibles. Bien sûr, une relance à chaque fois que le résultat est supérieur à 5 aggrave les choses (10/16 = 62,5%) de sorte que cela ne suffira pas à lui seul.
Notez que 2 * 6 = 12 <16, afin que nous puissions prendre en toute sécurité tout résultat inférieur à 12 et réduire ce module 6 pour répartir uniformément les résultats. Les 4 autres résultats doivent être rejetés, puis relancés comme dans l'approche précédente.
Sonne bien au début, mais vérifions les calculs:
Ce résultat est regrettable, mais réessayons avec 5 bits:
Une amélioration certaine, mais insuffisante dans de nombreux cas pratiques. La bonne nouvelle est que l' ajout de bits n'augmentera jamais les chances de devoir se défaire et relancer . Cela vaut non seulement pour les dés, mais dans tous les cas.
Comme démontré cependant, l'ajout d'un bit supplémentaire peut ne rien changer. En fait, si nous augmentons notre roulement à 6 bits, la probabilité reste de 6,25%.
Cela soulève 2 questions supplémentaires:
Solution générale
Heureusement, la réponse à la première question est oui. Le problème avec 6 est que 2 ^ x mod 6 bascule entre 2 et 4 qui sont par coïncidence un multiple de 2 les uns des autres, de sorte que pour un x pair> 1,
Ainsi, 6 est une exception plutôt que la règle. Il est possible de trouver des modules plus grands qui donnent des puissances consécutives de 2 de la même manière, mais finalement cela doit s'enrouler et la probabilité d'un rejet sera réduite.
Preuve de concept
Voici un exemple de programme qui utilise libcrypo d'OpenSSL pour fournir des octets aléatoires. Lors de la compilation, assurez-vous de créer un lien vers la bibliothèque avec
-lcrypto
laquelle tout le monde devrait avoir accès.J'encourage à jouer avec les valeurs
MODULUS
etROLLS
pour voir combien de relances se produisent réellement dans la plupart des conditions. Une personne sceptique peut également souhaiter enregistrer les valeurs calculées dans un fichier et vérifier que la distribution semble normale.la source
randomPool = RAND_bytes(...)
ligne résultera toujours enrandomPool == 1
raison de l'assertion. Cela se traduit toujours par un rejet et une relance. Je pense que vous vouliez déclarer sur une ligne distincte. Par conséquent, cela a fait revenir le RNG à1
chaque itération.randomPool
sera toujours évalué1
selon la documentationRAND_bytes()
OpenSSL car il réussira toujours grâce à l'RAND_status()
assertion.Il y a deux plaintes habituelles avec l'utilisation de modulo.
une est valable pour tous les générateurs. Il est plus facile de voir dans un cas limite. Si votre générateur a un RAND_MAX qui est 2 (qui n'est pas conforme à la norme C) et que vous ne voulez que 0 ou 1 comme valeur, l'utilisation de modulo générera 0 deux fois plus souvent (lorsque le générateur génère 0 et 2) comme il le fera générer 1 (lorsque le générateur génère 1). Notez que cela est vrai dès que vous ne supprimez pas de valeurs, quel que soit le mappage que vous utilisez des valeurs du générateur à la valeur souhaitée, l'une se produit deux fois plus souvent que l'autre.
certains types de générateurs ont leurs bits moins significatifs moins aléatoires que les autres, au moins pour certains de leurs paramètres, mais malheureusement, ces paramètres ont d'autres caractéristiques intéressantes (comme le fait d'avoir RAND_MAX un de moins qu'une puissance de 2). Le problème est bien connu et depuis longtemps l'implémentation de la bibliothèque évite probablement le problème (par exemple, l'implémentation de rand () dans le standard C utilise ce type de générateur, mais laisse tomber les 16 bits les moins significatifs), mais certains aiment se plaindre et vous risquez de ne pas avoir de chance
Utiliser quelque chose comme
générer un nombre aléatoire entre 0 et n évitera les deux problèmes (et cela évite le débordement avec RAND_MAX == INT_MAX)
BTW, C ++ 11 a introduit des moyens standard pour la réduction et un autre générateur que rand ().
la source
La solution de Mark (la solution acceptée) est presque parfaite.
Cependant, il a une mise en garde qui rejette 1 ensemble valide de résultats dans tout scénario où
RAND_MAX
(RM
) est 1 de moins qu'un multiple deN
(OùN
= le nombre de résultats valides possibles).c'est-à-dire, lorsque le «nombre de valeurs rejetées» (
D
) est égal àN
, alors il s'agit en fait d'un ensemble valide (V)
, et non d'un ensemble non valide (I
).Ce qui en est la cause à un moment donné, Mark perd de vue la différence entre
N
etRand_Max
.N
est un ensemble dont les membres valides sont composés uniquement d'entiers positifs, car il contient un nombre de réponses qui seraient valides. (par exemple: SetN
={1, 2, 3, ... n }
)Rand_max
Cependant, il s'agit d'un ensemble qui (tel que défini pour nos besoins) comprend un nombre quelconque d'entiers non négatifs.Dans sa forme la plus générique, ce qui est défini ici
Rand Max
est l'ensemble de tous les résultats valides, qui pourrait théoriquement inclure des nombres négatifs ou des valeurs non numériques.Par conséquent,
Rand_Max
est mieux défini comme l'ensemble des «réponses possibles».Cependant, il
N
opère par rapport au nombre de valeurs dans l'ensemble des réponses valides, donc même tel que défini dans notre cas spécifique, il yRand_Max
aura une valeur inférieure au nombre total qu'il contient.En utilisant la solution de Mark, les valeurs sont rejetées lorsque: X => RM - RM% N
Comme vous pouvez le voir dans l'exemple ci-dessus, lorsque la valeur de X (le nombre aléatoire que nous obtenons de la fonction initiale) est 252, 253, 254 ou 255, nous la rejetons même si ces quatre valeurs comprennent un ensemble valide de valeurs renvoyées .
IE: lorsque le nombre de valeurs rejetées (I) = N (le nombre de résultats valides), un ensemble valide de valeurs de retour sera rejeté par la fonction d'origine.
Si nous décrivons la différence entre les valeurs N et RM comme D, c'est-à-dire:
Puis, à mesure que la valeur de D diminue, le pourcentage de relances inutiles dues à cette méthode augmente à chaque multiplicatif naturel. (Lorsque RAND_MAX n'est PAS égal à un nombre premier, cela est une préoccupation valable)
PAR EXEMPLE:
Étant donné que le pourcentage de relances nécessaires augmente à mesure que N se rapproche de RM, cela peut être une préoccupation valable pour de nombreuses valeurs différentes en fonction des contraintes du système exécutant le code et des valeurs recherchées.
Pour annuler cela, nous pouvons apporter un amendement simple, comme indiqué ici:
Cela fournit une version plus générale de la formule qui tient compte des particularités supplémentaires de l'utilisation du module pour définir vos valeurs maximales.
Exemples d'utilisation d'une petite valeur pour RAND_MAX qui est un multiplicatif de N.
Version Mark'original:
Version généralisée 1:
De plus, dans le cas où N doit être le nombre de valeurs dans RAND_MAX; dans ce cas, vous pouvez définir N = RAND_MAX +1, sauf si RAND_MAX = INT_MAX.
En boucle, vous pouvez simplement utiliser N = 1, et toute valeur de X sera acceptée, cependant, et mettez une instruction IF pour votre multiplicateur final. Mais vous avez peut-être du code qui peut avoir une raison valable de retourner un 1 lorsque la fonction est appelée avec n = 1 ...
Il peut donc être préférable d'utiliser 0, qui fournirait normalement une erreur Div 0, lorsque vous souhaitez avoir n = RAND_MAX + 1
Version généralisée 2:
Ces deux solutions résolvent le problème avec des résultats valides inutilement rejetés qui se produiront lorsque RM + 1 est un produit de n.
La deuxième version couvre également le scénario de cas de bord lorsque vous avez besoin de n pour égaler l'ensemble total possible de valeurs contenues dans RAND_MAX.
L'approche modifiée dans les deux est la même et permet une solution plus générale au besoin de fournir des nombres aléatoires valides et de minimiser les valeurs rejetées.
Recommencer:
La solution générale de base qui prolonge l'exemple de Mark:
La solution générale étendue qui permet un scénario supplémentaire de RAND_MAX + 1 = n:
Dans certaines langues (en particulier les langues interprétées), le calcul de l'opération de comparaison en dehors de la condition while peut conduire à des résultats plus rapides car il s'agit d'un calcul unique, quel que soit le nombre de réessais requis. YMMV!
la source
RAND_MAX%n = n - 1
Avec une
RAND_MAX
valeur de3
(en réalité, elle devrait être beaucoup plus élevée que cela mais le biais existerait toujours), il est logique à partir de ces calculs qu'il existe un biais:1 % 2 = 1
2 % 2 = 0
3 % 2 = 1
random_between(1, 3) % 2 = more likely a 1
Dans ce cas,
% 2
c'est ce que vous ne devez pas faire lorsque vous voulez un nombre aléatoire entre0
et1
. Vous pouvez cependant obtenir un nombre aléatoire entre0
et2
en faisant% 3
, car dans ce cas:RAND_MAX
est un multiple de3
.Une autre méthode
Il y a beaucoup plus simple mais pour ajouter à d'autres réponses, voici ma solution pour obtenir un nombre aléatoire entre
0
etn - 1
, doncn
différentes possibilités, sans biais.>= n
, redémarrez (pas de module).Les données vraiment aléatoires ne sont pas faciles à obtenir, alors pourquoi utiliser plus de bits que nécessaire.
Voici un exemple dans Smalltalk, utilisant un cache de bits provenant d'un générateur de nombres pseudo-aléatoires. Je ne suis pas un expert en sécurité, alors utilisez-le à vos risques et périls.
la source
Comme l' indique la réponse acceptée , le "biais modulo" a ses racines dans la faible valeur de
RAND_MAX
. Il utilise une valeur extrêmement petite deRAND_MAX
(10) pour montrer que si RAND_MAX était 10, alors vous avez essayé de générer un nombre compris entre 0 et 2 en utilisant%, les résultats suivants en résulteraient:Il y a donc 4 sorties de 0 (4/10 chances) et seulement 3 sorties de 1 et 2 (3/10 chances chacune).
C'est donc biaisé. Les nombres inférieurs ont de meilleures chances de sortir.
Mais cela n'apparaît que si évidemment quand il
RAND_MAX
est petit . Ou plus précisément, lorsque le nombre par lequel vous modifiez est important par rapport àRAND_MAX
.Une bien meilleure solution que le bouclage (ce qui est incroyablement inefficace et ne devrait même pas être suggéré) consiste à utiliser un PRNG avec une plage de sortie beaucoup plus grande. L' algorithme Mersenne Twister a une sortie maximale de 4 294 967 295. En tant que tel, faire
MersenneTwister::genrand_int32() % 10
à toutes fins utiles sera réparti également et l'effet de biais modulo disparaîtra pratiquement.la source
MT::genrand_int32()%2
prend 0 (50 + 2,3e-8)% du temps et 1 (50 - 2,3e-8)% du temps. À moins que vous ne construisiez le RGN d'un casino (pour lequel vous utiliseriez probablement une gamme beaucoup plus grande de RGN), tout utilisateur ne remarquera pas 2,3e-8% de temps supplémentaire. Vous parlez de chiffres trop petits pour avoir de l'importance ici.RAND_MAX
valeur élevée diminue le biais modulo, mais ne l'élimine pas. Boucle sera.RAND_MAX
est suffisamment plus grand que le nombre que vous modifiez, le nombre de fois que vous devez régénérer le nombre aléatoire est extrêmement faible et n'affectera pas l'efficacité. Je dis garder la boucle, tant que vous testez contre le plus grand multiple den
plutôt quen
comme proposé par la réponse acceptée.Je viens d'écrire un code pour la méthode de retournement de pièces non biaisé de Von Neumann, qui devrait théoriquement éliminer tout biais dans le processus de génération de nombres aléatoires. Plus d'informations peuvent être trouvées sur ( http://en.wikipedia.org/wiki/Fair_coin )
la source
rand() % 100
100 fois. B) si tous les résultats sont différents, prenez le premier. C) sinon, GOTO A. Cela fonctionnera, mais avec un nombre prévu d'itérations d'environ 10 ^ 42, vous devrez être assez patient. Et immortel.else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}