Numeri casuali ponderati


102

Sto cercando di implementare numeri casuali ponderati. Al momento sto solo sbattendo la testa contro il muro e non riesco a capirlo.

Nel mio progetto (Hand-range Hold'em, analisi soggettiva all-in equity), sto usando le funzioni random di Boost. Quindi, diciamo che voglio scegliere un numero casuale tra 1 e 3 (quindi 1, 2 o 3). Il generatore di mersenne twister di Boost funziona come un incantesimo per questo. Tuttavia, voglio che la scelta sia ponderata, ad esempio, in questo modo:

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

Boost ha una sorta di funzionalità per questo?

Risposte:


179

Esiste un algoritmo semplice per selezionare un articolo a caso, in cui gli articoli hanno pesi individuali:

1) calcola la somma di tutti i pesi

2) scegli un numero casuale che sia 0 o maggiore e sia minore della somma dei pesi

3) passa in rassegna gli oggetti uno alla volta, sottraendo il loro peso dal tuo numero casuale, fino a ottenere l'oggetto il cui numero casuale è inferiore al peso di quell'oggetto

Pseudo-codice che illustra questo:

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");

Questo dovrebbe essere semplice da adattare ai tuoi contenitori boost e simili.


Se i tuoi pesi vengono cambiati raramente ma spesso ne scegli uno a caso e fintanto che il tuo contenitore memorizza i puntatori agli oggetti o è lungo più di qualche dozzina di elementi (in pratica, devi profilare per sapere se questo aiuta o ostacola) , poi c'è un'ottimizzazione:

Memorizzando la somma del peso cumulativo in ogni articolo è possibile utilizzare una ricerca binaria per selezionare l'articolo corrispondente al peso di prelievo.


Se non si conosce il numero di elementi nell'elenco, esiste un algoritmo molto preciso chiamato campionamento del serbatoio che può essere adattato per essere ponderato.


3
Come ottimizzazione potresti utilizzare pesi cumulativi e utilizzare una ricerca binaria. Ma solo per tre valori diversi questo è probabilmente eccessivo.
sellibitze

2
Presumo che quando dici "in order" tu stia omettendo intenzionalmente un passaggio di pre-ordinamento sull'array choice_weight, sì?
SilentDirge

2
@ Aureis, non è necessario ordinare l'array. Ho provato a chiarire la mia lingua.
Sarà il

1
@ Will: Sì, ma esiste un algoritmo con lo stesso nome. sirkan.iit.bme.hu/~szirmay/c29.pdf e en.wikipedia.org/wiki/Photon_mapping A Monte Carlo method called Russian roulette is used to choose one of these actions viene fuori a secchi quando si cerca su Google. "algoritmo della roulette russa". Potresti sostenere che tutte queste persone hanno il nome sbagliato però.
v.oddou

3
Nota per i futuri lettori: la parte che sottrae il loro peso al tuo numero casuale è facile da trascurare, ma cruciale per l'algoritmo (sono caduto nella stessa trappola di @kobik nel loro commento).
Frank Schmitt

48

Risposta aggiornata a una vecchia domanda. Puoi farlo facilmente in C ++ 11 con solo 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';
}

Uscita sul mio sistema:

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

Si noti che la maggior parte del codice sopra è dedicata solo alla visualizzazione e all'analisi dell'output. La generazione effettiva è solo poche righe di codice. L'output dimostra che le "probabilità" richieste sono state ottenute. Devi dividere l'output richiesto per 1,5 poiché questo è ciò a cui si sommano le richieste.


Solo una nota di promemoria sulla compilazione di questo esempio: richiede C ++ 11 ie. usa -std = c ++ 0x flag del compilatore, disponibile da gcc 4.6 in poi.
Pete855217

3
Ti interessa solo scegliere le parti necessarie che risolvono il problema?
Jonny

2
Questa è la risposta migliore, ma penso che std::discrete_distributioninvece std::piecewise_constant_distributionsarebbe stata anche meglio.
Dan

1
@ Dan, sì, sarebbe un altro ottimo modo per farlo. Se lo codifichi e rispondi con esso, lo voterò. Penso che il codice potrebbe essere abbastanza simile a quello che ho sopra. Dovresti solo aggiungerne uno all'output generato. E l'input per la distribuzione sarebbe più semplice. Un insieme di risposte di confronto / contrasto in quest'area potrebbe essere prezioso per i lettori.
Howard Hinnant

15

Se i tuoi pesi cambiano più lentamente di quanto vengono disegnati, C ++ 11 discrete_distributionsarà il più semplice:

#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...

Si noti, tuttavia, che c ++ 11 discrete_distributioncalcola tutte le somme cumulative durante l'inizializzazione. Di solito lo desideri perché accelera il tempo di campionamento per un costo O (N) una tantum. Ma per una distribuzione in rapida evoluzione, comporterà un pesante costo di calcolo (e memoria). Ad esempio, se i pesi rappresentavano quanti elementi ci sono e ogni volta che ne disegni uno, lo rimuovi, probabilmente vorrai un algoritmo personalizzato.

La risposta di Will https://stackoverflow.com/a/1761646/837451 evita questo sovraccarico ma sarà più lento da cui attingere rispetto a C ++ 11 perché non può utilizzare la ricerca binaria.

Per vedere che lo fa, puoi vedere le righe pertinenti ( /usr/include/c++/5/bits/random.tccsulla mia installazione di 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

Quello che faccio quando ho bisogno di pesare i numeri è usare un numero casuale per il peso.

Ad esempio: ho bisogno di generare numeri casuali da 1 a 3 con i seguenti pesi:

  • Il 10% di un numero casuale potrebbe essere 1
  • Il 30% di un numero casuale potrebbe essere 2
  • Il 60% di un numero casuale potrebbe essere 3

Quindi uso:

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;
}

Con questo, casualmente ha il 10% delle probabilità di essere 1, il 30% di essere 2 e il 60% di essere 3.

Puoi giocarci secondo le tue esigenze.

Spero di poterti aiutare, buona fortuna!


Ciò esclude la regolazione dinamica della distribuzione.
Josh C

2
Hacky ma mi piace. Bello per un prototipo veloce in cui vuoi una ponderazione approssimativa.
pareggi il

1
Funziona solo per pesi razionali. Avrai difficoltà a farlo con un peso di 1 / pi;)
Joseph Budin

1
@JosephBudin D'altra parte, non saresti mai in grado di avere un peso irrazionale. Un interruttore case da ~ 4,3 miliardi dovrebbe andare bene per i pesi flottanti. : D
Jason C

1
Giusto @JasonC, il problema è infinitamente più piccolo ora ma è ancora un problema;)
Joseph Budin

3

Costruisci una borsa (o std :: vector) di tutti gli oggetti che possono essere raccolti.
Assicurati che il numero di ogni articolo sia proporzionale alla tua ponderazione.

Esempio:

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

Quindi prendi una borsa con 100 articoli con 60 1, 35 2 e 5 3.
Ora ordina casualmente la borsa (std :: random_shuffle)

Scegli gli elementi dalla borsa in sequenza finché non è vuota.
Una volta vuoto, randomizza nuovamente la borsa e ricomincia.


6
se hai una borsa di biglie rosse e blu e ne selezioni una rossa e non la sostituisci la probabilità di selezionare un'altra biglia rossa è sempre la stessa? Allo stesso modo, la tua affermazione "Scegli gli elementi dal sacchetto in sequenza finché non è vuoto" produce una distribuzione completamente diversa da quella prevista.
ldog

@ldog: capisco il tuo argomento ma non stiamo cercando la vera casualità, stiamo cercando una distribuzione particolare. Questa tecnica garantisce la corretta distribuzione.
Martin York,

4
il mio punto esatto è che non produci correttamente la distribuzione, secondo il mio argomento precedente. considera il semplice esempio del contatore, supponiamo di avere un array di 3 come 1,2,2produttore di 1 1/3 delle volte e 2 2/3. Randomizza l'array, scegli il primo, diciamo un 2, ora l'elemento successivo che scegli segue la distribuzione di 1 1/2 del tempo e 2 1/2 del tempo. Esperto?
ldog

0

Scegli un numero casuale su [0,1), che dovrebbe essere l'operatore predefinito () per un RNG boost. Scegli l'elemento con la funzione di densità di probabilità cumulativa> = quel numero:

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
}

Dove random01 () restituisce un doppio> = 0 e <1. Nota che quanto sopra non richiede la somma delle probabilità a 1; li normalizza per te.

p è solo una funzione che assegna una probabilità a un elemento nella raccolta [inizio, fine). Puoi ometterlo (o usare un'identità) se hai solo una sequenza di probabilità.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.