Зважені випадкові числа


101

Я намагаюся реалізувати зважені випадкові числа. Я зараз просто стукаю головою об стіну і не можу цього зрозуміти.

У своєму проекті (Hold'em hand-range, суб'єктивний всебічний аналіз власного капіталу) я використовую випадкові функції Boost. Скажімо, я хочу вибрати випадкове число між 1 і 3 (тобто 1, 2 або 3). Мерсенний генератор твістера Boost працює як принадність для цього. Однак я хочу, щоб вибір був зважений, наприклад, таким:

1 (weight: 90)
2 (weight: 56)
3 (weight:  4)

Чи має Boost якусь функціональність для цього?

Відповіді:


179

Існує простий алгоритм вибору елемента навмання, де елементи мають індивідуальну вагу:

1) обчислити суму всіх ваг

2) вибрати випадкове число, яке дорівнює 0 або більше і менше суми ваг

3) проходьте деталі по одному, віднімаючи їх вагу від випадкового числа, поки не отримаєте предмет, де випадкове число менше ваги цього предмета

Псевдокод, що ілюструє це:

int sum_of_weight = 0;
for(int i=0; i<num_choices; i++) {
   sum_of_weight += choice_weight[i];
}
int rnd = random(sum_of_weight);
for(int i=0; i<num_choices; i++) {
  if(rnd < choice_weight[i])
    return i;
  rnd -= choice_weight[i];
}
assert(!"should never get here");

Це повинно бути зрозумілим для адаптації до ваших збірних контейнерів тощо.


Якщо ваги змінюються рідко, але ви часто вибираєте їх навмання, і поки ваш контейнер зберігає покажчики на об'єкти або має довжину більше декількох десятків предметів (в основному, ви повинні профілювати, щоб знати, чи це допомагає чи перешкоджає) , тоді відбувається оптимізація:

Зберігаючи сукупну суму ваги в кожному предметі, ви можете використовувати двійковий пошук, щоб вибрати предмет, відповідний вазі вибору.


Якщо ви не знаєте кількість елементів у списку, то існує дуже акуратний алгоритм, який називається відбір проб пласта, який можна адаптувати до зважування.


3
В якості оптимізації ви можете використовувати кумулятивні ваги та використовувати двійковий пошук. Але лише для трьох різних значень це, мабуть, надмірність.
sellibitze

2
Я припускаю, що коли ви говорите "по порядку", ви навмисно опускаєте крок попереднього сортування в масиві select_weight, так?
SilentDirge

2
@Aureis, немає потреби сортувати масив. Я спробував уточнити свою мову.
Буде

1
@Will: Так, але є однойменний алгоритм. sirkan.iit.bme.hu/~szirmay/c29.pdf та en.wikipedia.org/wiki/Photon_mapping A Monte Carlo method called Russian roulette is used to choose one of these actions він з’являється у відрах під час гуглінгу за нього. "алгоритм російської рулетки". Ви можете стверджувати, що всі ці люди мають неправильне ім'я.
v.oddou

3
Зверніть увагу на майбутніх читачів: частину, яка віднімає їх вагу від випадкового числа , легко не помітити, але важлива для алгоритму (я потрапив у ту ж пастку, що і @kobik у їхньому коментарі).
Френк Шмітт

48

Оновлена ​​відповідь на старе запитання. Ви можете легко зробити це в C ++ 11 за допомогою просто std :: lib:

#include <iostream>
#include <random>
#include <iterator>
#include <ctime>
#include <type_traits>
#include <cassert>

int main()
{
    // Set up distribution
    double interval[] = {1,   2,   3,   4};
    double weights[] =  {  .90, .56, .04};
    std::piecewise_constant_distribution<> dist(std::begin(interval),
                                                std::end(interval),
                                                std::begin(weights));
    // Choose generator
    std::mt19937 gen(std::time(0));  // seed as wanted
    // Demonstrate with N randomly generated numbers
    const unsigned N = 1000000;
    // Collect number of times each random number is generated
    double avg[std::extent<decltype(weights)>::value] = {0};
    for (unsigned i = 0; i < N; ++i)
    {
        // Generate random number using gen, distributed according to dist
        unsigned r = static_cast<unsigned>(dist(gen));
        // Sanity check
        assert(interval[0] <= r && r <= *(std::end(interval)-2));
        // Save r for statistical test of distribution
        avg[r - 1]++;
    }
    // Compute averages for distribution
    for (double* i = std::begin(avg); i < std::end(avg); ++i)
        *i /= N;
    // Display distribution
    for (unsigned i = 1; i <= std::extent<decltype(avg)>::value; ++i)
        std::cout << "avg[" << i << "] = " << avg[i-1] << '\n';
}

Вихід з моєї системи:

avg[1] = 0.600115
avg[2] = 0.373341
avg[3] = 0.026544

Зауважте, що більша частина коду, приведеного вище, присвячена просто відображенню та аналізу результатів. Фактичне покоління - це лише кілька рядків коду. Результат демонструє, що запитувані "ймовірності" отримані. Ви повинні розділити запитуваний результат на 1,5, оскільки саме так запити складаються.


Просто нагадування про компіляцію цього прикладу: вимагає C ++ 11, тобто. використовувати -std = c ++ 0x прапор компілятора, доступний від gcc 4.6 і далі.
Pete855217

3
Потрібно просто підібрати необхідні частини, які вирішують проблему?
Джоні

2
Це найкраща відповідь, але я думаю, std::discrete_distributionзамість цього std::piecewise_constant_distributionбуло б ще краще.
Дан

1
@Dan, Так, це був би ще один відмінний спосіб зробити це. Якщо ви це зашифруєте і відповісте, я проголосую за нього. Я думаю, що код може бути дуже схожим на те, що я маю вище. Вам просто потрібно буде додати його до створеного результату. І введення в розподіл було б простішим. Набір відповідей / протиставлення відповідей у ​​цій галузі може бути корисним для читачів.
Говард Хінант

15

Якщо ваги змінюються повільніше, ніж вони намальовані, C ++ 11 discrete_distributionстане найпростішим:

#include <random>
#include <vector>
std::vector<double> weights{90,56,4};
std::discrete_distribution<int> dist(std::begin(weights), std::end(weights));
std::mt19937 gen;
gen.seed(time(0));//if you want different results from different runs
int N = 100000;
std::vector<int> samples(N);
for(auto & i: samples)
    i = dist(gen);
//do something with your samples...

Однак зауважте, що c ++ 11 discrete_distributionобчислює всі сукупні суми при ініціалізації. Зазвичай ви цього хочете, оскільки це прискорює час вибірки на разову вартість O (N). Але для швидко змінюється дистрибуції це призведе до важкого розрахунку (і пам'яті) вартості. Наприклад, якщо ваги представляли, скільки предметів є, і кожного разу, коли ви малюєте, ви виймаєте його, ви, ймовірно, захочете користувацький алгоритм.

Відповідь Вілла https://stackoverflow.com/a/1761646/837451 дозволяє уникнути цього накладних витрат, але витягнути буде повільніше, ніж C ++ 11, оскільки він не може використовувати двійковий пошук.

Щоб побачити, що це робить, ви можете побачити відповідні рядки ( /usr/include/c++/5/bits/random.tccна моїй установці Ubuntu 16.04 + GCC 5.3):

  template<typename _IntType>
    void
    discrete_distribution<_IntType>::param_type::
    _M_initialize()
    {
      if (_M_prob.size() < 2)
        {
          _M_prob.clear();
          return;
        }

      const double __sum = std::accumulate(_M_prob.begin(),
                                           _M_prob.end(), 0.0);
      // Now normalize the probabilites.
      __detail::__normalize(_M_prob.begin(), _M_prob.end(), _M_prob.begin(),
                            __sum);
      // Accumulate partial sums.
      _M_cp.reserve(_M_prob.size());
      std::partial_sum(_M_prob.begin(), _M_prob.end(),
                       std::back_inserter(_M_cp));
      // Make sure the last cumulative probability is one.
      _M_cp[_M_cp.size() - 1] = 1.0;
    }

10

Що я роблю, коли мені потрібно зважувати числа, використовую випадкове число для ваги.

Наприклад: мені потрібно, щоб генерувати випадкові числа від 1 до 3 із наступними вагами:

  • 10% випадкового числа може бути 1
  • 30% випадкового числа може бути 2
  • 60% випадкового числа може бути 3

Тоді я використовую:

weight = rand() % 10;

switch( weight ) {

    case 0:
        randomNumber = 1;
        break;
    case 1:
    case 2:
    case 3:
        randomNumber = 2;
        break;
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
        randomNumber = 3;
        break;
}

При цьому випадково у неї 10% ймовірностей бути 1, 30% бути 2 і 60% бути 3.

Ви можете грати з ним як ваші потреби.

Сподіваюся, я можу вам допомогти, удачі!


Це виключає динамічне регулювання розподілу.
Джош C

2
Хаккі, але мені це подобається. Приємно для швидкого прототипу, де потрібно трохи грубо зважити.
весело

1
Це працює лише для раціональних ваг. Вам буде важко це робити з вагою 1 / пі;)
Джозеф Будін

1
@JosephBudin Знову ж таки, ви ніколи не зможете мати ірраціональну вагу. Перемикач корпусів ~ 4,3 мільярда повинен робити чудово для поплавкових ваг. : D
Джейсон C

1
Право @JasonC, проблем зараз нескінченно менше, але все ще проблема;)
Джозеф Будін

3

Створіть мішок (або std :: vector) з усіх предметів, які можна вибрати.
Переконайтесь, що кількість кожного предмета пропорційна вашій вазі.

Приклад:

  • 1 60%
  • 2 35%
  • 3 5%

Тож майте сумку зі 100 предметами з 60 1, 35 2 та 5 3.
Тепер довільно сортуйте мішок (std :: random_shuffle)

Вибирайте елементи з пакетика послідовно, поки він не порожній.
Після порожнього повторного рандомізації сумки і почніть знову.


6
якщо у вас є мішок з червоного і синього мармуру, і ви вибираєте з нього червоний мармур і не замінюєте його, чи є ймовірність вибрати ще один червоний мармур? Таким же чином, ваше твердження "Вибирайте елементи з пакета послідовно, поки він не порожній" виробляє абсолютно інший розподіл, ніж передбачалося.
ldog

@ldog: Я розумію ваш аргумент, але ми не шукаємо справжньої випадковості, ми шукаємо конкретний розподіл. Цей прийом гарантує правильний розподіл.
Мартін Йорк

4
моє значення саме в тому, що ви неправильно виробляєте розповсюдження за моїм попереднім аргументом. Розглянемо простий приклад лічильника, скажімо, у вас є масив 3, який 1,2,2створює 1 1/3 часу і 2 2/3. Рандомізуйте масив, виберіть перший, дозвольте сказати 2, тепер наступний вибраний елемент слід розподілу 1 1/2 часу та 2 1/2 часу. Кмітливий?
ldog

0

Виберіть випадкове число на [0,1), яке повинно бути оператором за замовчуванням () для прискореного RNG. Виберіть елемент із функцією накопичувальної щільності ймовірності> = це число:

template <class It,class P>
It choose_p(It begin,It end,P const& p)
{
    if (begin==end) return end;
    double sum=0.;
    for (It i=begin;i!=end;++i)
        sum+=p(*i);
    double choice=sum*random01();
    for (It i=begin;;) {
        choice -= p(*i);
        It r=i;
        ++i;
        if (choice<0 || i==end) return r;
    }
    return begin; //unreachable
}

Де random01 () повертає подвійний> = 0 і <1. Зауважте, що вищезгадане не вимагає, щоб ймовірність становила 1; це нормалізує їх для вас.

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.