Почему это случайное значение имеет распределение 25/75 вместо 50/50?

Изменить:. Так что в основном то, что я пытаюсь написать, это хэш 1 бит для double.

Я хочу сопоставить double с true или false с вероятностью 50/50. Для этого я написал код, который выбирает некоторые случайные числа (как пример, я хочу использовать это на данных с регулярностью и все равно получить результат 50/50), проверяет их последний бит и увеличивает y, если он равен 1, или n, если оно равно 0.

Однако этот код постоянно приводит к 25% y и 75% n. Почему это не 50/50? И почему такое странное, но прямолинейное (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);
    }
}

Пример вывода:

250167 749833

Ответ 1

Поскольку nextDouble работает так: (source)

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

next(x) делает x случайные биты.

Теперь почему это имеет значение? Поскольку около половины чисел, сгенерированных первой частью (перед делением), меньше 1L << 52, и поэтому их значение не полностью заполняет 53 бита, которые он может заполнить, то есть младший значащий бит значения всегда всегда равен нулю для тех.


Из-за большого количества внимания, которое это получает, вот какое-то дополнительное объяснение того, что на самом деле выглядит double на Java (и многих других языках) и почему это важно в этом вопросе.

В принципе, double выглядит следующим образом: (источник)

double layout

Очень важная деталь, которая не видна на этом рисунке, состоит в том, что числа "нормализованы" 1 так что 53-битная дроби начинается с 1 (путем выбора показателя, такого, что это так), что 1 затем опускается. Вот почему на рисунке показано 52 бита для фракции (значимо), но в ней есть 53 бит.

Нормализация означает, что если в коде для nextDouble установлен 53-й бит, этот бит является неявным ведущим 1, и он уходит, а остальные 52 бита копируются буквально в значение полученного результата double, Если этот бит не установлен, остальные биты должны быть сдвинуты влево до тех пор, пока он не будет установлен.

В среднем половина сгенерированных чисел попадает в случай, когда значение не было сдвинуто слева вообще (и примерно половина из них имеет 0 как младший значащий бит), а другая половина сдвигается по меньшей мере на 1 (или просто полностью равна нулю), поэтому их младший значащий бит всегда равен 0.

1: не всегда, очевидно, это невозможно сделать для нуля, который не имеет наивысшего значения 1. Эти числа называются денормальными или субнормальными числами, см. wikipedia: denormal номер.

Ответ 2

Из docs:

Метод nextDouble реализуется классом Random, как если бы:

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

Но в нем также говорится следующее (основное внимание):

[В ранних версиях Java результат был неправильно рассчитан как:

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

Это может показаться эквивалентным, если не лучше, но на самом деле он ввел большую неоднородность из-за смещения в округлении чисел с плавающей запятой: было в три раза вероятнее, что младший бит от значения будет 0, чем 1! Эта неоднородность, вероятно, практически не имеет большого значения на практике, но мы стремимся к совершенству.]

Эта заметка была там, по крайней мере, с тех пор, как Java 5 (документы для Java <= 1.4 находятся позади входа в систему, слишком ленив для проверки). Это интересно, потому что проблема, по-видимому, все еще существует даже в Java 8. Возможно, "исправленная" версия никогда не тестировалась?

Ответ 3

Этот результат не удивляет меня тем, как представлены числа с плавающей запятой. Предположим, что у нас был очень короткий тип с плавающей точкой с точностью до 4 бит. Если бы мы генерировали случайное число между 0 и 1, распределенное равномерно, было бы 16 возможных значений:

0.0000
0.0001
0.0010
0.0011
0.0100
...
0.1110
0.1111

Если это так, как они выглядели в машине, вы можете протестировать бит младшего разряда, чтобы получить 50/50 дистрибутив. Однако поплавки IEEE представлены как мощность в 2 раза больше мантиссы; одно поле в поплавке - это значение 2 (плюс фиксированное смещение). Мощность 2 выбирается так, что часть "мантисса" всегда равна числу >= 1.0 и < 2,0. Это означает, что в действительности числа, отличные от 0.0000, будут представлены следующим образом:

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

(1 до того, как двоичная точка является подразумеваемым значением; для 32- и 64-битных поплавков бит фактически не назначен для хранения этого 1.)

Но если посмотреть на вышеизложенное, следует продемонстрировать, почему, если вы преобразуете представление в биты и посмотрите на бит, вы получите нулевой 75% времени. Это происходит из-за всех значений менее 0,5 (двоичный 0.1000), что является половиной возможных значений, с переводом их мантисса, в результате чего 0 появляется в младшем бите. Ситуация по существу такая же, когда мантисса имеет 52 бита (не включая подразумеваемый 1), как это делает double.

(На самом деле, как пояснил @sneftel в комментарии, мы могли бы включить более 16 возможных значений в распределение, создав:

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

Но я не уверен, что это будет распространяться большинство программистов, поэтому, вероятно, это не стоит. Плюс это не принесет вам многого, когда значения используются для генерации целых чисел, поскольку часто используются значения с плавающей запятой.)