web-dev-qa-db-ja.com

置換ありおよび置換なしの重み付けランダム選択

最近、リストからの要素の重み付けランダム選択を、置換ありとなしの両方で行う必要がありました。非加重選択にはよく知られている優れたアルゴリズムがあり、一部には置換なしの加重選択(Resevoirアルゴリズムの変更など)がありますが、置換ありの加重選択に適したアルゴリズムは見つかりませんでした。また、メモリに保持するのに十分に小さいリストのかなりの部分を選択していたため、resevoirメソッドを回避したかったのです。

この状況での最善のアプローチについて何か提案はありますか?私は自分の解決策を持っていますが、より効率的でシンプルなもの、またはその両方を見つけたいと思っています。

46
Nick Johnson

不変のリストからの置換サンプルで多くを作成する最も速い方法の1つは、エイリアスメソッドです。核となる直感は、バイナリ検索を回避するために、ビット演算を介して非常に効率的にインデックスを付けることができる加重リストの一連の等しいサイズのビンを作成できることです。正しく行われると、ビンごとに元のリストから2つのアイテムのみを格納する必要があるため、分割を1つのパーセンテージで表すことができることがわかります。

均等に重み付けされた5つの選択肢_(a:1, b:1, c:1, d:1, e:1)_の例を見てみましょう

エイリアスルックアップを作成するには:

  1. 合計が_1.0_になるように重みを正規化します。 _(a:0.2 b:0.2 c:0.2 d:0.2 e:0.2)_これは、各重みを選択する確率です。

  2. 変数の数以上の最小の2のべき乗を見つけ、この数のパーティション_|p|_を作成します。各パーティションは、_1/|p|_の確率質量を表します。この場合、_8_パーティションを作成し、それぞれに_0.125_を含めることができます。

  3. 残りの重量が最も少ない変数を取得し、その質量のできるだけ多くを空のパーティションに配置します。この例では、aが最初のパーティションを満たしていることがわかります。 _(p1{a|null,1.0},p2,p3,p4,p5,p6,p7,p8)_と_(a:0.075, b:0.2 c:0.2 d:0.2 e:0.2)_

  4. パーティションがいっぱいになっていない場合は、最も重みの大きい変数を取り、その変数でパーティションをいっぱいにします。

元のパーティションの重みをリストに割り当てる必要がなくなるまで、手順3と4を繰り返します。

たとえば、3と4の別の反復を実行すると、

_(p1{a|null,1.0},p2{a|b,0.6},p3,p4,p5,p6,p7,p8)_が割り当てられた_(a:0, b:0.15 c:0.2 d:0.2 e:0.2)_

実行時:

  1. U(0,1)乱数を取得します。たとえば、バイナリ_0.001100000_を取得します

  2. インデックスパーティションを検索するlg2(p)をビットシフトします。したがって、それを_3_だけシフトして、_001.1_、または位置1、つまりパーティション2を生成します。

  3. パーティションが分割されている場合は、シフトされた乱数の小数部分を使用して分割を決定します。この場合、値は_0.5_および_0.5 < 0.6_なので、aを返します。

ここにいくつかのコードと別の説明があります ですが、残念ながらビットシフト技術を使用しておらず、実際に検証していません。

32

ここで言及されていない簡単なアプローチは、 Efraimidis and Spirakis で提案されているものです。 pythonでは、nからm個の項目を選択できます。厳密に正の重みが重みに格納されているm個の重み付き項目があり、選択したインデックスを返します。

import heapq
import math
import random

def WeightedSelectionWithoutReplacement(weights, m):
    elt = [(math.log(random.random()) / weights[i], i) for i in range(len(weights))]
    return [x[1] for x in heapq.nlargest(m, elt)]

これは、Nick Johnsonが提案した最初のアプローチと構造が非常に似ています。残念ながら、そのアプローチは要素の選択に偏っています(メソッドのコメントを参照)。 EfraimidisとSpirakisは、彼らのアプローチがリンクされた論文での置き換えなしのランダムサンプリングと同等であることを証明しました。

7
josliber

これは私が置き換えなしの重み付け選択のために思いついたものです:

def WeightedSelectionWithoutReplacement(l, n):
  """Selects without replacement n random elements from a list of (weight, item) tuples."""
  l = sorted((random.random() * x[0], x[1]) for x in l)
  return l[-n:]

これは、リストから選択する項目数のO(m log m)です。正式な意味での確認はしていませんが、これでアイテムの重量が正しく決まると確信しています。

これが、置換による重み付け選択のために私が思いついたものです:

def WeightedSelectionWithReplacement(l, n):
  """Selects with replacement n random elements from a list of (weight, item) tuples."""
  cuml = []
  total_weight = 0.0
  for weight, item in l:
    total_weight += weight
    cuml.append((total_weight, item))
  return [cuml[bisect.bisect(cuml, random.random()*total_weight)] for x in range(n)]

これはO(m + n log m)です。ここで、mは入力リスト内のアイテムの数、nは選択されるアイテムの数です。

5
Nick Johnson

最初にO(1)時間で追加のO(N)サイズのデータ​​構造を作成した後、O(N)時間で置換して重み付けランダム選択を実行することが可能です。アルゴリズムは、WalkerとVoseによって開発された エイリアスメソッド に基づいています。これは、 こちら

基本的な考え方は、ヒストグラムの各ビンは、一様なRNGによって確率1/Nで選択されるということです。そのため、それをウォークスルーし、過剰なヒットを受け取る可能性のある過小数のビンについては、過剰を過多のビンに割り当てます。各ビンには、それに属するヒットのパーセンテージと、超過分のパートナービンが格納されます。このバージョンでは、配置されている大小のビンが追跡されるため、追加のスタックが不要になります。これは、パートナーのインデックス(bucket[1]に格納されている)を、それらがすでに処理されていることを示す指標として使用します。

ここに、最小限のpython実装があり、ここに C実装に基づいています

def prep(weights):
    data_sz = len(weights)
    factor = data_sz/float(sum(weights))
    data = [[w*factor, i] for i,w in enumerate(weights)]
    big=0
    while big<data_sz and data[big][0]<=1.0: big+=1
    for small,bucket in enumerate(data):
        if bucket[1] is not small: continue
        excess = 1.0 - bucket[0]
        while excess > 0:
            if big==data_sz: break
            bucket[1] = big
            bucket = data[big]
            bucket[0] -= excess
            excess = 1.0 - bucket[0]
            if (excess >= 0):
                big+=1
                while big<data_sz and data[big][0]<=1: big+=1
    return data

def sample(data):
    r=random.random()*len(data)
    idx = int(r)
    return data[idx][1] if r-idx > data[idx][0] else idx

使用例:

TRIALS=1000
weights = [20,1.5,9.8,10,15,10,15.5,10,8,.2];
samples = [0]*len(weights)
data = prep(weights)

for _ in range(int(sum(weights)*TRIALS)):
    samples[sample(data)]+=1

result = [float(s)/TRIALS for s in samples]
err = [a-b for a,b in Zip(result,weights)]
print(result)
print([round(e,5) for e in err])
print(sum([e*e for e in err]))
4
AShelly

まず、Donald Knuthの Seminumerical Algorithms のセクション3.4.2をご覧になることをお勧めします。

配列が大きい場合は、John Dagpunarによる ランダム変量生成の原則 の第3章に、より効率的なアルゴリズムがあります。配列がそれほど大きくない場合、またはできるだけ多くの効率を引き出すことに関心がない場合は、Knuthのより単純なアルゴリズムでおそらく問題ありません。

4
John D. Cook

以下は、O(n)スペースとO(log n )時間。

これは、選択する要素でソートされたバイナリ検索ツリーの実装で構成されます。ツリーの各nodeには、次のものが含まれます。

  1. 要素自体(element
  2. 要素の正規化されていない重み(elementweight)、および
  3. 左の子ノードとそのすべての子の正規化されていないすべての重みの合計(leftbranchweight)。
  4. 右の子ノードのすべての正規化されていない重みとそのすべての子の合計(rightbranchweight)。

次に、ツリーを降りてBSTからランダムに要素を選択します。アルゴリズムの概要は次のとおりです。アルゴリズムには、ツリーのnodeが与えられます。次に、leftbranchweightrightbranchweight、およびelementweightnodeの値が合計され、重みはこれで除算されます合計すると、値leftbranchprobabilityrightbranchprobability、およびelementprobabilityになります。次に、0と1の間の乱数(randomnumber)が取得されます。

  • 数値がelementprobability、より小さい場合
    • 通常どおりBSTから要素を削除し、必要なすべてのノードのleftbranchweightおよびrightbranchweightを更新して、要素を返します。
  • それ以外の場合、数値が(elementprobability + leftbranchweight)より小さい場合
    • leftchildで再帰します(leftchildnodeとして使用してアルゴリズムを実行します)
  • そうしないと
    • rightchildを再帰します

これらの重みを使用して最終的に返される要素を見つけたら、単純にそれを返すか(置換あり)、削除してツリー内の関連する重みを更新します(置換なし)。

免責事項:アルゴリズムは大まかなものであり、BSTの適切な実装に関する論文はここでは試みません。むしろ、この答えがreallyが(私と同じように)置き換えなしで高速重み付け選択を必要とする人を助けることが期待されています。

4
djhaskin987

リスト['white'、 'blue'、 'black'、 'yellow'、 'green']から置き換えずに3つの要素を確率論的にサンプリングするとします。分布[0.1、0.2、0.4、0.1、0.2]。 numpy.randomモジュールを使用すると、次のように簡単です。

    import numpy.random as rnd

    sampling_size = 3
    domain = ['white','blue','black','yellow','green']
    probs = [.1, .2, .4, .1, .2]
    sample = rnd.choice(domain, size=sampling_size, replace=False, p=probs)
    # in short: rnd.choice(domain, sampling_size, False, probs)
    print(sample)
    # Possible output: ['white' 'black' 'blue']

replaceフラグをTrueに設定すると、置換されたサンプリングがあります。

詳細はこちら: http://docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice

0
Maroxo

K候補のNバリデーターをエポックごとに1回、その賭け金に比例してランダムに選択するという問題に直面しました。しかし、これにより次の問題が発生します。

各候補者の確率を想像してください:

_0.1
0.1
0.8
_

1'000'000選択後の各候補の確率_2_ of _3_ 置換なしは次のようになります:

_0.254315
0.256755
0.488930
_

元の確率は、_2_の_3_の選択に対して、置き換えなしでは達成できません。

しかし、私たちは初期確率が利益分配確率であることを望みます。そうでなければ、それは小さな候補者プールをより有益にします。したがって、ランダムな選択置換ありが_>K_ of Nをランダムに選択し、報酬分配のために各バリデーターの重みも保存するのに役立つことがわかりました:

_std::vector<int> validators;
std::vector<int> weights(n);
int totalWeights = 0;

for (int j = 0; validators.size() < m; j++) {
    int value = Rand() % likehoodsSum;
    for (int i = 0; i < n; i++) {
        if (value < likehoods[i]) {
            if (weights[i] == 0) {
                validators.Push_back(i);
            }
            weights[i]++;
            totalWeights++;
            break;
        }

        value -= likehoods[i];
    }
}
_

それは数百万のサンプルにほぼオリジナルの報酬の分布を与えます:

_0.101230
0.099113
0.799657
_
0
k06a