Editar: Entonces, básicamente, lo que estoy tratando de escribir es un hash de 1 bit double
.
Quiero mapear un double
to true
o false
con una probabilidad de 50/50. Para eso escribí un código que selecciona algunos números aleatorios (solo como ejemplo, quiero usar esto en datos con regularidades y aún así obtener un resultado 50/50) , verifica su último bit e incrementos y
si es 1, o n
si es 0.
Sin embargo, este código constantemente produce 25% y
y 75% n
. ¿Por qué no es 50/50? ¿Y por qué una distribución tan extraña pero directa (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);
}
}
Salida de ejemplo:
250167 749833
java
random
double
bit-manipulation
probability
gvlasov
fuente
fuente
doubleValue % 1 > 0.5
, pero eso sería demasiado grano ya que puede introducir regularidades visibles en algunos casos (todos los valores están dentro del rango de longitud 1). Si eso es demasiado de grano grueso, ¿deberíamos intentar rangos más pequeños, comodoubleValue % 1e-10 > 0.5e-10
? Bueno, sí. Y tomar solo el último bit como hash de adouble
es lo que sucede cuando sigues este enfoque hasta el final, con el mínimo módulo posible.(lastbit & 3) == 0
funcionaría sin embargo, por extraño que sea.Respuestas:
Porque nextDouble funciona así: ( fuente )
next(x)
hacex
bits al azar.Ahora, ¿por qué importa esto? Debido a que aproximadamente la mitad de los números generados por la primera parte (antes de la división) son menores
1L << 52
y, por lo tanto, su significado no llena por completo los 53 bits que podría llenar, lo que significa que el bit menos significativo del significado es siempre cero para esos.Debido a la cantidad de atención que está recibiendo, aquí hay una explicación adicional de cómo
double
se ve realmente un Java (y muchos otros lenguajes) y por qué importaba en esta pregunta.Básicamente, se
double
ve así: ( fuente )Un detalle muy importante que no se ve en esta imagen es que los números están "normalizados" 1 de tal manera que la fracción de 53 bits comienza con un 1 (al elegir el exponente de modo que sea así), que luego se omite 1. Es por eso que la imagen muestra 52 bits para la fracción (significado), pero efectivamente tiene 53 bits.
La normalización significa que si en el código para
nextDouble
el bit 53 está establecido, ese bit es el primer 1 implícito y desaparece, y los otros 52 bits se copian literalmente al significado del resultadodouble
. Sin embargo, si ese bit no está establecido, los bits restantes deben desplazarse hacia la izquierda hasta que se establezca.En promedio, la mitad de los números generados caen en el caso en el que el significado no se desplazó a la izquierda (y aproximadamente la mitad tiene un 0 como su bit menos significativo), y la otra mitad se desplaza por al menos 1 (o es simplemente completamente cero) por lo que su bit menos significativo siempre es 0.
1: no siempre, claramente no se puede hacer para cero, que no tiene el más alto 1. Estos números se llaman números denormales o subnormales, vea wikipedia: número denormal .
fuente
random.nextDouble()
suele ser la "mejor" forma para lo que está destinado, pero la mayoría de las personas no intentan producir un hash de 1 bit a partir de su doble aleatorio. ¿Está buscando una distribución uniforme, resistencia al criptoanálisis o qué?next
debe devolver unint
, por lo que solo puede tener hasta 32 bits de todos modosDe los documentos :
Pero también establece lo siguiente (énfasis mío):
Esta nota ha estado allí desde Java 5 al menos (los documentos para Java <= 1.4 están detrás de un inicio de sesión, demasiado flojo para verificar). Esto es interesante, porque el problema aparentemente todavía existe incluso en Java 8. ¿Quizás la versión "fija" nunca fue probada?
fuente
Este resultado no me sorprende dado cómo se representan los números de punto flotante. Supongamos que tenemos un tipo de coma flotante muy corto con solo 4 bits de precisión. Si generamos un número aleatorio entre 0 y 1, distribuido uniformemente, habría 16 valores posibles:
Si así es como se veían en la máquina, podría probar el bit de bajo orden para obtener una distribución 50/50. Sin embargo, los flotadores IEEE se representan como una potencia de 2 veces una mantisa; Un campo en el flotador es la potencia de 2 (más un desplazamiento fijo). La potencia de 2 se selecciona de modo que la parte "mantisa" sea siempre un número> = 1.0 y <2.0. Esto significa que, en efecto, los números que
0.0000
no sean representados así:(El
1
antes del punto binario es un valor implícito; para flotantes de 32 y 64 bits, en realidad no se asigna ningún bit para mantener esto1
).Pero mirar lo anterior debería demostrar por qué, si convierte la representación en bits y observa el bit bajo, obtendrá cero el 75% del tiempo. Esto se debe a todos los valores inferiores a 0.5 (binario
0.1000
), que es la mitad de los valores posibles, ya que sus mantisias se desplazaron y causaron que 0 aparezca en el bit bajo. La situación es esencialmente la misma cuando la mantisa tiene 52 bits (sin incluir el 1 implícito) como lodouble
hace.(En realidad, como sugirió @sneftel en un comentario, podríamos incluir más de 16 valores posibles en la distribución, generando:
Pero no estoy seguro de que sea el tipo de distribución que la mayoría de los programadores esperarían, por lo que probablemente no valga la pena. Además, no gana mucho cuando los valores se utilizan para generar enteros, como suelen ser los valores aleatorios de punto flotante).
fuente