Генерация случайных чисел в параллельных программах

Я изучаю многопроцессорный модуль Python. У меня есть два случая:

Ex. 1

def Foo(nbr_iter):
    for step in xrange(int(nbr_iter)) :
        print random.uniform(0,1)
...

from multiprocessing import Pool

if __name__ == "__main__":
    ...
    pool = Pool(processes=nmr_parallel_block)
    pool.map(Foo, nbr_trial_per_process)

Ex 2. (с использованием numpy)

 def Foo_np(nbr_iter):
     np.random.seed()
     print np.random.uniform(0,1,nbr_iter)

В обоих случаях генераторы случайных чисел засеваются в своих разветвленных процессах.

Почему мне нужно делать посев явно в примере numpy, но не в примере на Python?

Ответ 1

Если семя не указывается явно, numpy.random будет засеять себя с использованием зависимого от ОС источника случайности. Обычно он использует /dev/urandom в системах на основе Unix (или в некоторых эквивалентах Windows), но если это по какой-то причине недоступно, оно будет засеиваться с настенных часов. Так как самосеяние происходит в то время, когда новые подпроцессы вилки, возможно, что несколько подпроцессов наследуют одно и то же семя, если они раздвоены одновременно, что приводит к одинаковым случайным вариациям, создаваемым разными подпроцессами.

Часто это коррелирует с количеством одновременных потоков, которые вы используете. Например:

import numpy as np
import random
from multiprocessing import Pool

def Foo_np(seed=None):
    # np.random.seed(seed)
    return np.random.uniform(0, 1, 5)

pool = Pool(processes=8)
print np.array(pool.map(Foo_np, xrange(20)))

# [[ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.64672339  0.99851749  0.8873984   0.42734339  0.67158796]
#  [ 0.64672339  0.99851749  0.8873984   0.42734339  0.67158796]
#  [ 0.64672339  0.99851749  0.8873984   0.42734339  0.67158796]
#  [ 0.64672339  0.99851749  0.8873984   0.42734339  0.67158796]
#  [ 0.64672339  0.99851749  0.8873984   0.42734339  0.67158796]
#  [ 0.11283279  0.28180632  0.28365286  0.51190168  0.62864241]
#  [ 0.11283279  0.28180632  0.28365286  0.51190168  0.62864241]
#  [ 0.28917586  0.40997875  0.06308188  0.71512199  0.47386047]
#  [ 0.11283279  0.28180632  0.28365286  0.51190168  0.62864241]
#  [ 0.64672339  0.99851749  0.8873984   0.42734339  0.67158796]
#  [ 0.11283279  0.28180632  0.28365286  0.51190168  0.62864241]
#  [ 0.14463001  0.80273208  0.5559258   0.55629762  0.78814652] <-
#  [ 0.11283279  0.28180632  0.28365286  0.51190168  0.62864241]]

Вы можете видеть, что группы до 8 потоков одновременно раздваиваются одним и тем же семенем, давая мне одинаковые случайные последовательности (я отметил первую группу стрелками).

Вызов np.random.seed() внутри подпроцесса заставляет локальный экземпляр RNG с потоком снова сеять из /dev/urandom или настенные часы, что (вероятно) не позволит вам увидеть идентичный вывод из нескольких подпроцессов. Лучшей практикой является явное пропускание отдельного семени (или экземпляра numpy.random.RandomState) для каждого подпроцесса, например:

def Foo_np(seed=None):
    local_state = np.random.RandomState(seed)
    print local_state.uniform(0, 1, 5)

pool.map(Foo_np, range(20))

Я не совсем уверен, что лежит в основе различий между random и numpy.random в этом отношении (возможно, у него есть несколько разные правила для выбора источника случайности для самостоятельного семени по сравнению с numpy.random?). Я бы по-прежнему рекомендовал явно передавать семя или экземпляр random.Random для каждого подпроцесса, чтобы быть в безопасности. Вы также можете использовать метод .jumpahead() random.Random, который предназначен для перетасовки состояний экземпляров random в многопоточных программах.

Ответ 2

Вот хороший пост в блоге, который объясняет, как работает numpy.random.

Если вы используете np.random.rand() он получит начальное число, созданное при импорте модуля np.random. Таким образом, вам нужно создать новое начальное число в каждой теме вручную (см., Например, примеры в сообщении в блоге).

Случайный модуль Python не имеет этой проблемы и автоматически генерирует различное начальное число для каждого потока.

Ответ 3

numpy 1.17 только что представил [цитирование] ".. реализованы три стратегии, которые можно использовать для создания повторяющихся псевдослучайных чисел в нескольких процессах (локальных или распределенных).."

первая стратегия использует объект SeedSequence. Здесь есть много опций "родитель/потомок", но для нашего случая , если вам нужны одинаковые сгенерированные случайные числа, но разные при каждом запуске:

(python3, печать 3 случайных чисел из 4 процессов)

from numpy.random import SeedSequence, default_rng
from multiprocessing import Pool

def rng_mp(rng):
    return [ rng.random() for i in range(3) ]

seed_sequence = SeedSequence()
n_proc = 4
pool = Pool(processes=n_proc)
pool.map(rng_mp, [ default_rng(seed_sequence) for i in range(n_proc) ])

# 2 different runs
[[0.2825724770857644, 0.6465318335272593, 0.4620869345284885],
 [0.2825724770857644, 0.6465318335272593, 0.4620869345284885],
 [0.2825724770857644, 0.6465318335272593, 0.4620869345284885],
 [0.2825724770857644, 0.6465318335272593, 0.4620869345284885]]

[[0.04503760429109904, 0.2137916986051025, 0.8947678672387492],
 [0.04503760429109904, 0.2137916986051025, 0.8947678672387492],
 [0.04503760429109904, 0.2137916986051025, 0.8947678672387492],
 [0.04503760429109904, 0.2137916986051025, 0.8947678672387492]]

Если вы хотите получить тот же результат для целей воспроизведения, вы можете просто повторно заполнить numpy тем же семенем (17):

import numpy as np
from multiprocessing import Pool

def rng_mp(seed):
    np.random.seed(seed)
    return [ np.random.rand() for i in range(3) ]

n_proc = 4
pool = Pool(processes=n_proc)
pool.map(rng_mp, [17] * n_proc)

# same results each run:
[[0.2946650026871097, 0.5305867556052941, 0.19152078694749486],
 [0.2946650026871097, 0.5305867556052941, 0.19152078694749486],
 [0.2946650026871097, 0.5305867556052941, 0.19152078694749486],
 [0.2946650026871097, 0.5305867556052941, 0.19152078694749486]]