Как преобразовать количество псевдослучайных битов в статистически случайное значение с плавающей запятой от 0 до 1?

У меня есть реализация алгоритма xoshiro256** PRNG в приложении, которое я пишу на C#. Это прекрасно работает для создания псевдослучайных значений между 0 и UInt64.MaxValue, но я попал в точку, где мне нужно псевдослучайное значение двойной точности с плавающей запятой между 0 включительно и 1 исключая (то есть между 0 и 0,99999999...).

Я знаю, что могу использовать BitConverter для «грубого преобразования» из ulong в double, но я совершенно уверен, что это даст мне значения, которые находятся где-то между количеством миль в длине планки и количеством кубических миллиметров в Вселенная и их негативы, а также возможность получения бесконечности, отрицательной бесконечности, отрицательного 0 и NaN. Это немного амбициозно (читай: совершенно не подходит) для того, что я пытаюсь сделать, поэтому я надеюсь найти какой-то способ управления выводом, который я получаю, чтобы его можно было использовать для работы с процентными шансами вещей.

Я недостаточно знаю, как работают значения с плавающей запятой IEEE, чтобы точно знать, какие биты куда поместить, чтобы получить значения, которые я ищу. Я думаю (и ошибаюсь), что могу просто сдвинуть вправо ulong на 12 бит (таким образом превратив 52 верхних бита в нижние 52 бита), добавить 2^52 (установив нижний бит экспоненты на 1) и затем BitConverter получившуюся кашу в двойную. Что-то вроде этого:

public static double DoubleFromRand(ulong rand) {
    ulong resultUL = rand >> 12;
    resultUL += ULongPow(2UL, 52UL); // adapted from https://stackoverflow.com/a/383596/19474638
    return BitConverter.ToDouble(BitConverter.GetBytes(resultUL), 0);
}

public static ulong ULongPow(ulong x, ulong pow) {
    ulong ret = 1UL;
    while (pow != 0UL) {
        if ((pow & 1UL) == 1UL) {
            ret *= x;
        }
        x *= x;
        pow >>= 1;
    }
    return ret;
}

Если бы это сработало, я бы ожидал, что передача UInt64.MaxValue в this даст мне какое-то значение, очень близкое к 1, но не совсем там. То, что я на самом деле получаю от приведенного выше алгоритма, — это какое-то очень странное маленькое значение.

Любая подсказка, что делать здесь? Использование C# 4.0 в Mono 6.8.0.105, в 64-разрядной версии Raspberry Pi OS, в Raspberry Pi 4 Model B. Обратите внимание, что меня не интересует использование «настоящего» .NET.

(См.: Как преобразовать uint64_t в двойное/плавающее число между 0 и 1 с максимальной точностью (C++)? Это не отвечает на мой вопрос, поскольку оно преобразует ulong в двойное число с помощью математических операций, что, я считаю, не не гарантируют статистической случайности результата.

Также ответы, касающиеся того, как использовать встроенные случайные функции С#, бесполезны, поскольку я хочу использовать свою собственную реализацию PRNG. Мне также не помогают ответы о том, как генерировать случайные 64-битные числа с плавающей запятой. Я хочу, в частности, взять некоторое количество случайно сгенерированных битов и принудительно преобразовать их в число с плавающей запятой, которое будет находиться между 0 и 1. Вопрос в том, как выполнить преобразование, а не в генерации случайных вещей.)

Попробуйте double DoubleFromRand(ulong rand) { return (double)(rand >> 12)/(1ul << 52); }.

chux - Reinstate Monica 11.04.2023 23:29

Не пытайтесь сфабриковать представление о double. Просто возьмите 52 случайных бита в виде двоичного числа, преобразуйте их в double (как простое преобразование целого числа в double) и умножьте на 2 ^ −52, чтобы масштабировать до [0, 1). Это то, что делает приведенный выше код chux, и это нормально для большинства приложений.

Eric Postpischil 12.04.2023 01:01

@EricPostpischil: На самом деле, вы можете получить 53 бита из-за скрытого битового трюка IEEE 754, дающего вам дополнительную точность.

dan04 12.04.2023 01:47

@EricPostpischil Хм, похоже, что мой код и код chux делают одно и то же, но код chux явно более переносим. Тогда это может сработать.

ArrayBolt3 12.04.2023 01:53

@dan04: Да, 53 года.

Eric Postpischil 12.04.2023 13:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
67
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Я думаю, что понял это после некоторых экспериментов.

Поле экспоненты использует значение 1023 для представления 0 — большие значения являются положительными, меньшие — отрицательными. Таким образом, значение 1023 в показателе степени позволяет интерпретировать остальную часть поля мантиссы "как есть", т.е. - все после десятичной точки в числе.

Это приводит к тому, что UInt64.MaxValue сопоставляется с ~ 1,999, что почти равно 2 (и оно будет напечатано как 2, если вы используете Console.WriteLine() без установки количества отображаемых цифр). UInt64.MaxValue / 2 сопоставляется с 1.5, UInt64.MaxValue / 4 сопоставляется с 1.25.

Это здорово, за исключением того факта, что ко всем этим значениям прибавлено 1,0. К счастью, похоже, что я могу избавиться от этого, просто вычитая 1,0 из окончательного результата. На первый взгляд кажется, что это работает (UInt64.MaxValue / 4 теперь соответствует 0,25). Я не уверен, что это вызовет ошибки с меньшими значениями, но, надеюсь, этого не произойдет.

Окончательный, я думаю, рабочий код:

public static double DoubleFromRand(ulong rand) {
    ulong resultUL = rand >> 12;
    resultUL += ((ulong)1023 << 52);
    return BitConverter.ToDouble(BitConverter.GetBytes(resultUL), 0) - 1.0;
}

Обратите внимание, что этот беспорядок, вероятно, не переносим на все процессоры - согласно Википедии (где я получил всю информацию о математике с плавающей запятой IEEE754), некоторые процессоры делают действительно странные вещи с порядком байтов при работе с числами с плавающей запятой, поэтому в зависимости от вашего процессора, это может дать вам серьезно зашифрованные результаты. Но пока на моем конкретном 64-битном процессоре ARM это работает.

Ответ принят как подходящий

Общий подход к преобразованию «битов в статистически случайное значение с плавающей запятой от 0 до 1?» начинается с преобразования таких битов в значение между [1,0 ... 2,0) и последующего вычитания 1,0.

Числа с плавающей запятой распределяются линейно между последовательными степенями двойки.

При обычном кодировании double существует 2 52 значений между большинством степеней 2. Итак, возьмите 64-битные случайные данные и уменьшите до 52 и сформируйте случайное число [1.0... 2.0) (код OP делает это с различные битовые манипуляции), затем вычтите 1,0.

Это подход OP к самоответу .

Когда все сделано правильно, он обеспечивает быстрый подход, но имеет дополнительные проблемы с переносимостью по сравнению со следующим.


Другой подход заключается в использовании 52 из 64 предоставленных случайных битов, формировании значения [0...252-1] и делении на 252. Ожидается, что сдвиг, преобразование из ulong в double и деление будут точными и не зависят от режима округления. Таким образом, мы предотвращаем появление статистической погрешности, но формируем только 252 разных значения.

// Something like
double DoubleFromRand(ulong rand) { 
  return (double)(rand >> 12)/(1ul << 52);
}

Мы можем даже использовать на 1 бит больше @dan04 и по-прежнему иметь «сдвиг, преобразование из ulong в double и деление, как ожидается, будут точными и не зависят от режима округления». Использование более 53 бит теряет это свойство.

double DoubleFromRand53(ulong rand) {
  return (double)(rand >> 11)/(1ul << 53);
}

Другой подход, не показанный, будет использовать 64 из 64 бит и формировать значение [0...264-1] и делить на 264. К сожалению, это влечет за собой округление целого числа для преобразования и большее округление при делении, что приводит к нежелательному смещению и диапазону (может быть возвращено 1,0), но дает ближе к 264 различным значениям.

Как заметил dan04, мы можем использовать 53 бита. В [½, 1) можно представить 2^52 числа, и мы можем использовать 2^52 числа с тем же интервалом в [0, ½], что дает 2^53 для [0, 1).

Eric Postpischil 12.04.2023 13:30

@EricPostpischil Да, я тоже сейчас об этом вспоминаю. Ответ изменен.

chux - Reinstate Monica 12.04.2023 14:19

Другие вопросы по теме