Por que esse valor aleatório tem uma distribuição 25/75 em vez de 50/50?


139

Edit: Então, basicamente, o que estou tentando escrever é um hash de 1 bit double.

Quero mapear uma doublepara trueou falsecom uma chance de 50/50. Para isso, escrevi um código que seleciona alguns números aleatórios (como exemplo, quero usar isso em dados com regularidades e ainda obter um resultado 50/50) , yverifico seu último bit e incrementa se for 1 ou nse for 0

No entanto, esse código resulta constantemente em 25% ye 75% n. Por que não é 50/50? E por que uma distribuição tão estranha, mas direta (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);
    }
}

Exemplo de saída:

250167 749833

43
Eu realmente espero que a resposta seja algo fascinante sobre a geração aleatória de variáveis ​​de ponto flutuante, em vez de "LCG tem baixa entropia nos bits baixos".
— Sneftel

4
Estou muito curioso, qual é o propósito de um "hash de 1 bit para dobro"? Eu realmente não consigo pensar em nenhuma aplicação legítima de tal exigência.
— corsiKa

3
@corsiKa Em cálculos de geometria, geralmente há dois casos que procuramos escolher entre duas respostas possíveis (por exemplo, é o ponto à esquerda ou à direita da linha?) e, às vezes, apresenta o terceiro caso degenerado (o ponto é diretamente na linha), mas você só tem duas respostas disponíveis; portanto, você deve escolher pseudo-aleatoriamente uma das respostas disponíveis nesse caso. A melhor maneira de pensar é usar um hash de 1 bit de um dos valores duplos dados (lembre-se, esses são cálculos de geometria, portanto, há dobras em todo o lugar).
— gvlasov

2
@corsiKa (comentário dividido em dois porque é muito longo) Poderíamos começar com algo mais simples doubleValue % 1 > 0.5, mas isso seria muito grosseiro, pois pode introduzir regularidades visíveis em alguns casos (todos os valores estão dentro do intervalo 1). Se isso é muito granular, provavelmente deveríamos tentar intervalos menores, como doubleValue % 1e-10 > 0.5e-10? Bem, sim. E tomar apenas o último pedaço como um hash de a doubleé o que acontece quando você segue essa abordagem até o fim, com o módulo menos possível.
— gvlasov

1
O @kmote ainda terá o bit menos significativo e o outro não o compensa - na verdade, ele também é direcionado para zero (mas menos), pelo mesmo motivo. Portanto, a distribuição seria de cerca de 50, 12,5, 25, 12,5. (lastbit & 3) == 0iria funcionar, por mais estranho que seja.
— Harold

Respostas:


165

Porque nextDouble funciona assim: ( fonte )

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

next(x)faz xbits aleatórios.

Agora, por que isso importa? Como cerca da metade dos números gerados pela primeira parte (antes da divisão) é menor 1L << 52e, portanto, seu significado não preenche inteiramente os 53 bits que ele poderia preencher, significando que o bit menos significativo do significando é sempre zero para eles.


Devido à quantidade de atenção que está recebendo, aqui estão algumas explicações extras sobre o que doublerealmente é um Java (e muitas outras linguagens) e por que isso importava nesta pergunta.

Basicamente, a doubleaparência é a seguinte: ( fonte )

layout duplo

Um detalhe muito importante que não é visível nesta figura é que os números são "normalizados" 1, de modo que a fração de 53 bits começa com 1 (escolhendo o expoente tal que é assim), que 1 é omitido. É por isso que a imagem mostra 52 bits para a fração (significando), mas existem efetivamente 53 bits nela.

A normalização significa que, se o código nextDoubledo 53º bit estiver definido, esse bit será o líder implícito 1 e desaparecerá, e os outros 52 bits serão copiados literalmente para o significando do resultado double. Se esse bit não estiver definido, no entanto, os bits restantes deverão ser deslocados para a esquerda até que sejam definidos.

Em média, metade dos números gerados se enquadra no caso em que o significando não foi alterado para a esquerda (e cerca de metade deles tem 0 como o bit menos significativo), e a outra metade é alterada em pelo menos 1 (ou é apenas completamente zero), então o bit menos significativo é sempre 0.

1: nem sempre, claramente isso não pode ser feito para zero, que não possui o mais alto 1. Esses números são chamados de números anormais ou subnormais, consulte a Wikipedia: número denormal .


16
Viva! Apenas o que eu estava esperando.
— Sneftel

3
@Matt Presumivelmente, é uma otimização de velocidade. A alternativa seria gerar o expoente com uma distribuição geométrica e depois a mantissa separadamente.
— Sneftel

7
@ Matt: Defina "melhor". random.nextDouble()normalmente é o "melhor" caminho para o que se destina, mas a maioria das pessoas não está tentando produzir um hash de 1 bit a partir do dobro aleatório. Você está procurando distribuição uniforme, resistência à criptoanálise ou o quê?
— precisa

1
Essa resposta sugere que, se OP tivesse multiplicado o número aleatório por 2 ^ 53 e verificado se o número inteiro resultante era ímpar, haveria uma distribuição 50/50.
— rici

4
@ The111 diz aqui que nextdeve retornar um int, para que possa ter apenas até 32 bits de qualquer maneira #
— harold

48

Dos documentos :

O método nextDouble é implementado pela classe Random como se por:

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

Mas também afirma o seguinte (ênfase minha):

[Nas versões anteriores do Java, o resultado era calculado incorretamente como:

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

Isso pode parecer equivalente, se não melhor, mas, de fato, introduziu uma grande não uniformidade devido ao viés no arredondamento dos números de ponto flutuante: era três vezes mais provável que o bit de ordem inferior do significando fosse 0. do que isso seria 1 ! Essa não uniformidade provavelmente não importa muito na prática, mas buscamos a perfeição.]

Esta observação existe desde o Java 5 pelo menos (os documentos para Java <= 1.4 estão atrás de uma parede de login, com preguiça de verificar). Isso é interessante, porque o problema aparentemente ainda existe até no Java 8. Talvez a versão "fixa" nunca tenha sido testada?


4
Estranho. Acabei de reproduzir isso no Java 8. #
— aioobe 23/12/14

1
Agora isso é interessante, porque acabei de argumentar que o viés ainda se aplica ao novo método. Estou errado?
— Harold

3
@harold: Não, acho que você está certo e quem tentou corrigir esse viés pode ter cometido um erro.
— Thomas

6
@harold Hora de enviar um email para os caras do Java.
— Daniel

8
"Talvez a versão fixa nunca tenha sido testada?" Na verdade, ao reler isso, acho que o documento tratava de um problema diferente. Observe que ele menciona o arredondamento , o que sugere que eles não consideraram o problema "três vezes mais provável" diretamente, mas sim que isso leva a uma distribuição não uniforme quando os valores são arredondados . Observe que, na minha resposta, os valores que listo são distribuídos uniformemente, mas o bit de ordem inferior, representado no formato IEEE, não é uniforme. Eu acho que o problema que eles resolveram tinha a ver com a uniformidade geral, não com a uniformidade do bit baixo.
— ajb

33

Esse resultado não me surpreende, dado que os números de ponto flutuante são representados. Suponhamos que tivéssemos um tipo de ponto flutuante muito curto, com apenas 4 bits de precisão. Se gerássemos um número aleatório entre 0 e 1, distribuído uniformemente, haveria 16 valores possíveis:

0.0000
0.0001
0.0010
0.0011
0.0100
...
0.1110
0.1111

Se era assim que pareciam na máquina, você poderia testar o bit de ordem baixa para obter uma distribuição 50/50. No entanto, os flutuadores IEEE são representados como uma potência de 2 vezes uma mantissa; um campo no flutuador é a potência de 2 (mais um deslocamento fixo). A potência de 2 é selecionada para que a parte "mantissa" seja sempre um número> = 1.0 e <2.0. Isso significa que, na verdade, outros números que 0.0000não seriam representados assim:

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

( 1Antes do ponto binário é um valor implícito; para flutuações de 32 e 64 bits, nenhum bit é realmente alocado para reter isso 1.)

Mas observar o acima exposto deve demonstrar por que, se você converter a representação em bits e observar o bit baixo, receberá zero 75% do tempo. Isso ocorre devido a todos os valores menores que 0,5 (binários 0.1000), que são metade dos valores possíveis, com as mantissas deslocadas, fazendo com que 0 apareça no bit mais baixo. A situação é essencialmente a mesma quando a mantissa possui 52 bits (não incluindo o 1 implícito) como doublefaz.

(Na verdade, como @sneftel sugeriu em um comentário, poderíamos incluir mais de 16 valores possíveis na distribuição, gerando:

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

Mas não tenho certeza de que seja o tipo de distribuição que a maioria dos programadores esperaria, por isso provavelmente não vale a pena. Além disso, não ganha muito quando os valores são usados ​​para gerar números inteiros, como costumam ser os valores aleatórios de ponto flutuante.)


5
Usar ponto flutuante para obter bits aleatórios / bytes / qualquer coisa me faz estremecer de qualquer maneira. Mesmo para distribuições aleatórias entre 0 e n, temos melhores alternativas (veja arc4random_uniform) do que random * n…
— mirabilos
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.