在并行程序中播种随机数生成器

Posted

技术标签:

【中文标题】在并行程序中播种随机数生成器【英文标题】:Seeding random number generators in parallel programs 【发布时间】:2015-07-03 11:10:38 【问题描述】:

我正在研究 Python 的多处理模块。 我有两种情况:

例如。 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 示例中却不需要?

【问题讨论】:

请解释是什么让你认为你必须 因为如果我不这样做,那么每个分叉的进程都会生成相同的随机数序列(仅在 Ex.2 中) 无论导致不同行为的原因是什么 - 快速查看源代码并非易事 - numpy 的行为并不出人意料。可重复性是 PRNG 的一个重要特性,并且由于在导入 numpy 时 PRNG 已经播种,因此多处理的 fork() 不应再次播种。 查看类似但不重复问题的出色答案:***.com/a/5837352/2379433 @overcomer - numpy 1.17 只是introduced 新选项(我在下面添加了一个答案),用于“实现的策略,可用于跨多个生成可重复的伪随机数进程” 【参考方案1】:

如果没有明确提供种子,numpy.random 将使用依赖于操作系统的随机源为自己播种。通常它会在基于 Unix 的系统(或某些 Windows 等效系统)上使用/dev/urandom,但如果由于某种原因它不可用,那么它将从挂钟中播种。由于自播发生在新的子进程分叉时,如果多个子进程同时分叉,就有可能继承同一个种子,从而导致不同的子进程产生相同的随机变量。

这通常与您正在运行的并发线程数相关。例如:

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

我不完全确定randomnumpy.random 在这方面的区别是什么(与numpy.random 相比,它在选择随机源进行自播的规则可能略有不同?)。为了安全起见,我仍然建议将种子或 random.Random 实例显式传递给每个子进程。您还可以使用random.Random.jumpahead() 方法,该方法专为在多线程程序中改组Random 实例的状态而设计。

【讨论】:

我想与子进程共享父进程的 numpy 随机状态。我尝试过使用 Manager,但仍然没有运气。你能看看我的问题here,看看你能不能提供一个解决方案?如果我每次生成随机数时都执行 np.random.seed(None),我仍然可以获得不同的随机数,但这不允许我使用父进程的随机状态,这不是我想要的。非常感谢任何帮助。 是的,这是一个很好的解释,对我帮助很大。感谢@overcomer,提出这个问题。【参考方案2】:

这是一个很好的blog post,它将解释numpy.random 的工作方式。

如果您使用np.random.rand(),它将采用您导入np.random 模块时创建的种子。因此,您需要手动在每个线程上创建一个新种子(例如,参见博客文章中的示例)。

python random 模块没有这个问题,会自动为每个线程生成不同的种子。

【讨论】:

【参考方案3】:

numpy 1.17 刚刚介绍了 [quoting] “.. 实现了三种策略,可用于在多个进程中生成可重复的伪随机数(本地或分布式)..”

第一个策略是使用SeedSequence 对象。那里有许多父/子选项,但对于我们的例子,如果您想要生成相同的随机数,但每次运行时都不同

(python3,从 4 个进程中打印 3 个随机数)

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

如果您想要相同的结果用于复制目的,您可以简单地使用相同的种子 (17) 重新播种 numpy:

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

【讨论】:

以上是关于在并行程序中播种随机数生成器的主要内容,如果未能解决你的问题,请参考以下文章

如何为 scikit-learn 播种随机数生成器?

如何随机使用random.seed?

随机数和 Microsoft CNG

使用 scikit-learn 并行生成随机森林

通过并行化加速随机数生成

python生成随机数:uniform(), randint(), gauss(), expovariate()