Чому це випадкове значення має розподіл 25/75 замість 50/50?


139

Редагувати: В основному те, що я намагаюся написати, це 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

43
Я дуже сподіваюся, що відповідь є чимось захоплюючим щодо випадкового генерування змінних з плаваючою комою, а не "LCG має низьку ентропію в низьких бітах".
Снефтель

4
Мені дуже цікаво, яка мета "1 біт-хеш для подвійних"? Я серйозно не можу придумати жодного законного застосування такої вимоги.
corsiKa

3
@corsiKa У обчисленнях геометрії часто є два випадки, які ми шукаємо вибрати з двох можливих відповідей (наприклад, вказівник ліворуч або праворуч від рядка?), а іноді вводиться і третій, вироджений випадок (точка є прямо у рядку), але у вас є лише дві доступні відповіді, тому вам доведеться псевдовипадково вибрати одну з доступних відповідей у ​​цьому випадку. Найкращий спосіб, який я міг би придумати, - це взяти 1-бітний хеш одного із заданих подвійних значень (пам’ятайте, це обчислення геометрії, тому всюди є подвії).
гвласов

2
@corsiKa (коментар розділений на два, тому що він занадто довгий) Ми можемо почати з чогось більш простого типу doubleValue % 1 > 0.5, але це було б занадто грубозернистим, оскільки воно може ввести видимі закономірності в деяких випадках (усі значення знаходяться в межах 1 довжини). Якщо це занадто грубозерниста, то, мабуть, ми повинні спробувати менші діапазони, як doubleValue % 1e-10 > 0.5e-10? Ну так. І якщо взяти останній шматочок як хеш а, doubleце те, що відбувається, коли дотримуватися цього підходу до кінця, з найменшим можливим модулем.
гвласов

1
@kmote, тоді у вас все ще буде сильно упереджений найменш значущий біт, а інший біт не компенсує це - адже він також упереджений до нуля (але менше), саме з тієї ж причини. Тож розподіл буде приблизно 50, 12,5, 25, 12,5. (lastbit & 3) == 0Працює хоч як не дивно.
Гарольд

Відповіді:


165

Тому що nextDouble працює так: ( джерело )

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

next(x)робить xвипадкові біти.

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


Через кількість уваги, яку це привертає, ось додаткове пояснення того, як doubleнасправді виглядає a на Java (та багатьох інших мовах) і чому це має значення в цьому питанні.

В основному, doubleвиглядає приблизно так: ( джерело )

подвійний макет

Дуже важливою деталлю, яка не видно на цій фотографії, є те, що цифри "нормалізуються" 1 таким чином, що 53-бітний дріб починається з 1 (вибираючи показник таким, щоб він був таким), що 1 потім опускається. Ось чому на малюнку показано 52 біти для дробу (significand), але в ньому ефективно 53 біти.

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

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

1: не завжди, очевидно, що це не можна зробити для нуля, який не має найвищого числа 1. Ці числа називаються деннормальними або субнормальними числами, див. Вікіпедія: деннормальне число .


16
Ура! Тільки те, на що я сподівався.
Снефтель

3
@Matt Імовірно, це оптимізація швидкості. Альтернативою було б генерування експонента з геометричним розподілом, а потім мантісса окремо.
Sneftel

7
@Matt: визначте "найкраще". random.nextDouble()зазвичай є "найкращим" способом для того, для чого він призначений, але більшість людей не намагаються створити 1-бітний хеш із свого випадкового подвійного. Ви шукаєте рівномірного розподілу, стійкості до криптоаналізу чи чого?
Стриптинг-воїн

1
Ця відповідь говорить про те, що якби ОП помножило випадкове число на 2 ^ 53 і перевірило, чи отримане ціле число непарне, було б розподіл 50/50.
rici

4
@ The111 тут сказано, що nextпотрібно повернути int, тому в будь-якому випадку він може мати лише 32 біти
harold

48

З документів :

Метод 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. Можливо, "виправлена" версія ніколи не була протестована?


4
Дивно. Я щойно відтворив це на Java 8.
aioobe

1
Тепер це цікаво, адже я просто стверджував, що упередженість все ж стосується нового методу. Я помиляюся?
Гарольд

3
@harold: Ні, я думаю, ти маєш рацію, і хто б не намагався виправити цю упередженість, можливо, допустив помилку.
Томас

6
@harold Час для надсилання електронного листа хлопцям Java.
Даніель

8
"Можливо, виправлена ​​версія ніколи не була протестована?" Насправді, перечитуючи це, я думаю, що доктор говорив про іншу проблему. Зауважимо, що в ньому згадується округлення , що говорить про те, що вони не вважали проблему "три рази ймовірнішою" безпосередньо, а швидше, що це призводить до нерівномірного розподілу, коли значення округляються . Зауважте, що у своїй відповіді значення, які я перераховую, розподіляються рівномірно, але біт низького порядку, представлений у форматі IEEE, не є рівномірним. Я думаю, що проблема, яку вони вирішили, стосувалася загальної рівномірності, а не рівномірності низького біта.
ajb

33

Цей результат мене не дивує, враховуючи, як представлені числа з плаваючою комою. Припустимо, у нас був дуже короткий тип з плаваючою комою з лише 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

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


5
Використання плаваючої точки для отримання випадкових біт / байтів / нічого не змушує мене здригатися. Навіть для випадкових розподілів між 0 та n у нас є кращі альтернативи (дивіться на arc4random_uniform), ніж випадкові * n…
mirabilos
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.