web-dev-qa-db-fra.com

Pourquoi cette valeur aléatoire a-t-elle une distribution 25/75 au lieu de 50/50?

Edit: Donc, fondamentalement, ce que j'essaie d'écrire est un hachage 1 bit pour double.

Je veux mapper un double à true ou false avec une chance de 50/50. Pour cela, j'ai écrit du code qui sélectionne des nombres aléatoires (juste à titre d'exemple, je veux l'utiliser sur des données avec des régularités et toujours obtenir un résultat 50/50) , vérifie leur dernier bit et incrémente y s'il vaut 1, ou n s'il vaut 0.

Cependant, ce code génère constamment 25% y et 75% n. Pourquoi n'est-ce pas 50/50? Et pourquoi une distribution aussi bizarre, mais simple (1/3)?

public class DoubleToBoolean {
    @Test
    public void test() {

        int y = 0;
        int n = 0;
        Random r = new Random();
        for (int i = 0; i < 1000000; i++) {
            double randomValue = r.nextDouble();
            long lastBit = Double.doubleToLongBits(randomValue) & 1;
            if (lastBit == 1) {
                y++;
            } else {
                n++;
            }
        }
        System.out.println(y + " " + n);
    }
}

Exemple de sortie:

250167 749833
139
gvlasov

Parce que nextDouble fonctionne comme ceci: ( source )

public double nextDouble()
{
    return (((long) next(26) << 27) + next(27)) / (double) (1L << 53);
}

next(x) crée x bits aléatoires.

Maintenant, pourquoi est-ce important? Parce qu'environ la moitié des nombres générés par la première partie (avant la division) sont inférieurs à 1L << 52, Et donc leur significande ne remplit pas entièrement les 53 bits qu'elle pourrait remplir, c'est-à-dire le bit le moins significatif de la significande est toujours nul pour ceux-là.


En raison de la quantité d'attention que cela reçoit, voici quelques explications supplémentaires sur ce à quoi ressemble vraiment un double dans Java (et beaucoup d'autres langages) et pourquoi il importait dans cette question .

Fondamentalement, un double ressemble à ceci: ( source )

double layout

Un détail très important non visible sur cette image est que les nombres sont "normalisés"1 de telle sorte que la fraction de 53 bits commence par un 1 (en choisissant l'exposant tel qu'il est), ce 1 est alors omis. C'est pourquoi l'image montre 52 bits pour la fraction (significande) mais il y a effectivement 53 bits.

La normalisation signifie que si dans le code de nextDouble le 53e bit est défini, ce bit est le premier implicite et il disparaît, et les 52 autres bits sont copiés littéralement dans la signification du double. Cependant, si ce bit n'est pas défini, les bits restants doivent être décalés vers la gauche jusqu'à ce qu'il soit activé.

En moyenne, la moitié des nombres générés tombent dans le cas où le significand n'était pas décalé vers la gauche du tout (et environ la moitié d'entre eux ont un 0 comme leur moins significatif bit), et l'autre moitié est décalée d'au moins 1 (ou est tout à fait zéro) de sorte que leur bit le moins significatif est toujours 0.

1: pas toujours, clairement cela ne peut pas être fait pour zéro, qui n'a pas de plus haut 1. Ces nombres sont appelés nombres dénormaux ou sous-normaux, voir wikipedia: nombre dénormal .

164
harold

De la docs :

La méthode nextDouble est implémentée par la classe Random comme si:

public double nextDouble() {
  return (((long)next(26) << 27) + next(27))
      / (double)(1L << 53);
}

Mais il indique également ce qui suit (je souligne):

[Dans les premières versions de Java, le résultat était incorrectement calculé comme suit:

 return (((long)next(27) << 27) + next(27))
     / (double)(1L << 54);

Cela peut sembler être équivalent, sinon meilleur, mais en fait, il a introduit une grande non-uniformité en raison du biais dans l'arrondi des nombres à virgule flottante: il était trois fois plus probable que le faible -le bit d'ordre de la signification serait 0 que ce serait 1 ! Cette non-uniformité n'a probablement pas beaucoup d'importance dans la pratique, mais nous visons la perfection.]

Cette note existe depuis Java 5 au moins (les documents pour Java <= 1.4 sont derrière un mur de connexion, trop paresseux pour vérifier). C'est intéressant, car le problème existe toujours apparemment même en Java 8. Peut-être que la version "fixe" n'a jamais été testée?

48
Thomas

Ce résultat ne m'étonne pas étant donné la représentation des nombres à virgule flottante. Supposons que nous avions un type à virgule flottante très court avec seulement 4 bits de précision. Si nous devions générer un nombre aléatoire entre 0 et 1, distribué uniformément, il y aurait 16 valeurs possibles:

0.0000
0.0001
0.0010
0.0011
0.0100
...
0.1110
0.1111

Si c'est à cela qu'ils ressemblaient dans la machine, vous pouvez tester le bit de faible poids pour obtenir une distribution 50/50. Cependant, les flotteurs IEEE sont représentés comme une puissance de 2 fois une mantisse; un champ dans le flotteur est la puissance de 2 (plus un décalage fixe). La puissance de 2 est choisie pour que la partie "mantisse" soit toujours un nombre> = 1.0 et <2.0. Cela signifie que, en fait, les nombres autres que 0.0000 serait représenté comme ceci:

0.0001 = 2^(-4) x 1.000
0.0010 = 2^(-3) x 1.000
0.0011 = 2^(-3) x 1.100
0.0100 = 2^(-2) x 1.000
... 
0.0111 = 2^(-2) x 1.110
0.1000 = 2^(-1) x 1.000
0.1001 = 2^(-1) x 1.001
...
0.1110 = 2^(-1) x 1.110
0.1111 = 2^(-1) x 1.111

(Le 1 avant le point binaire est une valeur implicite; pour les flottants 32 et 64 bits, aucun bit n'est réellement alloué pour contenir ce 1.)

Mais regarder ce qui précède devrait montrer pourquoi, si vous convertissez la représentation en bits et regardez le bit bas, vous n'obtiendrez aucun 75% du temps. Cela est dû à toutes les valeurs inférieures à 0,5 (binaire 0.1000), qui est la moitié des valeurs possibles, avec leur mantisse décalée, ce qui fait apparaître 0 dans le bit bas. La situation est essentiellement la même lorsque la mantisse a 52 bits (non compris le 1 implicite) comme le fait un double.

(En fait, comme @sneftel l'a suggéré dans un commentaire, nous pourrions inclure plus de 16 valeurs possibles dans la distribution, en générant:

0.0001000 with probability 1/128
0.0001001 with probability 1/128
...
0.0001111 with probability 1/128
0.001000  with probability 1/64
0.001001  with probability 1/64
...
0.01111   with probability 1/32 
0.1000    with probability 1/16
0.1001    with probability 1/16
...
0.1110    with probability 1/16
0.1111    with probability 1/16

Mais je ne suis pas sûr que ce soit le type de distribution auquel la plupart des programmeurs s'attendent, donc cela ne vaut probablement pas la peine. De plus, cela ne vous rapporte pas beaucoup lorsque les valeurs sont utilisées pour générer des entiers, comme le sont souvent les valeurs aléatoires à virgule flottante.)

33
ajb