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

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

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

Ответ 1

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

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

  • bithift it lg2(p), находя индексный раздел. Таким образом, мы сдвигаем его на 3, давая 001.1, или позицию 1, и, следовательно, разделяем 2.

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

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

Ответ 2

Простой подход, который здесь не упоминался, - это вариант, предложенный в 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)]

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

Ответ 3

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

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

Ответ 4

I'd recommend you start by looking at section 3.4.2 of Donald Knuth Я бы порекомендовал вам начать с изучения раздела 3.4.2 Получисленных алгоритмов Дональда Кнута.

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

Ответ 5

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

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

  • сам элемент (элемент)
  • ненормированный вес элемента (элементный вес) и
  • сумма всех ненормированных весов левого ребенка node и всех его детей (слева).
  • сумма всех ненормированных весов права-ребенка node и всех его chilren (rightbranchweight).

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

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

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

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Алгоритм является грубым, а трактат о правильной реализации BST не предпринимается здесь; скорее, есть надежда, что этот ответ поможет тем, кому действительно нужен быстрый взвешенный выбор без замены (как и я).

Ответ 6

Можно выполнить взвешенный случайный выбор с заменой в O (1) раз, сначала создав дополнительную O (N) -размерную структуру данных в O (N) времени. Алгоритм основан на методе Alias ​​Method, разработанном Уокер и Восе, который хорошо описан .

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

Ниже приведена минимальная реализация python, основанная на реализации C здесь

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

Ответ 7

Предположим, вы хотите пробовать 3 элемента без замены из списка ['белый', 'синий', 'черный', 'желтый', 'зеленый'] с пробкой. распределение [0,1, 0,2, 0,4, 0,1, 0,2]. С помощью модуля numpy.random это так просто:

    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

Ответ 8

Мы столкнулись с проблемой случайного выбора K валидаторов кандидатов N один раз за эпоху пропорционально их ставкам. Но это дает нам следующую проблему:

Представьте вероятности каждого кандидата:

0.1
0.1
0.8

Вероятности каждого кандидата после 1'000'000 выборов 2 из 3 без замены стали:

0.254315
0.256755
0.488930

Вы должны знать, что эти исходные вероятности недостижимы для 2 из 3 выбора без замены.

Но мы хотим, чтобы начальные вероятности были вероятностями распределения прибыли. Иначе это делает небольшие пулы кандидатов более прибыльными. Таким образом, мы поняли, что случайный выбор с заменой поможет нам - случайным образом выбрать >K из N и сохранить также вес каждого валидатора для распределения вознаграждений:

std::vector<int> validators;
std::vector<int> weights(n);
int totalWeights = 0;

for (int j = 0; validators.size() < m; j++) {
    int value = rand() % likehoodsSum;
    for (int i = 0; i < n; i++) {
        if (value < likehoods[i]) {
            if (weights[i] == 0) {
                validators.push_back(i);
            }
            weights[i]++;
            totalWeights++;
            break;
        }

        value -= likehoods[i];
    }
}

Это дает почти оригинальное распределение вознаграждений по миллионам образцов:

0.101230
0.099113
0.799657