it-swarm.com.de

Gewichtete Zufallsauswahl mit und ohne Ersatz

Kürzlich musste ich eine gewichtete zufällige Auswahl von Elementen aus einer Liste vornehmen, sowohl mit als auch ohne Ersatz. Zwar gibt es bekannte und gute Algorithmen für die nicht gewichtete Auswahl und einige für die gewichtete Auswahl ohne Ersatz (z. B. Änderungen des Resevoir-Algorithmus). Ich konnte jedoch keine guten Algorithmen für die gewichtete Auswahl mit Ersatz finden. Ich wollte auch die Resevoir-Methode vermeiden, da ich einen erheblichen Teil der Liste auswählte, der klein genug ist, um im Speicher zu bleiben.

Hat jemand Vorschläge für die beste Vorgehensweise in dieser Situation? Ich habe meine eigenen Lösungen, aber ich hoffe, etwas effizienter, einfacher oder beides zu finden.

45
Nick Johnson

Eine der schnellsten Möglichkeiten, viele mit Ersatz-Samples aus einer unveränderlichen Liste zu erstellen, ist die Alias-Methode. Die Kernintuition besteht darin, dass wir einen Satz gleichgroßer Fächer für die gewichtete Liste erstellen können, die durch Bitoperationen sehr effizient indiziert werden können, um eine binäre Suche zu vermeiden. Es wird sich herausstellen, dass wir, wenn richtig ausgeführt, nur zwei Elemente aus der ursprünglichen Liste pro Ablage speichern müssen und daher die Aufteilung mit einem einzelnen Prozentsatz darstellen können.

Nehmen wir das Beispiel von fünf gleichgewichteten Optionen, (a:1, b:1, c:1, d:1, e:1)

So erstellen Sie die Alias-Suche:

  1. Normalisieren Sie die Gewichte so, dass sie sich zu 1.0 summieren. (a:0.2 b:0.2 c:0.2 d:0.2 e:0.2) Dies ist die Wahrscheinlichkeit, jedes Gewicht zu wählen.

  2. Suchen Sie die kleinste Potenz von 2, die größer oder gleich der Anzahl der Variablen ist, und erstellen Sie diese Anzahl von Partitionen, |p|. Jede Partition repräsentiert eine Wahrscheinlichkeitsmasse von 1/|p|. In diesem Fall erstellen wir 8-Partitionen, die jeweils 0.125 enthalten können.

  3. Nehmen Sie die Variable mit dem geringsten Restgewicht und legen Sie so viel Masse wie möglich in eine leere Partition. In diesem Beispiel sehen wir, dass a die erste Partition füllt. (p1{a|null,1.0},p2,p3,p4,p5,p6,p7,p8) mit (a:0.075, b:0.2 c:0.2 d:0.2 e:0.2)

  4. Wenn die Partition nicht gefüllt ist, nehmen Sie die Variable mit der höchsten Gewichtung und füllen Sie die Partition mit dieser Variablen. 

Wiederholen Sie die Schritte 3 und 4, bis der Liste kein Gewicht der ursprünglichen Partition zugewiesen werden muss.

Wenn wir beispielsweise eine weitere Iteration von 3 und 4 ausführen, sehen wir 

(p1{a|null,1.0},p2{a|b,0.6},p3,p4,p5,p6,p7,p8) mit (a:0, b:0.15 c:0.2 d:0.2 e:0.2) übrig zu vergeben

Zur Laufzeit:

  1. Holen Sie sich eine U(0,1)-Zufallszahl, sagen Sie binär 0.001100000

  2. bitshift es lg2(p) und findet die Indexpartition. Daher verschieben wir es um 3, wodurch 001.1 oder Position 1 und somit Partition 2 erhalten wird.

  3. Wenn die Partition aufgeteilt ist, verwenden Sie den dezimalen Teil der verschobenen Zufallszahl, um die Aufteilung zu bestimmen. In diesem Fall lautet der Wert 0.5 und 0.5 < 0.6, geben Sie also a zurück.

Hier ist etwas Code und eine andere Erklärung , aber leider verwendet es weder die Bitverschiebungstechnik, noch habe ich es tatsächlich verifiziert.

30

Hier ist was ich für die gewichtete Auswahl ohne Ersatz gefunden habe:

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:]

Dies ist O (m log m) für die Anzahl der Elemente in der Liste, aus denen ausgewählt werden soll. Ich bin mir ziemlich sicher, dass die Artikel korrekt gewichtet werden, obwohl ich sie in keiner formalen Hinsicht überprüft habe.

Folgendes habe ich für die gewichtete Auswahl mit Ersatz gefunden:

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)]

Dies ist O (m + n log m), wobei m die Anzahl der Elemente in der Eingabeliste und n die Anzahl der Elemente ist, die ausgewählt werden sollen.

5
Nick Johnson

Ein einfacher Ansatz, der hier nicht erwähnt wurde, wurde in Efraimidis und Spirakis vorgeschlagen. In Python können Sie m Elemente aus n> = m gewichteten Elementen auswählen, deren positive Gewichte streng in Gewichten gespeichert sind. Die ausgewählten Indizes werden zurückgegeben mit:

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)]

Dies ist in seiner Struktur dem ersten von Nick Johnson vorgeschlagenen Ansatz sehr ähnlich. Leider ist dieser Ansatz bei der Auswahl der Elemente voreingenommen (siehe die Kommentare zur Methode). Efraimidis und Spirakis haben bewiesen, dass ihre Vorgehensweise der Stichprobenentnahme ohne Ersatz in der verlinkten Arbeit entspricht.

4
josliber

Ich empfehle Ihnen, sich zunächst mit Abschnitt 3.4.2 von Donald Knuths Seminumerical Algorithms zu beschäftigen. 

Wenn Ihre Arrays groß sind, gibt es in Kapitel 3 von Prinzipien der Erzeugung von Zufallsvarianten von John Dagpunar effizientere Algorithmen. Wenn Ihre Arrays nicht so groß sind oder Sie nicht so viel Effizienz wie möglich auspressen möchten, sind die einfacheren Algorithmen in Knuth wahrscheinlich in Ordnung.

4
John D. Cook

Das Folgende ist eine Beschreibung der zufällig gewichteten Auswahl eines Elements einer - Gruppe (oder eines Multisets, wenn Wiederholungen zulässig sind), sowohl mit als auch ohne Ersetzung in O(n) Space und O (log n) Zeit.

Es besteht aus der Implementierung eines binären Suchbaums, der nach den zu wählenden Elementen sortiert ist. Dabei enthält jeder Knoten des Baums:

  1. das Element selbst ( element )
  2. das nicht normalisierte Gewicht des Elements ( Elementgewicht ) und
  3. die Summe aller nicht normalisierten Gewichte des Links-Kind-Knotens und aller seiner Kinder ( Leftbranchweight ).
  4. die Summe aller nicht normalisierten Gewichte des Right-Child-Knotens und aller seiner Kinder ( Rightbranchweight ).

Dann wählen wir zufällig ein Element aus der BST aus, indem wir den Baum hinabsteigen. Eine grobe Beschreibung des Algorithmus folgt. Der Algorithmus erhält einen Knoten von Des Baums. Dann werden die Werte von leftbranchweight , rightbranchweight , Und elementweight of node aufsummiert und die Gewichte durch diese Summe dividiert. Daraus ergeben sich die Werte Leftbranchprobability , Rightbranchprobability und Elementwobability . Dann wird eine Zufallszahl zwischen 0 und 1 ( Zufallszahl ) erhalten.

  • wenn die Anzahl kleiner als Elementwahrscheinlichkeit ist ,
    • entfernen Sie das Element wie gewohnt aus der BST. Aktualisieren Sie leftbranchweight und rightbranchweight aller erforderlichen Knoten und geben Sie das - Element zurück.
  • sonst, wenn die Anzahl kleiner ist als ( Elementwahrscheinlichkeit + linkes Zweiggewicht )
    • recurse auf leftchild (den Algorithmus mit leftchild als node ausführen)
  • sonstiges
    • rekurs auf rightchild

Wenn wir schließlich anhand dieser Gewichtungen herausfinden, welches Element zurückgegeben werden soll, senden wir es entweder einfach zurück (mit Ersatz) oder wir entfernen es und aktualisieren relevante Gewichtungen im Baum (ohne Ersatz).

HAFTUNGSAUSSCHLUSS: Der Algorithmus ist grob, und eine Abhandlung über die korrekte Implementierung Einer BST wird hier nicht versucht; Vielmehr hofft man, dass diese Antwort denjenigen hilft, die wirklich eine schnell gewichtete Auswahl ohne Ersatz benötigen (wie ich).

4
djhaskin987

Es ist möglich, die gewichtete zufällige Auswahl mit Ersetzung in O(1) Zeit durchzuführen, nachdem zuerst eine zusätzliche O (N) -große Datenstruktur in O(N) Zeit erstellt wurde. Der Algorithmus basiert auf der von Walker und Vose entwickelten Alias-Methode , die hier gut beschrieben wird. 

Die wesentliche Idee ist, dass jedes Fach in einem Histogramm mit einer Wahrscheinlichkeit von 1/N durch einen einheitlichen RNG ausgewählt wird. Wir gehen also durch, und für jeden unterfüllten Behälter, der übermäßige Treffer erhalten würde, weisen Sie den Überschuss einem überfüllten Behälter zu. Für jede Ablage speichern wir den Prozentsatz der Treffer, die dazu gehören, und die Partner-Ablage für den Überschuss. In dieser Version werden kleine und große Behälter an Ort und Stelle verfolgt, sodass kein zusätzlicher Stapel erforderlich ist. Sie verwendet den Index des Partners (gespeichert in bucket[1]) als Indikator dafür, dass sie bereits verarbeitet wurden.

Hier ist eine minimale Python-Implementierung, basierend auf der C-Implementierung hier

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

Verwendungsbeispiel:

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]))
3
AShelly

Angenommen, Sie möchten drei Elemente ohne Ersatz aus der Liste ["Weiß", "Blau", "Schwarz", "Gelb", "Grün") mit einem Prüfling probieren. Verteilung [0,1, 0,2, 0,4, 0,1, 0,2]. Mit dem numpy.random-Modul ist es so einfach:

    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']

Wenn Sie das Flag replace auf True setzen, erhalten Sie eine Stichprobe mit Ersetzung.

Weitere Informationen hier: http://docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice

0
Maroxo

Wir hatten ein Problem damit, die K - Validierer von N - Kandidaten einmal pro Epoche proportional zu ihren Einsätzen zufällig auszuwählen. Dies gibt uns jedoch das folgende Problem:

Stellen Sie sich die Wahrscheinlichkeiten jedes Kandidaten vor:

0.1
0.1
0.8

Wahrscheinlichkeiten jedes Kandidaten nach 1'000'000 Auswahlen 2 Von 3ohne Ersatz wurden:

0.254315
0.256755
0.488930

Sie sollten wissen, dass diese ursprünglichen Wahrscheinlichkeiten für die 2 - Auswahl von 3 Nicht ersatzlos erreichbar sind.

Wir möchten jedoch, dass die anfänglichen Wahrscheinlichkeiten Gewinnausschüttungswahrscheinlichkeiten sind. Andernfalls werden kleine Kandidatenpools rentabler. So haben wir festgestellt, dass die zufällige Auswahl mit Ersetzung uns helfen würde - zufällig >K Von N auszuwählen und auch das Gewicht jedes Validators für die Belohnungsverteilung zu speichern:

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

Es gibt eine fast originelle Verteilung der Belohnungen auf Millionen von Proben:

0.101230
0.099113
0.799657
0
k06a