web-dev-qa-db-fra.com

Pourquoi les gens disent-ils qu'il existe un biais modulo lors de l'utilisation d'un générateur de nombres aléatoires?

J'ai beaucoup vu cette question, mais je n'ai jamais vu de réponse concrète à cette question. Je vais donc en publier un ici qui, espérons-le, aidera les gens à comprendre pourquoi il existe exactement un "biais modulo" dans l'utilisation d'un générateur de nombres aléatoires, comme Rand() en C++.

252
user1413793

Donc Rand() est un générateur de nombres pseudo-aléatoires qui choisit un nombre naturel compris entre 0 et Rand_MAX, constante définie dans cstdlib (voir cet article pour un aperçu général de Rand()).

Maintenant que se passe-t-il si vous voulez générer un nombre aléatoire compris entre 0 et 2? Par souci d'explication, supposons que Rand_MAX soit égal à 10 et que je décide de générer un nombre aléatoire compris entre 0 et 2 en appelant Rand()%3. Cependant, Rand()%3 ne produit pas les nombres compris 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 étendues, ce n’est peut-être pas le problème le plus important, mais pour une plus grande gamme, cela pourrait fausser la distribution et fausser les chiffres. 

Alors, quand Rand()%n renvoie-t-il 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.

Alors, comment pouvons-nous résoudre ce problème? Une méthode grossière consiste à 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 c'est inefficace pour les faibles valeurs de n, puisque vous avez seulement une chance n/Rand_MAX d'obtenir une valeur dans votre plage et vous devez donc effectuer des appels Rand_MAX/n à 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, à générer des nombres aléatoires jusqu'à ce que vous obteniez un nombre situé 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, il faudra rarement plus d'un appel à Rand().


Ouvrages cités et lectures complémentaires:


357
user1413793

Continuer à choisir un hasard est un bon moyen de supprimer le biais.

Mettre à jour

Nous pourrions accélérer le code si nous recherchions un x compris dans une plage divisible par n.

// Assumptions
// Rand() in [0, Rand_MAX]
// n in (0, Rand_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = Rand();
} while (x >= Rand_MAX - (Rand_MAX % n)) 

x %= n;

La boucle ci-dessus devrait être très rapide, disons 1 itération en moyenne.

35
Nick Dandoulakis

@ user1413793 est correct à propos du problème. Je ne discuterai pas de cela plus avant, sauf pour préciser un point: oui, pour les petites valeurs de n et les grandes valeurs de Rand_MAX, le biais modulo peut être très faible. Cependant, l'utilisation d'un modèle induisant des biais signifie que vous devez tenir compte du biais chaque fois que vous calculez un nombre aléatoire et choisir différents modèles pour différents cas. Et si vous faites le mauvais choix, les bugs qu’il introduit sont subtils et presque impossibles à tester un peu. Comparé à l’utilisation du bon outil (tel que arc4random_uniform), c’est un travail supplémentaire, pas moins. Faire plus de travail et obtenir une solution pire est une ingénierie redoutable, surtout lorsque le faire correctement à chaque fois est facile sur la plupart des plateformes.

Malheureusement, les implémentations de la solution sont toutes incorrectes ou moins efficaces qu’elles ne devraient l’être. (Chaque solution contient divers commentaires expliquant les problèmes, mais aucune des solutions n'a été résolue pour les résoudre.) Cela risquerait de confondre le demandeur occasionnel, alors je fournis ici une bonne mise en œuvre.

Là encore, la meilleure solution consiste simplement à utiliser arc4random_uniform sur les plates-formes qui le fournissent, ou une solution à distance similaire pour votre plate-forme (telle que Random.nextInt sur Java). Il fera la bonne chose sans aucun coût en code. C’est presque toujours le bon appel à faire.

Si vous n'avez pas arc4random_uniform, vous pouvez utiliser la puissance d'OpenSource pour voir exactement comment il est implémenté au dessus d'un RNG plus large (ar4random dans ce cas, mais une approche similaire pourrait également fonctionner au dessus d'un autre RNG). . 

Voici la mise en œuvre OpenBSD :

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

Il convient de noter le dernier commentaire de validation sur ce code pour ceux qui ont besoin d'implémenter des choses similaires:

Modifiez arc4random_uniform () pour calculer 2**32 % upper_bound'' as - upper_bound% upper_bound ''. Simplifie le code et en fait le identique sur les architectures ILP32 et LP64, et légèrement plus rapide sur Les architectures LP64 en utilisant un reste 32 bits au lieu d'un. 64 bits. reste.

Signalé par Jorden Verwer sur tech @ ok deraadt; aucune objection de djm ou otto

L'implémentation Java est également facilement trouvable (voir lien précédent):

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }
17
Rob Napier

Définition

Modulo Bias est le biais inhérent à l'utilisation de l'arithmétique modulo pour réduire un jeu de sorties en un sous-ensemble du jeu d'entrées. En général, un biais existe chaque fois que le mappage entre le jeu d'entrée et le jeu de sortie n'est pas distribué de manière égale, comme dans le cas d'utilisation de l'arithmétique modulo lorsque la taille de l'ensemble de sortie n'est pas un diviseur de la taille du jeu d'entrée.

Ce biais est particulièrement difficile à éviter en informatique, où les nombres sont représentés par des chaînes de bits: 0 et 1. Trouver des sources vraiment aléatoires de hasard est également extrêmement difficile, mais dépasse le cadre de la présente discussion. Pour le reste de cette réponse, supposons qu'il existe une source illimitée de bits réellement aléatoires.

Exemple de problème

Considérons la simulation d'un jet de dé (0 à 5) en utilisant 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 3 bits. Malheureusement, 3 bits aléatoires donne 8 résultats possibles:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

Nous pouvons réduire la taille de l'ensemble de résultats à 6 exactement en prenant la valeur modulo 6, mais ceci présente le problème biais modulo: 110 donne un 0 et 111 donne un 1. Ce dé est chargé .

Solutions potentielles

Approche 0:

Plutôt que de compter sur des éléments aléatoires, on pourrait en théorie engager une petite armée pour lancer les 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 en a l'air, et il est fort probable que cela ne produirait 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 à ignorer les résultats générant 110 et 111 et à simplement essayer à nouveau avec 3 nouveaux bits. Malheureusement, cela signifie qu’il y a 25% de chance sur chaque lancer qu’un relancement sera nécessaire, y compris chacun des relances. Ceci est clairement impraticable pour tous les usages, sauf le plus trivial.

Approche 2:

Utilisez plus de bits: au lieu de 3 bits, utilisez 4. Cela donne 16 résultats possibles. Bien entendu, une relance à chaque fois que le résultat est supérieur à 5 aggrave les choses (10/16 = 62,5%), de sorte que cela ne va pas suffire.

Notez que 2 * 6 = 12 <16, de sorte que nous pouvons sans risque prendre un résultat inférieur à 12 et réduire ce modulo 6 pour répartir équitablement les résultats. Les 4 autres résultats doivent être ignorés, puis relancés comme dans l'approche précédente.

Cela sonne bien au début, mais vérifions le calcul:

4 discarded results / 16 possibilities = 25%

Dans ce cas, 1 bit supplémentaire n'a pas aidé du tout!

Ce résultat est regrettable, mais essayons encore avec 5 bits:

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

Une nette amélioration, mais insuffisante dans de nombreux cas pratiques. La bonne nouvelle est que ajouter plus de bits n'augmentera jamais les chances de devoir supprimer et relancer. Cela vaut non seulement pour les dés, mais dans tous les cas.

Comme démontré cependant, ajouter un bit supplémentaire ne changera rien. En fait, si nous augmentons notre rôle à 6 bits, la probabilité reste de 6,25%.

Cela soulève 2 questions supplémentaires:

  1. Si nous ajoutons assez de bits, existe-t-il une garantie que la probabilité d'un rejet diminuera?
  2. _ {Combien de bits suffisent-ils) dans le cas général?

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, comme par hasard, sont un multiple de 2 les uns des autres, de sorte que pour un même x> 1, 

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

Donc 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 cela doit finalement être bouclé et la probabilité d'un rejet sera réduite.

Sans autre preuve, en général, on utilise le double du nombre de bits requis fournira une valeur plus petite, généralement insignifiante, chance d'un rejet.

Preuve de concept

Voici un exemple de programme utilisant libcrypo d'OpenSSL pour fournir des octets aléatoires. Lors de la compilation, veillez à créer un lien vers la bibliothèque avec -lcrypto dont la plupart des utilisateurs devraient pouvoir disposer.

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/Rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(Rand_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = Rand_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        Rand_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

J'encourage à jouer avec les valeurs MODULUS et ROLLS 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.

12
Jim Wood

Il y a deux plaintes habituelles avec l'utilisation de modulo.

  • l'un 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 voulez seulement 0 ou 1 comme valeur, utiliser modulo générera 0 fois plus souvent (lorsque le générateur génère 0 et 2) qu'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 sur celui souhaité, l'un se produit deux fois plus souvent que l'autre.

  • certains types de générateurs ont des bits moins significatifs moins aléatoires que les autres, du moins pour certains de leurs paramètres, mais malheureusement, ces paramètres ont une autre caractéristique intéressante (par exemple, Rand_MAX peut avoir une puissance inférieure à 2). Le problème est bien connu et l’implémentation de la bibliothèque l’a longtemps évité (par exemple, l’implémentation de Rand () dans le standard C utilise ce type de générateur, mais supprime les 16 bits les moins significatifs), mais certains se plaignent cela et vous pouvez avoir de la malchance

En utilisant quelque chose comme

int alea(int n){ 
 assert (0 < n && n <= Rand_MAX); 
 int partSize = 
      n == Rand_MAX ? 1 : 1 + (Rand_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = Rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

générer un nombre aléatoire compris entre 0 et n évitera les deux problèmes (et évite les débordements avec Rand_MAX == INT_MAX)

BTW, C++ 11 a introduit des méthodes standard pour le générateur de réduction et autre que Rand ().

9
AProgrammer

La solution de Mark (la solution acceptée) est presque parfaite.

int x;

do {
    x = Rand();
} while (x >= (Rand_MAX - Rand_MAX % n));

x %= n;

édité le 25 mars 16 à 23:16

Mark Amery 39k21170211

Cependant, il comporte une mise en garde qui supprime 1 ensemble de résultats valide dans tout scénario où Rand_MAX (RM) est égal à 1 de moins qu'un multiple de N (où N = le nombre de résultats valides possibles).

c'est-à-dire que lorsque le «nombre de valeurs rejetées» (D) est égal à N, il s'agit en fait d'un ensemble valide (V) et non d'un ensemble non valide (I).

À l'aide de la solution de Mark, les valeurs sont rejetées lorsque: X => RM - RM% N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

Comme vous pouvez le constater dans l'exemple ci-dessus, lorsque la valeur de X (le nombre aléatoire obtenu à partir 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 Discarded (I) = N (nombre de résultats valides), un ensemble valide de valeurs renvoyées est ignoré par la fonction d'origine.

Si nous décrivons la différence entre les valeurs N et RM sous la forme D, c'est-à-dire:

D = (RM - N)

Puis, lorsque la valeur de D devient plus petite, le pourcentage de nouveaux relances inutiles dus à cette méthode augmente à chaque multiplication naturelle. (Lorsque Rand_MAX n'est PAS égal à un nombre premier, ceci est une préoccupation valide)

PAR EXEMPLE:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

Etant donné que le pourcentage de rerolls nécessaires augmente à mesure que N s'approche de RM, cela peut constituer un problème valable pour de nombreuses valeurs différentes en fonction des contraintes du système exécutant le code et des valeurs recherchées.

Pour nier cela, nous pouvons faire un amendement simple, comme indiqué ici:

 int x;

 do {
     x = Rand();
 } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

 x %= n;

Ceci 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 une multiplicative de N.

Version Mark'original:

Rand_MAX = 3, n = 2, Values in Rand_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (Rand_MAX - ( Rand_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

Version généralisée 1:

Rand_MAX = 3, n = 2, Values in Rand_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (Rand_MAX - ( ( Rand_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set Rand_MAX so there will be no discard.

De plus, dans le cas où N devrait ê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 insérez une instruction IF pour votre multiplicateur final. Mais vous avez peut-être un code qui peut avoir une raison valable de renvoyer 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:

int x;

if n != 0 {
    do {
        x = Rand();
    } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = Rand();
}

Ces deux solutions résolvent le problème en éliminant inutilement les résultats valides qui se produiront lorsque RM + 1 est un produit de n.

La deuxième version couvre également le scénario Edge si vous avez besoin que n soit égal au nombre total possible de valeurs contenues dans Rand_MAX.

L'approche modifiée dans les deux cas 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 étend l'exemple de la marque:

 int x;

 do {
     x = Rand();
 } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

 x %= n;

La solution générale étendue qui permet un scénario supplémentaire de Rand_MAX + 1 = n:

int x;

if n != 0 {
    do {
        x = Rand();
    } while (x > (Rand_MAX - ( ( ( Rand_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = Rand();
}
6
Ben Personick

Avec une valeur Rand_MAX de 3 (en réalité, il devrait être beaucoup plus élevé que cela, mais le biais subsisterait), il est logique à partir de ces calculs qu'il existe un biais:

1 % 2 = 12 % 2 = 03 % 2 = 1random_between(1, 3) % 2 = more likely a 1

Dans ce cas, le % 2 est ce que vous ne devriez pas faire lorsque vous souhaitez un nombre aléatoire compris entre 0 et 1. Vous pouvez obtenir un nombre aléatoire entre 0 et 2 en faisant % 3, car dans ce cas: Rand_MAX est un multiple de 3.

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 et n - 1, donc n différentes possibilités, sans biais.

  • le nombre de bits (pas d'octets) nécessaires pour coder le nombre de possibilités est le nombre de bits de données aléatoires dont vous aurez besoin
  • encoder le nombre de bits aléatoires
  • si ce nombre est >= n, redémarrez (pas de modulo).

Il est difficile d'obtenir des données vraiment aléatoires, alors pourquoi utiliser plus de bits que nécessaire.

Vous trouverez ci-dessous un exemple en 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é, utilisez-le à vos risques et périls.

next: n

    | bitSize r from to |
    n < 0 ifTrue: [^0 - (self next: 0 - n)].
    n = 0 ifTrue: [^nil].
    n = 1 ifTrue: [^0].
    cache isNil ifTrue: [cache := OrderedCollection new].
    cache size < (self randmax highBit) ifTrue: [
        Security.DSSRandom default next asByteArray do: [ :byte |
            (1 to: 8) do: [ :i |    cache add: (byte bitAt: i)]
        ]
    ].
    r := 0.
    bitSize := n highBit.
    to := cache size.
    from := to - bitSize + 1.
    (from to: to) do: [ :i |
        r := r bitAt: i - from + 1 put: (cache at: i)
    ].
    cache removeFrom: from to: to.
    r >= n ifTrue: [^self next: n].
    ^r
0
Rivenfall