Взвешенный случайный выбор с заменой и без замены

недавно мне нужно было сделать взвешенный случайный выбор элементов из списка, так и без замены. Хотя есть хорошо известные и хорошие алгоритмы для невзвешенного выбора, а некоторые для взвешенного выбора без замены (например, модификации алгоритма resevoir), я не смог найти хороших алгоритмов для взвешенного выбора с заменой. Я также хотел избежать метода resevoir, поскольку я выбирал значительную часть списка, которая достаточно мала, чтобы запомните.

есть ли у кого-нибудь предложения по наилучшему подходу в этой ситуации? У меня есть свои решения, но я надеюсь найти что-то более эффективное, более простое или и то и другое.

7 ответов


одним из самых быстрых способов сделать много с заменой образцов из неизменяемого списка является метод псевдонима. Основная интуиция заключается в том, что мы можем создать набор ячеек одинакового размера для взвешенного списка, которые могут быть очень эффективно индексированы с помощью битовых операций, чтобы избежать двоичного поиска. Получится, что, сделав все правильно, нам нужно будет хранить только два элемента из исходного списка на Бин, и таким образом можно будет представлять сплит с одним процентом.

давайте начнем пример пяти одинаково взвешенных вариантов,(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|. В этом случае, мы create 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) случайное число, скажем binary 0.001100000

  2. bitshift это lg2(p) найти раздел индекса. Таким образом, мы сдвигаем его на 3, урожайный 001.1, или положение 1, и, таким образом, раздел 2.

  3. если раздел разделен, используйте десятичную часть сдвинутого случайного числа, чтобы решить разделение. В этом случае значение 0.5 и 0.5 < 0.6, поэтому a.

здесь какой-то код и другое объяснение, но, к сожалению, он не использует метод bitshifting, и я фактически не проверил его.


вот что я придумал для взвешенного выбора без замены:

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-количество элементы для выбора.


Я бы рекомендовал вам начать с просмотра раздела 3.4.2 Дональда Кнута Получисловые Алгоритмы.

Если массивы большие, есть более эффективные алгоритмы в главе 3 принципы генерации случайных вариаций Джон Dagpunar. Если ваши массивы не очень велики или вы не заботитесь о том, чтобы выжать как можно больше эффективности, более простые алгоритмы в Knuth, вероятно, прекрасны.


простой подход, который не был упомянут здесь, предлагается в Efraimidis и Spirakis. В python вы можете выбрать m элементов из n >= 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)]

это очень похоже по структуре на первый подход, предложенный ником Джонсоном. К сожалению, этот подход является предвзятым при выборе элементов (см. комментарии к методу). Эфраимидис и Спиракис доказал, что их подход эквивалентен случайной выборке без замены в связанной статье.


ниже приводится описание случайного взвешенного выбора элемента set (или multiset, если допускаются повторы), как с заменой, так и без замены в o(n) пространстве и O (log n) время.

Он состоит из реализации бинарного дерева поиска, отсортированного по элементам, которые должны быть выбрано, где каждый узел дерева содержит:

  1. сам элемент (элемент)
  2. ненормированный вес элемента (elementweight), и
  3. сумма всех ненормализованных Весов левого дочернего узла и всех его дети (leftbranchweight).
  4. сумма всех ненормализованных Весов правого дочернего узла и всех своих детей (rightbranchweight).

затем мы случайным образом выбираем элемент из BST, спускаясь вниз по дереву. Ля ниже приводится примерное описание алгоритма. Алгоритм задается a узел of дерево. Тогда значения leftbranchweight, rightbranchweight, и elementweight of узел суммируется, и веса, делятся по этому сумма, в результате чего значения leftbranchprobability, rightbranchprobability и elementprobability, соответственно. Затем случайное число от 0 до 1 (датчика случайных чисел) является полученный.

  • если число меньше elementprobability,
    • удалите элемент из BST как обычно, обновив leftbranchweight и rightbranchweight всех необходимых узлов, и вернуть элемент.
  • else, если число меньше (elementprobability + leftbranchweight)
    • recurse on leftchild (запуск алгоритм с использованием leftchild as узел)
  • еще
    • recurse on rightchild

когда мы наконец найдем, используя эти веса, какой элемент должен быть возвращен, мы либо просто вернем его (с заменой), либо удалим его и обновим соответствующие веса в дереве (без замены).

отказ от ответственности: алгоритм является грубым, и Трактат о надлежащая реализация BST здесь не предпринимается; скорее, есть надежда, что этот ответ поможет те, кто действительно нужен быстрый взвешенный выбор без замены (как и я).


можно сделать взвешенную случайную выборку с заменой в O(1) раз, после первого создания дополнительной структуры данных размером O(N) в O (N) раз. Алгоритм основан на Метод Псевдоним разработано Walker и Vose, который хорошо описан здесь.

основная идея заключается в том, что каждый Бин в гистограмме будет выбран с вероятностью 1/N равномерным RNG. Поэтому мы пройдем через него, и для любого малонаселенного бункера, который будет получил бы лишние удары, назначил бы лишнее перенаселенному бункеру. Для каждого бункера мы храним процент попаданий, которые принадлежат ему, и партнерский бункер для избытка. Эта версия отслеживает малые и большие ящики на месте, извлекая потребность для дополнительного стога. Он использует индекс партнера (хранится в bucket[1]) как индикатор того, что они уже обработаны.

вот минимальная реализация python, основанная на реализация C вот!--5-->

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

Предположим, вы хотите попробовать 3 элемента без замены из списка ['белый','синий','черный','желтый',' зеленый'] с пробой. распределение [0.1, 0.2, 0.4, 0.1, 0.2]. Используя библиотеки numpy.случайный модуль это так же просто, как это:

    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