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
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 )
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 .
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?
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.)