web-dev-qa-db-fra.com

Pourquoi Rand ()% 6 est-il biaisé?

En lisant comment utiliser std :: Rand, j'ai trouvé ce code sur cppreference.com

int x = 7;
while(x > 6) 
    x = 1 + std::Rand()/((Rand_MAX + 1u)/6);  // Note: 1+Rand()%6 is biased

Quel est le problème avec l'expression à droite? J'ai essayé et ça fonctionne parfaitement.

106
yO_

Rand() % 6 présente deux problèmes (le 1+ n'affecte aucun de ces problèmes).

Tout d’abord, comme plusieurs réponses l’ont souligné, si les bits faibles de Rand() ne sont pas uniformes, le résultat de l’opérateur restant n’est pas uniforme non plus.

Deuxièmement, si le nombre de valeurs distinctes produites par Rand() n'est pas un multiple de 6, le reste produira davantage de valeurs basses que de valeurs élevées. Cela est vrai même si Rand() renvoie des valeurs parfaitement distribuées.

Comme exemple extrême, supposons que Rand() génère des valeurs uniformément réparties dans la plage [0..6]. Si vous examinez les restes pour ces valeurs, lorsque Rand() renvoie une valeur comprise dans la plage [0..5], le reste produit des résultats uniformément répartis dans la plage [0..5]. Lorsque Rand() renvoie 6, Rand() % 6 renvoie 0, comme si Rand() avait renvoyé 0. Vous obtenez donc une distribution avec deux fois plus de 0 que toute autre valeur.

Le second est le problème réel avec Rand() % 6.

Le moyen d'éviter ce problème est de rejeter les valeurs qui produiraient des doublons non uniformes. Vous calculez le plus grand multiple de 6 inférieur ou égal à Rand_MAX, et chaque fois que Rand() renvoie une valeur supérieure ou égale à ce multiple, vous le rejetez et appelez `Rand () à nouveau, autant de fois que fois un besoin.

Alors:

int max = 6 * ((Rand_MAX + 1u) / 6)
int value = Rand();
while (value >= max)
    value = Rand();

C'est une application différente du code en question, destinée à montrer plus clairement ce qui se passe.

136
Pete Becker

Il y a des profondeurs cachées ici:

  1. L'utilisation du petit u dans Rand_MAX + 1u. Rand_MAX est défini comme étant un type int et est souvent le plus grand possible int. Le comportement de Rand_MAX + 1 serait non défini dans les cas où vous surchargeriez un type signed. Ecrire 1u force la conversion du type de Rand_MAX en unsigned, évitant ainsi le débordement.

  2. L’utilisation de % 6can (mais à chaque implémentation de std::Rand j’ai vu ne le fait pas) introduit un biais statistique supplémentaire au-dessus et au-delà l'alternative présentée. De tels exemples où % 6 est dangereux sont ceux où le générateur de nombres a des plaines de corrélation dans les bits de poids faible, comme par exemple une implémentation assez célèbre d'IBM (en C) de Rand dans les années 1970, qui renversé les bits haut et bas comme "un fleuron final". Une autre considération est que 6 est très petit, cf. Rand_MAX, il y aura donc un effet minimal si Rand_MAX n'est pas un multiple de 6, ce qui n'est probablement pas le cas.

En conclusion, ces jours-ci, en raison de sa facilité de traitement, j'utiliserais % 6. Il est peu probable que des anomalies statistiques soient introduites au-delà de celles introduites par le générateur lui-même. Si vous avez encore des doutes, testez votre générateur pour voir s'il possède les propriétés statistiques appropriées pour votre cas d'utilisation.

19
Bathsheba

Cet exemple de code illustre le fait que std::Rand est un cas typique de cauchemar de cargaison qui doit faire lever vos sourcils chaque fois que vous le voyez.

Il y a plusieurs problèmes ici:

Le contrat que les gens assument habituellement - même les pauvres âmes malchanceuses qui ne connaissent pas mieux et qui n’y penseront pas précisément - est que Rand échantillons provenant duuniforme distributionsur les nombres entiers de 0, 1, 2,…, Rand_MAX, et chaque appel donne un/independentsample.

Le ​​premier problème est que le contrat supposé, des échantillons aléatoires uniformes indépendants dans chaque appel, n'est pas ce que dit la documentation - et dans la pratique, les mises en œuvre ont toujours échoué à fournir le simulacre le plus simple d'indépendance. Par exemple , C99 §7.20.2.1 'La fonction Rand' dit, sans explication:

La fonction Rand calcule une séquence d'entiers pseudo-aléatoires compris entre 0 et Rand_MAX.

Ceci est une phrase dénuée de sens, car le pseudo-aléatoire est une propriété defonction(oufamille de fonctions), pas d'un nombre entier, mais cela n'empêche même pas les bureaucrates d'ISO d'abuser de la langue. Après tout, les seuls lecteurs qui en seraient choqués savent qu'il vaut mieux que de lire la documentation de Rand de peur que leurs cellules cérébrales ne se décomposent.

Une implémentation historique typique en C fonctionne comme ceci:

static unsigned int seed = 1;

static void
srand(unsigned int s)
{
    seed = s;
}

static unsigned int
Rand(void)
{
    seed = (seed*1103515245 + 12345) % ((unsigned long)Rand_MAX + 1);
    return (int)seed;
}

Ceci a la propriété malheureuse quemême si un seul échantillon peut être uniformément distribuésous un germe aléatoire uniforme (qui dépend de la valeur spécifique de Rand_MAX), il alterne entre les entiers pairs et impairs dans les appels consécutifs - après

int a = Rand();
int b = Rand();

l'expression (a & 1) ^ (b & 1) donne 1 avec une probabilité de 100%, ce qui n'est pas le cas pourindependentdes échantillons aléatoires sur toute distribution prise en charge sur des entiers pairs et impairs. Ainsi, un culte de la cargaison est apparu selon lequel il fallait jeter les mors de faible poids pour chasser la bête insaisissable du "meilleur caractère aléatoire". (Alerte spoiler: ceci n’est pas un terme technique. C’est un signe que, quelle que soit la prose que vous lisez, elle ne sait pas de quoi elle parle, ou pensevoussont désemparés et doivent être condescendus à)

Le ​​deuxième problème est quemême si chaque appel échantillonne indépendamment d'une distribution aléatoire uniformele 0, 1, 2,…, Rand_MAX, le résultat de Rand() % 6 ne serait pas uniformément distribué en 0, 1, 2, 3, 4, 5 comme un jet de dé, sauf si Rand_MAX est congru à -1 modulo 6. Contre-exemple simple: Si Rand_MAX = 6, à partir de Rand(), tous les résultats ont une probabilité égale de 1/7, mais à partir de Rand() % 6, le résultat 0 a une probabilité de 2/7, tandis que tous les autres résultats ont une probabilité 1/sept.

La bonne façon de faire est d'utiliser un échantillon de rejet:à plusieurs reprisesdessine un échantillon aléatoire uniforme indépendant s à partir de 0, 1, 2 ,…, Rand_MAX etrejette(par exemple) les résultats 0, 1, 2,…, ((Rand_MAX + 1) % 6) - 1— si vous obtenez l'un des ceux-ci recommencent; sinon, donnez s % 6.

unsigned int s;
while ((s = Rand()) < ((unsigned long)Rand_MAX + 1) % 6)
    continue;
return s % 6;

De cette façon, l'ensemble des résultats de Rand() que nous acceptons est divisible par 6, et chaque résultat possible de s % 6 est obtenu par le même nombre deacceptérésulte de Rand(), donc si Rand() est uniformément distribué, il en est de même de s. Il n'y a pas deliésur le nombre d'essais, mais lenombre attenduest inférieur à 2 et la probabilité de succès croît de manière exponentielle avec le nombre d'essais.

Le choix dequeles résultats de Rand() que vous refusez est indifférent, à condition que vous en affectiez un nombre égal à chaque entier inférieur à 6. Le code de référence. com fait un choix dedifférent, à cause du premier problème ci-dessus: rien n'est garanti sur la distribution ou l'indépendance des sorties de Rand(), et dans la pratique le plus bas Les bits de commande présentent des motifs qui ne "semblent pas assez aléatoires" (peu importe que la sortie suivante soit une fonction déterministe de la précédente).

Exercice pour le lecteur: prouvez que le code sur cppreference.com donne une distribution uniforme sur les jets de dés si Rand() donne une distribution uniforme sur 0, 1, 2,…, Rand_MAX.

Exercice pour le lecteur: Pourquoi préféreriez-vous que l’un ou l’autre des sous-ensembles soit rejeté? Quel calcul est nécessaire pour chaque essai dans les deux cas?

n troisième problème est que l'espace entre les semences est si petit que même si la graine est uniformément distribuée, un adversaire armé de la connaissance de votre programme et d'un résultat mais pas de la graine peut facilement prédire la graine et les résultats ultérieurs, ce qui rend ils ne semblent pas si aléatoires après tout. Alors, ne pensez même pas à utiliser ceci pour la cryptographie.

Vous pouvez vous rendre dans la classe std::uniform_int_distribution de C++ 11 avec un périphérique aléatoire approprié et votre moteur aléatoire favori comme le très populaire twister Mersenne std::mt19937 pour jouer au dés avec votre joueur de quatre ans. vieux cousin, mais même cela ne sera pas apte à générer du matériel de clé cryptographique - et le twister Mersenne est un terrible cochon spatial avec un état de plusieurs kilo-octets qui ravage le cache de votre CPU avec un temps de paramétrage obscène, il est donc mauvais même pour,par exemple, simulations parallèles de Monte Carlo avec des arbres reproductibles de sous-calculs; sa popularité provient probablement de son nom accrocheur. Mais vous pouvez l'utiliser pour lancer des dés comme dans cet exemple!

Une autre approche consiste à utiliser un simple générateur de nombres pseudo-aléatoires cryptographiques avec un petit état, tel qu'un simple effacement rapide de clé PRNG , ou simplement un chiffrement de flux tel que AES-CTR ou ChaCha20 si vous êtes sûr (eg, dans une simulation de Monte Carlo pour la recherche en sciences naturelles), il n'y a aucune conséquence défavorable sur la prévision des résultats passés si l'état est jamais compromis.

13

Je ne suis en aucun cas un utilisateur expérimenté de C++, mais je voulais savoir si les autres réponses concernant std::Rand()/((Rand_MAX + 1u)/6) étant moins biaisée que 1+std::Rand()%6 sont vraies. J'ai donc écrit un programme de test pour compiler les résultats des deux méthodes (je n'ai pas écrit en C++ depuis bien longtemps, vérifiez-le). Un lien pour exécuter le code est trouvé ici . Il est également reproduit comme suit:

// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>

int main()
{
    std::srand(std::time(nullptr)); // use current time as seed for random generator

    // Roll the die 6000000 times using the supposedly unbiased method and keep track of the results

    int results[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::Rand()/((Rand_MAX + 1u)/6);  // Note: 1+Rand()%6 is biased

        results[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results[n] << ' ';
    }

    std::cout << "\n";


    // Roll the die 6000000 times using the supposedly biased method and keep track of the results

    int results_bias[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::Rand()%6;

        results_bias[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results_bias[n] << ' ';
    }
}

J'ai ensuite pris la sortie de ceci et utilisé la fonction chisq.test dans R pour exécuter un test de Khi-deux afin de voir si les résultats sont significativement différents de ceux attendus. Cette question de stackexchange décrit plus en détail l'utilisation du test du Khi-deux pour vérifier l'équité des dés: Comment puis-je vérifier si un dé est juste? . Voici les résultats pour quelques essais:

> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )

> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 8.6168, df = 5, p-value = 0.1254

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 1.6034, df = 5, p-value = 0.9008

> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075   )
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.051, df = 5, p-value = 0.2169

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 4.319, df = 5, p-value = 0.5045

> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.9592, df = 5, p-value = 0.1585

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 2.8229, df = 5, p-value = 0.7273

Dans les trois analyses que j'ai effectuées, la valeur p pour les deux méthodes était toujours supérieure aux valeurs alpha typiques utilisées pour tester la significativité (0,05). Cela signifie que nous ne considérerions ni l'un ni l'autre d'être partial. Il est intéressant de noter que la méthode supposée non biaisée a systématiquement des valeurs p plus basses, ce qui indique qu’elle pourrait en réalité être plus biaisée. La mise en garde étant que je n'ai fait que 3 courses.

MISE À JOUR: Pendant que j'écrivais ma réponse, Konrad Rudolph a publié une réponse qui adopte la même approche, mais donne un résultat très différent. Je n'ai pas la réputation de commenter sa réponse, alors je vais en parler ici. Tout d’abord, l’essentiel est que le code utilise la même graine pour le générateur de nombres aléatoires à chaque exécution. Si vous changez la graine, vous obtiendrez une variété de résultats. Deuxièmement, si vous ne changez pas la graine, mais le nombre d'essais, vous obtiendrez également une variété de résultats. Essayez d’augmenter ou de diminuer d’un ordre de grandeur pour voir ce que je veux dire. Troisièmement, il y a une troncature ou un arrondi entier qui se produit lorsque les valeurs attendues ne sont pas assez précises. Ce n'est probablement pas suffisant pour faire une différence, mais c'est là.

En résumé, en résumé, il a juste eu la bonne graine et le nombre d’essais pour obtenir un faux résultat.

2
anjama

On peut penser à un générateur de nombres aléatoires comme travaillant sur un flux de chiffres binaires. Le générateur transforme le flux en chiffres en le découpant en morceaux. Si la fonction std:Rand fonctionne avec un Rand_MAX sur 32767, elle utilise 15 bits par tranche.

Quand on prend les modules d'un nombre compris entre 0 et 32767 inclus, on trouve que 5462 '0 et' 1 mais seulement 5461 '2,' 3's, '4's et' 5's. Par conséquent, le résultat est biaisé. Plus la valeur Rand_MAX est grande, moins il y aura de biais, mais c'est inévitable.

Ce qui n'est pas biaisé, c'est un nombre compris dans l'intervalle [0 .. (2 ^ n) -1]. Vous pouvez générer un nombre (théoriquement) meilleur dans la plage 0..5 en extrayant 3 bits, en les convertissant en un entier compris dans la plage 0..7 et en rejetant 6 et 7.

On espère que tous les bits du train de bits ont la même chance d'être "0" ou "1", quel que soit leur emplacement dans le flux ou les valeurs des autres bits. Ceci est exceptionnellement difficile dans la pratique. Les nombreuses implémentations différentes de PRNG logiciels offrent différents compromis entre vitesse et qualité. Un générateur de congruence linéaire tel que std::Rand offre la vitesse la plus rapide pour une qualité moindre. Un générateur cryptographique offre la plus haute qualité pour une vitesse minimale.

2
Simon G.