Selección aleatoria ponderada de matriz

Me gustaría seleccionar aleatoriamente un elemento de una matriz, pero cada elemento tiene una probabilidad de selección conocida.

Todas las posibilidades juntas (dentro de la matriz) sumn a 1.

¿Qué algoritmo sugerirías como el más rápido y el más adecuado para cálculos enormes?

Ejemplo:

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

para este pseudocódigo, el algoritmo en cuestión debería devolver cuatro elementos estadísticamente en cuatro identificadores en id 0 para un elemento en id 1 .

Calcule la función de densidad acumulativa discreta (CDF) de su lista, o en términos simples, la matriz de sums acumulativas de los pesos. Luego genere un número aleatorio en el rango entre 0 y la sum de todos los pesos (podría ser 1 en su caso), haga una búsqueda binaria para encontrar este número aleatorio en su matriz CDF discreta y obtenga el valor correspondiente a esta entrada; esto es su número aleatorio ponderado.

El algoritmo es directo

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

Un ejemplo en 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] 

Esto se puede hacer en O (1) tiempo esperado por muestra de la siguiente manera.

Calcule el CDF F (i) para cada elemento i como la sum de probabilidades menores o iguales a i.

Defina el rango r (i) de un elemento i como el intervalo [F (i – 1), F (i)].

Para cada intervalo [(i – 1) / n, i / n], cree un depósito que consista en la lista de los elementos cuyo rango se solapa con el intervalo. Esto toma O (n) tiempo en total para la matriz completa, siempre que sea razonablemente cuidadoso.

Cuando muestreas aleatoriamente la matriz, simplemente calcula en qué depósito está el número aleatorio y lo compara con cada elemento de la lista hasta que encuentre el intervalo que lo contiene.

El costo de una muestra es O (la longitud esperada de una lista elegida al azar) <= 2.

He encontrado que este artículo es el más útil para comprender este problema por completo. Esta pregunta de stackoverflow también puede ser lo que estás buscando.


Creo que la solución óptima es usar el Método Alias ​​(wikipedia) . Requiere O (n) tiempo para inicializar, O (1) tiempo para hacer una selección y O (n) memoria.

Aquí está el algoritmo para generar el resultado de hacer rodar un dado ponderado en n (de aquí es trivial seleccionar un elemento de un conjunto de longitud y n ) como tomar de este artículo . El autor asume que tienes funciones para lanzar un dado justo ( floor(random() * n) ) y lanzar una moneda sesgada ( random() < p ).

Algoritmo: Método Alias ​​de Vose

Inicialización:

  1. Crear matrices Alias y Prob , cada una de tamaño n .
  2. Crea dos listas de trabajo, pequeña y grande .
  3. Multiplica cada probabilidad por n .
  4. Para cada probabilidad escalada p i :
    1. Si p i <1 , agregue i a Small .
    2. De lo contrario ( p i ≥ 1 ), agregue i a Grande .
  5. Mientras que Small y Large no están vacíos: ( Large se puede vaciar primero)
    1. Retire el primer elemento de Pequeño ; llámalo l .
    2. Retire el primer elemento de Grande ; llámalo g .
    3. Set Prob [l] = p l .
    4. Establecer Alias ​​[l] = g .
    5. Establecer pg: = (p g + p l ) -1 . (Esta es una opción más numéricamente estable).
    6. Si p g <1 , agregue g a Small .
    7. De lo contrario ( p g ≥ 1 ), agregue g a Grande .
  6. Mientras Large no está vacío:
    1. Retire el primer elemento de Grande ; llámalo g .
    2. Set Prob [g] = 1 .
  7. Mientras que Small no está vacío: esto solo es posible debido a la inestabilidad numérica.
    1. Retire el primer elemento de Pequeño ; llámalo l .
    2. Set Prob [l] = 1 .

Generacion:

  1. Genera una tirada justa de un dado n -dado; llamar al lado i .
  2. Lanza una moneda sesgada que sale cara con la probabilidad Prob [i] .
  3. Si la moneda sale "cara", devuelve i .
  4. De lo contrario, devuelva Alias ​​[i] .

Otro ejemplo de 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 # Eg don't allow duplicates, don't allow non-numeric values, etc. # Ignore elements with probability 0 weights = weights.reject { |k, v| v == 0.0 } # eg => {"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] } # eg => [[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 # eg => 0.4651073966724186 # Find the first value that has an accumulated probability greater than the random number u ranges.find { |p, v| p > u }.last # eg => "b" end 

Cómo utilizar:

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

Que esperar:

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

Solución Ruby usando la gem pickup :

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

Ejemplo:

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

dio salida:

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

Si la matriz es pequeña, le daría a la matriz una longitud de, en este caso, cinco y asignar los valores según corresponda:

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

Este es un código PHP que utilicé en producción:

 /** * @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; } } } 

el truco podría ser muestrear una matriz auxiliar con elementos repetidos que reflejan la probabilidad

Teniendo en cuenta los elementos asociados con su probabilidad, como porcentaje:

 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 

si quieres ser lo más genérico posible, debes calcular el multiplicador según la cantidad máxima de dígitos fraccionarios y usarlo en lugar de 100:

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

Me imagino que los números mayores o iguales que 0.8 pero menores que 1.0 seleccionan el tercer elemento.

En otros términos:

x es un número aleatorio entre 0 y 1

si 0.0> = x <0.2: elemento 1

si 0.2> = x <0.8: elemento 2

si 0.8> = x <1.0: elemento 3

Voy a mejorar la respuesta de https://stackoverflow.com/users/626341/masciugo .

Básicamente, se crea una gran matriz donde la cantidad de veces que aparece un elemento es proporcional al peso.

Tiene algunos inconvenientes.

  1. El peso puede no ser un número entero. Imagine que el elemento 1 tiene probabilidad de pi y el elemento 2 tiene probabilidad de 1-pi. ¿Cómo divides eso? O imagine si hay cientos de tales elementos.
  2. La matriz creada puede ser muy grande. Imagine que si el multiplicador menos común es 1 millón, entonces necesitaremos una matriz de 1 millón de elementos en la matriz que queremos elegir.

Para contrarrestar eso, esto es lo que haces.

Cree dicha matriz, pero solo inserte un elemento al azar. La probabilidad de que se inserte un elemento es proporcional al peso.

Luego selecciona un elemento aleatorio de lo habitual.

Entonces, si hay 3 elementos con varios pesos, simplemente elige un elemento de una matriz de 1-3 elementos.

Pueden surgir problemas si el elemento construido está vacío. Es que simplemente sucede que no aparecen elementos en la matriz porque sus dados ruedan de manera diferente.

En cuyo caso, propongo que la probabilidad de insertar un elemento es p (insertado) = wi / wmax.

De esta forma, se insertará un elemento, a saber, el que tiene la probabilidad más alta. Los otros elementos se insertarán por la probabilidad relativa.

Digamos que tenemos 2 objetos.

el elemento 1 aparece .20% del tiempo. el elemento 2 aparece un .40% del tiempo y tiene la probabilidad más alta.

En thearray, el elemento 2 aparecerá todo el tiempo. El elemento 1 aparecerá la mitad del tiempo.

Así que el elemento 2 se llamará 2 veces más que el elemento 1. Por general, todos los demás elementos se denominarán proporcionales a su peso. Además, la sum de todas sus probabilidades es 1 porque la matriz siempre tendrá al menos 1 elemento.