Pourquoi est-ce que j'obtiens des résultats inégalement répartis lorsque j'utilise $ RANDOM?

14

J'ai lu sur les RNG sur Wikipedia et $RANDOMfonctionne sur TLDP mais cela n'explique pas vraiment ce résultat:

$ max=$((6*3600))
$ for f in {1..100000}; do echo $(($RANDOM%max/3600)); done | sort | uniq -c
  21787 0
  22114 1
  21933 2
  12157 3
  10938 4
  11071 5

Pourquoi les valeurs ci-dessus environ 2x sont-elles plus enclines à être 0, 1, 2 que 3, 4, 5 mais quand je change le module max, elles sont presque également réparties sur les 10 valeurs?

$ max=$((9*3600))
$ for f in {1..100000}; do echo $(($RANDOM%max/3600)); done | sort | uniq -c
  11940 0
  11199 1
  10898 2
  10945 3
  11239 4
  10928 5
  10875 6
  10759 7
  11217 8
cprn
la source
9
La réponse habituelle à cela est de relancer (jeter le numéro que vous avez reçu et en choisir un autre) si vous êtes entre la valeur maximale pour RANDOM et la valeur la plus élevée possible qui peut se diviser également dans votre modulo. Ce n'est pas habituel à RANDOM, c'est habituel à utiliser-modulo-pour-restreindre-RNG-domaine dans toutes les langues / outils / etc. mettre en œuvre des RNG de ce type.
Charles Duffy
7
Voir mon article de 2013 sur la source de ce biais si vous voulez de beaux graphiques sur la façon dont il se détériore
Eric Lippert
1
"La génération de nombres aléatoires est trop importante pour être laissée au hasard." - Robert Coveyou. FYI cependant: la plupart des programmes sont incapables de générer des nombres vraiment aléatoires
jesse_b
@Eric Lippert merci, je vais le lire avec plaisir!
cprn
1
Notez que, même si vous rencontrez des problèmes en raison du biais modulo, la $RANDOMvariable n'utilise pas un bon PRNG en interne.
forêt

Réponses:

36

Pour développer le sujet du biais modulo, votre formule est la suivante:

max=$((6*3600))
$(($RANDOM%max/3600))

Et dans cette formule, $RANDOMest une valeur aléatoire dans la plage 0-32767.

   RANDOM Each time this parameter is referenced, a random integer between
          0 and 32767 is generated.

Il aide à visualiser comment cela correspond aux valeurs possibles:

0 = 0-3599
1 = 3600-7199
2 = 7200-10799
3 = 10800-14399
4 = 14400-17999
5 = 18000-21599
0 = 21600-25199
1 = 25200-28799
2 = 28800-32399
3 = 32400-32767

Donc, dans votre formule, la probabilité de 0, 1, 2 est le double de 4, 5. Et la probabilité de 3 est légèrement supérieure à 4, 5 également. D'où votre résultat avec 0, 1, 2 comme gagnants et 4, 5 comme perdants.

Lors du passage à 9*3600, il s'avère que:

0 = 0-3599
1 = 3600-7199
2 = 7200-10799
3 = 10800-14399
4 = 14400-17999
5 = 18000-21599
6 = 21600-25199
7 = 25200-28799
8 = 28800-32399
0 = 32400-32767

1-8 ont la même probabilité, mais il y a toujours un léger biais pour 0, et donc 0 était toujours le vainqueur de votre test avec 100'000 itérations.

Pour corriger le biais du modulo, vous devez d'abord simplifier la formule (si vous ne voulez que 0-5, alors le modulo est 6, pas 3600 ou un nombre encore plus fou, cela n'a aucun sens). À elle seule, cette simplification réduira considérablement votre biais (32766 correspond à 0, 32767 à 1, ce qui donne un tout petit biais à ces deux nombres).

Pour éliminer complètement le biais, vous devez relancer (par exemple) quand $RANDOMest inférieur à 32768 % 6(éliminer les états qui ne correspondent pas parfaitement à la plage aléatoire disponible).

max=6
for f in {1..100000}
do
    r=$RANDOM
    while [ $r -lt $((32768 % $max)) ]; do r=$RANDOM; done
    echo $(($r%max))
done | sort | uniq -c | sort -n

Résultat du test:

  16425 5
  16515 1
  16720 0
  16769 2
  16776 4
  16795 3

L'alternative serait d'utiliser une source aléatoire différente qui n'a pas de biais notable (ordres de grandeur supérieurs à seulement 32 768 valeurs possibles). Mais l'implémentation d'une logique de relance de toute façon ne fait pas de mal (même si cela n'arrive probablement jamais).

frostschutz
la source
Votre réponse est largement correcte, sauf: "vous devez relancer lorsque $ RANDOM est inférieur à 32768% 6" devrait en fait être "égal ou supérieur à floor ((RANDMAX + 1) / 6) * 6" (ie 32766 ) et corrigez le code shell associé en dessous.
Nayuki
@Nayuki si vous pouvez signaler une erreur spécifique (qui s'applique dans le contexte donné), je serai heureux de la corriger. Ma solution n'est qu'un exemple, il existe différentes façons de le faire. Vous pouvez supprimer le biais de la plage de début ou de fin, ou quelque part au milieu, cela ne fait aucune différence. Vous pouvez mieux le calculer (et ne pas faire de modulo à chaque itération). Vous pouvez gérer des cas spéciaux tels que des modules arbitraires et des valeurs randmax, également gérer RANDMAX = INTMAX où RANDMAX + 1 n'existe pas, mais ce n'était pas le sujet ici.
frostschutz
Votre réponse est bien pire que votre message. Tout d’abord, j’ai indiqué précisément quelle phrase de la vôtre est en fait erronée. Notez que "32768% 6" == 2, vous voulez donc relancer chaque fois que $ RANDOM <2? En ce qui concerne le biais au début / à la fin / au milieu de la plage, votre message entier vise à supprimer le biais à la fin de la plage, et ma réponse répond exactement à cela aussi. Troisièmement, vous parlez de gérer RANDMAX = INTMAX, mais dans votre réponse, vous avez mentionné la valeur 32768 (= 32767 + 1) à plusieurs reprises, ce qui implique que vous êtes à l'aise avec le calcul RANDMAX + 1.
Nayuki
1
@Nayuki mon code supprime 0 et 1, le vôtre supprime 32766 et 32767 et j'aimerais que vous élaboriez: quelle différence cela fait-il? Je ne suis qu'humain, je fais des erreurs, mais tout ce que vous avez dit jusqu'à présent est "c'est faux" sans expliquer ni montrer pourquoi. Je vous remercie.
frostschutz
1
Tant pis, compris. Désolé pour la fausse alarme.
Nayuki
23

Il s'agit d'un biais modulo. Si RANDOMest bien construit, chaque valeur entre 0 et 32767 est produite avec une probabilité égale. Lorsque vous utilisez modulo, vous modifiez les probabilités: les probabilités de toutes les valeurs au-dessus du modulo sont ajoutées aux valeurs auxquelles elles correspondent.

Dans votre exemple, 6 × 3600 représente environ les deux tiers de la plage de valeurs. Les probabilités du tiers supérieur s'ajoutent donc à celles du tiers inférieur, ce qui signifie que des valeurs de 0 à 2 (environ) sont deux fois plus susceptibles d'être produites que des valeurs de 3 à 5. 9 × 3600 est près de 32767, donc le le biais modulo est beaucoup plus petit et n'affecte que les valeurs de 32400 à 32767.

Pour répondre à votre question principale, au moins dans Bash, la séquence aléatoire est entièrement prévisible si vous connaissez la graine. Voir intrand32dans variables.c.

Stephen Kitt
la source