NXEP 4 — 默认随机接口#

作者:

Ross Barnowski (rossbar@berkeley.edu)

状态:

草稿

类型:

标准路径

创建日期:

2022-02-24

摘要#

伪随机数在 NetworkX 中的许多图和网络分析算法中起着重要作用。NetworkX 提供了一个随机数生成器的标准接口,其中包括对numpy.random 和 Python 内置的random 模块的支持。numpy.random 在 NetworkX 中广泛使用,并且在某些情况下是随机数生成的首选包。NumPy 在 NumPy 1.17 版本中的numpy.random 包中引入了一个新接口。根据NEP19,基于numpy.random.Generator 的新接口优先于旧版numpy.random.RandomState,因为它具有更好的统计特性更多功能改进的性能。本 NXEP 提出了一种采用numpy.random.Generator 作为 NetworkX 中随机数生成的**默认**接口的策略。

动机与范围#

numpy.random.Generator 作为 NetworkX 中默认的随机数生成引擎的主要动机是让用户受益于numpy.random.Generator 的改进,包括: - 现代伪随机数生成器统计质量的提升 - 性能改进 - 额外功能

numpy.random.Generator API 与numpy.random.RandomState API 非常相似,因此用户无需对其现有 NetworkX 代码进行任何额外更改[1]即可受益于这些改进。

原则上,这一变化将影响使用由np_random_statepy_random_state 装饰的任何函数的 NetworkX 用户(当 random_state 参数涉及 numpy 时)。详见下一节。

用法与影响#

在 NetworkX 中,随机数生成器通常通过装饰器创建

from networkx.utils import np_random_state

@np_random_state("seed")  # Or could be the arg position, i.e. 0
def foo(seed=None):
    return seed

装饰器负责将各种不同的输入映射到函数内的随机数生成器实例。目前,返回的随机数生成器实例是一个numpy.random.RandomState 对象

>>> type(foo(None))
numpy.random.mtrand.RandomState
>>> type(foo(12345))
numpy.random.mtrand.RandomState

从随机状态装饰器中获取numpy.random.Generator 实例的唯一方法是直接传入该实例

>>> import numpy as np
>>> rng = np.random.default_rng()
>>> type(foo(rng))
numpy.random._generator.Generator

本 NXEP 建议更改行为,使得例如当为 seed 参数给定整数或None 时,取而代之返回一个numpy.random.Generator 实例,即

>>> type(foo(None))
numpy.random._generator.Generator
>>> type(foo(12345))
numpy.random._generator.Generator

numpy.random.RandomState 实例仍可用作 seed,但必须显式传入

>>> rs = np.random.RandomState(12345)
>>> type(foo(rs))
numpy.random.mtrand.RandomState

向后兼容性#

主要有三个方面的担忧

  1. Generator 接口与 RandomState 不兼容流,因此 Generator 方法的结果将与对应的 RandomState 方法结果不完全相同。

  2. RandomStateGenerator API 之间的方法名称和可用性存在一些细微差异。

  3. numpy.random 内部没有全局的 Generator 实例,不像 numpy.random.RandomState 那样。

numpy.random.Generator 接口打破了numpy.random.RandomState 维持的精确值可重现性的流兼容性保证。将默认随机数生成器从 RandomState 切换到 Generator 意味着由 np_random_state 装饰的函数在使用除已实例化 rng 之外的值作为 seed 时,将产生不同的结果。例如,以以下函数为例

@np_random_state("seed")
def bar(num, seed=None):
    """Return an array of `num` uniform random numbers."""
    return seed.random(num)

np_random_state 的当前实现中,用户可以向 seed 传入一个整数值,该值将用于为一个新的 RandomState 实例设置种子。使用相同的 seed 值保证输出始终精确可重现

>>> bar(10, seed=12345)
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
       0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
>>> bar(10, seed=12345)
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
       0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])

然而,在将由 np_random_state 返回的默认 rng 更改为 Generator 实例后,对于整数 seed,由装饰函数 bar 产生的数值将不再相同

>>> bar(10, seed=12345)
array([0.22733602, 0.31675834, 0.79736546, 0.67625467, 0.39110955,
       0.33281393, 0.59830875, 0.18673419, 0.67275604, 0.94180287])

为了恢复原始结果的精确可重现性,需要显式创建并传入已设置种子的 RandomState 实例通过 seed 参数

>>> import numpy as np
>>> rng = np.random.RandomState(12345)
>>> bar(10, seed=rng)
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
       0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])

由于流将不再兼容,本 NXEP 建议仅在主要版本发布时考虑切换默认随机数生成器,例如从 NetworkX 2.X 过渡到 NetworkX 3.0。

第二点担忧仅影响在其自己的库中使用create_random_state 以及对应装饰器np_random_state 的用户。例如,numpy.random.RandomState.randint 方法已被numpy.random.Generator.integers 替换。因此,任何使用 create_random_statecreate_py_random_state 并依赖于返回的 rng 的 randint 方法的代码将导致 AttributeError。这可以通过类似于 networkx.utils.misc.PythonRandomInterface 类的兼容类解决,它提供了randomnumpy.random.RandomState 之间的兼容层。

create_random_state 当前在输入为Nonenumpy.random 模块时返回全局 numpy.random.mtrand._rand RandomState 实例。通过切换到numpy.random.Generator,这将不再可能,因为在numpy.random 模块中没有全局的内部 Generator 实例。这对用户应该没有影响,因为当前的 seed=None 并不能保证可重现的结果。

详细描述#

本 NXEP 建议更改由create_random_state 函数(以及相关的装饰器np_random_state)生成的默认随机数生成器,当输入为整数或None 时,从numpy.random.RandomState 实例更改为numpy.random.Generator 实例。

实现#

实现本身相当简单。决定输入如何映射到随机数生成器的逻辑封装在create_random_state 函数(以及相关的create_py_random_state)中。目前(即 NetworkX <= 2.X),此函数将 Nonenumpy.random 以及整数等输入映射到 RandomState 实例

def create_random_state(random_state=None):
    if random_state is None or random_state is np.random:
        return np.random.mtrand._rand
    if isinstance(random_state, np.random.RandomState):
        return random_state
    if isinstance(random_state, int):
        return np.random.RandomState(random_state)
    if isinstance(random_state, np.random.Generator):
        return random_state
    msg = (
        f"{random_state} cannot be used to create a numpy.random.RandomState or\n"
        "numpy.random.Generator instance"
    )
    raise ValueError(msg)

本 NXEP 建议修改该函数,以便为这些输入生成 Generator 实例。一个示例实现可能如下所示

def create_random_state(random_state=None):
    if random_state is None or random_state is np.random:
        return np.random.default_rng()
    if isinstance(random_state, (np.random.RandomState, np.random.Generator)):
        return random_state
    if isinstance(random_state, int):
        return np.random.default_rng(random_state)
    msg = (
        f"{random_state} cannot be used to create a numpy.random.RandomState or\n"
        "numpy.random.Generator instance"
    )
    raise ValueError(msg)

上述代码捕获了逻辑的核心变化,尽管实现细节可能有所不同。实施此更改的大部分工作将与改进/重组测试相关;包括添加 rng 流可重现性测试。

替代方案#

现状,即默认使用 RandomState,是完全可接受的替代方案。RandomState 并未弃用,预计将永久保持其流兼容性保证。

另一种可能的替代方案是提供包级别的开关,用户可以使用它来切换 seed 关键字参数的行为,对于所有由 np_random_statepy_random_state 装饰的函数。举例说明(忽略实现细节)

>>> import networkx as nx
>>> from networkx.utils.misc import create_random_state

# NetworkX 2.X behavior: RandomState by default

>>> type(create_random_state(12345))
numpy.random.mtrand.RandomState

# Change random backend by setting pkg attr

>>> nx._random_backend = "Generator"

>>> type(create_random_state(12345))
numpy.random._generator.Generator

讨论#

本 NXEP 已在多次社区会议上讨论过,例如参见这些会议纪要

这些讨论中出现的主要担忧是 NumPy Generator 接口不提供与旧版 RandomState 相同的严格流兼容性保证。因此,如果按照建议实施本 NXEP,依赖于设置种子的随机数的代码原则上可能会在未来的某个 NumPy 版本中由于默认的 BitGeneratorGenerator 方法的更改而返回不同的结果。

许多 NetworkX 函数对随机种子相当敏感。例如,更改默认 spring_layout 函数的种子可以产生一个截然不同(但同样有效)的网络布局。流兼容性对于在这些场景中的可重现性非常重要。

因此,我们通过各种讨论得出结论:**不**实施本 NXEP 中提出的更改。RandomState 将继续是 random_state 装饰器的默认随机数生成器,旨在支持所有依赖于 random_state 的 NetworkX 用户代码的严格向后兼容性。Generator 接口在 random_state 装饰器中**受支持**,鼓励用户在流兼容性不是优先事项的新代码中使用 Generator 实例。