Gewichtete zufällige Auswahl aus dem Array

72

Ich möchte zufällig ein Element aus einem Array auswählen, aber jedes Element hat eine bekannte Auswahlwahrscheinlichkeit.

Alle Chancen zusammen (innerhalb des Arrays) summieren sich zu 1.

Welchen Algorithmus würden Sie als den schnellsten und am besten geeigneten für große Berechnungen vorschlagen?

Beispiel:

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

Für diesen Pseudocode sollte der betreffende Algorithmus bei mehreren Aufrufen statistisch vier Elemente auf ID 0für ein Element auf ID zurückgeben 1.

Mikulas Dite
quelle

Antworten:

73

Berechnen Sie die diskrete kumulative Dichtefunktion (CDF) Ihrer Liste - oder einfach ausgedrückt das Array der kumulativen Summen der Gewichte. Generieren Sie dann eine Zufallszahl im Bereich zwischen 0 und der Summe aller Gewichte (in Ihrem Fall möglicherweise 1), führen Sie eine binäre Suche durch, um diese Zufallszahl in Ihrem diskreten CDF-Array zu finden und den diesem Eintrag entsprechenden Wert zu erhalten ist Ihre gewichtete Zufallszahl.

Sven Marnach
quelle
5
@Mikulas Dite: Diese binäre Suche würde log2(500) = 9Schritte pro Suche dauern .
Thejh
2
Wenn Sie eine Zufallszahl zwischen 0 und der Summe der Gewichte generieren, wer kann dann garantieren, dass die generierte Zufallszahl im cdf-Array enthalten ist? Nehmen wir an, [0,1 0,2 0,4 0,3] als Array der Gewichte zu haben. Das cdf-Array ist [0,1 0,3 0,7 1,0]. Der Rand-Wert muss zwischen 0 und 1,0 generiert werden. dann könnte zum Beispiel 0,62 sein, aber dieser Wert befindet sich nicht im cdf-Array.
Mazzy
2
@Mazzy: Sie suchen nach dem Intervall, das die von Ihnen generierte Zufallszahl enthält - in diesem Fall zwischen 0,3 und 0,7. Natürlich können Sie nicht erwarten, dass der genaue Wert angezeigt wird, aber eine binäre Suche zum Finden des Intervalls funktioniert trotzdem.
Sven Marnach
1
@SvenMarnach Vielleicht ist mir etwas nicht klar. Wenn ich eine binäre Suche auf ein PDF-Array anwende [0.1 0.3 0.7 0.1], erwarte ich, den Rand-Wert im Array zu finden. In diesem Beispiel oben beträgt der Rand-Wert 0,62. Der auf das cdf-Array angewendete binäre Suchalgorithmus sucht im Array nach einem Wert von 0,62. Wenn dieser Wert nicht gefunden wird, wird "nicht gefunden" ausgegeben. Was ich meine ist, dass die binäre Suche den richtigen Wert finden muss, sonst wird kein Wert zurückgegeben
Mazzy
2
@Mazzy: Die binäre Suche kann leicht verwendet werden, um das Intervall zu finden, in dem der gesuchte Wert liegt, und das ist alles, was Sie brauchen. Die meisten binären Suchimplementierungen in Standardbibliotheken von Programmiersprachen erfordern nicht den genauen Wert, der gefunden werden muss, z. B. lower_bound()in C ++ oder bisect_left()in Python .
Sven Marnach
14

Der Algorithmus ist einfach

rand_no = rand(0,1)
for each element in array 
     if(rand_num < element.probablity)
          select and break
     rand_num = rand_num - element.probability

quelle
Das würde nicht funktionieren, weil ich die Chancen habe, nicht die Gegend. | Obwohl jemand diese Antwort abgelehnt hat, gab es mir eine tragfähige Idee. Die Grenzwerte werden ganz einfach berechnet und sollten keinen Einfluss auf die Leistung haben.
Mikulas Dite
@Mikulas unter der Annahme, dass Sie diskrete Chancen und Zufallszahlen haben, die gleichmäßig zwischen 0 und 1 verteilt sind, ergibt sich eine Wahrscheinlichkeit, die ihrem Gewicht entspricht. Für Ihren Fall besteht eine Wahrscheinlichkeit von 80%, dass die Zufallszahl kleiner als 0,8 ist, daher wird das erste Element ausgewählt, und eine Wahrscheinlichkeit von 20% ist größer als 0,8. In diesem Fall wird das zweite Element ausgewählt.
Dies würde die Bestellung des Arrays erfordern, beginnend mit den geringsten Chancen, ausgewählt zu werden, nicht wahr? Das ist eine Berechnung, die ich mir nicht leisten kann. (Beachten Sie, dass ich die Liste der zuvor ausgewählten Elemente nicht
behalte
1
Nein, es funktioniert ohne Sortierung und schneller als die binäre Suche, wenn Sie das Element entfernen möchten, sobald es ausgewählt ist.
6
Entschuldigung für die Frage, was wäre, wenn ich zwei Elemente mit dem gleichen Gewicht hätte? In diesem Fall würde ich nur das erste der beiden Elemente im Array erhalten oder irre ich mich?
Arpho
8

Ich habe festgestellt, dass dieser Artikel am nützlichsten ist, um dieses Problem vollständig zu verstehen. Diese Stackoverflow-Frage ist möglicherweise auch das, wonach Sie suchen.


Ich glaube, die optimale Lösung ist die Verwendung der Alias-Methode (Wikipedia) . Es erfordert O (n) Zeit zum Initialisieren, O (1) Zeit zum Treffen einer Auswahl und O (n) Speicher.

Hier ist der Algorithmus zum Generieren des Ergebnisses des Würfelns eines gewichteten n- seitigen Chips (von hier aus ist es trivial, ein Element aus einem Array mit einer Länge von n auszuwählen ), wie aus diesem Artikel entnommen . Der Autor geht davon aus, dass Sie Funktionen zum Werfen eines fairen Würfels ( floor(random() * n)) und zum Werfen einer voreingenommenen Münze ( random() < p) haben.

Algorithmus: Voses Alias-Methode

Initialisierung:

  1. Erstellen Sie Arrays Alias und Prob mit der Größe n .
  2. Erstellen Sie zwei Arbeitslisten, Klein und Groß .
  3. Multiplizieren Sie jede Wahrscheinlichkeit mit n .
  4. Für jede skalierte Wahrscheinlichkeit p i :
    1. Wenn p i <1 ist , füge i zu Small hinzu .
    2. Andernfalls ( p i ≥ 1 ) addiere i zu Large .
  5. Während Small und Large nicht leer sind: ( Large wird möglicherweise zuerst geleert)
    1. Entfernen Sie das erste Element von Small . nenne es l .
    2. Entfernen Sie das erste Element von Large . nenne es g .
    3. Setze Prob [l] = p l .
    4. Setze Alias ​​[l] = g .
    5. Setze p g : = (p g + p l ) −1 . (Dies ist eine numerisch stabilere Option.)
    6. Wenn p g <1 ist , füge g zu Small hinzu .
    7. Andernfalls ( p g ≥ 1 ) addieren Sie g zu Large .
  6. Während Large nicht leer ist:
    1. Entfernen Sie das erste Element von Large . nenne es g .
    2. Setze Prob [g] = 1 .
  7. Während Small nicht leer ist: Dies ist nur aufgrund numerischer Instabilität möglich.
    1. Entfernen Sie das erste Element von Small . nenne es l .
    2. Setze Prob [l] = 1 .

Generation:

  1. Erzeugen Sie einen fairen Würfelwurf aus einem n- seitigen Würfel. rufe die Seite i .
  2. Wirf eine voreingenommene Münze, die mit der Wahrscheinlichkeit Prob [i] auftaucht .
  3. Wenn die Münze "Köpfe" hochkommt, geben Sie i zurück .
  4. Andernfalls geben Sie Alias ​​[i] zurück .
Simon Baumgardt-Wellander
quelle
8

Ein weiteres Ruby-Beispiel:

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

Wie benutzt man:

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

weighted_rand weights

Was ungefähr zu erwarten ist:

sample = 1000.times.map{ weighted_rand weights }
sample.count('a') # 396
sample.count('b') # 406
sample.count('c') # 198
sample.count('d') # 0
Knugie
quelle
Ich habe das gerade benutzt und den Namen erkannt! Danke @ wolfgang-teuber!
Abe Petrillo
1
Eine Einschränkung bei dieser Methode ist, dass diese Methode nicht wie erwartet funktioniert, wenn Sie eine Gewichtung von 1,0 und den Rest von 0,0 haben. Wir hatten die Gewichtungen als ENV-Variablen und als wir eine der Gewichtungen auf 1,0 umstellten (dh immer wahr machten), hatte dies den gegenteiligen Effekt. Nur zu Ihrer Information für andere da draußen, die diese Methode anwenden!
Abe Petrillo
@AbePetrillo Ich habe die weighted_randMethode aktualisiert , um das von Ihnen beschriebene Problem zu beheben.
Knugie
6

Ein Beispiel in Rubin

#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]
krusty.ar
quelle
6
In diesem Algorithmus wird das letzte Element niemals ausgewählt, da seine Wahrscheinlichkeit 1,0 beträgt und Rand immer zwischen 0 und 1 liegt.
Matt Darby
6

Dies kann in O (1) erwarteter Zeit pro Probe wie folgt erfolgen.

Berechnen Sie die CDF F (i) für jedes Element i als die Summe der Wahrscheinlichkeiten kleiner oder gleich i.

Definieren Sie den Bereich r (i) eines Elements i als Intervall [F (i - 1), F (i)].

Erstellen Sie für jedes Intervall [(i - 1) / n, i / n] einen Bucket, der aus der Liste der Elemente besteht, deren Bereich das Intervall überlappt. Dies dauert insgesamt O (n) Zeit für das gesamte Array, solange Sie einigermaßen vorsichtig sind.

Wenn Sie das Array zufällig auswählen, berechnen Sie einfach, in welchem ​​Bucket sich die Zufallszahl befindet, und vergleichen es mit jedem Element der Liste, bis Sie das Intervall finden, in dem es enthalten ist.

Die Kosten einer Stichprobe betragen O (die erwartete Länge einer zufällig ausgewählten Liste) <= 2.

Jonderry
quelle
Dieser Algorithmus hat eine Worst-Case-Komplexität von O (n), wenn die Gewichte sehr unterschiedliche Größen haben. Es kann vorkommen, dass alle Intervalle zum selben Bucket gehören. Ohne zusätzliche Gewichtsbeschränkungen ist dies definitiv nicht O (1) und nicht einmal O (log n).
Sven Marnach
Der schlimmste Fall tritt nur selten auf. Wenn sich alle n Intervalle mit einem Bucket überlappen würden, würden fast alle Abfragen einen Vergleich mit nur einem Intervall erfordern. In der Praxis ist dies erheblich schneller als die binäre Suche. Wenn Sie darauf bestehen, für den schlimmsten Fall zu optimieren, können Sie in jedem Bucket eine binäre Suche durchführen, wobei die Kosten für jede Abfrage im schlimmsten Fall O (lg (die Länge des größten Buckets)) und O (die Erwartung von lg) betragen (die Länge einer zufällig ausgewählten Liste)) in Erwartung, die immer noch nur O (1) ist.
Jonderry
Danke, es sieht wirklich gut aus. Ich muss einige Versuche durchführen, um festzustellen, ob es sich bei meiner Lösung um eine wirklich schnellere Methode als CDF handelt.
Mikulas Dite
1
@Mikulas Dite, es ist erwähnenswert, dass dies auch eine CDF-Array-Lösung ist, und der Unterschied zur reinen binären Suche ähnelt dem Unterschied zwischen der binären Suche und dem Hashing zur Suche nach einem Element in einem Array. Eine andere Sichtweise ist, dass Sie das CDF-Array berechnen und anstatt eine binäre Suche durchzuführen, die Zufallszahl in den Array-Index hashen, der dem Start des Buckets entspricht. Anschließend können Sie eine beliebige Suchstrategie verwenden (z. B. lineare Brute-Force-Suche oder binäre Suche), um das richtige Stichprobenelement weiter einzugrenzen.
Jonderry
1
Beachten Sie, dass Sie bessere Garantien als in Ihrer üblichen „worst case“ Auswertung haben, weil Ihre Zugriffe bekannt durch den Bau zufällig sein ...
comingstorm
5

Dies ist ein PHP-Code, den ich in der Produktion verwendet habe:

/**
 * @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;
        }
    }
}
Gustav.Calder
quelle
3

Rubinlösung mit dem Pickup Gem :

require 'pickup'

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

Beispiel:

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

gab Ausgabe:

[[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]]
devstopfix
quelle
2

Wenn das Array klein ist, würde ich dem Array eine Länge von in diesem Fall fünf geben und die entsprechenden Werte zuweisen:

array[
    0 => 0
    1 => 0
    2 => 0
    3 => 0
    4 => 1
]
thejh
quelle
Das ist die naheliegendste Lösung, aber ich kann sie nicht wirklich für die Datenmenge verwenden, die ich verarbeiten möchte.
Mikulas Dite
1

Der Trick könnte darin bestehen, ein Hilfsarray mit Elementwiederholungen abzutasten, die die Wahrscheinlichkeit widerspiegeln

In Anbetracht der mit ihrer Wahrscheinlichkeit verbundenen Elemente als Prozentsatz:

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

Wenn Sie so allgemein wie möglich sein möchten, müssen Sie den Multiplikator basierend auf der maximalen Anzahl von Bruchziffern berechnen und anstelle von 100 verwenden:

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

"Glücksrad" O (n), nur für kleine Arrays verwenden:

function pickRandomWeighted(array, weights) {
    var sum = 0;
    for (var i=0; i<weights.length; i++) sum += weights[i];
    for (var i=0, pick=Math.random()*sum; i<weights.length; i++, pick-=weights[i])
        if (pick-weights[i]<0) return array[i];
}
Sarsaparille
quelle
0

Ich würde mir vorstellen, dass Zahlen größer oder gleich 0,8, aber kleiner als 1,0 das dritte Element auswählen.

Mit anderen Worten:

x ist eine Zufallszahl zwischen 0 und 1

wenn 0,0> = x <0,2: Punkt 1

wenn 0,2> = x <0,8: Punkt 2

wenn 0,8> = x <1,0: Punkt 3

Ryan Rich
quelle
0

Ich werde die Antwort auf https://stackoverflow.com/users/626341/masciugo verbessern .

Grundsätzlich erstellen Sie ein großes Array, bei dem die Häufigkeit, mit der ein Element angezeigt wird, proportional zum Gewicht ist.

Es hat einige Nachteile.

  1. Das Gewicht ist möglicherweise nicht ganzzahlig. Stellen Sie sich vor, Element 1 hat eine Wahrscheinlichkeit von pi und Element 2 hat eine Wahrscheinlichkeit von 1-pi. Wie teilt man das auf? Oder stellen Sie sich vor, es gibt Hunderte solcher Elemente.
  2. Das erstellte Array kann sehr groß sein. Stellen Sie sich vor, wenn der kleinste gemeinsame Multiplikator 1 Million beträgt, benötigen wir ein Array mit 1 Million Elementen in dem Array, das wir auswählen möchten.

Um dem entgegenzuwirken, tun Sie Folgendes.

Erstellen Sie ein solches Array, fügen Sie jedoch nur zufällig ein Element ein. Die Wahrscheinlichkeit, dass ein Element eingefügt wird, ist proportional zum Gewicht.

Wählen Sie dann ein zufälliges Element aus dem Üblichen aus.

Wenn es also 3 Elemente mit unterschiedlichem Gewicht gibt, wählen Sie einfach ein Element aus einem Array von 1-3 Elementen aus.

Probleme können auftreten, wenn das konstruierte Element leer ist. Das heißt, es kommt einfach vor, dass keine Elemente im Array angezeigt werden, weil ihre Würfel unterschiedlich würfeln.

In diesem Fall schlage ich vor, dass die Wahrscheinlichkeit, mit der ein Element eingefügt wird, p (eingefügt) = wi / wmax ist.

Auf diese Weise wird ein Element eingefügt, nämlich das mit der höchsten Wahrscheinlichkeit. Die anderen Elemente werden durch die relative Wahrscheinlichkeit eingefügt.

Angenommen, wir haben 2 Objekte.

Element 1 wird in 0,20% der Fälle angezeigt. Element 2 zeigt 0,40% der Zeit und hat die höchste Wahrscheinlichkeit.

Im Bereich wird Element 2 ständig angezeigt. Element 1 wird die halbe Zeit angezeigt.

Element 2 wird also zweimal so oft wie Element 1 genannt. Der Allgemeinheit halber werden alle anderen Elemente proportional zu ihrem Gewicht genannt. Auch die Summe aller ihrer Wahrscheinlichkeiten ist 1, da das Array immer mindestens 1 Element hat.

user4951
quelle
Meine Mathematik ist aus. Es sieht so aus, als hätten Elemente mit einer höheren Anzahl bei dieser Technik eine höhere tatsächliche Wahrscheinlichkeit. Ich würde jetzt die am meisten gewählte Antwort vorschlagen.
user4951