Производительность для рисования чисел из распределения Пуассона с низким средним - PullRequest
1 голос
/ 05 мая 2020

Чтобы получить случайное число из распределения Пуассона в C ++, обычно рекомендуется использовать

RNG_type rng;
std::poisson_distribution<size_t> d(1e-6);
auto r = d(rng);

При каждом вызове объекта std::poisson_distribution потребляется вся последовательность случайных битов ( например, 32 бита с std :: mt19937 , 64 бита для std :: mt19937_64 ). Мне кажется, что при таком низком среднем значении (mean = 1e-6) в подавляющем большинстве случаев достаточно всего нескольких битов, чтобы определить, что возвращаемое значение равно 0. Остальные биты затем можно кэшировать для дальнейшего использования.

Предполагая, что последовательность битов, установленная в истину, связана с высоким значением, возвращаемым из распределения Пуассона, при использовании среднего значения 1e-6 любая последовательность, не начинающаяся с 19 истинных значений, обязательно возвращает ноль! Действительно,

1 - 1/2^19 < P(0, 1e-6) < 1 - 1/2^20

, где P(n, r) обозначает вероятность получения n из распределения Пуассона со средним r. Алгоритм, который не тратит впустую биты, будет использовать один бит в половине случаев, два бита в четверть случаев, три бита в восьмую часть времени, ....

Есть ли алгоритм что может улучшить производительность, потребляя как можно меньше бит при рисовании чисел Пуассона? Есть ли другой способ улучшить производительность по сравнению с std::poisson_distribution, когда мы рассматриваем низкое среднее значение?


В ответ на комментарий @ Jarod42, который сказал:

Интересно, если использование меньшего количества бит не нарушит равновероятность ...

Я не думаю, что это нарушит равновероятность. В смутной попытке проверить это, я рассматриваю тот же вопрос с простым распределением Бернулли. Я выбираю истину с вероятностью 1/2^4 и выборку ложь с вероятностью 1 - 1/2^4. Функция drawWithoutWastingBits останавливается, как только видит в кэше истинное значение, а функция drawWastingBits потребляет 4 бита, какими бы ни были эти биты.

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <random>

bool drawWithoutWastingBits(std::vector<bool>& cache, size_t& cache_index)
{
    /* 
        Get a true with probability 1/2^4 (=1/16=0.0625) and a false otherwise
    */

    size_t nbTrues = 0;
    while (cache[cache_index])
    {
        ++nbTrues;
        ++cache_index;
        if (nbTrues == 4)
        {
            return true;
        }
    }
    ++cache_index;
    return false;
}


bool drawWastingBits(std::vector<bool>& cache, size_t& cache_index)
{
    /* 
        Get a true with probability 1/2^4 (=1/16=0.0625) and a false otherwise
    */

    bool isAnyTrue = false;
    for (size_t i = 0 ; i < 4; ++i)
    {
        if (cache[cache_index])
        {
            isAnyTrue = true;
        }
        ++cache_index;
    }
    return !isAnyTrue;
}

int main()
{
    /*
        Just cache a lot of bits in advance in `cache`. The same sequence of bits will be used by both function.
        I am just caching way enough bits to make sure they don't run out of bits below
        I made sure to have the same number of zeros and ones so that any deviation is caused by the methodology and not by the RNG
    */

    // Produce cache
    std::vector<bool> cache;
    size_t nbBitsToCache = 1e7;
    cache.reserve(nbBitsToCache);
    for (size_t i = 0 ; i < nbBitsToCache/2 ; ++i)
    {
        cache.push_back(false);
        cache.push_back(true);
    }
    // Shuffle cache
    {
        std::mt19937 mt(std::random_device{}());
        std::shuffle(cache.begin(), cache.end(), mt);
    }


    // Draw without wasting bits
    {
        size_t nbDraws = 1e6;
        size_t cache_index = 0;
        std::pair<size_t, size_t> outcomes = {0,0};
        for (size_t r = 0 ; r < nbDraws ; ++r)
        {
            drawWithoutWastingBits(cache, cache_index) ? ++outcomes.first : ++outcomes.second;
            assert(cache_index <= cache.size());
        }   

        assert(outcomes.first + outcomes.second == nbDraws);
        std::cout << "Draw Without Wasting Bits: prob true = " << (double)outcomes.first / nbDraws << "\n";
    }


    // Draw wasting bits
    {
        size_t nbDraws = 1e6;
        size_t cache_index = 0;
        std::pair<size_t, size_t> outcomes = {0,0};
        for (size_t r = 0 ; r < nbDraws ; ++r)
        {
            drawWastingBits(cache, cache_index) ? ++outcomes.first : ++outcomes.second;
            assert(cache_index <= cache.size());
        }   

        assert(outcomes.first + outcomes.second == nbDraws);
        std::cout << "Draw Wit Wasting Bits: prob true = " << (double)outcomes.first / nbDraws << "\n";
    }
}

Возможный вывод

Draw Without Wasting Bits: prob true = 0.062832
Draw Wit Wasting Bits: prob true = 0.062363

Ответы [ 2 ]

1 голос
/ 05 мая 2020

Devroye's Генерация неоднородной случайной переменной , стр. 505 и 86, упоминает инверсию с помощью алгоритма последовательного поиска.

На основе этого алгоритма, если вы знайте, что mean значительно меньше 1, тогда, если вы сгенерируете однородное случайное число u в [0, 1], переменная Пуассона будет равна 0, если u <= exp(-mean), и больше 0 в противном случае.

Если среднее значение низкое и вы можете допускать приблизительное распределение, то вы можете использовать следующий подход (см. Приложение A к « Дискретный гауссиан для дифференциальной конфиденциальности »):

  1. Express mean в виде рационального числа, в виде numer / denom. Например, если mean - фиксированное значение, тогда numer и denom могут быть соответственно предварительно вычислены, например, во время компиляции.
  2. Произвольно сгенерировать число Бернулли (numer / denom) (сгенерировать 1 с вероятностью numer / denom или 0 иначе). Если 1 был сгенерирован таким образом, повторите этот шаг с Бернулли (numer / (denom * 2)), Бернулли (numer / (denom * 3)) и так далее, пока 0 не будет сгенерирован таким образом. Сгенерируйте эти числа с помощью алгоритма, который минимизирует потерю битов, например, упомянутого в Приложении B статьи Lumbroso's Fast Dice Roller (2013) или метода «ZeroToOne», модифицированного оттуда и приведенного в моем разделе Логические условия . См. Также этот вопрос .
  3. Если на шаге 2 получено четное количество единиц, переменная Пуассона равна точно 0.
  4. Если на шаге 2 получено нечетное количество единиц, переменная Пуассона больше 0, и необходим «более медленный» алгоритм, который выбирает только переменные Пуассона больше 0.

Например, скажем, среднее значение равно 1e-6 (1/1000000), Сгенерируйте число Бернулли (1/1000000), затем Бернулли (1/2000000) и т. Д. c. пока вы не сгенерируете 0 таким образом. Если было сгенерировано четное количество единиц, то переменная Пуассона равна нулю. В противном случае переменная Пуассона равна 1 или больше, и необходим «более медленный» алгоритм.

Одним из примеров является алгоритм ниже, который основан на методе со страниц 505 и 86, но выбирает только переменные Пуассона 1 или больше:

METHOD Poisson1OrGreater(mean)
 sum=Math.exp(-mean)
 prod=sum
 u=RNDRANGE(sum, 1)
 i=0
 while i==0 or u>sum
   prod*=mean/(i+1)
   sum+=prod
   i=i+1
 end
 return i
END METHOD

Этот метод, однако, не очень надежен, тем более что он использует числа, близкие к 1 (где плавающие -точечное пространство более разрежено), а не числа, близкие к 0.


РЕДАКТИРОВАТЬ (7 мая):

Обратите внимание, что сумма n независимого Пуассона (mean) случайные числа распределены Пуассоном (mean*n) (стр. 501). Таким образом, приведенное выше обсуждение в этом ответе применимо к сумме n случайных чисел Пуассона до тех пор, пока n их среднее значение остается небольшим. Например, чтобы сгенерировать сумму 1000 случайных чисел Пуассона со средним значением 1e-6, просто сгенерируйте одно случайное число Пуассона со средним значением 0,001. Это значительно сэкономит на вызовах генератора случайных чисел.


РЕДАКТИРОВАТЬ (13 мая): Обычно редактируется.

0 голосов
/ 05 мая 2020

Вы можете сгенерировать время до следующего события с помощью уравнения (-ln U) / λ, где 0 < U ≤ 1 - равномерное случайное число, а λ - частота событий (также известная как 1e-6).

https://preshing.com/20111007/how-to-generate-random-timings-for-a-poisson-process/

...