Números aleatórios ponderados


101

Estou tentando implementar números aleatórios ponderados. No momento, estou apenas batendo minha cabeça contra a parede e não consigo descobrir isso.

No meu projeto (intervalos de mãos de Hold'em, análise subjetiva de equidade total), estou usando as funções aleatórias de Boost. Então, digamos que eu queira escolher um número aleatório entre 1 e 3 (então 1, 2 ou 3). O gerador de torção mersenne do Boost funciona perfeitamente para isso. No entanto, quero que a escolha seja ponderada, por exemplo, assim:

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

O Boost tem algum tipo de funcionalidade para isso?

Respostas:


179

Existe um algoritmo simples para escolher um item aleatoriamente, onde os itens têm pesos individuais:

1) calcular a soma de todos os pesos

2) escolha um número aleatório que seja 0 ou maior e menor que a soma dos pesos

3) analise os itens um de cada vez, subtraindo o peso do seu número aleatório, até obter o item em que o número aleatório é menor que o peso do item

Pseudo-código que ilustra isso:

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

Isso deve ser simples para se adaptar aos seus recipientes de impulso e tal.


Se seus pesos são raramente alterados, mas você costuma escolher um ao acaso, e desde que seu contêiner esteja armazenando ponteiros para os objetos ou tenha mais do que algumas dezenas de itens (basicamente, você tem que fazer um perfil para saber se isso ajuda ou atrapalha) , então há uma otimização:

Ao armazenar a soma do peso cumulativo em cada item, você pode usar uma pesquisa binária para escolher o item correspondente ao peso escolhido.


Se você não souber o número de itens na lista, então existe um algoritmo muito bom chamado amostragem de reservatório que pode ser adaptado para ser ponderado.


3
Como uma otimização, você pode usar pesos cumulativos e usar uma pesquisa binária. Mas, para apenas três valores diferentes, isso provavelmente é um exagero.
sellibitze

2
Suponho que quando você diz "in order", está omitindo propositalmente uma etapa de pré-classificação no array choice_weight, certo?
SilentDirge,

2
@Aureis, não há necessidade de classificar o array. Tentei esclarecer minha linguagem.
Será

1
@Will: Sim, mas existe um algoritmo com o mesmo 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 aparece em baldes ao pesquisar no Google. "algoritmo de roleta russa". Você pode argumentar que todas essas pessoas têm o nome errado.
v.oddou

3
Observação para futuros leitores: a parte que subtrai o peso do número aleatório é fácil de ignorar, mas é crucial para o algoritmo (caí na mesma armadilha que @kobik em seu comentário).
Frank Schmitt

48

Resposta atualizada para uma pergunta antiga. Você pode fazer isso facilmente em C ++ 11 com apenas 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';
}

Saída em meu sistema:

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

Observe que a maior parte do código acima é dedicado apenas a exibir e analisar a saída. A geração real é apenas algumas linhas de código. A saída demonstra que as "probabilidades" solicitadas foram obtidas. Você tem que dividir a saída solicitada por 1,5, pois é isso que as solicitações somam.


Apenas um lembrete sobre a compilação deste exemplo: requer C ++ 11 ie. use -std = sinalizador do compilador c ++ 0x, disponível a partir do gcc 4.6.
Pete855217

3
Quer apenas escolher as peças necessárias para resolver o problema?
Jonny

2
Esta é a melhor resposta, mas acho que em std::discrete_distributionvez de std::piecewise_constant_distributionteria sido ainda melhor.
Dan

1
@Dan, Sim, seria outra excelente forma de o fazer. Se você codificar e responder com isso, eu votarei a favor. Acho que o código pode ser muito semelhante ao que tenho acima. Você só precisa adicionar um à saída gerada. E a entrada para a distribuição seria mais simples. Um conjunto de comparação / contraste de respostas nesta área pode ser valioso para os leitores.
Howard Hinnant,

15

Se seus pesos mudam mais lentamente do que são desenhados, C ++ 11 discrete_distributionserá o mais fácil:

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

Observe, entretanto, que o c ++ 11 discrete_distributioncalcula todas as somas cumulativas na inicialização. Normalmente, você deseja isso porque acelera o tempo de amostragem para um custo O (N) único. Mas, para uma distribuição que muda rapidamente, isso incorrerá em um alto custo de cálculo (e memória). Por exemplo, se os pesos representavam quantos itens existem e cada vez que você desenha um e o remove, provavelmente desejará um algoritmo personalizado.

A resposta de Will https://stackoverflow.com/a/1761646/837451 evita essa sobrecarga, mas será mais lento para desenhar do que o C ++ 11 porque não pode usar a pesquisa binária.

Para ver se ele faz isso, você pode ver as linhas relevantes ( /usr/include/c++/5/bits/random.tccna minha instalação do 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

O que eu faço quando preciso pesar números é usar um número aleatório para o peso.

Por exemplo: preciso que gere números aleatórios de 1 a 3 com os seguintes pesos:

  • 10% de um número aleatório pode ser 1
  • 30% de um número aleatório pode ser 2
  • 60% de um número aleatório pode ser 3

Então eu 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;
}

Com isso, aleatoriamente tem que 10% das probabilidades sejam 1, 30% sejam 2 e 60% sejam 3.

Você pode brincar com ele conforme suas necessidades.

Espero poder ajudar você, boa sorte!


Isso exclui o ajuste dinâmico da distribuição.
Josh C

2
Hacky, mas eu gosto. Bom para um protótipo rápido onde você quer um pouco de peso bruto.
empatou em

1
Só funciona para pesos racionais. Você terá dificuldade em fazer isso com um peso de 1 / pi;)
Joseph Budin

1
@JosephBudin Então, novamente, você nunca seria capaz de ter um peso irracional. Uma chave de caixa de ~ 4,3 bilhões deve servir para pesos flutuantes. : D
Jason C

1
Certo @JasonC, o problema é infinitamente menor agora, mas ainda é um problema;)
Joseph Budin

3

Construa uma bolsa (ou std :: vector) com todos os itens que podem ser coletados.
Certifique-se de que o número de cada item seja proporcional à sua ponderação.

Exemplo:

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

Portanto, tenha uma bolsa com 100 itens com 60 1's, 35 2's e 5 3's.
Agora, classifique o saco aleatoriamente (std :: random_shuffle)

Escolha os elementos do saco sequencialmente até que esteja vazio.
Uma vez vazio, re-randomize o saco e comece novamente.


6
se você tiver um saco de bolinhas vermelhas e azuis e selecionar uma bolinha vermelha dela e não substituí-la, a probabilidade de selecionar outra bolinha vermelha ainda é a mesma? Da mesma forma, sua afirmação "Escolha os elementos da sacola sequencialmente até que esteja vazia" produz uma distribuição totalmente diferente da pretendida.
Cão de

@ldog: Eu entendo seu argumento, mas não estamos procurando por uma verdadeira aleatoriedade, estamos procurando por uma distribuição particular. Esta técnica garante a distribuição correta.
Martin York

4
meu ponto exatamente é que você não produz distribuição corretamente, por meu argumento anterior. considere o exemplo de contador simples, digamos que você coloque uma matriz de 3 como 1,2,2produzindo 1 1/3 do tempo e 2 2/3. Randomize o array, escolha o primeiro, digamos 2, agora o próximo elemento que você escolhe segue a distribuição de 1 1/2 do tempo e 2 1/2 do tempo. Savvy?
Cão de

0

Escolha um número aleatório em [0,1), que deve ser o operador padrão () para um RNG de reforço. Escolha o item com função de densidade de probabilidade cumulativa> = esse número:

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
}

Onde random01 () retorna um duplo> = 0 e <1. Observe que o exposto acima não exige que as probabilidades sejam 1; isso os normaliza para você.

p é apenas uma função que atribui uma probabilidade a um item da coleção [início, fim). Você pode omiti-lo (ou usar uma identidade) se tiver apenas uma sequência de probabilidades.


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.