Взвешенный случайный выбор с заменой и без замены
недавно мне нужно было сделать взвешенный случайный выбор элементов из списка, так и без замены. Хотя есть хорошо известные и хорошие алгоритмы для невзвешенного выбора, а некоторые для взвешенного выбора без замены (например, модификации алгоритма resevoir), я не смог найти хороших алгоритмов для взвешенного выбора с заменой. Я также хотел избежать метода resevoir, поскольку я выбирал значительную часть списка, которая достаточно мала, чтобы запомните.
есть ли у кого-нибудь предложения по наилучшему подходу в этой ситуации? У меня есть свои решения, но я надеюсь найти что-то более эффективное, более простое или и то и другое.
7 ответов
одним из самых быстрых способов сделать много с заменой образцов из неизменяемого списка является метод псевдонима. Основная интуиция заключается в том, что мы можем создать набор ячеек одинакового размера для взвешенного списка, которые могут быть очень эффективно индексированы с помощью битовых операций, чтобы избежать двоичного поиска. Получится, что, сделав все правильно, нам нужно будет хранить только два элемента из исходного списка на Бин, и таким образом можно будет представлять сплит с одним процентом.
давайте начнем пример пяти одинаково взвешенных вариантов,(a:1, b:1, c:1, d:1, e:1)
чтобы создать поиск псевдонима:
Нормализуйте веса так, чтобы они суммировались в
1.0
.(a:0.2 b:0.2 c:0.2 d:0.2 e:0.2)
Это вероятность выбора каждого веса.найдите наименьшую мощность 2, большую или равную количеству переменных, и создайте это количество разделов,
|p|
. Каждая секция представляет собой массу вероятности1/|p|
. В этом случае, мы create8
разделов, каждый из которых может содержать0.125
.возьмите переменную с наименьшим оставшимся весом и поместите как можно больше ее массы в пустой раздел. В этом примере мы видим, что
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)
если раздел не заполнен, возьмите переменную с наибольшим весом и заполните раздел этой переменной.
повторить шаги 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)
осталось назначить
во время работы:
получить
U(0,1)
случайное число, скажем binary0.001100000
bitshift это
lg2(p)
найти раздел индекса. Таким образом, мы сдвигаем его на3
, урожайный001.1
, или положение 1, и, таким образом, раздел 2.если раздел разделен, используйте десятичную часть сдвинутого случайного числа, чтобы решить разделение. В этом случае значение
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) время.
Он состоит из реализации бинарного дерева поиска, отсортированного по элементам, которые должны быть выбрано, где каждый узел дерева содержит:
- сам элемент (элемент)
- ненормированный вес элемента (elementweight), и
- сумма всех ненормализованных Весов левого дочернего узла и всех его дети (leftbranchweight).
- сумма всех ненормализованных Весов правого дочернего узла и всех своих детей (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