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非常相似,因此用户可以在不进行任何额外更改的情况下受益于这些改进

[1]

原则上,这一变化将影响使用由 np_random_statepy_random_state 修饰的任何函数的NetworkX用户(当 random_state 参数涉及 numpy 时)。 有关详细信息,请参阅下一节。

使用和影响#

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

from networkx.utils import np_random_state

@np_random_state("seed")  # 或者可以是参数位置,例如 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 实例的唯一方法是直接传递实例:

```plaintext
```

这个NXEP提议改变行为,使得当例如整数或 None 被用作 seed 参数时,将返回一个 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 将意味着使用值*而不是已实例化的rng*作为种子时,使用 np_random_state 装饰的函数会产生不同的结果。 例如,让我们看下面的函数:

@np_random_state("seed")
def bar(num, seed=None):
    """返回一个包含 `num` 个均匀随机数的数组。"""
    return seed.random(num)

在当前的 np_random_state 实现中,用户可以向 seed 传递一个整数值,该值将用于生成一个新的 RandomState 实例。 使用相同的种子值保证输出始终完全可重现:

>>> 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 实例后,对于整数种子,装饰的 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 )生成的默认随机数生成器从 numpy.random.RandomState 实例更改为 numpy.random.Generator 实例,当函数的输入为整数或 None 时。

相关工作#

Scikit-learn对依赖随机性的函数施加确定性有类似的模式。 例如, scikit-learn 中的许多函数都有一个 random_state 参数,其功能类似于许多NetworkX函数签名中 seed 的行为。

scikit-learnnetworkx 之间的一个区别是,scikit-learn仅通过 random_state 关键字参数支持 RandomState ,而NetworkX隐式支持内置的 random 模块,以及numpy的 RandomStateGenerator 实例(取决于 seed 的类型)。

这反映在关键字参数的名称上,因为 random_state (由scikit-learn使用)比 seed (由NetworkX使用)更不含糊。

scikit-learn社区中有多个关于支持新的NumPy随机接口的潜在方法的相关讨论:

实现#

实现本身非常简单。确定输入如何映射到随机数生成器的逻辑封装在 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} 无法用于创建 numpy.random.RandomState 或\n"
        "numpy.random.Generator 实例"
    )
    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} 无法用于创建 numpy.random.RandomState 或\n"
        "numpy.random.Generator 实例"
    )
    raise ValueError(msg)

上述内容捕捉了逻辑上的基本更改,尽管实现细节可能有所不同。 与实现此更改相关的大部分工作将涉及改进/重新组织测试;包括添加测试以实现rng流的可重现性。

替代方案#

现状,即默认情况下使用 RandomState ,是一个完全可接受的替代方案。

RandomState 没有被弃用,并且预计将永久保持其流兼容性保证。

另一个可能的替代方案是提供一个包级别的切换,用户可以使用该切换来切换由 np_random_statepy_random_state 修饰的所有函数的 seed kwarg的行为。 为了说明(忽略实现细节):

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

# NetworkX 2.X 行为:默认情况下为 RandomState

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

# 通过设置 pkg 属性更改随机后端

>>> 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 实例,其中流兼容性不是首要考虑因素。