Perché questo valore casuale ha una distribuzione 25/75 anziché 50/50?


139

Modifica: Quindi sostanzialmente quello che sto cercando di scrivere è un hash a 1 bit per double.

Voglio mappare un doublea trueo falsecon una probabilità 50/50. Per questo ho scritto un codice che seleziona alcuni numeri casuali (solo come esempio, voglio usarlo su dati con regolarità e ottenere comunque un risultato 50/50) , controlla il loro ultimo bit e incrementa yse è 1, o nse lo è 0.

Tuttavia, questo codice risulta costantemente nel 25% ye nel 75% n. Perché non è 50/50? E perché una distribuzione così strana, ma diretta (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);
    }
}

Esempio di output:

250167 749833

43
Spero davvero che la risposta sia qualcosa di affascinante nella generazione casuale di variati in virgola mobile, piuttosto che "LCG ha una bassa entropia nei bit bassi".
Sneftel,

4
Sono molto curioso, qual è lo scopo di un "hash 1 bit per doppio"? Davvero non riesco a pensare a nessuna legittima applicazione di tale requisito.
corsiKa

3
@corsiKa Nei calcoli geometrici ci sono spesso due casi che stiamo cercando di scegliere tra due possibili risposte (ad es. è il punto a sinistra o a destra della linea?), e talvolta introduce il terzo caso degenerato (punto è proprio sulla linea), ma hai solo due risposte disponibili, quindi devi scegliere pseudorandomly una delle risposte disponibili in quel caso. Il modo migliore che mi viene in mente è di prendere un hash di 1 bit di uno dei doppi valori dati (ricorda, quelli sono calcoli geometrici, quindi ci sono doppi ovunque).
gvlasov,

2
@corsiKa (commento diviso in due perché troppo lungo) Potremmo iniziare con qualcosa di più semplice doubleValue % 1 > 0.5, ma sarebbe troppo grossolano poiché in alcuni casi può introdurre regolarità visibili (tutti i valori sono compresi nell'intervallo di lunghezza 1). Se è troppo a grana grossa, allora dovremmo probabilmente provare intervalli più piccoli, come doubleValue % 1e-10 > 0.5e-10? Beh si. E prendere solo l'ultimo bit come hash di a doubleè ciò che accade quando segui questo approccio fino alla fine, con il modulo il meno possibile.
gvlasov,

1
@kmote allora avresti ancora il bit meno significativo pesantemente distorto, e l'altro bit non lo compensa - in effetti è anche distorto verso zero (ma meno), esattamente per lo stesso motivo. Quindi la distribuzione sarebbe di circa 50, 12,5, 25, 12,5. (lastbit & 3) == 0funzionerebbe comunque, per quanto sia strano.
Harold,

Risposte:


165

Perché nextDouble funziona in questo modo: ( fonte )

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

next(x)fa xbit casuali.

Ora perché è importante? Perché circa la metà dei numeri generati dalla prima parte (prima della divisione) sono minori di 1L << 52, e quindi il loro significato non riempie interamente i 53 bit che potrebbe riempire, il che significa che il bit meno significativo del significato è sempre zero per quelli.


A causa della quantità di attenzione che sta ricevendo, ecco qualche spiegazione in più su doublecome sia realmente un in Java (e molte altre lingue) e perché sia ​​importante in questa domanda.

Fondamentalmente, doubleassomiglia a questo: ( fonte )

doppio layout

Un dettaglio molto importante non visibile in questa immagine è che i numeri sono "normalizzati" 1 in modo tale che la frazione di 53 bit inizi con un 1 (scegliendo l'esponente in modo che sia così), che 1 sia quindi omesso. Questo è il motivo per cui l'immagine mostra 52 bit per la frazione (significato), ma in realtà ci sono 53 bit.

La normalizzazione significa che se nel codice per nextDoubleil 53 ° bit è impostato, quel bit è il primo implicito 1 e scompare e gli altri 52 bit vengono copiati letteralmente nel significato del risultato double. Se quel bit non è impostato, i bit rimanenti devono essere spostati a sinistra fino a quando non viene impostato.

In media, metà dei numeri generati cade nel caso in cui il significato non sia stato spostato affatto a sinistra (e circa la metà ha uno 0 come bit meno significativo) e l'altra metà è spostata di almeno 1 (o è solo completamente zero) quindi il loro bit meno significativo è sempre 0.

1: non sempre, chiaramente non può essere fatto per zero, che non ha il massimo 1. Questi numeri sono chiamati numeri denormali o subnormali, vedi Wikipedia: numero denormale .


16
Evviva! Proprio quello che speravo.
Sneftel,

3
@Matt Presumibilmente è un'ottimizzazione della velocità. L'alternativa sarebbe quella di generare l'esponente con una distribuzione geometrica e quindi la mantissa separatamente.
Sneftel,

7
@Matt: definire "migliore". random.nextDouble()è in genere il modo "migliore" per quello a cui è destinato, ma la maggior parte delle persone non sta provando a produrre un hash a 1 bit dal loro doppio casuale. Stai cercando una distribuzione uniforme, resistenza alla crittoanalisi o cosa?
StriplingWarrior il

1
Questa risposta suggerisce che se OP avesse moltiplicato il numero casuale per 2 ^ 53 e verificato se il numero intero risultante fosse dispari, ci sarebbe stata una distribuzione 50/50.
rici,

4
@ The111 dice qui che nextdeve restituire un int, quindi può avere solo fino a 32 bit comunque
harold

48

Dai documenti :

Il metodo nextDouble è implementato dalla classe Random come se da:

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

Ma afferma anche quanto segue (sottolineatura mia):

[Nelle prime versioni di Java, il risultato era calcolato erroneamente come:

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

Questo potrebbe sembrare equivalente, se non migliore, ma in realtà ha introdotto una grande non uniformità a causa della propensione all'arrotondamento dei numeri in virgola mobile: era tre volte più probabile che il bit di significato basso del significato fosse 0 di quello sarebbe 1 ! Questa non uniformità probabilmente non ha molta importanza nella pratica, ma ci sforziamo di raggiungere la perfezione.]

Questa nota è presente almeno da Java 5 (i documenti per Java <= 1.4 sono dietro un loginwall, troppo pigro per essere verificato). Questo è interessante, perché a quanto pare il problema esiste ancora anche in Java 8. Forse la versione "riparata" non è mai stata testata?


4
Strano. L'ho appena riprodotto su Java 8.
aioobe il

1
Questo è interessante, perché ho appena sostenuto che il pregiudizio si applica ancora al nuovo metodo. Ho sbagliato?
Harold,

3
@harold: No, penso che tu abbia ragione e chiunque abbia provato a correggere questo pregiudizio potrebbe aver fatto un errore.
Thomas,

6
@harold È ora di inviare un'e-mail ai ragazzi di Java.
Daniel,

8
"Forse la versione fissa non è mai stata testata?" In realtà, rileggendolo, penso che il documento riguardasse un problema diverso. Si noti che menziona l' arrotondamento , il che suggerisce che non hanno considerato direttamente il problema "tre volte probabile", ma piuttosto che ciò porta a una distribuzione non uniforme quando i valori sono arrotondati . Si noti che nella mia risposta, i valori che ho elencato sono distribuiti uniformemente, ma i bit di ordine inferiore come rappresentato nel formato IEEE non sono uniformi. Penso che il problema che hanno risolto abbia avuto a che fare con l'uniformità generale, non con l'uniformità del bit basso.
Aj

33

Questo risultato non mi sorprende dato il modo in cui sono rappresentati i numeri in virgola mobile. Supponiamo di avere un tipo a virgola mobile molto breve con solo 4 bit di precisione. Se dovessimo generare un numero casuale compreso tra 0 e 1, distribuito uniformemente, ci sarebbero 16 possibili valori:

0.0000
0.0001
0.0010
0.0011
0.0100
...
0.1110
0.1111

Se è così che apparivano nella macchina, potresti testare il bit di ordine basso per ottenere una distribuzione 50/50. Tuttavia, i galleggianti IEEE sono rappresentati come una potenza di 2 volte una mantissa; un campo nel galleggiante è la potenza di 2 (più un offset fisso). La potenza di 2 è selezionata in modo che la parte "mantissa" sia sempre un numero> = 1.0 e <2.0. Ciò significa che, in effetti, i numeri diversi da quelli 0.0000sarebbero rappresentati in questo modo:

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

(Il punto 1prima del punto binario è un valore implicito; per i float a 32 e 64 bit, nessun bit è effettivamente assegnato per trattenerlo 1.)

Ma guardando quanto sopra dovrebbe dimostrare perché, se si converte la rappresentazione in bit e si osserva il bit basso, si otterrà zero il 75% delle volte. Ciò è dovuto a tutti i valori inferiori a 0,5 (binari 0.1000), che è la metà dei valori possibili, con le loro mantisse spostate sopra, facendo apparire 0 nel bit basso. La situazione è essenzialmente la stessa quando la mantissa ha 52 bit (escluso il 1 implicito) double.

(In realtà, come @sneftel suggerito in un commento, abbiamo potuto includere più di 16 possibili valori nella distribuzione, generando:

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

Ma non sono sicuro che sia il tipo di distribuzione che la maggior parte dei programmatori si aspetterebbe, quindi probabilmente non vale la pena. Inoltre, non ti guadagna molto quando i valori vengono utilizzati per generare numeri interi, come spesso accade per i valori a virgola mobile casuali.)


5
L'uso di virgola mobile per ottenere bit / byte casuali / qualsiasi cosa mi fa rabbrividire comunque. Anche per distribuzioni casuali tra 0 e n, abbiamo alternative migliori (guarda arc4random_uniform) rispetto a random * n…
mirabilos
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.