web-dev-qa-db-ja.com

配列からの重み付きランダム選択

配列から1つの要素をランダムに選択したいのですが、各要素には既知の選択確率があります。

(配列内の)すべてのチャンスの合計は1です。

巨大な計算に最も適した、最も適したアルゴリズムは何ですか?

例:

id => chance
array[
    0 => 0.8
    1 => 0.2
]

この擬似コードの場合、問題のアルゴリズムは複数の呼び出しで統計的にid 0 idの1つの要素1

67
Mikulas Dite

リストの離散累積密度関数(CDF)を計算するか、単純な言葉で重みの累積和の配列を計算します。次に、0からすべての重みの合計(場合によっては1)の範囲の乱数を生成し、バイナリ検索を実行して、離散CDF配列でこの乱数を見つけ、このエントリに対応する値を取得します-これ重み付けされた乱数です。

69
Sven Marnach

アルゴリズムは簡単です

Rand_no = Rand(0,1)
for each element in array 
     if(Rand_num < element.probablity)
          select and break
     Rand_num = Rand_num - element.probability
13
Rohit J

この記事 は、この問題を完全に理解するのに最も役立つことがわかりました。 このstackoverflowの質問 もあなたが探しているものかもしれません。


最適な解決策は エイリアスメソッド(wikipedia) を使用することだと思います。初期化するにはO(n)時間、選択を行うにはO(1)時間、およびO(n)メモリ。

重み付きn-sided die(ここから長さnから要素を選択するのは簡単です)の結果を生成するためのアルゴリズム配列) この記事 から取得します。作者は、フェアダイスを振る(floor(random() * n))関数と、偏ったコインを反転させる関数(random() < p)があると仮定しています。

アルゴリズム:Voseのエイリアス法

初期化:

  1. 配列AliasおよびProb、サイズnをそれぞれ作成します。
  2. 2つのワークリストSmallおよびLargeを作成します。
  3. 各確率にnを掛けます。
  4. スケーリングされた確率ごとp
    1. Ifp <1iSmallに追加します。
    2. それ以外の場合(p ≥1)、iLargeに追加します。
  5. SmallおよびLargeは空ではありませんが、(Largeが最初に空になる場合があります)
    1. Small;から最初の要素を削除します;それを呼び出すl
    2. Large;から最初の要素を削除します;呼び出すg
    3. セットProb [l] = pl
    4. Alias [l] = gを設定します。
    5. セットpg :=(pg+ pl)−1。 (これは、数値的に安定したオプションです。)
    6. Ifpg<1gSmallに追加します。
    7. それ以外の場合(pg ≥1)、gLargeに追加します。
  6. Largeは空ではありませんが:
    1. Large;から最初の要素を削除します;呼び出すg
    2. Prob [g] = 1を設定します。
  7. Smallは空ではありませんが、これは数値が不安定なためにのみ可能です。
    1. Small;から最初の要素を削除します;それを呼び出すl
    2. Prob [l] = 1を設定します。

世代:

  1. n-sided dieから公平なダイスロールを生成します;サイドiを呼び出します。
  2. 確率Prob [i]で頭にくるバイアスのかかったコインを反転します。
  3. コインが「ヘッド」になったら、iを返します。
  4. それ以外の場合は、Alias [i]を返します。

これは、O(1)サンプルあたりの予想時間で次のように実行できます。

各要素iのCDF F(i)を計算して、i以下の確率の合計にします。

要素iの範囲r(i))を間隔[F(i-1)、F(i)]に定義します。

間隔[(i-1)/ n、i/n]ごとに、範囲が間隔と重複する要素のリストで構成されるバケットを作成します。これには、十分な注意を払う限り、配列全体で合計O(n)時間がかかります。

配列をランダムにサンプリングするときは、単に乱数が入っているバケットを計算し、それを含む間隔が見つかるまでリストの各要素と比較します。

サンプルのコストはO(ランダムに選択されたリストの予想される長さ)<= 2です。

6
jonderry

Rubyの例

#each element is associated with its probability
a = {1 => 0.25 ,2 => 0.5 ,3 => 0.2, 4 => 0.05}

#at some point, convert to ccumulative probability
acc = 0
a.each { |e,w| a[e] = acc+=w }

#to select an element, pick a random between 0 and 1 and find the first   
#cummulative probability that's greater than the random number
r = Rand
selected = a.find{ |e,w| w>r }

p selected[0]
6
krusty.ar

別のRuby例:

def weighted_Rand(weights = {})
  raise 'Probabilities must sum up to 1' unless weights.values.inject(&:+) == 1.0
  raise 'Probabilities must not be negative' unless weights.values.all? { |p| p >= 0 }
  # Do more sanity checks depending on the amount of trust in the software component using this method
  # E.g. don't allow duplicates, don't allow non-numeric values, etc.

  # Ignore elements with probability 0
  weights = weights.reject { |k, v| v == 0.0 }   # e.g. => {"a"=>0.4, "b"=>0.4, "c"=>0.2}

  # Accumulate probabilities and map them to a value
  u = 0.0
  ranges = weights.map { |v, p| [u += p, v] }   # e.g. => [[0.4, "a"], [0.8, "b"], [1.0, "c"]]

  # Generate a (pseudo-)random floating point number between 0.0(included) and 1.0(excluded)
  u = Rand   # e.g. => 0.4651073966724186

  # Find the first value that has an accumulated probability greater than the random number u
  ranges.find { |p, v| p > u }.last   # e.g. => "b"
end

使い方:

weights = {'a' => 0.4, 'b' => 0.4, 'c' => 0.2, 'd' => 0.0}

weighted_Rand weights

何を期待します:

d = 1000.times.map{ weighted_Rand weights }
d.count('a') # 396
d.count('b') # 406
d.count('c') # 198
5
knugie

pickup gem を使用したRubyソリューション:

require 'pickup'

chances = {0=>80, 1=>20}
picker = Pickup.new(chances)

例:

5.times.collect {
  picker.pick(5)
}

出力を与えました:

[[0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 1, 1], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 1]]
3
devstopfix

これはPHP私が本番環境で使用したコードです:

/**
 * @return \App\Models\CdnServer
*/
protected function selectWeightedServer(Collection $servers)
{
    if ($servers->count() == 1) {
        return $servers->first();
    }

    $totalWeight = 0;

    foreach ($servers as $server) {
        $totalWeight += $server->getWeight();
    }

    // Select a random server using weighted choice
    $randWeight = mt_Rand(1, $totalWeight);
    $accWeight = 0;

    foreach ($servers as $server) {
        $accWeight += $server->getWeight();

        if ($accWeight >= $randWeight) {
            return $server;
        }
    }
}
2
Gustav.Calder

配列が小さい場合、配列の長さ(この場合は5)を指定し、必要に応じて値を割り当てます。

array[
    0 => 0
    1 => 0
    2 => 0
    3 => 0
    4 => 1
]
2
thejh

トリックは、確率を反映する要素の繰り返しで補助配列をサンプリングすることです

確率に関連付けられた要素をパーセントで指定すると:

h = {1 => 0.5, 2 => 0.3, 3 => 0.05, 4 => 0.05 }

auxiliary_array = h.inject([]){|memo,(k,v)| memo += Array.new((100*v).to_i,k) }   

Ruby-1.9.3-p194 > auxiliary_array 
 => [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,                                 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4] 

auxiliary_array.sample

できるだけ汎用的にしたい場合は、小数の最大数に基づいて乗数を計算し、100の代わりに使用する必要があります。

m = 10**h.values.collect{|e| e.to_s.split(".").last.size }.max
1
masciugo

https://stackoverflow.com/users/626341/masciugo answerを改善します。

基本的に、要素が現れる回数が重みに比例する1つの大きな配列を作成します。

これにはいくつかの欠点があります。

  1. 重みは整数ではない場合があります。要素1にpiの確率があり、要素2に1-piの確率があるとします。どのように分割しますか?または、そのような要素が何百もあると想像してください。
  2. 作成される配列は非常に大きくなる可能性があります。最小公倍数が100万の場合、選択する配列に100万の要素の配列が必要になると想像してください。

それに対抗するために、これがあなたのすることです。

そのような配列を作成しますが、要素をランダムに挿入するだけです。要素が挿入される確率は、重みに比例します。

次に、通常からランダムな要素を選択します。

したがって、さまざまな重みを持つ3つの要素がある場合、1〜3個の要素の配列から要素を選択するだけです。

構成された要素が空の場合、問題が発生する可能性があります。サイコロの出方が異なるため、配列に要素が表示されないことが起こります。

その場合、要素が挿入される確率はp(挿入)= wi/wmaxであると提案します。

そのようにして、1つの要素、つまり最も高い確率を持つ要素が挿入されます。他の要素は相対確率で挿入されます。

2つのオブジェクトがあるとします。

要素1は、0.20%の時間で表示されます。要素2は、時間の0.40%を示し、最も高い確率を持ちます。

配列では、要素2が常に表示されます。要素1は半分の時間で表示されます。

したがって、要素2は要素1の2倍の数で呼び出されます。一般的には、他のすべての要素はその重みに比例して呼び出されます。また、配列には常に少なくとも1つの要素があるため、すべての確率の合計は1です。

0
user4951

0.8以上1.0未満の数値が3番目の要素を選択すると想像します。

言い換えれば:

xは0〜1の乱数です

0.0> = x <0.2の場合:アイテム1

0.2> = x <0.8の場合:アイテム2

0.8> = x <1.0の場合:アイテム3

0
Ryan Rich