Эффективный генератор случайных чисел для очень большого диапазона (в python)

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

from random import shuffle

def MyGenerator(foo, num):
    order = list(range(num))
    shuffle(order)
    for i in order:
        if foo(i):
            yield i

Проблема

проблема с этим решением заключается в том, что иногда диапазон будет довольно большим (num может быть порядка 10**8 и вверх). Эта функция может стать медленной, имея такой большой список в памяти. Я попытался избежать этой проблемы, со следующим кодом:

from random import randint    

def MyGenerator(foo, num):
    tried = set()
    while len(tried) <= num - 1:
        i = randint(0, num-1)
        if i in tried:
            continue
        tried.add(i)
        if foo(i):
            yield i

это работает хорошо большую часть времени, так как в большинстве случаев num будет довольно большой, foo пройдет разумное количество чисел и общее количество раз __next__ метод будет вызываться будет относительно небольшим (скажем, максимум 200 часто намного меньше). Поэтому его разумная вероятность того, что мы наткнемся на значение, которое проходит и размером tried никогда не становится большим. (Даже если он проходит только 10% времени, мы не ожидаем tried получать больше, чем около 2000 примерно.)

, когда num мало (близко к числу раз, что __next__ вызывается метод, или foo терпит неудачу большую часть времени, вышеупомянутое решение становится очень неэффективным - случайно угадывая числа, пока не угадает тот, который не находится в tried.

моя попытка решения...

я надеялся используйте какую-то функцию, которая отображает числа 0,1,2,..., n на себя примерно случайным образом. (Это не используется для каких-либо целей безопасности, и поэтому не имеет значения, если это не самая "случайная" функция в мире). Функция здесь (создайте случайную биективную функцию, которая имеет ту же область и диапазон) карты подписали 32-битные целые числа на себя, но я не уверен, как адаптировать отображение к меньшему диапазону. Дано num мне даже не нужна биекция на 0,1,..num просто значение n больше, чем и "близко" к num (используя любое определение close, которое вы считаете нужным). Тогда я могу сделать следующее:

def mix_function_factory(num):
    # something here???
    def foo(index):
        # something else here??
    return foo

def MyGenerator(foo, num):
    mix_function = mix_function_factory(num):
    for i in range(num):
        index = mix_function(i)
        if index <= num:
            if foo(index):
                yield index

(до тех пор, пока биекция не находится на множестве чисел, массово превышающих num количество раз index <= num не правда будет мало).

У Меня Вопрос

можете ли вы подумать об одном из следующих:

  • потенциальное решение для mix_function_factory или даже несколько другие потенциальные функции mix_function что я мог бы попытаться обобщить для разных значений num?
  • лучший способ решения исходной задачи?

заранее большое спасибо....

3 ответов


проблема в основном генерирует случайную перестановку целых чисел в диапазоне 0..n-1.

к счастью для нас, эти числа имеют очень полезное свойство: все они имеют отличное значение по модулю n. Если мы можем применить некоторые математические операции к этим числам, заботясь о том, чтобы каждое число отличалось по модулю n, легко создать перестановку, которая появляется случайные. И лучше всего то, что нам не нужна память, чтобы отслеживать числа, которые мы уже сгенерировали, потому что каждое число вычисляется по простой формуле.


примеры операций, которые мы можем выполнить на каждом номере x в ассортимент входят:

  • дополнение: мы можем добавить любое целое число c to x.
  • умножение: мы можем умножить x С m это не разделяет никаких простых факторов с n.

применение только эти две операции на диапазоне 0..n-1 уже дает вполне удовлетворительные результаты:

>>> n = 7
>>> c = 1
>>> m = 3
>>> [((x+c) * m) % n for x in range(n)]
[3, 6, 2, 5, 1, 4, 0]

выглядит случайным, не так ли?

при создании c и m из случайного числа, это будет на самом деле be случайные тоже. Но имейте в виду, что нет никакой гарантии, что этот алгоритм генерирует все возможные перестановки, или что каждая перестановка имеет одинаковую вероятность быть сгенерированный.


реализация

трудная часть о реализации действительно просто генерирует подходящий случайный m. Я использовал код простой факторизации из ответ чтобы сделать так.

import random

# credit for prime factorization code goes
# to https://stackoverflow.com/a/17000452/1222951
def prime_factors(n):
    gaps = [1,2,2,4,2,4,2,4,6,2,6]
    length, cycle = 11, 3
    f, fs, next_ = 2, [], 0
    while f * f <= n:
        while n % f == 0:
            fs.append(f)
            n /= f
        f += gaps[next_]
        next_ += 1
        if next_ == length:
            next_ = cycle
    if n > 1: fs.append(n)
    return fs

def generate_c_and_m(n, seed=None):
    # we need to know n's prime factors to find a suitable multiplier m
    p_factors = set(prime_factors(n))

    def is_valid_multiplier(m):
        # m must not share any prime factors with n
        factors = prime_factors(m)
        return not p_factors.intersection(factors)

    # if no seed was given, generate random values for c and m
    if seed is None:
        c = random.randint(n)
        m = random.randint(1, 2*n)
    else:
        c = seed
        m = seed

    # make sure m is valid
    while not is_valid_multiplier(m):
        m += 1

    return c, m

теперь, когда мы можем генерировать подходящие значения для c и m, создание перестановки тривиально:

def random_range(n, seed=None):
    c, m = generate_c_and_m(n, seed)

    for x in range(n):
        yield ((x + c) * m) % n

и ваша функция генератора может быть реализована как

def MyGenerator(foo, num):
    for x in random_range(num):
        if foo(x):
            yield x

что может быть случай, когда лучший алгоритм зависит по стоимости num, Так почему бы не использовать 2 выбираемых алгоритма, обернутых в один генератор ?

вы могли бы смешать ваш shuffle и set решения с пороговым значением num. Это в основном сборка ваших 2 первых решений в одном генераторе:

from random import shuffle,randint

def MyGenerator(foo, num):
    if num < 100000 # has to be adjusted by experiments
      order = list(range(num))
      shuffle(order)
      for i in order:
          if foo(i):
              yield i
    else:   # big values, few collisions with random generator 
      tried = set()
      while len(tried) < num:
        i = randint(0, num-1)
        if i in tried:
           continue
        tried.add(i)
        if foo(i):
           yield i

на randint решение (для больших значений num) работает хорошо, потому что в случайном порядке не так много повторов генератор.


получить лучшую производительность в Python намного сложнее, чем в языках более низкого уровня. Например, в C вы часто можете сохранить немного в горячих внутренних циклах, заменив умножение сдвигом. Накладные расходы на ориентацию байт-кода python стирают это. Конечно, это меняет снова когда вы рассматриваете, какой вариант "python" вы нацеливаете (pypy? и NumPy? на Cython?)- ты действительно нужно написать код на основе которого вы с помощью.

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


один из самых простых способов получить немного больше существующих ответов будет путем генерации чисел в кусках с помощью numpy.arange () и применение ((x + c) * m) % n непосредственно к numpy ndarray. Каждый цикл уровня python этого можно избежать.

если функция может быть применена непосредственно к numpy ndarrays, это может быть даже лучше. Конечно, достаточно маленькая функция в python в любом случае будет доминировать над служебными вызовами.


лучший быстрый генератор случайных чисел сегодня PCG. Я написал порт pure-python здесь но сосредоточены на гибкости и легкости понимания, а не на скорости.

Xoroshiro128 + is второе-лучшее качество и быстрее, но менее информативно для изучения.

Python (и многие другие) выбор по умолчанию Мерсенн Твистер является одним из худших.

(есть также что - то под названием splitmix64, о котором я недостаточно знаю - некоторые люди говорят, что это лучше, чем xoroshiro128+, но у него есть проблема с периодом-конечно, вы можете хочу здесь)

и default-PCG и xoroshiro128 + используют 2n-разрядное состояние для генерации N-разрядных чисел. Этот вообще желательно, но значит числа будут повторяться. Однако PCG имеет альтернативные режимы, которые этого избегают.

конечно, многое из этого зависит, будет ли num (близко к) степень 2. Теоретически, варианты PCG могут быть созданы для любой ширины бита, но в настоящее время реализованы только различные размеры слов, так как вам понадобится явная маскировка. Я не уверен, как именно генерировать параметры для новых битовых размеров (возможно, это в статье?), но их можно проверить, просто выполнив период / 2 прыжок и проверка того, что значение отличается.

конечно, если вы делаете только 200 вызовов RNG, вам, вероятно, на самом деле не нужно избегать дубликатов на математической стороне.


кроме того, вы можете использовать РСЛОС, который тут существует для каждого размера бита (хотя обратите внимание, что он никогда не генерирует значение всех нулей (или, что эквивалентно, значение всех единиц)). LFSRs являются последовательными и (AFAIK) не скачкообразными и, следовательно, не могут легко разделить на несколько задач. Edit: я понял, что это неправда, просто представить шаг продвижения как матрицу и экспоненциально его прыгать.

обратите внимание, что LFSRs do имеют те же очевидные предубеждения, что и просто генерирование чисел в последовательном порядке на основе случайной начальной точки - например, если rng_outputs[a: b] все терпят неудачу