web-dev-qa-db-fra.com

Nombres aléatoires pondérés

J'essaie d'implémenter un nombre aléatoire pondéré. Je me tape actuellement la tête contre le mur et je n'arrive pas à comprendre cela.

Dans mon projet (gammes de mains Hold'em, analyse d'équité tout-en-un subjective), j'utilise les fonctions aléatoires de Boost. Alors, disons que je veux choisir un nombre aléatoire entre 1 et 3 (donc 1, 2 ou 3). Le générateur mersenne twister de Boost fonctionne comme un charme pour cela. Cependant, je veux que le choix soit pondéré par exemple comme ceci:

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

Boost a-t-il une sorte de fonctionnalité pour cela?

85
nhaa123

Il existe un algorithme simple pour choisir un article au hasard, où les articles ont des poids individuels:

1) calculer la somme de tous les poids

2) choisissez un nombre aléatoire égal ou supérieur à 0 et inférieur à la somme des poids

3) passez en revue les articles un par un, en soustrayant leur poids de votre nombre aléatoire, jusqu'à ce que vous obteniez l'article dont le nombre aléatoire est inférieur au poids de cet article

Pseudo-code illustrant ceci:

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

Cela devrait être simple à adapter à vos conteneurs boost et autres.


Si vos poids sont rarement modifiés, mais vous en choisissez souvent un au hasard, et tant que votre conteneur stocke des pointeurs vers les objets ou fait plus de quelques dizaines d'articles (en gros, vous devez profiler pour savoir si cela aide ou gêne) , puis il y a une optimisation:

En stockant la somme des poids cumulés dans chaque article, vous pouvez utiliser un recherche binaire pour sélectionner l'article correspondant au poids de prélèvement.


Si vous ne connaissez pas le nombre d'articles dans la liste, alors il y a un algorithme très soigné appelé échantillonnage du réservoir qui peut être adapté pour être pondéré.

146
Will

Réponse mise à jour à une ancienne question. Vous pouvez facilement le faire en C++ 11 avec juste le 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';
}

Sortie sur mon système:

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

Notez que la plupart du code ci-dessus est consacré uniquement à l'affichage et à l'analyse de la sortie. La génération réelle n'est que quelques lignes de code. Le résultat montre que les "probabilités" demandées ont été obtenues. Vous devez diviser la sortie demandée par 1,5 car c'est à cela que s'ajoutent les demandes.

47
Howard Hinnant

Si vos pondérations changent plus lentement qu'elles ne le sont, C++ 11 discrete_distribution va être le plus simple:

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

Notez cependant que c ++ 11 discrete_distribution calcule toutes les sommes cumulées à l'initialisation. Habituellement, vous le souhaitez car cela accélère le temps d'échantillonnage pour une seule fois O(N) coût. Mais pour une distribution qui évolue rapidement, cela entraînera un coût de calcul (et de mémoire) élevé. Pour Par exemple, si les poids représentaient le nombre d'éléments et à chaque fois que vous en dessinez un, vous le supprimez, vous voudrez probablement un algorithme personnalisé.

La réponse de Will https://stackoverflow.com/a/1761646/837451 évite ce surcoût mais sera plus lent à tirer que le C++ 11 car il ne peut pas utiliser la recherche binaire.

Pour voir ce qu'il fait, vous pouvez voir les lignes pertinentes (/usr/include/c++/5/bits/random.tcc sur mon installation 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;
    }
13
mmdanziger

Ce que je fais quand j'ai besoin de pondérer des nombres, c'est d'utiliser un nombre aléatoire pour le poids.

Par exemple: j'ai besoin de générer des nombres aléatoires de 1 à 3 avec les poids suivants:

  • 10% d'un nombre aléatoire pourrait être 1
  • 30% d'un nombre aléatoire pourrait être 2
  • 60% d'un nombre aléatoire pourrait être 3

Ensuite j'utilise:

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

Avec cela, il a au hasard 10% des probabilités d'être 1, 30% d'être 2 et 60% d'être 3.

Vous pouvez jouer avec selon vos besoins.

J'espère que je pourrais vous aider, bonne chance!

10
Chirry

Construisez un sac (ou std :: vector) de tous les articles qui peuvent être choisis.
Assurez-vous que le nombre de chaque élément est proportionnel à votre pondération.

Exemple:

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

Donc, ayez un sac avec 100 articles avec 60 1, 35 2 et 5 3.
Triez maintenant aléatoirement le sac (std :: random_shuffle)

Choisissez les éléments du sac séquentiellement jusqu'à ce qu'il soit vide.
Une fois vide, re-randomiser le sac et recommencer.

3
Martin York

Choisissez un nombre aléatoire sur [0,1), qui devrait être l'opérateur par défaut () pour un RNG boost. Choisissez l'élément avec la fonction de densité de probabilité cumulative> = ce nombre:

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
}

Où random01 () renvoie un double> = 0 et <1. Notez que ce qui précède ne nécessite pas que les probabilités totalisent 1; il les normalise pour vous.

p est juste une fonction attribuant une probabilité à un élément de la collection [début, fin). Vous pouvez l'omettre (ou utiliser une identité) si vous n'avez qu'une séquence de probabilités.

0
Jonathan Graehl