web-dev-qa-db-fra.com

Sélection aléatoire pondérée avec et sans remplacement

Récemment, j’avais besoin de faire une sélection aléatoire pondérée d’éléments d’une liste, avec ou sans remplacement. Bien qu'il existe de bons algorithmes bien connus pour la sélection non pondérée, et certains pour la sélection pondérée sans remplacement (comme les modifications de l'algorithme de réservoir), je ne pouvais trouver aucun bon algorithme pour la sélection pondérée avec remplacement. Je voulais aussi éviter la méthode du réservoir, car je sélectionnais une fraction importante de la liste, qui est suffisamment petite pour être conservée en mémoire.

Quelqu'un a-t-il des suggestions sur la meilleure approche à adopter dans cette situation? J'ai mes propres solutions, mais j'espère trouver quelque chose de plus efficace, plus simple, ou les deux.

45
Nick Johnson

La méthode des alias est l’un des moyens les plus rapides d’en fabriquer plusieurs avec des échantillons de remplacement à partir d’une liste non modifiable. L'intuition de base est que nous pouvons créer un ensemble de corbeilles de taille égale pour la liste pondérée, pouvant être indexé de manière très efficace via des opérations sur bits, afin d'éviter une recherche binaire. Il s'avèrera que, correctement effectué, nous n'aurons besoin que de stocker deux éléments de la liste d'origine par bac, ce qui permet de représenter le fractionnement avec un seul pourcentage.

Prenons l'exemple de cinq choix à pondération égale, (a:1, b:1, c:1, d:1, e:1)

Pour créer la recherche d'alias:

  1. Normalisez les poids de manière à ce qu'ils totalisent 1.0. (a:0.2 b:0.2 c:0.2 d:0.2 e:0.2) C'est la probabilité de choisir chaque poids.

  2. Trouvez la plus petite puissance de 2 supérieure ou égale au nombre de variables et créez ce nombre de partitions, |p|. Chaque partition représente une masse de probabilité de 1/|p|. Dans ce cas, nous créons des partitions 8, chacune pouvant contenir 0.125.

  3. Prenez la variable qui a le moins de poids et placez-en le plus possible dans une partition vide. Dans cet exemple, nous voyons que a remplit la première partition. (p1{a|null,1.0},p2,p3,p4,p5,p6,p7,p8) avec (a:0.075, b:0.2 c:0.2 d:0.2 e:0.2)

  4. Si la partition n'est pas remplie, prenez la variable avec le plus de poids et remplissez la partition avec cette variable. 

Répétez les étapes 3 et 4 jusqu'à ce qu'il ne soit plus nécessaire d'affecter le poids de la partition d'origine à la liste.

Par exemple, si nous exécutons une autre itération de 3 et 4, nous voyons 

(p1{a|null,1.0},p2{a|b,0.6},p3,p4,p5,p6,p7,p8) avec (a:0, b:0.15 c:0.2 d:0.2 e:0.2) restant à attribuer

À l'exécution:

  1. Obtenez un nombre aléatoire U(0,1), dites binaire 0.001100000

  2. bitshift it lg2(p), recherchant la partition d'index. Ainsi, nous le décalons de 3, ce qui donne 001.1, ou la position 1, et donc la partition 2.

  3. Si la partition est divisée, utilisez la partie décimale du nombre aléatoire décalé pour décider de la division. Dans ce cas, la valeur est 0.5 et 0.5 < 0.6; retournez donc a.

Voici du code et une autre explication , mais malheureusement, il n’utilise pas la technique de bitshifting, et je ne l’ai pas encore vérifiée.

30
John with waffle

Voici ce que je propose pour la sélection pondérée sans remplacement:

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

Il s’agit de O (m log m) sur le nombre d’éléments de la liste à sélectionner. Je suis à peu près sûr que le poids des articles sera correct, même si je ne l'ai pas vérifié formellement.

Voici ce que j'ai proposé pour la sélection pondérée avec remplacement:

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

Il s’agit de O (m + n log m), où m correspond au nombre d’éléments de la liste des entrées et n au nombre d’éléments à sélectionner.

5
Nick Johnson

Une approche simple qui n’a pas été mentionnée ici est celle proposée dans Efraimidis et Spirakis . En python, vous pouvez sélectionner m éléments parmi n> = m éléments pondérés avec des poids strictement positifs stockés dans des poids, renvoyant les indices sélectionnés, avec:

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

Cette structure est très similaire à la première approche proposée par Nick Johnson. Malheureusement, cette approche est biaisée dans la sélection des éléments (voir les commentaires sur la méthode). Efraimidis et Spirakis ont prouvé que leur approche était équivalente à un échantillonnage aléatoire sans remplacement dans le document lié.

4
josliber

Je vous recommande de commencer par regarder la section 3.4.2 de Algorithmes de séminaire de Donald Knuth

Si vos tableaux sont grands, il existe des algorithmes plus efficaces au chapitre 3 de Principes de la génération de variables aléatoires par John Dagpunar. Si vos tableaux ne sont pas très grands ou si vous ne souhaitez pas tirer le maximum d’efficacité, les algorithmes les plus simples de Knuth conviennent probablement.

4
John D. Cook

Vous trouverez ci-dessous une description de la sélection pondérée aléatoire d’un élément d’un ensemble de (Ou de plusieurs ensembles, si les répétitions sont autorisées), avec et sans remplacement dans O(n) espace et O (log n) fois.

Il consiste à implémenter un arbre de recherche binaire, trié selon les éléments à sélectionner , Où chaque noeud de l’arbre contient:

  1. l'élément lui-même ( element )
  2. le poids non normalisé de l'élément ( elementweight ), et
  3. la somme de tous les poids non normalisés du nœud enfant gauche et de tous les enfants ( leftbranchweight ).
  4. la somme de tous les poids non normalisés du nœud enfant droit et de tous les ses enfants ( rightbranchweight ).

Ensuite, nous sélectionnons au hasard un élément de la BST en descendant dans l’arbre. Une brève description de l'algorithme suit . L'algorithme reçoit un noeud de L'arbre. Ensuite, les valeurs de poids de la branche gauche , poids de la branche droite , Et poids de l'élément de noeud sont additionnées et les poids sont divisés par cette somme, ce qui donne les valeurs probabilité de franchise à gauche , probabilité de franchise à droite , et probabilité de résistance , respectivement. Ensuite, un nombre aléatoire Compris entre 0 et 1 ( randomnumber ) est obtenu.

  • si le nombre est inférieur à probabilité d'élément ,
    • supprimez l'élément de la BST normalement, en mettant à jour leftbranchweight et rightbranchweight de tous les nœuds nécessaires et renvoyez l'élément .
  • sinon, si le nombre est inférieur à ( elementprobability + leftbranchweight )
    • recurse sur leftchild (exécuter l'algorithme en utilisant leftchild comme noeud )
  • autre
    • recurse sur rightchild

Lorsque nous trouvons finalement, en utilisant ces poids, quel élément doit être renvoyé, nous le renvoyons simplement (avec remplacement) ou nous le retirons et mettons à jour les poids pertinents dans l’arbre (sans remplacement).

CLAUSE DE NON-RESPONSABILITÉ: L’algorithme est grossier et on ne tente pas ici de traiter de la bonne mise en œuvre D’une BST; on espère plutôt que cette réponse aidera ceux qui vraiment ont besoin d'une sélection pondérée rapide sans remplacement (comme je le fais).

4
djhaskin987

Il est possible d'effectuer une sélection aléatoire pondérée avec remplacement dans le temps O(1), après avoir créé une structure de données supplémentaire de la taille O (N) dans le temps O(N). L'algorithme est basé sur la méthode Alias ​​ développée par Walker et Vose, qui est bien décrite ici

L'idée essentielle est que chaque casier de l'histogramme serait choisi avec une probabilité de 1/N par un générateur de ressources de réseau uniforme. Nous allons donc le parcourir, et pour tout bac sous-peuplé qui recevrait un nombre de hits excessif, attribuez le surplus à un bac surpeuplé. Pour chaque groupe, nous stockons le pourcentage de hits qui lui appartiennent et le groupe partenaire pour le surplus. Cette version suit les petits et les grands bacs en place, éliminant ainsi le besoin d'une pile supplémentaire. Il utilise l'index du partenaire (stocké dans bucket[1]) comme indicateur du fait qu'il a déjà été traité.

Voici une implémentation minimale de python, basée sur l'implémentation de C ici

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

Exemple d'utilisation:

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

Supposons que vous vouliez échantillonner 3 éléments sans remplacement dans la liste ['blanc', 'bleu', 'noir', 'jaune', 'vert'] avec un prob. distribution [0,1, 0,2, 0,4, 0,1, 0,2]. Utiliser le module numpy.random est aussi simple que cela:

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

Si vous définissez l’option replace sur True, vous disposez d’un échantillon avec remplacement.

Plus d'infos ici: http://docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice

0
Maroxo